packages feed

musicw-0.3.2: src/Sound/MusicW/AudioContext.hs

{-# LANGUAGE FlexibleInstances, JavaScriptFFI #-}

module Sound.MusicW.AudioContext where

import GHCJS.DOM.Types (js_eq)
import GHCJS.Types
import GHCJS.Marshal.Pure
import Data.Text
import Data.Time
import Control.Monad.IO.Class
import Control.Monad.Trans.Reader

type AudioTime = Double

newtype AudioContext = AudioContext JSVal

instance PToJSVal AudioContext where pToJSVal (AudioContext val) = val

instance PFromJSVal AudioContext where pFromJSVal = AudioContext

data AudioContextState = ACRunning | ACSuspended | ACClosed deriving (Show, Eq)

instance PToJSVal AudioContextState where
  pToJSVal ACRunning = js_acRunning
  pToJSVal ACSuspended = js_acSuspended
  pToJSVal ACClosed = js_acClosed

instance PFromJSVal AudioContextState where
  pFromJSVal x | x `js_eq` js_acRunning = ACRunning
  pFromJSVal x | x `js_eq` js_acSuspended = ACSuspended
  pFromJSVal x | x `js_eq` js_acClosed = ACClosed

foreign import javascript unsafe "\"running\"" js_acRunning :: JSVal
foreign import javascript unsafe "\"suspended\"" js_acSuspended :: JSVal
foreign import javascript unsafe "\"closed\"" js_acClosed :: JSVal

foreign import javascript safe
  "new (window.AudioContext || window.webkitAudioContext)({ latencyHint: \"playback\", sampleRate: 48000 })"
  newAudioContext :: IO AudioContext

foreign import javascript unsafe
  "window.___ac = $1;"
  setGlobalAudioContext :: AudioContext -> IO ()

foreign import javascript safe
  "if (window.___ac == null) { \
  \    window.___ac = new (window.AudioContext || window.webkitAudioContext)(\
  \      { latencyHint: \"playback\", sampleRate: 48000 } \
  \      );\
  \} $r = window.___ac;"
  getGlobalAudioContext :: IO AudioContext

foreign import javascript unsafe
  "$1.currentTime"
  getAudioTime :: AudioContext -> IO AudioTime

-- State management functions, both a sync and async version

foreign import javascript unsafe
  "$1.state"
  js_getState :: AudioContext -> IO JSVal

getState :: AudioContext -> IO AudioContextState
getState ac = pFromJSVal <$> js_getState ac

foreign import javascript interruptible
  "$1.resume().then($c)['catch'](function(e){$c(''+e);});" -- ffi parser treats 'catch' as keyword even in id ctx
  js_resumeSync :: AudioContext -> IO JSVal

foreign import javascript unsafe
  "$1.resume();"
  resume :: AudioContext -> IO ()

resumeSync :: AudioContext -> IO (Maybe Text)
resumeSync ac = pFromJSVal <$> js_resumeSync ac

foreign import javascript interruptible
  "$1.suspend().then($c)['catch'](function(e){$c(''+e);});"
  js_suspendSync :: AudioContext -> IO JSVal

foreign import javascript unsafe
  "$1.suspend();"
  suspend :: AudioContext -> IO ()

suspendSync :: AudioContext -> IO (Maybe Text)
suspendSync ac = pFromJSVal <$> js_suspendSync ac

foreign import javascript interruptible
  "$1.close().then($c)['catch'](function(e){$c(''+e);});"
  js_closeSync :: AudioContext -> IO JSVal

foreign import javascript unsafe
  "$1.close();"
  close :: AudioContext -> IO ()

closeSync :: AudioContext -> IO (Maybe Text)
closeSync ac = pFromJSVal <$> js_closeSync ac

foreign import javascript unsafe
  "$1.sampleRate"
  getSampleRate :: AudioContext -> IO Double

foreign import javascript unsafe
  "$1.destination"
  getDestination :: AudioContext -> IO JSVal

class (MonadIO m) => AudioIO m where
  audioContext :: m AudioContext

audioTime :: AudioIO m => m AudioTime
audioTime = audioContext >>= liftIO . getAudioTime

sampleRate :: AudioIO m => m Double
sampleRate = audioContext >>= liftIO . getSampleRate

destination :: AudioIO m => m JSVal -- TODO: not crazy about the JSVal return type here...
destination = audioContext >>= liftIO . getDestination

type AudioContextIO = ReaderT AudioContext IO

instance AudioIO AudioContextIO where
  audioContext = ask

runAudioContextIO :: AudioContext -> AudioContextIO a -> IO a
runAudioContextIO ac acio = (runReaderT acio) ac

liftAudioIO :: MonadIO m => AudioContextIO a -> m a
liftAudioIO x = do
  ac <- liftIO $ getGlobalAudioContext
  liftIO $ runAudioContextIO ac x

-- | Utility functions

dbamp :: Double -> Double
dbamp x = 10.0 ** (x / 20.0)

ampdb :: Double -> Double
ampdb x = 20.0 * (logBase 10 x)

midicps :: Double -> Double
midicps x = 440.0 * (2.0 ** ((x - 69.0) / 12.0))

cpsmidi :: Double -> Double
cpsmidi x = 69.0 + 12.0 * (logBase 2 (x / 440.0))