alsa-pcm-0.6: src/Sound/ALSA/PCM.hs
{-# 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)