packages feed

opus-0.3.0.0: src/Codec/Audio/Opus/Encoder.hs

{-# LANGUAGE FlexibleContexts #-}
-- | This module contains the high-level API for encoding Opus audio.
module Codec.Audio.Opus.Encoder
  ( -- * Encoder
    Encoder, OpusException(..)
    -- ** create
  , withOpusEncoder, opusEncoderCreate, opusEncoderDestroy
    -- ** run
  , opusEncode, opusEncodeLazy
    -- * re-exports
  , module Codec.Audio.Opus.Types
  ) where

import           Codec.Audio.Opus.Internal.Opus
import           Codec.Audio.Opus.Types
import           Lens.Micro
import           Control.Monad.Catch
import           Control.Monad.IO.Class
import           Control.Monad.Trans.Resource
import           Data.ByteString                (ByteString)
import qualified Data.ByteString                as BS
import qualified Data.ByteString.Lazy           as BL
import           Foreign

-- | Encoder. Internally, it holds a pointer to the libopus encoder state and
-- a pointer to the (potential) last Opus error code.
newtype Encoder = Encoder (ForeignPtr EncoderT, ForeignPtr ErrorCode)
  deriving (Eq, Ord, Show)

-- | Allocates and initializes an encoder.
opusEncoderCreate :: (HasEncoderConfig cfg, MonadIO m) => cfg -> m Encoder
opusEncoderCreate cfg = liftIO $ do
  let cs = if isStereo then 2 else 1
      sr = cfg ^. (encoderConfig . samplingRate)
      isStereo = cfg ^. encoderIsStereo
      cm = cfg ^. (encoderConfig . codingMode)
  err <- mallocForeignPtr
  e <- withForeignPtr err (c_opus_encoder_create sr cs cm)
  e' <- newForeignPtr cp_opus_encoder_destroy e
  let enc = Encoder (e', err)
  opusLastError enc >>= maybe (pure enc) throwM

-- | Encode an Opus frame.
opusEncode
  :: (HasStreamConfig cfg, MonadIO m)
  => Encoder
  -- ^ 'Encoder' state
  -> cfg
  -- ^ The stream configuration that specifies the frame size, the output size,
  -- and the encoder configuration (sampling rate, channels, coding mode).
  -> ByteString
  -- ^ Input signal (interleaved if 2 channels)
  -> m ByteString
opusEncode e cfg i =
  let fs = cfg ^. streamFrameSize
      n = cfg ^. streamOutSize
  in liftIO $
  BS.useAsCString i $ \i' ->
    allocaArray n $ \os ->
      runEncoderAction e $ \e' -> do
        r <- c_opus_encode e' (castPtr i') (fromInteger . toInteger $ fs) os
          (fromInteger . toInteger $ n)
        let l = fromInteger . toInteger $ r
            ol = (os, l)
        if l < 0 then throwM OpusInvalidPacket else
          BS.packCStringLen ol

-- | Encode an Opus frame. Returns a lazy 'BL.ByteString'.
opusEncodeLazy :: (HasStreamConfig cfg, MonadIO m)
  => Encoder
  -- ^ 'Encoder' state
  -> cfg
  -- ^ The stream configuration that specifies the frame size, the output size,
  -- and the encoder configuration (sampling rate, channels, coding mode).
  -> ByteString
  -- ^ Input signal (interleaved if 2 channels)
  -> m BL.ByteString
opusEncodeLazy e cfg = fmap BL.fromStrict . opusEncode e cfg


-- | For use with 'ResourceT' or any other monad that implements 'MonadResource'.
-- Safely allocate an 'Encoder' that will be freed upon exiting the monad, an
-- exception, or an explicit call to 'Control.Monad.Trans.Resource.release'.
withOpusEncoder :: (HasEncoderConfig cfg) => MonadResource m
  => cfg
  -> (Encoder -> IO ())
  -> m Encoder
withOpusEncoder cfg a =
  snd <$> allocate (opusEncoderCreate cfg) a


-- | Frees an 'Encoder'.
opusEncoderDestroy :: MonadIO m => Encoder -> m ()
opusEncoderDestroy (Encoder (e, err)) = liftIO $
  finalizeForeignPtr e >> finalizeForeignPtr err


-- | Get the last error from the encoder.
opusLastError :: MonadIO m => Encoder -> m (Maybe OpusException)
opusLastError (Encoder (_, fp)) =
  liftIO $ (^? _ErrorCodeException) <$> withForeignPtr fp peek

-- | An 'EncoderAction' is an IO action that uses a 'EncoderT' for its operation.
type EncoderAction  a = Ptr EncoderT -> IO a

-- | Run an 'EncoderAction' using an 'Encoder', returning either 'OpusException'
-- for errors or the result of the action.
withEncoder' :: MonadIO m =>
  Encoder -> EncoderAction a -> m (Either OpusException a)
withEncoder' e@(Encoder (fp_a, _)) m = liftIO $
  withForeignPtr fp_a $ \a -> do
    r <- m a
    le <- opusLastError e
    pure $ maybe (Right r) Left le

-- | Run an 'EncoderAction'. Might throw an 'OpusException' if the action fails.
runEncoderAction :: (MonadIO m, MonadThrow m) =>
  Encoder -> EncoderAction a -> m a
runEncoderAction e m = withEncoder' e m >>= either throwM pure