{-# LANGUAGE ForeignFunctionInterface #-}
module Sound.ALSA.PCM
    (Class.SampleFmt(..),
     SampleFreq,
     Time,
     Size,
     SoundFmt(..),
     SoundSource(..),
     SoundSink(..),
     SoundBufferTime(..),
     Pcm, File,
     withSoundSource,
     withSoundSourceRunning,
     withSoundSink,
     withSoundSinkRunning,
     soundFmtMIME,
     audioBytesPerSample,
     audioBytesPerFrame,
     soundSourceBytesPerFrame,
     soundSinkBytesPerFrame,
     copySound,
     alsaSoundSource,
     alsaSoundSink,
     alsaSoundSourceTime,
     alsaSoundSinkTime,
     alsaSoundSourceParams,
     alsaSoundSinkParams,
     fileSoundSource,
     fileSoundSink,
    ) where

import qualified Sound.ALSA.PCM.Node.ALSA as PCM
import qualified Sound.ALSA.PCM.Node.File as File

import Sound.ALSA.PCM.Parameters.Hardware (Time, SampleFreq, Size, )
import Sound.ALSA.PCM.Core.Handle (arraySize, )
import qualified Sound.ALSA.PCM.Parameters.Software as SwParam
import qualified Sound.ALSA.PCM.Parameters.Hardware as HwParam
import qualified Sound.ALSA.PCM.Core.Class as Class
import qualified Sound.ALSA.Exception as AlsaExc
import qualified Sound.ALSA.PCM.Debug as Debug

import qualified Sound.Frame as Frame

import Control.Exception (bracket, bracket_, )
import Control.Monad (when, liftM2, liftM4, )

import qualified Foreign.C.Types as C
import Foreign.Marshal.Array (allocaArray, )
import Foreign.Ptr (Ptr, )
import Foreign.Storable (Storable, )
import qualified System.IO as IO

--
-- * Generic sound API
--

data SoundFmt y = SoundFmt {
        sampleFreq :: SampleFreq
        }
  deriving (Show)

data SoundBufferTime = SoundBufferTime {
        bufferTime, periodTime :: Time
        }
  deriving (Show)


type Pcm = PCM.Handle PCM.Interleaved

type File = File.Handle


-- | Counts are in samples, not bytes. Multi-channel data is interleaved.
data SoundSource handle y =
   SoundSource {
      soundSourceOpen  :: IO (handle y),
      soundSourceClose :: handle y -> IO (),
      soundSourceStart :: handle y -> IO (),
      soundSourceStop  :: handle y -> IO (),
      soundSourceRead  :: handle y -> Ptr y -> Size -> IO Size
   }

data SoundSink handle y =
   SoundSink {
      soundSinkOpen  :: IO (handle y),
      soundSinkClose :: handle y -> IO (),
      soundSinkWrite :: handle y -> Ptr y -> Size -> IO (),
      soundSinkStart :: handle y -> IO (),
      soundSinkStop  :: handle y -> IO ()
   }


withSoundSource :: SoundSource handle y -> (handle y -> IO a) -> IO a
withSoundSource source =
    bracket (soundSourceOpen source) (soundSourceClose source)

withSoundSourceRunning :: SoundSource handle y -> handle y -> IO a -> IO a
withSoundSourceRunning src h =
    bracket_ (soundSourceStart src h) (soundSourceStop src h)

withSoundSink :: SoundSink handle y -> (handle y -> IO a) -> IO a
withSoundSink sink =
    bracket (soundSinkOpen sink) (soundSinkClose sink)

withSoundSinkRunning :: SoundSink handle y -> handle y -> IO a -> IO a
withSoundSinkRunning src h =
    bracket_ (soundSinkStart src h) (soundSinkStop src h)


withSampleFmt :: (y -> a) -> (SoundFmt y -> a)
withSampleFmt f _ = f undefined

withNodeSample :: (y -> a) -> (node y -> a)
withNodeSample f _ = f undefined


soundFmtMIME :: Class.SampleFmt y => SoundFmt y -> String
soundFmtMIME fmt = t ++ r ++ c
  where t = "audio/basic"
{-
        t = case sampleFmt fmt of
                SampleFmtLinear16BitSignedLE -> "audio/L16"
                SampleFmtMuLaw8Bit           -> "audio/basic"
-}
        r = ";rate=" ++ show (sampleFreq fmt)
        c =
           if numChannels fmt == 1
             then ""
             else ";channels=" ++ show (numChannels fmt)

numChannels :: Class.SampleFmt y => SoundFmt y -> Int
numChannels = withSampleFmt Frame.numberOfChannels

audioBytesPerSample :: Class.SampleFmt y => SoundFmt y -> Int
audioBytesPerSample = withSampleFmt Frame.sizeOfElement

{-
assumes interleaved data

Due to alignment constraints
a frame might occupy more than the calculated size
in an array in memory.
-}
audioBytesPerFrame :: Class.SampleFmt y => SoundFmt y -> Int
audioBytesPerFrame fmt = numChannels fmt * audioBytesPerSample fmt

soundSourceBytesPerFrame :: Class.SampleFmt y => SoundSource handle y -> Int
soundSourceBytesPerFrame =
   withNodeSample $ \y -> Frame.numberOfChannels y * Frame.sizeOfElement y

soundSinkBytesPerFrame :: Class.SampleFmt y => SoundSink handle y -> Int
soundSinkBytesPerFrame =
   withNodeSample $ \y -> Frame.numberOfChannels y * Frame.sizeOfElement y

copySound ::
   Class.SampleFmt y =>
      SoundSource handleIn y
   -> SoundSink handleOut y
   -> Size -- ^ Buffer size (in sample frames) to use
   -> IO ()
copySound source sink bufSize =
    allocaArray     (fromIntegral bufSize) $ \buf ->
    withSoundSource source  $ \from ->
    withSoundSink   sink    $ \to ->
       let loop = do n <- soundSourceRead source from buf bufSize
                     when (n > 0) $ do soundSinkWrite sink to buf n
                                       loop
        in loop

--
-- * Alsa stuff
--


{- |
The buffer is initialized with an empty block
which means that the zero bit pattern
should equal the number zero in the Class.SampleFmt type.
-}
alsaOpen :: Class.SampleFmt y =>
   PCM.Stream ->
   HwParam.T PCM.Interleaved y a ->
   (a -> SwParam.T PCM.Interleaved y ()) ->
   String {- ^ device, e.g @"default"@ -} ->
   IO (Pcm y)
alsaOpen stream hwParams swParams dev = AlsaExc.rethrow $ do
   Debug.put "alsaOpen"
   {-
   Debug.put $ "requested bufferTime = " ++ show (bufferTime time)
   Debug.put $ "requested periodTime = " ++ show (periodTime time)
   -}
   ((bufferTime_,bufferSize_,periodTime_,periodSize_), h) <-
      PCM.open (PCM.modes []) stream
         (liftM2 (,) hwParams $
          liftM4 (,,,)
             HwParam.getBufferSize
             HwParam.getBufferTime
             (fmap fst HwParam.getPeriodSize)
             (fmap fst HwParam.getPeriodTime))
         (\(a, params) -> swParams a >> return params)
         dev
   PCM.prepare h
   Debug.put $ "bufferTime = " ++ show bufferTime_
   Debug.put $ "bufferSize = " ++ show bufferSize_
   Debug.put $ "periodTime = " ++ show periodTime_
   Debug.put $ "periodSize = " ++ show periodSize_
   when (stream == PCM.StreamPlayback) $
      callocaArray periodSize_ $ \buf ->
         PCM.writei h buf (fromIntegral periodSize_) >> return ()
   return h

alsaClose :: Pcm y -> IO ()
alsaClose h = AlsaExc.rethrow $ do
   Debug.put "alsaClose"
   PCM.drain h
   PCM.close h


alsaStart :: Pcm y -> IO ()
alsaStart h = AlsaExc.rethrow $ do
   Debug.put "alsaStart"
   PCM.prepare h
   PCM.start h

-- FIXME: use PCM.drain for sinks?
alsaStop :: Pcm y -> IO ()
alsaStop h = AlsaExc.rethrow $ do
   Debug.put "alsaStop"
   PCM.drain h


alsaRead ::
   Class.SampleFmt y =>
   Pcm y -> Ptr y -> Size -> IO Size
alsaRead h buf0 n =
   AlsaExc.rethrow $ PCM.readiRetry h buf0 n

alsaWrite ::
   Class.SampleFmt y =>
   Pcm y -> Ptr y -> Size -> IO Size
alsaWrite h buf0 n =
   AlsaExc.rethrow $ PCM.writeiRetry h buf0 n


defaultBufferTime :: SoundBufferTime
defaultBufferTime =
   SoundBufferTime {
      bufferTime = 500000, -- 0.5s
      periodTime = 100000  -- 0.1s
   }

bufferTimeParams ::
   SoundFmt y ->
   SoundBufferTime ->
   (HwParam.T PCM.Interleaved y (Size,Size),
    (Size,Size) -> SwParam.T PCM.Interleaved y ())
bufferTimeParams fmt time =
   (HwParam.setRateBufferTime
       (sampleFreq fmt)
       (bufferTime time)
       (periodTime time),
    uncurry SwParam.setBufferSize)

alsaSoundSource ::
   Class.SampleFmt y =>
   String -> SoundFmt y -> SoundSource Pcm y
alsaSoundSource dev fmt =
   alsaSoundSourceTime dev fmt defaultBufferTime

alsaSoundSink ::
   Class.SampleFmt y =>
   String -> SoundFmt y -> SoundSink Pcm y
alsaSoundSink dev fmt =
   alsaSoundSinkTime dev fmt defaultBufferTime

alsaSoundSourceTime ::
   Class.SampleFmt y =>
   String -> SoundFmt y -> SoundBufferTime -> SoundSource Pcm y
alsaSoundSourceTime dev fmt time =
   uncurry (alsaSoundSourceParams dev) $
   bufferTimeParams fmt time

alsaSoundSinkTime ::
   Class.SampleFmt y =>
   String -> SoundFmt y -> SoundBufferTime -> SoundSink Pcm y
alsaSoundSinkTime dev fmt time =
   uncurry (alsaSoundSinkParams dev) $
   bufferTimeParams fmt time

alsaSoundSourceParams ::
   Class.SampleFmt y =>
   String ->
   HwParam.T PCM.Interleaved y a ->
   (a -> SwParam.T PCM.Interleaved y ()) ->
   SoundSource Pcm y
alsaSoundSourceParams dev hwParams swParams =
   SoundSource {
      soundSourceOpen  = alsaOpen PCM.StreamCapture hwParams swParams dev,
      soundSourceClose = alsaClose,
      soundSourceStart = alsaStart,
      soundSourceStop  = alsaStop,
      soundSourceRead  = alsaRead
   }

alsaSoundSinkParams ::
   Class.SampleFmt y =>
   String ->
   HwParam.T PCM.Interleaved y a ->
   (a -> SwParam.T PCM.Interleaved y ()) ->
   SoundSink Pcm y
alsaSoundSinkParams dev hwParams swParams =
   SoundSink {
      soundSinkOpen  = alsaOpen PCM.StreamPlayback hwParams swParams dev,
      soundSinkClose = alsaClose,
      soundSinkStart = alsaStart,
      soundSinkStop  = alsaStop,
      soundSinkWrite = \h buf n -> alsaWrite h buf n >> return ()
   }

--
-- * File stuff
--

fileSoundSource ::
   Class.SampleFmt y =>
   FilePath -> SoundSource File y
fileSoundSource file =
   SoundSource {
      soundSourceOpen  = File.open IO.ReadMode file,
      soundSourceClose = File.close,
      soundSourceRead  = File.read,
      soundSourceStart = const $ return (),
      soundSourceStop  = const $ return ()
   }

fileSoundSink ::
   Class.SampleFmt y =>
   FilePath -> SoundSink File y
fileSoundSink file =
   SoundSink {
      soundSinkOpen  = File.open IO.WriteMode file,
      soundSinkClose = File.close,
      soundSinkWrite = File.write,
      soundSinkStart = const $ return (),
      soundSinkStop  = const $ return ()
   }

--
-- * Marshalling utilities
--

callocaArray :: Storable y => Size -> (Ptr y -> IO b) -> IO b
callocaArray n0 f =
   case fromIntegral n0 of
      n ->
         allocaArray n $ \p ->
            clearBytes p (arraySize p n) >>
            f p

clearBytes :: Ptr a -> Int -> IO ()
clearBytes p n = memset p 0 (fromIntegral n) >> return ()

foreign import ccall unsafe "string.h" memset :: Ptr a -> C.CInt -> C.CSize -> IO (Ptr a)