lame-0.2.1: Codec/Audio/LAME.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
-- |
-- Module : Codec.Audio.LAME
-- Copyright : © 2017–present Mark Karpov
-- License : BSD 3 clause
--
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- The module provides an interface to the LAME encoder.
module Codec.Audio.LAME
( encodeMp3,
EncoderSettings (..),
defaultEncoderSettings,
Compression (..),
VbrMode (..),
MetadataPlacement (..),
FilterSetup (..),
I.LameException (..),
)
where
import qualified Codec.Audio.LAME.Internal as I
import Codec.Audio.Wave
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word
import System.Directory
import System.FilePath
import System.IO
-- | LAME encoder settings.
--
-- Currently some noise-shaping and psycho-acoustic parameters are omitted,
-- but can be added on request.
data EncoderSettings = EncoderSettings
{ -- | How to determine amount of compression to apply, see 'Compression'.
-- Default value: @'CompressionRatio' 11@ (corresponds to 128 kbps).
encoderCompression :: !Compression,
-- | Select the algorithm. This variable will affect the quality by
-- selecting expensive or cheap algorithms. 0 gives the best quality
-- (very slow). 9 is very fast, but gives the 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.
encoderQuality :: !(Maybe Word),
-- | 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.
encoderNoGap :: !(Maybe (Word32, Word32)),
-- | Whether to enable the error protection. Error protection means that
-- 2 bytes from each frame are used for CRC checksum. Default value:
-- 'False'.
encoderErrorProtection :: !Bool,
-- | Whether to enforce strict ISO compliance. Default value: 'False'.
encoderStrictISO :: !Bool,
-- | Sample rate of the output file. Default value: 'Nothing', which
-- means that the sample rate of the input file will be used.
encoderSampleRate :: !(Maybe Word32),
-- | Scale input stream by multiplying samples by this value. Default
-- value: 1 (no scaling).
encoderScale :: !Float,
-- | Use free format. Default value: 'False'.
encoderFreeFormat :: !Bool,
-- | Mark as copyright protected. Default value: 'False'.
encoderCopyright :: !Bool,
-- | Mark as original. Default value: 'True'.
encoderOriginal :: !Bool,
-- | Whether to write Xing VBR header frame. Default value: 'True'.
encoderWriteVbrTag :: !Bool,
-- | Which container to use to write metadata. Default value:
-- 'Id3v2Only'.
encoderMetadataPlacement :: !MetadataPlacement,
-- | Title tag to write. Default value: 'Nothing'.
encoderTagTitle :: !(Maybe Text),
-- | Artist tag to write. Default value: 'Nothing'.
encoderTagArtist :: !(Maybe Text),
-- | Album tag to write. Default value: 'Nothing'.
encoderTagAlbum :: !(Maybe Text),
-- | Year tag to write. Default value: 'Nothing'.
encoderTagYear :: !(Maybe Text),
-- | Comment tag to write. Default value: 'Nothing'.
encoderTagComment :: !(Maybe Text),
-- | Track number (the first component of the tuple) and (optionally)
-- the total number of tracks (the second component of the tuple).
-- Default value: 'Nothing'.
encoderTagTrack :: !(Maybe (Word8, Maybe Word8)),
-- | Genre tag to write. Default value: 'Nothing'.
encoderTagGenre :: !(Maybe Text),
-- | Album art (a picture) to write (JPEG\/PNG\/GIF). Default value:
-- 'Nothing'.
encoderAlbumArt :: !(Maybe ByteString),
-- | Settings for the low-pass filter. Default value: 'FilterAuto'.
encoderLowpassFilter :: !FilterSetup,
-- | Settings for the high-pass filter. Default value: 'FilterAuto'.
encoderHighpassFilter :: !FilterSetup
}
deriving (Show, Read, Eq, Ord)
-- | The default value of 'EncoderSettings'.
--
-- @since 0.2.0
defaultEncoderSettings :: EncoderSettings
defaultEncoderSettings =
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 supported options for compression. You can specify either fixed
-- bitrate, compression ratio, or use the VBR mode.
data Compression
= -- | Specify the compression by setting a fixed bitrate, e.g.
-- @'CompressionBitrate' 320@.
CompressionBitrate Word
| -- | Specify the compression ratio.
CompressionRatio Float
| -- | VBR. Here you can specify which mode to use and the second argument
-- is VBR quality from 0 (highest), to 9 (lowest). A good default is 4.
CompressionVBR VbrMode Word
deriving (Show, Read, Eq, Ord)
-- | Variable bitrate (VBR) modes and their parameters.
data VbrMode
= -- | VBR RH.
VbrRh
| -- | 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).
VbrAbr (Maybe Word) (Maybe Word) (Maybe Word) Bool
| -- | VBR MTRH
VbrMtrh
deriving (Show, Read, Eq, Ord)
-- | Which container to use to write tags\/metadata.
data MetadataPlacement
= -- | Only write ID3v1 metadata
Id3v1Only
| -- | Only write ID3v2 metadata
Id3v2Only
| -- | Write ID3v1 and ID3v2
Id3Both
deriving (Show, Read, Eq, Ord)
-- | Settings for a filter.
data FilterSetup
= -- | Filter settings are chosen automatically
FilterAuto
| -- | Filter is disabled
FilterDisabled
| -- | 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).
FilterManual Word (Maybe Word)
deriving (Show, Read, Eq, Ord)
-- | Encode a WAVE file or RF64 file.
--
-- If the input file is not a valid WAVE file, 'WaveException' will be
-- thrown. 'I.LameException' is thrown when the LAME encoder reports a
-- problem.
--
-- Not all sample formats and bit depths are currently supported. The
-- 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 something else, 'I.LameUnsupportedSampleFormat'
-- will be thrown.
encodeMp3 ::
MonadIO m =>
-- | Encoder settings
EncoderSettings ->
-- | WAVE file to encode
FilePath ->
-- | Where to save the resulting MP3 file
FilePath ->
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 a given 'I.Lame'.
setupFilter ::
-- | How to set cut-off frequncy
(I.Lame -> Int -> IO ()) ->
-- | How to set width of transition band
(I.Lame -> Int -> IO ()) ->
-- | Settings to edit
I.Lame ->
-- | Filter setup to apply
FilterSetup ->
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 an action ignoring IO exceptions it may throw.
ignoringIOErrors :: IO () -> IO ()
ignoringIOErrors ioe = ioe `catch` handler
where
handler :: IOError -> IO ()
handler = const (return ())
-- | Render the track number and optionally the total number of tracks as a
-- strict 'Text' value.
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