packages feed

YampaSynth-0.1.1: src/Player/OpenAL.hs

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

module Player.OpenAL (
   Player.OpenAL.play
 , initOpenAL
 , deInitOpenAL
 , frpSynth
 , Chunk (..)
 ) where

import Data.Audio

import FRP.Yampa

import Sound.OpenAL 

import Data.Int()
import Data.IORef
import Foreign
import Control.Concurrent  
import Control.Monad
import Data.Maybe
import Control.Applicative

play :: Int -> Int -> Int -> SF () (Sample, Event ()) -> IO ()
play sampleRate' sampleNumber' numBuffs sf = do
  (device,context,pSource,pBuffers) <- initOpenAL numBuffs
  frpSynth sampleRate' pSource pBuffers sampleNumber' sf () (return ())
  deInitOpenAL device context pSource pBuffers

frpSynth :: Int -> Source -> [Buffer] -> Int -> SF a (Sample, Event b) -> a -> IO a -> IO ()
frpSynth sampleRate' pSource pBuffers sampleNumber' sf ret senseEvt = do
  mVarMaybeChunk <- newEmptyMVar
  mVarReplyPlayer <- newEmptyMVar

  _ <- forkIO $ process sampleRate' pSource pBuffers [] mVarMaybeChunk mVarReplyPlayer

  ir <- newIORef (0 :: Int)

  chunkData' <- mallocArray sampleNumber'
  let sense = (\x -> (1.0 / fromIntegral sampleRate', x)) . Just <$> senseEvt -- ghc 6.12 required for TupleSections :(
      chunk = Chunk chunkData' sampleNumber'
      actuate _ (s,e) = if (isEvent e)
        then return True
        else do
          i <- readIORef ir
          let samp = fromSample s :: Int16 -- the only place we have to specify our sample representation
          when (i /= 0 || samp /= 0) $ do -- don't put leading zeros in a chunk
                  pokeElemOff chunkData' i samp
                  if i == (sampleNumber' - 1)
                     then do
                          putMVar mVarMaybeChunk $ Just chunk
                          takeMVar mVarReplyPlayer
                          writeIORef ir 0
                     else writeIORef ir (i + 1)
          return False
  
  reactimate (return ret) (const sense) actuate sf

  i <- readIORef ir
  putMVar mVarMaybeChunk . Just $ chunk {numElems = i}
  takeMVar mVarReplyPlayer
  putMVar mVarMaybeChunk Nothing
  takeMVar mVarReplyPlayer
  free chunkData'

initOpenAL :: Int -> IO (Device, Context, Source, [Buffer])
initOpenAL numBuffs = do
  mDevice <- openDevice Nothing
  case mDevice of
    Nothing -> fail "opening OpenAL device"
    Just device -> do
      mContext <- createContext device []
      case mContext of 
        Nothing -> fail "opening OpenAL context"
        Just context -> do
          currentContext $= Just context
          [pSource] <- genObjectNames 1
          pBuffers <- genObjectNames numBuffs
          printErrs
          return (device,context,pSource,pBuffers)
        
deInitOpenAL :: Device -> Context -> Source -> [Buffer] -> IO ()
deInitOpenAL device context pSource pBuffers = do 
  dequeue pSource
  deleteObjectNames [pSource]
  deleteObjectNames pBuffers
  currentContext $= Nothing
  destroyContext context
  whenM (not <$> closeDevice device) $ fail "closing OpenAL device"
  printErrs
    
data Chunkable a => Chunk a = Chunk {
    chunkData :: Ptr a
  , numElems  :: Int
  } deriving (Eq, Show)

 -- does the Bits constraint basically guarantee that it's Integral?
class (Storable a, Bits a, Audible a) => Chunkable a where

instance (Storable a, Bits a, Audible a) => Chunkable a -- thx copumpkin @ #haskell

-- from http://www.haskell.org/pipermail/beginners/2009-January/000690.html (via byorgey @ #haskell)
untilM :: (Monad m) => (a -> Bool) -> (a -> m a) -> a -> m a
untilM p f x | p x       = return x
             | otherwise = f x >>= untilM p f

lastInd :: (Chunkable a) => (a -> Bool) -> Chunk a -> IO (Maybe Int)
lastInd p c = do 
  (_,mInd) <- untilM (\(i,x) -> isJust x || i < 0)
                     (\(i,_) -> do e <- peekElemOff (chunkData c) i
                                   return (i-1, if p e then Just i else Nothing)
                                   )
                     (numElems c - 1,Nothing)
  return $ (+ 1) <$> mInd

process :: (Chunkable a) => Int -> Source -> [Buffer] -> [Buffer] -> MVar (Maybe (Chunk a)) -> MVar () -> IO ()
process sampleRate' pSource freeBuffers usedBuffers mVarMaybeChunk mVarReply = do
  mChunk <- takeMVar mVarMaybeChunk
  void $ reply mChunk (\chunk -> do
    mInd <- lastInd (/= 0) chunk -- we aren't sent chunks with leading zeros
    (f,u) <- reply mInd (\ind -> do  
      (buff,newFree,newUsed) <- if null freeBuffers 
         then do waitForBuffer pSource
                 let b = head usedBuffers
                 unqueueBuffers pSource [b]
                 return (b,[],tail usedBuffers ++ [b])
         else do let h = head freeBuffers
                 return (h, tail freeBuffers, usedBuffers ++ [h])
      ((bufferData buff) $=) =<< createBufferData sampleRate' chunk ind
      _ <- reply Nothing undefined
      queueBuffers pSource [buff]
      whenM ((/= Playing) <$> (get $ sourceState pSource)) $ Sound.OpenAL.play [pSource]
      printErrs
      return (newFree,newUsed)
      )
    process sampleRate' pSource f u mVarMaybeChunk mVarReply
    return (undefined,undefined)
    )
  dequeue pSource
  where reply = flip . maybe $ putMVar mVarReply undefined >> return (freeBuffers,usedBuffers)

printErrs :: IO ()
printErrs = do e <- get alErrors
               when (not $ null e) . putStrLn $ show e

dequeue :: Source -> IO ()
dequeue pSource = waitForSource pSource >> buffer pSource $= Nothing

createBufferData :: (Chunkable a) => Int -> Chunk a -> Int -> IO (BufferData a)
createBufferData sampleRate' chunk n = do
  ex <- peekElemOff (chunkData chunk) 0
  let elemSize = sizeOf ex
      format = case elemSize of
                 2 -> Mono16
                 1 -> Mono8
                 _ -> error "1 or 2 byte buffer required"
  when (not $ isSigned ex) $ fail "signed buffer required" -- how enforce these statically?
  return $ BufferData (MemoryRegion (chunkData chunk) (fromIntegral $ n * elemSize))
                      format
                      (fromIntegral sampleRate')

{-
untilM_ :: (Functor m, Monad m) => (a -> Bool) -> m a -> m ()
-- untilM_ p f = void $ untilM p (const f) undefined -- isn't there something in this spirit?
untilM_ p f = do b <- p <$> f
                 if b then return () else untilM_ p f

void :: (Monad m) => m a -> m ()
void = (>> return ())
-}

waitForBuffer :: Source -> IO () -- better to express using untilM_
waitForBuffer s = do b <- (> 0) <$> (get $ buffersProcessed s)
                     if b then return () else threadDelay 10 >> waitForBuffer s

whenM :: (Monad m, Functor m) => m Bool -> m () -> m ()
whenM test action = join $ flip when action <$> test

waitForSource :: Source -> IO ()
waitForSource pSource = whenM ((== Playing) <$> (get $ sourceState pSource)) delWait
  where delWait = do threadDelay 10 -- micro seconds
                     waitForSource pSource