diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## LAME 0.1.0
+
+* Initial release.
diff --git a/Codec/Audio/LAME.hs b/Codec/Audio/LAME.hs
new file mode 100644
--- /dev/null
+++ b/Codec/Audio/LAME.hs
@@ -0,0 +1,314 @@
+-- |
+-- Module      :  Codec.Audio.LAME
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- The module provides an interface to LAME MP3 encoder. All you need to do
+-- to encode a WAVE (or RF64) file is to call 'encodeMp3', which see.
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+module Codec.Audio.LAME
+  ( encodeMp3
+  , EncoderSettings (..)
+  , Compression (..)
+  , VbrMode (..)
+  , MetadataPlacement (..)
+  , FilterSetup (..)
+  , I.LameException (..) )
+where
+
+import Codec.Audio.Wave
+import Control.Monad
+import Control.Monad.Catch
+import Control.Monad.IO.Class
+import Data.ByteString (ByteString)
+import Data.Default.Class
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Data.Word
+import System.Directory
+import System.FilePath
+import System.IO
+import qualified Codec.Audio.LAME.Internal as I
+import qualified Data.Text                 as T
+
+-- | LAME encoder settings.
+--
+-- Currently some noise-shaping and psycho-acoustic parameters are omitted,
+-- but can be added on request.
+
+data EncoderSettings = EncoderSettings
+  { encoderCompression :: !Compression
+    -- ^ How to determine amount of compression to apply, see 'Compression'.
+    -- Default value: @'CompressionRatio' 11@ (corresponds to 128 kbps).
+  , encoderQuality :: !(Maybe Word)
+    -- ^ Select algorithm. This variable will effect quality by selecting
+    -- expensive or cheap algorithms. 0 gives the best quality (very slow). 9 is
+    -- very fast, but gives worst quality.
+    --
+    --     * 2 — near-best quality, not too slow.
+    --     * 5 — good quality, fast.
+    --     * 7 — OK quality, really fast.
+    --
+    -- Default value: 'Nothing', meaning that LAME will select the best
+    -- value itself.
+  , encoderNoGap :: !(Maybe (Word32, Word32))
+    -- ^ Enable “no gap” encoding. The first value in the tuple specifies
+    -- current index and the second value specifies total number of tracks.
+    -- Default value: 'Nothing' — disabled.
+  , encoderErrorProtection :: !Bool
+    -- ^ Whether to enable error protection. Error protection means that 2
+    -- bytes from each frame for CRC checksum. Default value: 'False'.
+  , encoderStrictISO :: !Bool
+    -- ^ Whether to enforce strict ISO compliance. Default value: 'False'.
+  , encoderSampleRate :: !(Maybe Word32)
+    -- ^ Sample rate of output file. Default value: 'Nothing', which means
+    -- use sample rate of input file.
+  , encoderScale :: !Float
+    -- ^ Scale input stream by multiplying samples by this value. Default
+    -- value: 1 (no scaling).
+  , encoderFreeFormat :: !Bool
+    -- ^ Use free format. Default value: 'False'.
+  , encoderCopyright :: !Bool
+    -- ^ Mark as copyright protected. Default value: 'False'.
+  , encoderOriginal :: !Bool
+    -- ^ Mark as original. Default value: 'True'.
+  , encoderWriteVbrTag :: !Bool
+    -- ^ Whether to write Xing VBR header frame. Default value: 'True'.
+  , encoderMetadataPlacement :: !MetadataPlacement
+    -- ^ Which container to use to write metadata. Default value:
+    -- 'Id3v2Only'.
+  , encoderTagTitle :: !(Maybe Text)
+    -- ^ Title tag to write. Default value: 'Nothing'.
+  , encoderTagArtist :: !(Maybe Text)
+    -- ^ Artist tag to write. Default value: 'Nothing'.
+  , encoderTagAlbum :: !(Maybe Text)
+    -- ^ Album tag to write. Default value: 'Nothing'.
+  , encoderTagYear :: !(Maybe Text)
+    -- ^ Year tag to write. Default value: 'Nothing'.
+  , encoderTagComment :: !(Maybe Text)
+    -- ^ Comment tag to write. Default value: 'Nothing'.
+  , encoderTagTrack :: !(Maybe (Word8, Maybe Word8))
+    -- ^ Track number (first in tuple) and (optionally) total number of
+    -- tracks (second in tuple). Default value: 'Nothing'.
+  , encoderTagGenre :: !(Maybe Text)
+    -- ^ Genre tag to write. Default value: 'Nothing'.
+  , encoderAlbumArt :: !(Maybe ByteString)
+    -- ^ Album art (a picture) to write (JPEG\/PNG\/GIF). Default value:
+    -- 'Nothing'.
+  , encoderLowpassFilter :: !FilterSetup
+    -- ^ Settings for low-pass filter. Default value: 'FilterAuto'.
+  , encoderHighpassFilter :: !FilterSetup
+    -- ^ Settings for high-pass filter. Default value: 'FilterAuto'.
+  } deriving (Show, Read, Eq, Ord)
+
+instance Default EncoderSettings where
+  def = EncoderSettings
+    { encoderScale           = 1
+    , encoderSampleRate      = Nothing
+    , encoderFreeFormat      = False
+    , encoderNoGap           = Nothing
+    , encoderCopyright       = False
+    , encoderOriginal        = True
+    , encoderErrorProtection = False
+    , encoderStrictISO       = False
+    , encoderWriteVbrTag     = True
+    , encoderQuality         = Nothing
+    , encoderCompression     = CompressionRatio 11
+    , encoderMetadataPlacement = Id3v2Only
+    , encoderTagTitle        = Nothing
+    , encoderTagArtist       = Nothing
+    , encoderTagAlbum        = Nothing
+    , encoderTagYear         = Nothing
+    , encoderTagComment      = Nothing
+    , encoderTagTrack        = Nothing
+    , encoderTagGenre        = Nothing
+    , encoderAlbumArt        = Nothing
+    , encoderLowpassFilter   = FilterAuto
+    , encoderHighpassFilter  = FilterAuto }
+
+-- | The data type represents supported options for compression. You can
+-- specify either fixed bitrate, compression ratio, or use the VBR mode.
+
+data Compression
+  = CompressionBitrate Word
+    -- ^ Specify compression by setting a fixed bitrate, e.g.
+    -- @'CompressionBitrate' 320@.
+  | CompressionRatio Float
+    -- ^ Specify compression ratio.
+  | CompressionVBR VbrMode Word
+    -- ^ VBR. Here you can specify which mode to use and the second argument
+    -- is VBR quality from 0 (highest), to 9 (lowest). Good default is 4.
+  deriving (Show, Read, Eq, Ord)
+
+-- | Variable bitrate (VBR) modes with their associated parameters.
+
+data VbrMode
+  = VbrRh
+    -- ^ VBR RH.
+  | VbrAbr (Maybe Word) (Maybe Word) (Maybe Word) Bool
+    -- ^ VBR ABR. Parameters of the data constructor in order:
+    --
+    --     * Minimal bitrate
+    --     * Mean bitrate
+    --     * Maximal bitrate
+    --     * “Hard minimal bitrate”, if 'True', then the specified minimal
+    --       bitrate will be enforced in all cases (normally it's violated
+    --       for silence).
+  | VbrMtrh
+    -- ^ VBR MTRH
+  deriving (Show, Read, Eq, Ord)
+
+-- | Which container to use to write tags\/metadata.
+
+data MetadataPlacement
+  = Id3v1Only          -- ^ Only write ID3v1 metadata
+  | Id3v2Only          -- ^ Only write ID3v2 metadata
+  | Id3Both            -- ^ Write ID3v1 and ID3v2
+  deriving (Show, Read, Eq, Ord)
+
+-- | Settings for a filter.
+
+data FilterSetup
+  = FilterAuto         -- ^ Filter settings are chosen automatically
+  | FilterDisabled     -- ^ Filter is disabled
+  | FilterManual Word (Maybe Word)
+    -- ^ The first parameter is the filter's frequency (cut-off frequency)
+    -- in Hz and the second parameter is width of transition band in Hz (if
+    -- 'Nothing', it's chosen automatically).
+  deriving (Show, Read, Eq, Ord)
+
+-- | Encode a WAVE file or RF64 file to MP3.
+--
+-- If the input file is not a valid WAVE file, 'WaveException' will be
+-- thrown. 'I.LameException' is thrown when underlying LAME encoder reports
+-- a problem.
+--
+-- Not all sample formats and bit depth are currently supported. Supported
+-- sample formats include:
+--
+--     * PCM with samples represented as signed integers > 8 bit and <= 16
+--       bit per mono-sample.
+--     * IEEE floating point (32 bit) samples.
+--     * IEEE floating point (64 bit) samples.
+--
+-- If you feed the encoder with something else, expect
+-- 'I.LameUnsupportedSampleFormat' to be thrown.
+
+encodeMp3 :: MonadIO m
+  => EncoderSettings   -- ^ Encoder settings
+  -> FilePath          -- ^ WAVE file to encode
+  -> FilePath          -- ^ Where to save the resulting MP3 file
+  -> m ()
+encodeMp3 EncoderSettings {..} ipath' opath' = liftIO . I.withLame $ \l -> do
+  ipath <- makeAbsolute ipath'
+  opath <- makeAbsolute opath'
+  wave@Wave {..}  <- readWaveFile ipath
+  case waveSampleFormat of
+    SampleFormatPcmInt bps ->
+      when (bps > 16 || bps <= 8) $
+        throwM (I.LameUnsupportedSampleFormat waveSampleFormat)
+    _ -> return ()
+  I.setNumSamples l waveSamplesTotal
+  I.setInputSampleRate l (fromIntegral waveSampleRate)
+  I.setNumChannels l (fromIntegral $ waveChannels wave)
+  I.setScale l encoderScale
+  let targetSampleRate = fromMaybe waveSampleRate encoderSampleRate
+  I.setOutputSampleRate l (fromIntegral targetSampleRate)
+  I.setFreeFormat l encoderFreeFormat
+  forM_ encoderNoGap $ \(currentIndex, total) -> do
+    I.setNoGapCurrentIndex l (fromIntegral currentIndex)
+    I.setNoGapTotal        l (fromIntegral total)
+  I.setCopyright l encoderCopyright
+  I.setOriginal l encoderOriginal
+  I.setErrorProtection l encoderErrorProtection
+  I.setStrictISO l encoderStrictISO
+  I.setWriteVbrTag l encoderWriteVbrTag
+  forM_ encoderQuality (I.setQuality l . fromIntegral . min 9)
+  case encoderCompression of
+    CompressionBitrate brate ->
+      I.setBitrate l (fromIntegral brate)
+    CompressionRatio ratio ->
+      I.setCompressionRatio l ratio
+    CompressionVBR vbrMode vbrQuality -> do
+      (I.setVBRQ l . fromIntegral . min 9) vbrQuality
+      case vbrMode of
+        VbrRh -> I.setVBR l I.VbrRh
+        VbrAbr minRate meanRate maxRate hardMin -> do
+          I.setVBR l I.VbrAbr
+          forM_ minRate  (I.setVBRMinBitrate  l . fromIntegral)
+          forM_ meanRate (I.setVBRMeanBitrate l . fromIntegral)
+          forM_ maxRate  (I.setVBRMaxBitrate  l . fromIntegral)
+          I.setVBRHardMin l hardMin
+        VbrMtrh -> I.setVBR l I.VbrMtrh
+  I.id3TagInit l
+  case encoderMetadataPlacement of
+    Id3v1Only -> I.id3TagV1Only l
+    Id3v2Only -> I.id3TagV2Only l
+    Id3Both   -> I.id3TagAddV2  l
+  forM_ encoderTagTitle   (I.id3TagSetTextInfo l "TIT2")
+  forM_ encoderTagArtist  (I.id3TagSetTextInfo l "TPE1")
+  forM_ encoderTagAlbum   (I.id3TagSetTextInfo l "TALB")
+  forM_ encoderTagYear    (I.id3TagSetTextInfo l "TYER")
+  forM_ encoderTagComment (I.id3TagSetComment l)
+  forM_ encoderTagTrack   (uncurry renderTrackNumber >=> I.id3TagSetTextInfo l "TRCK")
+  forM_ encoderTagGenre   (I.id3TagSetTextInfo l "TCON")
+  forM_ encoderAlbumArt   (I.id3TagSetAlbumArt l)
+  setupFilter I.setLowpassFreq  I.setLowpassWidth  l encoderLowpassFilter
+  setupFilter I.setHighpassFreq I.setHighpassWidth l encoderHighpassFilter
+  I.initParams l
+  withTempFile' opath $ \otemp -> do
+    I.encodingHelper l wave ipath otemp
+    renameFile otemp opath
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | Setup a filter for given 'I.Lame'.
+
+setupFilter
+  :: (I.Lame -> Int -> IO ()) -- ^ How to set cut-off frequncy
+  -> (I.Lame -> Int -> IO ()) -- ^ How to set width of transition band
+  -> I.Lame            -- ^ Settings to edit
+  -> FilterSetup       -- ^ Filter setup to apply
+  -> IO ()
+setupFilter setFreq _ l FilterAuto = setFreq l 0
+setupFilter setFreq _ l FilterDisabled = setFreq l (-1)
+setupFilter setFreq setWidth l (FilterManual freq mwidth) = do
+  setFreq l (fromIntegral freq)
+  forM_ mwidth (setWidth l . fromIntegral)
+
+-- | A custom wrapper for creating temporary files in the same directory as
+-- given file. 'Handle' is not opened, you only get 'FilePath' in the
+-- callback.
+
+withTempFile' :: FilePath -> (FilePath -> IO a) -> IO a
+withTempFile' path = bracketOnError acquire cleanup
+  where
+    acquire = do
+      (path',h) <- openBinaryTempFile dir file
+      hClose h
+      return path'
+    cleanup = ignoringIOErrors . removeFile -- in case exception strikes
+              -- before we create the actual file
+    dir     = takeDirectory path
+    file    = takeFileName  path
+
+-- | Perform specified action ignoring IO exceptions it may throw.
+
+ignoringIOErrors :: IO () -> IO ()
+ignoringIOErrors ioe = ioe `catch` handler
+  where
+    handler :: IOError -> IO ()
+    handler = const (return ())
+
+renderTrackNumber :: Word8 -> Maybe Word8 -> IO Text
+renderTrackNumber 0 t          = throwM (I.LameInvalidTrackNumber 0 t)
+renderTrackNumber n t@(Just 0) = throwM (I.LameInvalidTrackNumber n t)
+renderTrackNumber n t = return . T.pack $ show n ++ maybe "" (("/" ++) . show) t
diff --git a/Codec/Audio/LAME/Internal.hs b/Codec/Audio/LAME/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Codec/Audio/LAME/Internal.hs
@@ -0,0 +1,529 @@
+-- |
+-- Module      :  Codec.Audio.LAME.Internal
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Low-level non-public helpers.
+
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE RecordWildCards          #-}
+
+module Codec.Audio.LAME.Internal
+  ( -- * Types
+    Lame
+  , VbrMode (..)
+  , LameException (..)
+    -- * Low-level API
+  , withLame
+  , initParams
+    -- ** Input stream description
+  , setNumSamples
+  , setInputSampleRate
+  , setNumChannels
+  , setScale
+  , setOutputSampleRate
+    -- ** General control parameters
+  , setWriteVbrTag
+  , setQuality
+  , setFreeFormat
+  , setFindReplayGain
+  , setNoGapTotal
+  , setNoGapCurrentIndex
+  , setBitrate
+  , setCompressionRatio
+    -- ** Frame parameters
+  , setCopyright
+  , setOriginal
+  , setErrorProtection
+  , setStrictISO
+    -- ** VBR control
+  , setVBR
+  , setVBRQ
+  , setVBRMinBitrate
+  , setVBRMeanBitrate
+  , setVBRMaxBitrate
+  , setVBRHardMin
+    -- ** Filtering control
+  , setLowpassFreq
+  , setLowpassWidth
+  , setHighpassFreq
+  , setHighpassWidth
+    -- * Tags
+  , id3TagInit
+  , id3TagAddV2
+  , id3TagV1Only
+  , id3TagV2Only
+  , id3TagSetTextInfo
+  , id3TagSetComment
+  , id3TagSetAlbumArt
+    -- * Encoding
+  , encodingHelper )
+where
+
+import Codec.Audio.Wave
+import Control.Monad.Catch
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Data.Void
+import Foreign hiding (void)
+import Foreign.C.String
+import Foreign.C.Types (CSize (..))
+import Unsafe.Coerce
+import qualified Data.ByteString.Unsafe as B
+import qualified Data.Text.Foreign      as TF
+
+----------------------------------------------------------------------------
+-- Types
+
+-- | An opaque newtype wrapper around @'Ptr' 'Void'@, serves to represent
+-- pointer to the structure that does all the bookkeeping in LAME.
+
+newtype Lame = Lame (Ptr Void)
+
+-- | Enumeration of VBR modes.
+
+data VbrMode
+  = VbrRh              -- ^ VBR RH
+  | VbrAbr             -- ^ VBR ABR
+  | VbrMtrh            -- ^ VBR MTRH
+  deriving (Show, Read, Eq, Ord, Bounded, Enum)
+
+-- | Enumeration of problems you can have with LAME.
+
+data LameException
+  = LameGenericError   -- ^ A “generic error” happened
+  | LameNoMemory       -- ^ Memory allocation issue
+  | LameBadBitrate     -- ^ Unsupported bitrate
+  | LameBadSampleFreq  -- ^ Unsupported sample rate
+  | LameInternalError  -- ^ An “Internal error” happened
+  | LameInvalidTrackNumber Word8 (Maybe Word8)
+    -- ^ Invalid track number (first argument) or total number of tracks
+    -- (second argument) was supplied
+  | LameUnsupportedSampleFormat SampleFormat
+    -- ^ This sample format is not supported at this time
+  deriving (Eq, Show, Read)
+
+instance Exception LameException
+
+----------------------------------------------------------------------------
+-- Low-level API
+
+-- | Create and use a 'Lame' (pointer structure needed for talking to the
+-- LAME API).
+--
+-- If memory cannot be allocated, corresponding 'LameException' is raised.
+
+withLame :: (Lame -> IO a) -> IO a
+withLame f = bracket lameInit (mapM_ lameClose) $ \mlame ->
+  case mlame of
+    Nothing -> throwM LameNoMemory
+    Just x -> f x
+
+-- | Create a new 'Lame'. In the case of memory allocation problem 'Nothing'
+-- is returned.
+
+lameInit :: IO (Maybe Lame)
+lameInit = maybePtr <$> c_lame_init
+
+foreign import ccall unsafe "lame_init"
+  c_lame_init :: IO Lame
+
+-- | Free 'Lame' structure and all associated buffers.
+
+lameClose :: Lame -> IO ()
+lameClose = c_lame_close
+
+foreign import ccall unsafe "lame_close"
+  c_lame_close :: Lame -> IO ()
+
+-- | Set more internal configuration based on previously set parameters.
+-- Should be called when all the other stuff is set.
+
+initParams :: Lame -> IO ()
+initParams = handleErrors . c_lame_init_params
+
+foreign import ccall unsafe "lame_init_params"
+  c_lame_init_params :: Lame -> IO Int
+
+----------------------------------------------------------------------------
+-- Input stream description
+
+-- | Set total number of samples to encode.
+
+setNumSamples :: Lame -> Word64 -> IO ()
+setNumSamples l x = handleErrors (c_lame_set_num_samples l x)
+
+foreign import ccall unsafe "lame_set_num_samples"
+  c_lame_set_num_samples :: Lame -> Word64 -> IO Int
+
+-- | Set sample rate of the input stream.
+
+setInputSampleRate :: Lame -> Int -> IO ()
+setInputSampleRate l x = handleErrors (c_lame_set_in_samplerate l x)
+
+foreign import ccall unsafe "lame_set_in_samplerate"
+  c_lame_set_in_samplerate :: Lame -> Int -> IO Int
+
+-- | Set number of channels in input stream.
+
+setNumChannels :: Lame -> Int -> IO ()
+setNumChannels l x = handleErrors (c_lame_set_num_channels l x)
+
+foreign import ccall unsafe "lame_set_num_channels"
+  c_lame_set_num_channels :: Lame -> Int -> IO Int
+
+-- | Scale the input by this amount before encoding.
+
+setScale :: Lame -> Float -> IO ()
+setScale l x = handleErrors (c_lame_set_scale l x)
+
+foreign import ccall unsafe "lame_set_scale"
+  c_lame_set_scale :: Lame -> Float -> IO Int
+
+-- | Set output sample rate in Hz. 0 (default) means that LAME will pick
+-- this value automatically.
+
+setOutputSampleRate :: Lame -> Int -> IO ()
+setOutputSampleRate l x = handleErrors (c_lame_set_out_samplerate l x)
+
+foreign import ccall unsafe "lame_set_out_samplerate"
+  c_lame_set_out_samplerate :: Lame -> Int -> IO Int
+
+----------------------------------------------------------------------------
+-- General control parameters
+
+-- | Set whether to write Xing VBR header frame.
+
+setWriteVbrTag :: Lame -> Bool -> IO ()
+setWriteVbrTag l x = handleErrors (c_lame_set_bWriteVbrTag l (fromBool x))
+
+foreign import ccall unsafe "lame_set_bWriteVbrTag"
+  c_lame_set_bWriteVbrTag :: Lame -> Int -> IO Int
+
+-- | Select algorithm. This variable will effect quality by selecting
+-- expensive or cheap algorithms. 0 gives the best quality (very slow). 9 is
+-- very fast, but gives worst quality.
+--
+--     * 2 — near-best quality, not too slow.
+--     * 5 — good quality, fast.
+--     * 7 — OK quality, really fast.
+
+setQuality :: Lame -> Int -> IO ()
+setQuality l x = handleErrors (c_lame_set_quality l x)
+
+foreign import ccall unsafe "lame_set_quality"
+  c_lame_set_quality :: Lame -> Int -> IO Int
+
+-- | Set whether we should use free format.
+
+setFreeFormat :: Lame -> Bool -> IO ()
+setFreeFormat l x = handleErrors (c_lame_set_free_format l (fromBool x))
+
+foreign import ccall unsafe "lame_set_free_format"
+  c_lame_set_free_format :: Lame -> Int -> IO Int
+
+-- | Set whether we should do ReplayGain analysis.
+
+setFindReplayGain :: Lame -> Bool -> IO ()
+setFindReplayGain l x = handleErrors (c_lame_set_findReplayGain l (fromBool x))
+
+foreign import ccall unsafe "lame_set_findReplayGain"
+  c_lame_set_findReplayGain :: Lame -> Int -> IO Int
+
+-- | Set total number of tracks to encode in “no gap” mode.
+
+setNoGapTotal :: Lame -> Int -> IO ()
+setNoGapTotal l x = handleErrors (c_lame_set_nogap_total l x)
+
+foreign import ccall unsafe "lame_set_nogap_total"
+  c_lame_set_nogap_total :: Lame -> Int -> IO Int
+
+-- | Set index of current track to encode in “no gap” mode.
+
+setNoGapCurrentIndex :: Lame -> Int -> IO ()
+setNoGapCurrentIndex l x = handleErrors (c_lame_set_nogap_currentindex l x)
+
+foreign import ccall unsafe "lame_set_nogap_currentindex"
+  c_lame_set_nogap_currentindex :: Lame -> Int -> IO Int
+
+-- | Set bitrate.
+
+setBitrate :: Lame -> Int -> IO ()
+setBitrate l x = handleErrors (c_lame_set_brate l x)
+
+foreign import ccall unsafe "lame_set_brate"
+  c_lame_set_brate :: Lame -> Int -> IO Int
+
+-- | Set compression ratio.
+
+setCompressionRatio :: Lame -> Float -> IO ()
+setCompressionRatio l x = handleErrors (c_lame_set_compression_ratio l x)
+
+foreign import ccall unsafe "lame_set_compression_ratio"
+  c_lame_set_compression_ratio :: Lame -> Float -> IO Int
+
+----------------------------------------------------------------------------
+-- Frame parameters
+
+-- | Mark as copyright protected.
+
+setCopyright :: Lame -> Bool -> IO ()
+setCopyright l x = handleErrors (c_lame_set_copyright l (fromBool x))
+
+foreign import ccall unsafe "lame_set_copyright"
+  c_lame_set_copyright :: Lame -> Int -> IO Int
+
+-- | Mark as original.
+
+setOriginal :: Lame -> Bool -> IO ()
+setOriginal l x = handleErrors (c_lame_set_original l (fromBool x))
+
+foreign import ccall unsafe "lame_set_original"
+  c_lame_set_original :: Lame -> Int -> IO Int
+
+-- | Set whether to use 2 bytes from each frame for CRC checksum.
+
+setErrorProtection :: Lame -> Bool -> IO ()
+setErrorProtection l x =
+  handleErrors (c_lame_set_error_protection l (fromBool x))
+
+foreign import ccall unsafe "lame_set_error_protection"
+  c_lame_set_error_protection :: Lame -> Int -> IO Int
+
+-- | Enforce strict ISO compliance.
+
+setStrictISO :: Lame -> Bool -> IO ()
+setStrictISO l x = handleErrors (c_lame_set_strict_ISO l (fromBool x))
+
+foreign import ccall unsafe "lame_set_strict_ISO"
+  c_lame_set_strict_ISO :: Lame -> Int -> IO Int
+
+----------------------------------------------------------------------------
+-- VBR control
+
+-- | Set type of VBR.
+
+setVBR :: Lame -> VbrMode -> IO ()
+setVBR l x' = handleErrors (c_lame_set_VBR l x)
+  where
+    x = case x' of
+          VbrRh   -> 2
+          VbrAbr  -> 3
+          VbrMtrh -> 4
+
+foreign import ccall unsafe "lame_set_VBR"
+  c_lame_set_VBR :: Lame -> Int -> IO Int
+
+-- | Set VBR quality level, 0 is highest, 9 is lowest.
+
+setVBRQ :: Lame -> Int -> IO ()
+setVBRQ l x = handleErrors (c_lame_set_VBR_q l x)
+
+foreign import ccall unsafe "lame_set_VBR_q"
+  c_lame_set_VBR_q :: Lame -> Int -> IO Int
+
+-- | Only for VBR ABR: set min bitrate in kbps.
+
+setVBRMinBitrate :: Lame -> Int -> IO ()
+setVBRMinBitrate l x = handleErrors (c_lame_set_VBR_min_bitrate_kbps l x)
+
+foreign import ccall unsafe "lame_set_VBR_min_bitrate_kbps"
+  c_lame_set_VBR_min_bitrate_kbps :: Lame -> Int -> IO Int
+
+-- | Only for VBR ABR: set mean bitrate in kbps.
+
+setVBRMeanBitrate :: Lame -> Int -> IO ()
+setVBRMeanBitrate l x = handleErrors (c_lame_set_VBR_mean_bitrate_kbps l x)
+
+foreign import ccall unsafe "lame_set_VBR_mean_bitrate_kbps"
+  c_lame_set_VBR_mean_bitrate_kbps :: Lame -> Int -> IO Int
+
+-- | Only for VBR ABR: set max bitrate in kbps.
+
+setVBRMaxBitrate :: Lame -> Int -> IO ()
+setVBRMaxBitrate l x = handleErrors (c_lame_set_VBR_max_bitrate_kbps l x)
+
+foreign import ccall unsafe "lame_set_VBR_max_bitrate_kbps"
+  c_lame_set_VBR_max_bitrate_kbps :: Lame -> Int -> IO Int
+
+-- | Set whether to strictly enforce VBR min bitrate. Normally it will be
+-- violated for analog silence.
+
+setVBRHardMin :: Lame -> Bool -> IO ()
+setVBRHardMin l x = handleErrors (c_lame_set_VBR_hard_min l (fromBool x))
+
+foreign import ccall unsafe "lame_set_VBR_hard_min"
+  c_lame_set_VBR_hard_min :: Lame -> Int -> IO Int
+
+----------------------------------------------------------------------------
+-- Filtering control
+
+-- | Set frequency to put low-pass filter on. Default is 0 (LAME chooses),
+-- -1 will disable the filter altogether.
+
+setLowpassFreq :: Lame -> Int -> IO ()
+setLowpassFreq l x = handleErrors (c_lame_set_lowpassfreq l x)
+
+foreign import ccall unsafe "lame_set_lowpassfreq"
+  c_lame_set_lowpassfreq :: Lame -> Int -> IO Int
+
+-- | Set width of low-pass transition band. Default is one polyphase filter
+-- band.
+
+setLowpassWidth :: Lame -> Int -> IO ()
+setLowpassWidth l x = handleErrors (c_lame_set_lowpasswidth l x)
+
+foreign import ccall unsafe "lame_set_lowpasswidth"
+  c_lame_set_lowpasswidth :: Lame -> Int -> IO Int
+
+-- | Set frequency to put high-pass filter on. Default is 0 (LAME chooses),
+-- -1 will disable the filter altogether.
+
+setHighpassFreq :: Lame -> Int -> IO ()
+setHighpassFreq l x = handleErrors (c_lame_set_highpassfreq l x)
+
+foreign import ccall unsafe "lame_set_highpassfreq"
+  c_lame_set_highpassfreq :: Lame -> Int -> IO Int
+
+-- | Set width of high-pass transition band. Default is one polyphase filter
+-- band.
+
+setHighpassWidth :: Lame -> Int -> IO ()
+setHighpassWidth l x = handleErrors (c_lame_set_highpasswidth l x)
+
+foreign import ccall unsafe "lame_set_highpasswidth"
+  c_lame_set_highpasswidth :: Lame -> Int -> IO Int
+
+----------------------------------------------------------------------------
+-- Tags
+
+-- | Initialize something about tag editing library. The docs are silent
+-- what this does, but I guess we'll take it into the business.
+
+id3TagInit :: Lame -> IO ()
+id3TagInit = c_id3tag_init
+
+foreign import ccall unsafe "id3tag_init"
+  c_id3tag_init :: Lame -> IO ()
+
+-- | Force addition of version 2 tag.
+
+id3TagAddV2 :: Lame -> IO ()
+id3TagAddV2 = c_id3tag_add_v2
+
+foreign import ccall unsafe "id3tag_add_v2"
+  c_id3tag_add_v2 :: Lame -> IO ()
+
+-- | Add only a version 1 tag.
+
+id3TagV1Only :: Lame -> IO ()
+id3TagV1Only = c_id3tag_v1_only
+
+foreign import ccall unsafe "id3tag_v1_only"
+  c_id3tag_v1_only :: Lame -> IO ()
+
+-- | Add only a version 2 tag.
+
+id3TagV2Only :: Lame -> IO ()
+id3TagV2Only = c_id3tag_v2_only
+
+foreign import ccall unsafe "id3tag_v2_only"
+  c_id3tag_v2_only :: Lame -> IO ()
+
+-- | Set a textual tag identifying it by its ID.
+
+id3TagSetTextInfo :: Lame -> String -> Text -> IO ()
+id3TagSetTextInfo l id' text = handleErrors $
+  withCString id' $ \idPtr ->
+    TF.useAsPtr text $ \textPtr len ->
+      c_id3tag_set_textinfo_utf16 l idPtr textPtr (fromIntegral len)
+
+foreign import ccall unsafe "id3tag_set_textinfo_utf16_"
+  c_id3tag_set_textinfo_utf16 :: Lame -> CString -> Ptr Word16 -> Int -> IO Int
+
+-- | Set the comment tag.
+
+id3TagSetComment :: Lame -> Text -> IO ()
+id3TagSetComment l text = handleErrors $
+  TF.useAsPtr text $ \textPtr len ->
+    c_id3tag_set_comment_utf16 l textPtr (fromIntegral len)
+
+foreign import ccall unsafe "id3tag_set_comment_utf16_"
+  c_id3tag_set_comment_utf16 :: Lame -> Ptr Word16 -> Int -> IO Int
+
+-- | Set album art.
+
+id3TagSetAlbumArt :: Lame -> ByteString -> IO ()
+id3TagSetAlbumArt l img = handleErrors $
+  B.unsafeUseAsCStringLen img $ \(dataPtr, dataLen) ->
+    c_id3tag_set_albumart l dataPtr (fromIntegral dataLen)
+
+foreign import ccall unsafe "id3tag_set_albumart"
+  c_id3tag_set_albumart :: Lame -> CString -> CSize -> IO Int
+
+----------------------------------------------------------------------------
+-- Encoding
+
+-- | Encode given input file.
+
+encodingHelper
+  :: Lame              -- ^ The settings
+  -> Wave              -- ^ Information about input WAVE file
+  -> FilePath          -- ^ Location of input file (normalized)
+  -> FilePath          -- ^ Location of output file (normalized)
+  -> IO ()
+encodingHelper l wave@Wave {..} ipath opath = handleErrors $
+  withCString ipath $ \ipathPtr ->
+    withCString opath $ \opathPtr ->
+      c_lame_encoding_helper
+        l              -- lame settings structure
+        (fromIntegral waveDataOffset) -- offset of data chunk
+        waveDataSize   -- size of data chunk
+        (case waveSampleFormat of
+           SampleFormatPcmInt _       -> 0
+           SampleFormatIeeeFloat32Bit -> 1
+           SampleFormatIeeeFloat64Bit -> 2)
+        (waveBitsPerSample wave)  -- bits per sample
+        ipathPtr       -- path to input file
+        opathPtr       -- path to output file
+
+foreign import ccall unsafe "lame_encoding_helper"
+  c_lame_encoding_helper
+    :: Lame            -- lame settings structure
+    -> Word64          -- offset of data chunk
+    -> Word64          -- size of data chunk
+    -> Word16          -- code of sample format
+    -> Word16          -- bits per sample
+    -> CString         -- path to input file
+    -> CString         -- path to output file
+    -> IO Int
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | Coerce to 'Ptr' and check if it's a null pointer, return 'Nothing' if
+-- it is, otherwise return the given pointer unchanged. Needless to say that
+-- this thing is unsafe.
+
+maybePtr :: a -> Maybe a
+maybePtr a
+  | unsafeCoerce a == nullPtr = Nothing
+  | otherwise                 = Just a
+
+-- | Treat the 'Int' value as a error code. Unless it's 0, throw
+-- corresponding 'LameException', otherwise just return the unit.
+
+handleErrors :: IO Int -> IO ()
+handleErrors m = do
+  n <- m
+  case n of
+    0   -> return ()
+    -10 -> throwM LameNoMemory
+    -11 -> throwM LameBadBitrate
+    -12 -> throwM LameBadSampleFreq
+    -13 -> throwM LameInternalError
+    _   -> throwM LameGenericError
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,28 @@
+Copyright © 2017 Mark Karpov
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+* Neither the name Mark Karpov nor the names of contributors may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# LAME for Haskell
+
+[![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause)
+[![Hackage](https://img.shields.io/hackage/v/lame.svg?style=flat)](https://hackage.haskell.org/package/lame)
+[![Stackage Nightly](http://stackage.org/package/lame/badge/nightly)](http://stackage.org/nightly/package/lame)
+[![Stackage LTS](http://stackage.org/package/lame/badge/lts)](http://stackage.org/lts/package/lame)
+[![Build Status](https://travis-ci.org/mrkkrp/lame.svg?branch=master)](https://travis-ci.org/mrkkrp/lame)
+[![Coverage Status](https://coveralls.io/repos/mrkkrp/lame/badge.svg?branch=master&service=github)](https://coveralls.io/github/mrkkrp/lame?branch=master)
+
+This is a fairly complete high-level Haskell binding
+to [LAME](http://lame.sourceforge.net/).
+
+## Provided functionality
+
+* Fast MP3 encoder working in various modes (you specify compression ratio,
+  desired bit-rate, or parameters for VBR), with quite a few interesting
+  options to tweak.
+* Setting of all common tags, including pictures.
+
+## Limitations
+
+* No decoding for now. It's actually done via a separate library in LAME.
+* Relatively limited (compared
+  to [ID3 specs](http://id3.org/id3v2.3.0#Text_information_frames)) number
+  of tag fields available for setting.
+* Some sample widths are not supported: less than or equal to 8 bit and
+  greater than 16 bits (for integer samples, floats work OK).
+* Some psycho-acoustic and noise-shaping settings are not available for
+  tweaking.
+
+Open an issue if something of this is a deal breaker for you, some of these
+limitations are easily lifted.
+
+## Contribution
+
+Please kindly direct all issues, bugs, and questions to [the GitHub issue
+tracker for this project](https://github.com/mrkkrp/lame/issues).
+
+## License
+
+Copyright © 2017 Mark Karpov
+
+Distributed under BSD 3 clause license.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,6 @@
+module Main (main) where
+
+import Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/audio-samples/16bit-int.wav b/audio-samples/16bit-int.wav
new file mode 100644
Binary files /dev/null and b/audio-samples/16bit-int.wav differ
diff --git a/audio-samples/32bit-float.wav b/audio-samples/32bit-float.wav
new file mode 100644
Binary files /dev/null and b/audio-samples/32bit-float.wav differ
diff --git a/audio-samples/64bit-float.wav b/audio-samples/64bit-float.wav
new file mode 100644
Binary files /dev/null and b/audio-samples/64bit-float.wav differ
diff --git a/cbits/helpers.c b/cbits/helpers.c
new file mode 100644
--- /dev/null
+++ b/cbits/helpers.c
@@ -0,0 +1,190 @@
+/*
+ * This file is part of ‘lame’ package.
+ *
+ * Copyright © 2017 Mark Karpov
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name Mark Karpov nor the names of contributors may be used to
+ *   endorse or promote products derived from this software without specific
+ *   prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ * NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "helpers.h"
+
+int id3tag_set_textinfo_utf16_
+  ( lame_global_flags *gfp
+  , const char *id
+  , const uint16_t *raw_text
+  , int len)
+{
+  uint16_t *text = alloca(sizeof(uint16_t) * (len + 2));
+  int i;
+  *(text + 0) = 0xfeff; /* BOM */
+  for (i = 0; i < len; i++)
+    {
+      *(text + i + 1) = *(raw_text + i);
+    }
+  *(text + len + 1) = 0;
+  return id3tag_set_textinfo_utf16(gfp, id, text);
+}
+
+int id3tag_set_comment_utf16_
+  ( lame_global_flags *gfp
+  , const uint16_t *raw_text
+  , int len )
+{
+  uint16_t *text = alloca(sizeof(uint16_t) * (len + 2));
+  int i;
+  *(text + 0) = 0xfeff; /* BOM */
+  for (i = 0; i < len; i++)
+    {
+      *(text + i + 1) = *(raw_text + i);
+    }
+  *(text + len + 1) = 0;
+  return id3tag_set_comment_utf16(gfp, 0, 0, text);
+}
+
+static unsigned round_to_bytes(unsigned bits)
+{
+  return (bits + (bits % 8)) / 8;
+}
+
+int lame_encoding_helper
+  ( lame_global_flags *gfp
+  , uint64_t data_offset
+  , uint64_t data_size
+  , uint16_t sample_format
+  , uint16_t bits_per_sample
+  , const char * ifile_name
+  , const char * ofile_name )
+{
+  unsigned channels = lame_get_num_channels(gfp);
+  unsigned msw = round_to_bytes(bits_per_sample);
+  unsigned block_align = channels * msw;
+  uint64_t samples_to_process = data_size / block_align;
+  uint64_t read_size = 0xffff; /* we need a bit more here because of pictures in metadata */
+  void *buffer = malloc(read_size * block_align);
+  size_t mp3buffer_size = 1.3 * read_size + 7200;
+  void *mp3buffer = malloc(mp3buffer_size);
+  int imp3, status;
+  FILE *ifile = fopen(ifile_name, "r");
+  FILE *ofile = fopen(ofile_name, "w");
+  uint64_t samples;
+
+  if ((data_size % block_align) ||
+      bits_per_sample <= 8      ||
+      (sample_format == 0 && bits_per_sample > 16))
+    {
+      free(buffer);
+      fclose(ifile);
+      fclose(ofile);
+      return -1;
+    }
+
+  /* move position indicator to beginning of audio data */
+  fseek(ifile, data_offset, SEEK_SET);
+
+  while (samples_to_process)
+    {
+      /* The reading happens by blocks. Every block has read_size samples in
+       * it (multi-channel samples, that is). Since we will be using the
+       * value returned by fread as indicator of whether something went
+       * wrong or not, we need to know beforehand how many samples we need
+       * to read. */
+      samples = read_size <= samples_to_process ? read_size : samples_to_process;
+
+      /* Read the samples into the “raw” buffer, fail by returning false if
+       * number of read samples differs from the expected. */
+      if (fread(buffer, block_align, samples, ifile) != samples)
+        {
+          free(buffer);
+          fclose(ifile);
+          fclose(ofile);
+          return -1;
+        }
+
+      switch (sample_format)
+        {
+        case LAME_HASKELL_INT: /* samples are integers */
+          imp3 = lame_encode_buffer_interleaved
+            (gfp, buffer, samples, mp3buffer, mp3buffer_size);
+          break;
+        case LAME_HASKELL_FLOAT: /* samples are 32 bit floats */
+          imp3 = lame_encode_buffer_interleaved_ieee_float
+            (gfp, buffer, samples, mp3buffer, mp3buffer_size);
+          break;
+        case LAME_HASKELL_DOUBLE: /* samples are 64 bit floats (doubles) */
+          imp3 = lame_encode_buffer_interleaved_ieee_double
+            (gfp, buffer, samples, mp3buffer, mp3buffer_size);
+          break;
+        default: /* got something else? you're screwed */
+          free(buffer);
+          fclose(ifile);
+          fclose(ofile);
+          return -1;
+        }
+
+      if (imp3 < 0)
+        {
+          free(buffer);
+          fclose(ifile);
+          fclose(ofile);
+          return -1;
+        }
+
+      status = (int) fwrite(mp3buffer, 1, imp3, ofile);
+
+      if (status != imp3)
+        {
+          free(buffer);
+          fclose(ifile);
+          fclose(ofile);
+          return -1;
+        }
+
+      samples_to_process -= samples;
+    }
+
+  if (lame_get_nogap_total(gfp) > 0)
+    imp3 = lame_encode_flush_nogap(gfp, mp3buffer, mp3buffer_size);
+  else
+    imp3 = lame_encode_flush(gfp, mp3buffer, mp3buffer_size);
+
+      if (imp3 < 0)
+        {
+          free(buffer);
+          fclose(ifile);
+          fclose(ofile);
+          return -1;
+        }
+
+  status = (int) fwrite(mp3buffer, 1, imp3, ofile);
+
+  free(buffer);
+  fclose(ifile);
+  fclose(ofile);
+
+  return (status == imp3) ? 0 : -1;
+}
diff --git a/cbits/helpers.h b/cbits/helpers.h
new file mode 100644
--- /dev/null
+++ b/cbits/helpers.h
@@ -0,0 +1,49 @@
+/*
+ * This file is part of ‘lame’ package.
+ *
+ * Copyright © 2017 Mark Karpov
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name Mark Karpov nor the names of contributors may be used to
+ *   endorse or promote products derived from this software without specific
+ *   prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS
+ * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
+ * NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LAME_HASKELL_HELPERS_H
+#define LAME_HASKELL_HELPERS_H
+
+#define LAME_HASKELL_INT    0
+#define LAME_HASKELL_FLOAT  1
+#define LAME_HASKELL_DOUBLE 2
+
+#include <lame/lame.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+int id3tag_set_textinfo_utf16_(lame_global_flags *, const char *, const uint16_t *, int);
+int id3tag_set_comment_utf16_(lame_global_flags *, const uint16_t *, int);
+int lame_encoding_helper(lame_global_flags *, uint64_t, uint64_t, uint16_t, uint16_t, const char *, const char *);
+
+#endif /* LAME_HASKELL_HELPERS_H */
diff --git a/lame.cabal b/lame.cabal
new file mode 100644
--- /dev/null
+++ b/lame.cabal
@@ -0,0 +1,99 @@
+--
+-- Cabal configuration for ‘lame’ package.
+--
+-- Copyright © 2017 Mark Karpov <markkarpov@openmailbox.org>
+--
+-- Redistribution and use in source and binary forms, with or without
+-- modification, are permitted provided that the following conditions are
+-- met:
+--
+-- * Redistributions of source code must retain the above copyright notice,
+--   this list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright
+--   notice, this list of conditions and the following disclaimer in the
+--   documentation and/or other materials provided with the distribution.
+--
+-- * Neither the name Mark Karpov nor the names of contributors may be used
+--   to endorse or promote products derived from this software without
+--   specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY
+-- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+-- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+-- STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+-- ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+-- POSSIBILITY OF SUCH DAMAGE.
+
+name:                 lame
+version:              0.1.0
+cabal-version:        >= 1.10
+license:              BSD3
+license-file:         LICENSE.md
+author:               Mark Karpov <markkarpov@openmailbox.org>
+maintainer:           Mark Karpov <markkarpov@openmailbox.org>
+homepage:             https://github.com/mrkkrp/lame
+bug-reports:          https://github.com/mrkkrp/lame/issues
+category:             Codec, Audio
+synopsis:             Fairly complete high-level binding to LAME encoder
+build-type:           Simple
+description:          Fairly complete high-level binding to LAME encoder.
+extra-source-files:   cbits/*.h
+extra-doc-files:      CHANGELOG.md
+                    , README.md
+data-files:           audio-samples/*.wav
+
+source-repository head
+  type:               git
+  location:           https://github.com/mrkkrp/lame.git
+
+flag dev
+  description:        Turn on development settings.
+  manual:             True
+  default:            False
+
+library
+  build-depends:      base             >= 4.8   && < 5.0
+                    , bytestring       >= 0.2   && < 0.11
+                    , data-default-class
+                    , directory        >= 1.2.2 && < 1.4
+                    , exceptions       >= 0.6   && < 0.9
+                    , filepath         >= 1.2   && < 1.5
+                    , text             >= 0.2   && < 1.3
+                    , transformers     >= 0.4   && < 0.6
+                    , wave             >= 0.1.2 && < 0.2
+  extra-libraries:    mp3lame
+  exposed-modules:    Codec.Audio.LAME
+  other-modules:      Codec.Audio.LAME.Internal
+  c-sources:          cbits/helpers.c
+  include-dirs:       cbits
+  if flag(dev)
+    ghc-options:      -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
+
+test-suite tests
+  main-is:            Spec.hs
+  hs-source-dirs:     tests
+  type:               exitcode-stdio-1.0
+  build-depends:      base             >= 4.8   && < 5.0
+                    , data-default-class
+                    , directory        >= 1.2.2 && < 1.4
+                    , filepath         >= 1.2   && < 1.5
+                    , hspec            >= 2.0   && < 3.0
+                    , htaglib          >= 1.0   && < 1.2
+                    , lame             >= 0.1.0
+                    , temporary        >= 1.1   && < 1.3
+                    , text             >= 0.2   && < 1.3
+  other-modules:      Codec.Audio.LAMESpec
+  if flag(dev)
+    ghc-options:      -Wall -Werror
+  else
+    ghc-options:      -O2 -Wall
+  default-language:   Haskell2010
diff --git a/tests/Codec/Audio/LAMESpec.hs b/tests/Codec/Audio/LAMESpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Codec/Audio/LAMESpec.hs
@@ -0,0 +1,147 @@
+--
+-- Tests for the ‘lame’ package.
+--
+-- Copyright © 2017 Mark Karpov <markkarpov@openmailbox.org>
+--
+-- Redistribution and use in source and binary forms, with or without
+-- modification, are permitted provided that the following conditions are
+-- met:
+--
+-- * Redistributions of source code must retain the above copyright notice,
+--   this list of conditions and the following disclaimer.
+--
+-- * Redistributions in binary form must reproduce the above copyright
+--   notice, this list of conditions and the following disclaimer in the
+--   documentation and/or other materials provided with the distribution.
+--
+-- * Neither the name Mark Karpov nor the names of contributors may be used
+--   to endorse or promote products derived from this software without
+--   specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY
+-- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+-- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+-- STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+-- ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+-- POSSIBILITY OF SUCH DAMAGE.
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+module Codec.Audio.LAMESpec
+  ( spec )
+where
+
+import Codec.Audio.LAME
+import Control.Monad
+import Data.Default.Class
+import Data.Text (Text)
+import Data.Word (Word8)
+import Sound.HTagLib
+import System.Directory
+import System.FilePath
+import System.IO.Temp (withSystemTempDirectory)
+import Test.Hspec
+import qualified Data.Text as T
+
+data Info = Info
+  { infoTitle       :: Title
+  , infoArtist      :: Artist
+  , infoAlbum       :: Album
+  , infoComment     :: Comment
+  , infoGenre       :: Genre
+  , infoYear        :: Maybe Year
+  , infoTrackNumber :: Maybe TrackNumber
+  , infoDuration    :: Duration
+  , infoBitRate     :: BitRate
+  , infoSampleRate  :: SampleRate
+  , infoChannels    :: Channels
+  } deriving (Eq, Show)
+
+spec :: Spec
+spec =
+  describe "encodeMp3" $
+    withVariousWaves $
+      it "produces correct MP3 file" $ \(ipath, opath) -> do
+        encodeMp3 def
+          { encoderTagTitle   = pure tagTitle
+          , encoderTagArtist  = pure tagArtist
+          , encoderTagAlbum   = pure tagAlbum
+          , encoderTagYear    = pure tagYear
+          , encoderTagComment = pure tagComment
+          , encoderTagTrack   = pure tagTrack
+          , encoderTagGenre   = pure tagGenre }
+          ipath
+          opath
+        Info {..} <- getTags' opath MPEG $ Info
+          <$> titleGetter
+          <*> artistGetter
+          <*> albumGetter
+          <*> commentGetter
+          <*> genreGetter
+          <*> yearGetter
+          <*> trackNumberGetter
+          <*> durationGetter
+          <*> bitRateGetter
+          <*> sampleRateGetter
+          <*> channelsGetter
+        unTitle infoTitle `shouldBe` tagTitle
+        unArtist infoArtist `shouldBe` tagArtist
+        unAlbum infoAlbum `shouldBe` tagAlbum
+        fmap (T.pack . show . unYear) infoYear  `shouldBe` pure tagYear
+        unComment infoComment `shouldBe` tagComment
+        fmap unTrackNumber infoTrackNumber `shouldBe`
+          (pure . fromIntegral . fst) tagTrack
+        unGenre infoGenre `shouldBe` tagGenre
+        unDuration infoDuration `shouldBe` 1
+        unBitRate infoBitRate `shouldBe` 128
+        unSampleRate infoSampleRate `shouldBe` 44100
+        unChannels infoChannels `shouldBe` 2
+
+----------------------------------------------------------------------------
+-- Helpers
+
+-- | Run given test with various WAVE files.
+
+withVariousWaves :: SpecWith (FilePath, FilePath) -> SpecWith ()
+withVariousWaves m =
+  forM_ waveFiles $ \(path, desc) ->
+    context ("when given " ++ desc) (around (withSandbox path) m)
+
+-- | Make a temporary copy of given file and provide the path to the file in
+-- a sandbox directory. Automatically remove the files when the test
+-- finishes.
+
+withSandbox :: FilePath -> ActionWith (FilePath, FilePath) -> IO ()
+withSandbox path action =
+  withSystemTempDirectory "lame-test" $ \dir -> do
+    let ipath = dir </> "файл" -- testing Unicode
+        opath = dir </> "результат"
+    copyFile path ipath
+    action (ipath, opath)
+
+waveFiles :: [(FilePath, String)]
+waveFiles =
+  [ ( "audio-samples/16bit-int.wav"
+    , "2 channels 44100 Hz 16 bit PCM" )
+  , ( "audio-samples/32bit-float.wav"
+    , "2 channels 44100 Hz 32 bit float PCM" )
+  , ( "audio-samples/64bit-float.wav"
+    , "2 channels 44100 Hz 64 bit float PCM" )
+  ]
+
+tagTitle, tagArtist, tagAlbum, tagYear, tagComment, tagGenre :: Text
+tagTitle   = "Название"
+tagArtist  = "Исполнитель"
+tagAlbum   = "Альбом"
+tagYear    = "2017"
+tagComment = "Комментарий тут…"
+tagGenre   = "Жанр"
+
+tagTrack :: (Word8, Maybe Word8)
+tagTrack = (1, Just 10)
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
