packages feed

lame-0.2.0: Codec/Audio/LAME/Internal.hs

-- |
-- Module      :  Codec.Audio.LAME.Internal
-- Copyright   :  © 2017–2019 Mark Karpov
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- Low-level non-public helpers.

{-# LANGUAGE FlexibleContexts         #-}
{-# 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.Coerce
import Data.Text (Text)
import Data.Void
import Foreign hiding (void)
import Foreign.C.String
import Foreign.C.Types (CSize (..))
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 the 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 :: Coercible a (Ptr p) => a -> Maybe a
maybePtr a
  | coerce 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