packages feed

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

{-# LANGUAGE RecordWildCards #-}

module Dahdit.Audio.Convert
  ( convertMod
  , convertModGeneric
  , loadAiff
  , loadWav
  , Neutral (..)
  , aiffToNeutral
  , wavToNeutral
  , loadNeutral
  , neutralToWav
  , neutralMono
  , neutralCrossFade
  , neutralCropLoop
  , neutralToSampleWav
  , neutralToMonoWav
  )
where

import Control.Monad (unless, (>=>))
import Dahdit (Int16LE, Int24LE, Int32LE, Seq (..), StaticByteSized, decodeFile)
import Dahdit.Audio.Aiff (Aiff, aiffGatherMarkers, aiffToPcmContainer)
import Dahdit.Audio.Common
  ( ConvertErr (..)
  , LoopMarkNames
  , LoopMarkPoints
  , LoopMarks (..)
  , SimpleMarker (..)
  , adjustMarker
  , findLoopMarks
  , recallLoopMarkNames
  , rethrow
  )
import Dahdit.Audio.Dsp
  ( Mod
  , PcmContainer (..)
  , PcmMeta (..)
  , SampleCount
  , applyMod
  , applyModGeneric
  , changeBitDepth
  , crop
  , ensureMonoFromLeft
  , linearCrossFade
  )
import Dahdit.Audio.Wav
  ( Wav
  , WavChunk (..)
  , wavAddChunks
  , wavFromPcmContainer
  , wavGatherMarkers
  , wavToPcmContainer
  , wavUseLoopPoints
  , wavUseMarkers
  )
import Data.Maybe (isJust)
import Data.Primitive (Prim)
import Data.Sequence qualified as Seq
import System.FilePath (splitExtension)

convertMod
  :: (StaticByteSized a, Prim b, StaticByteSized b) => Mod a b -> PcmContainer -> Either ConvertErr PcmContainer
convertMod modx con = either (Left . ConvertErrDsp) Right (applyMod modx con)

convertModGeneric
  :: (forall a. (Prim a, StaticByteSized a, Integral a) => Mod a a) -> PcmContainer -> Either ConvertErr PcmContainer
convertModGeneric modx con = either (Left . ConvertErrDsp) Right (applyModGeneric modx con)

loadAiff :: FilePath -> IO Aiff
loadAiff = decodeFile >=> rethrow . fst

loadWav :: FilePath -> IO Wav
loadWav = decodeFile >=> rethrow . fst

data Neutral = Neutral
  { neCon :: !PcmContainer
  , neMarks :: !(Seq SimpleMarker)
  , neLoopMarks :: !(Maybe LoopMarkPoints)
  }
  deriving stock (Eq, Show)

guardSr :: Int -> Neutral -> IO ()
guardSr expectedSr ne = do
  let actualSr = pmSampleRate (pcMeta (neCon ne))
  unless (expectedSr == actualSr) (fail ("Expected SR: " ++ show expectedSr ++ " but got " ++ show actualSr))

-- NOTE: Taking sr as a param here so we don't have to interpret extended fp
aiffToNeutral :: Int -> Aiff -> Maybe LoopMarkNames -> Either ConvertErr Neutral
aiffToNeutral sr aiff mayNames = do
  neCon <- aiffToPcmContainer sr aiff
  let !neMarks = aiffGatherMarkers aiff
  neLoopMarks <- maybe (pure Nothing) (fmap Just . (`findLoopMarks` neMarks)) mayNames
  pure $! Neutral {..}

wavToNeutral :: Wav -> Maybe LoopMarkNames -> Either ConvertErr Neutral
wavToNeutral wav mayNames = do
  neCon <- wavToPcmContainer wav
  let !neMarks = wavGatherMarkers wav
  neLoopMarks <- maybe (pure Nothing) (fmap Just . (`findLoopMarks` neMarks)) mayNames
  pure $! Neutral {..}

loadNeutral :: Int -> Maybe LoopMarkNames -> FilePath -> IO Neutral
loadNeutral sr mayNames fp = do
  let (_, ext) = splitExtension fp
  ne <-
    if
      | ext == ".wav" -> do
          wav <- loadWav fp
          rethrow (wavToNeutral wav mayNames)
      | ext == ".aif" || ext == ".aifc" || ext == ".aiff" -> do
          aiff <- loadAiff fp
          rethrow (aiffToNeutral sr aiff mayNames)
      | otherwise -> fail ("Could not load with unknown extension: " ++ fp)
  guardSr sr ne
  pure ne

neutralToWav :: Int -> Neutral -> Wav
neutralToWav note (Neutral {..}) =
  let !sr = pmSampleRate (pcMeta neCon)
      !maySampleChunk = fmap (WavChunkSample . wavUseLoopPoints sr note) neLoopMarks
      !markChunks = if Seq.null neMarks then [] else let (!wcc, !wac) = wavUseMarkers neMarks in [WavChunkCue wcc, WavChunkAdtl wac]
      !chunks = Seq.fromList (markChunks ++ maybe [] pure maySampleChunk)
      !wav = wavFromPcmContainer neCon
  in  wavAddChunks chunks wav

neutralMono :: Neutral -> Either ConvertErr Neutral
neutralMono ne@(Neutral {..}) = do
  con' <- convertModGeneric ensureMonoFromLeft neCon
  pure $! ne {neCon = con'}

neutralReduceDepth :: Neutral -> Either ConvertErr Neutral
neutralReduceDepth ne@(Neutral {..}) = do
  case pmBitsPerSample (pcMeta neCon) of
    16 -> pure ne
    24 -> do
      con' <- convertMod (changeBitDepth @Int24LE @Int16LE 24 16) neCon
      pure $! ne {neCon = con'}
    32 -> do
      con' <- convertMod (changeBitDepth @Int32LE @Int16LE 32 16) neCon
      pure $! ne {neCon = con'}
    y -> Left (ConvertErrBadBps y)

neutralCrossFade :: SampleCount -> Neutral -> Either ConvertErr Neutral
neutralCrossFade width ne@(Neutral {..}) = do
  LoopMarks _ (_, !loopStart) (_, !loopEnd) _ <- maybe (Left ConvertErrNoLoopMarks) Right neLoopMarks
  let !loopStartPos = smPosition loopStart
      !loopEndPos = smPosition loopEnd
  con' <- convertModGeneric (linearCrossFade width (fromIntegral loopStartPos) (fromIntegral loopEndPos)) neCon
  pure $! ne {neCon = con'}

neutralCropLoop :: Neutral -> Either ConvertErr Neutral
neutralCropLoop (Neutral {..}) = do
  initLoopMarks <- maybe (Left ConvertErrNoLoopMarks) Right neLoopMarks
  let !names = recallLoopMarkNames initLoopMarks
      LoopMarks (_, !start) _ (_, !loopEnd) (_, !end) = initLoopMarks
      !startPos = smPosition start
      !loopEndPos = smPosition loopEnd
      !endPos = smPosition end
      !filteredMarks = Seq.filter (\sm -> let p = smPosition sm in p >= startPos && p <= loopEndPos) neMarks
      !withEndMarks = if endPos <= loopEndPos then filteredMarks else filteredMarks :|> end {smPosition = loopEndPos}
      !finalMarks = fmap (adjustMarker (-startPos)) withEndMarks
  !finalLoopMarks <- findLoopMarks names finalMarks
  con' <- convertModGeneric (crop (fromIntegral startPos) (fromIntegral loopEndPos)) neCon
  pure $! Neutral {neCon = con', neLoopMarks = Just finalLoopMarks, neMarks = finalMarks}

neutralIfHasMarks :: (Neutral -> Either e Neutral) -> Neutral -> Either e Neutral
neutralIfHasMarks f ne = do
  if isJust (neLoopMarks ne)
    then f ne
    else Right ne

-- | Example converting to instrument sample representing a particular note (with loop marks)
neutralToSampleWav :: SampleCount -> Int -> Neutral -> Either ConvertErr Wav
neutralToSampleWav width note ne =
  fmap
    (neutralToWav note)
    ( neutralMono ne
        >>= neutralReduceDepth
        >>= neutralIfHasMarks (neutralCrossFade width)
        >>= neutralIfHasMarks neutralCropLoop
    )

-- | Example simply converting to mono 16-bit
neutralToMonoWav :: Neutral -> Either ConvertErr Wav
neutralToMonoWav ne = fmap (wavFromPcmContainer . neCon) (neutralMono ne >>= neutralReduceDepth)