packages feed

dahdit-audio-0.8.0: src/Dahdit/Audio/Dsp.hs

{-# LANGUAGE RecordWildCards #-}

module Dahdit.Audio.Dsp
  ( SampleCount (..)
  , DspErr (..)
  , monoFromLeft
  , monoFromRight
  , monoFromAvg
  , ensureMonoFromLeft
  , changeBitDepth
  , stereoFromMono
  , linearCrossFade
  , crop
  , ModMeta (..)
  , Mod (..)
  , modId
  , modAndThen
  , PcmMeta (..)
  , PcmContainer (..)
  , applyMod
  , applyModGeneric
  )
where

import Control.Exception (Exception)
import Control.Monad (unless)
import Dahdit
  ( ByteCount (..)
  , ElemCount (..)
  , Int16LE
  , Int24LE
  , Int32LE
  , Int8
  , StaticByteSized
  , proxyForF
  , staticByteSize
  )
import Data.Bits (Bits (..))
import Data.Coerce (coerce)
import Data.Primitive (Prim)
import Data.Primitive.ByteArray (ByteArray (..), sizeofByteArray)
import Data.Primitive.PrimArray (PrimArray (..), clonePrimArray, generatePrimArray, indexPrimArray, sizeofPrimArray)
import Data.Proxy (Proxy (..))
import Data.Word (Word64)

newtype SampleCount = SampleCount {unSampleCount :: Int}
  deriving stock (Show)
  deriving newtype (Eq, Ord, Num, Enum, Real, Integral)

data Sampled f where
  Sampled :: (Prim a, StaticByteSized a, Integral a) => !(f a) -> Sampled f

getSampled :: Int -> Maybe (Sampled Proxy)
getSampled = \case
  8 -> Just (Sampled (Proxy :: Proxy Int8))
  16 -> Just (Sampled (Proxy :: Proxy Int16LE))
  24 -> Just (Sampled (Proxy :: Proxy Int24LE))
  32 -> Just (Sampled (Proxy :: Proxy Int32LE))
  _ -> Nothing

data DspErr
  = DspErrOddSamples
  | DspErrBadElemSize
  | DspErrBadBitWidth
  | DspErrNotStereo
  | DspErrNotMono
  | DspErrBadFade
  | DspErrBadCrop
  deriving stock (Eq, Show)

instance Exception DspErr

newtype Sel a = Sel {runSel :: PrimArray a -> ElemCount -> a}

selMonoLeft, selMonoRight :: (Prim a) => Sel a
selMonoLeft = Sel $ \arr i -> indexPrimArray arr (unElemCount i * 2)
selMonoRight = Sel $ \arr i -> indexPrimArray arr (unElemCount i * 2 + 1)

selMonoAvg :: (Prim a, Integral a, Bits a) => Sel a
selMonoAvg = Sel $ \arr i ->
  let !ix = i * 2
      !lval = indexPrimArray arr (unElemCount ix)
      !rval = indexPrimArray arr (unElemCount ix + 1)
      !halfLval = div lval 2
      !halfRval = div rval 2
      !extra = lval .&. rval .&. 1
  in  halfLval + halfRval + extra

data ModMeta = ModMeta
  { mmNumChannels :: !Int
  , mmBitsPerSample :: !Int
  , mmSampleRate :: !Int
  }
  deriving stock (Eq, Show)

-- Array layout: samples are interspersed: [chan1 samp1, chan2 samp1, chan1 samp2, chan2 samp2, chan1 samp3, ...]
-- numChannels -> array -> (newNumChannels, newArray)
newtype Mod a b = Mod {runMod :: ModMeta -> PrimArray a -> Either DspErr (ModMeta, PrimArray b)}

modId :: Mod a a
modId = Mod (curry Right)

modAndThen :: Mod a b -> Mod b c -> Mod a c
modAndThen modAB modBC = Mod $ \nc src -> do
  (nc', src') <- runMod modAB nc src
  runMod modBC nc' src'

monoFromSel :: (Prim a) => Sel a -> Mod a a
monoFromSel sel = Mod $ \mm src -> do
  unless (mmNumChannels mm == 2) (Left DspErrNotStereo)
  let !srcLen = sizeofPrimArray src
  unless (even srcLen) (Left DspErrOddSamples)
  let !destLen = div srcLen 2
      !dest = generatePrimArray destLen (runSel sel src . ElemCount)
  Right (mm {mmNumChannels = 1}, dest)

monoFromLeft, monoFromRight :: (Prim a) => Mod a a
monoFromLeft = monoFromSel selMonoLeft
monoFromRight = monoFromSel selMonoRight

monoFromAvg :: (Prim a, Integral a, Bits a) => Mod a a
monoFromAvg = monoFromSel selMonoAvg

ensureMonoFromSel :: (Prim a) => Sel a -> Mod a a
ensureMonoFromSel sel = Mod $ \mm src -> do
  if mmNumChannels mm == 1
    then pure (mm, src)
    else runMod (monoFromSel sel) mm src

ensureMonoFromLeft :: (Prim a) => Mod a a
ensureMonoFromLeft = ensureMonoFromSel selMonoLeft

changeBitDepth :: (Prim a, Integral a, Prim b, Num b) => Int -> Int -> Mod a b
changeBitDepth srcBitDepth destBitDepth = Mod $ \mm0 src ->
  let bitShift = destBitDepth - srcBitDepth
  in  pure $
        let mm1 = mm0 {mmBitsPerSample = destBitDepth}
            dest = generatePrimArray (sizeofPrimArray src) $ \i ->
              let x = indexPrimArray src i
                  y = shift (fromIntegral @_ @Word64 x) bitShift
              in  fromIntegral y
        in  (mm1, dest)

stereoFromMono :: (Prim a) => Mod a a
stereoFromMono = Mod $ \mm src -> do
  unless (mmNumChannels mm == 1) (Left DspErrNotMono)
  let !srcLen = sizeofPrimArray src
      !destLen = srcLen * 2
      !dest = generatePrimArray destLen (\i -> indexPrimArray src (div i 2))
  Right (mm {mmNumChannels = 2}, dest)

guardFade :: SampleCount -> SampleCount -> SampleCount -> Either DspErr ()
guardFade width loopStart loopEnd = do
  if width <= 0
    || loopEnd <= loopStart
    || loopStart <= loopStart - width
    || loopStart + width <= loopStart
    || loopEnd <= loopEnd - width
    || loopEnd + width <= loopEnd
    then Left DspErrBadFade
    else Right ()

combine :: (Integral a) => Int -> Int -> a -> a -> a
combine intDistTot intDist1 one two =
  let dist1 = fromIntegral intDist1
      dist2 = fromIntegral (intDistTot - intDist1)
      distTot = fromIntegral intDistTot
  in  fromInteger (div (dist1 * fromIntegral one + dist2 * fromIntegral two) distTot)

-- Cross fade:  | --------- PreStart Start PostStart PreEnd End PostEnd ---- |
-- Guarded to ensure inequalities are strict
-- Width here is one-sided
-- Width is number of samples to fade over
linearCrossFade :: (Prim a, Integral a) => SampleCount -> SampleCount -> SampleCount -> Mod a a
linearCrossFade width loopStart loopEnd = Mod $ \mm src -> do
  guardFade width loopStart loopEnd
  let !nc = mmNumChannels mm
      !sampWidth = nc * unSampleCount width
      !sampTotDist = 2 * sampWidth
      !sampBetween = nc * unSampleCount (loopEnd - loopStart)
      !sampPreStart = nc * unSampleCount (loopStart - width)
      !sampStart = nc * unSampleCount loopStart
      !sampPostStart = nc * unSampleCount (loopStart + width)
      !sampPreEnd = nc * unSampleCount (loopEnd - width)
      !sampEnd = nc * unSampleCount loopStart
      !sampPostEnd = nc * unSampleCount (loopEnd + width)
      !sz = sizeofPrimArray src
      !sampLast = sz - sampBetween
      genElem i =
        let !v = indexPrimArray src i
        in  if
              | i >= sampPreStart && i <= sampPostStart ->
                  if i >= sampLast
                    then v
                    else
                      let !w = indexPrimArray src (i + sampBetween)
                          f = combine sampTotDist (coerce (sampPostStart - i))
                      in  if i < sampStart then f v w else f w v
              | i >= sampPreEnd && i <= sampPostEnd ->
                  if i < sampBetween
                    then v
                    else
                      let !w = indexPrimArray src (i - sampBetween)
                          f = combine sampTotDist (coerce (sampPostEnd - i))
                      in  if i < sampEnd then f v w else f w v
              | otherwise -> v
      !dest = generatePrimArray sz genElem
  Right (mm, dest)

guardCrop :: SampleCount -> SampleCount -> Either DspErr ()
guardCrop start end = do
  if end <= start
    then Left DspErrBadCrop
    else Right ()

crop :: (Prim a) => SampleCount -> SampleCount -> Mod a a
crop start end = Mod $ \mm src -> do
  guardCrop start end
  let !nc = mmNumChannels mm
      !sampStart = nc * unSampleCount start
      !sampEnd = nc * unSampleCount end
      !dest = clonePrimArray src sampStart (sampEnd - sampStart)
  Right (mm, dest)

data PcmMeta = PcmMeta
  { pmNumChannels :: !Int
  , pmNumSamples :: !SampleCount
  , pmBitsPerSample :: !Int
  , pmSampleRate :: !Int
  }
  deriving stock (Eq, Show)

data PcmContainer = PcmContainer
  { pcMeta :: !PcmMeta
  , pcData :: !ByteArray
  }
  deriving stock (Eq, Show)

pmToMm :: PcmMeta -> ModMeta
pmToMm (PcmMeta {..}) = ModMeta {mmNumChannels = pmNumChannels, mmBitsPerSample = pmBitsPerSample, mmSampleRate = pmSampleRate}

toLifted :: (StaticByteSized a) => Proxy a -> PcmContainer -> Either DspErr (ModMeta, PrimArray a)
toLifted prox (PcmContainer pm arr@(ByteArray ba)) = do
  let !elemSize = staticByteSize prox
  let !actualNs = div (sizeofByteArray arr) (unByteCount elemSize * pmNumChannels pm)
  unless
    (unByteCount elemSize * 8 == pmBitsPerSample pm && actualNs == unSampleCount (pmNumSamples pm))
    (Left DspErrBadElemSize)
  let !mm = pmToMm pm
  Right (mm, PrimArray ba)

fromLifted :: (Prim b, StaticByteSized b) => ModMeta -> PrimArray b -> Either DspErr PcmContainer
fromLifted mm arr@(PrimArray ba) = do
  let !elemSize = staticByteSize (proxyForF arr)
      !nc = mmNumChannels mm
      !ns = div (sizeofPrimArray arr) nc
      !bps = unByteCount elemSize * 8
      !sr = mmSampleRate mm
      !extraElems = rem (sizeofPrimArray arr) nc
  unless (extraElems == 0) (Left DspErrBadElemSize)
  let !pm = PcmMeta nc (SampleCount ns) bps sr
  Right $! PcmContainer pm (ByteArray ba)

proxyFromFirst :: m a b -> Proxy a
proxyFromFirst _ = Proxy

applyMod :: (StaticByteSized a, Prim b, StaticByteSized b) => Mod a b -> PcmContainer -> Either DspErr PcmContainer
applyMod modx con = do
  (mm, src) <- toLifted (proxyFromFirst modx) con
  (mm', dest) <- runMod modx mm src
  fromLifted mm' dest

proxMod
  :: (Prim a, StaticByteSized a, Integral a)
  => Proxy a
  -> (forall x. (Prim x, StaticByteSized x, Integral x) => Mod x x)
  -> Mod a a
proxMod _ allMod = allMod

applyModGeneric
  :: (forall a. (Prim a, StaticByteSized a, Integral a) => Mod a a) -> PcmContainer -> Either DspErr PcmContainer
applyModGeneric allMod con =
  case getSampled (pmBitsPerSample (pcMeta con)) of
    Nothing -> Left DspErrBadBitWidth
    Just (Sampled prox) -> applyMod (proxMod prox allMod) con