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 +3/−0
- Codec/Audio/LAME.hs +314/−0
- Codec/Audio/LAME/Internal.hs +529/−0
- LICENSE.md +28/−0
- README.md +43/−0
- Setup.hs +6/−0
- audio-samples/16bit-int.wav binary
- audio-samples/32bit-float.wav binary
- audio-samples/64bit-float.wav binary
- cbits/helpers.c +190/−0
- cbits/helpers.h +49/−0
- lame.cabal +99/−0
- tests/Codec/Audio/LAMESpec.hs +147/−0
- tests/Spec.hs +1/−0
+ 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++[](http://opensource.org/licenses/BSD-3-Clause)+[](https://hackage.haskell.org/package/lame)+[](http://stackage.org/nightly/package/lame)+[](http://stackage.org/lts/package/lame)+[](https://travis-ci.org/mrkkrp/lame)+[](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 #-}