packages feed

lame (empty) → 0.1.0

raw patch · 14 files changed

+1409/−0 lines, 14 filesdep +basedep +bytestringdep +data-default-classsetup-changedbinary-added

Dependencies added: base, bytestring, data-default-class, directory, exceptions, filepath, hspec, htaglib, lame, temporary, text, transformers, wave

Files

+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## LAME 0.1.0++* Initial release.
+ Codec/Audio/LAME.hs view
@@ -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
+ Codec/Audio/LAME/Internal.hs view
@@ -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
+ LICENSE.md view
@@ -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.
+ README.md view
@@ -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.
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Distribution.Simple++main :: IO ()+main = defaultMain
+ audio-samples/16bit-int.wav view

binary file changed (absent → 176444 bytes)

+ audio-samples/32bit-float.wav view

binary file changed (absent → 352888 bytes)

+ audio-samples/64bit-float.wav view

binary file changed (absent → 705688 bytes)

+ cbits/helpers.c view
@@ -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;+}
+ cbits/helpers.h view
@@ -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 */
+ lame.cabal view
@@ -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
+ tests/Codec/Audio/LAMESpec.hs view
@@ -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)
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}