musicw-0.3.2: src/Sound/MusicW/Node.hs
{-# LANGUAGE JavaScriptFFI #-}
module Sound.MusicW.Node where
import GHCJS.Types
import GHCJS.Marshal.Pure
import GHCJS.Prim (toJSArray,toJSString)
import GHCJS.Foreign.Callback(Callback, asyncCallback1, releaseCallback)
import Control.Monad.IO.Class
import Sound.MusicW.AudioContext
import Sound.MusicW.AudioBuffer
import Sound.MusicW.FloatArraySpec
-- | A Node is an instantiated and wrapped node from the Web Audio API, or it is
-- a settable parameter of such a node. Nodes can potentially be connected and
-- disconnected to/from each other, started, stopped, and can have values/ramps/curves
-- scheduled on their parameters.
newtype Node = Node JSVal
instance PToJSVal Node where pToJSVal (Node j) = j
instance PFromJSVal Node where pFromJSVal = Node
instance Show Node where show _ = "a Node"
data ParamType
= Frequency
| Gain
| Q
| DelayTime
| Threshold
| Knee
| CompressionRatio
| Attack
| Release
-- obviously more are needed here...
instance Show ParamType where
show Frequency = "frequency"
show Gain = "gain"
show Q = "Q"
show DelayTime = "delayTime"
show Threshold = "threshold"
show Knee = "knee"
show CompressionRatio = "ratio"
show Attack = "attack"
show Release = "release"
createConstantSource :: AudioIO m => Double -> m Node
createConstantSource v = do
ctx <- audioContext
node <- liftIO $ js_createConstantSource ctx
liftIO $ js_setConstantOffset node v
setNodeField node "isSource" True
setNodeField node "isSink" False
setNodeField node "startable" True
data OscillatorType
= Sine
| Square
| Sawtooth
| Triangle
deriving (Show, Eq)
instance PToJSVal OscillatorType where
pToJSVal Sine = pToJSVal "sine"
pToJSVal Square = pToJSVal "square"
pToJSVal Sawtooth = pToJSVal "sawtooth"
pToJSVal Triangle = pToJSVal "triangle"
createOscillator :: AudioIO m => OscillatorType -> Double -> m Node
createOscillator t f = do
ctx <- audioContext
node <- liftIO $ js_createOscillator ctx
setNodeField node "type" t
setFrequency node f
setNodeField node "isSource" True
setNodeField node "isSink" False
temp <- setNodeField node "startable" True
return temp
data BufferParams = BufferParams Double Double Bool deriving (Show, Eq)
createAudioBufferSource :: AudioIO m => AudioBuffer -> BufferParams -> m Node
createAudioBufferSource buf params@(BufferParams loopstart loopend loop) = do
ctx <- audioContext
node <- liftIO $ js_createBufferSource ctx
setNodeField node "buffer" buf
setNodeField node "loopstart" loopstart
setNodeField node "loopend" loopend
setNodeField node "loop" loop
setNodeField node "isSource" True
setNodeField node "isSink" False
setNodeField node "startable" True
data FilterSpec -- first argument always frequency, second argument when two is q or gain, when three: frequency q gain
= LowPass Double Double
| HighPass Double Double
| BandPass Double Double
| LowShelf Double Double
| HighShelf Double Double
| Peaking Double Double Double
| Notch Double Double
| AllPass Double Double
deriving (Show)
-- temp note: filter types: LowPass, HighPass, BandPass,LowShelf,HighShelf,Peaking,Notch,AllPass
createBiquadFilter :: AudioIO m => FilterSpec -> m Node
createBiquadFilter spec = do
ctx <- audioContext
node <- liftIO $ js_createBiquadFilter ctx
configureBiquadFilter spec node
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
configureBiquadFilter :: AudioIO m => FilterSpec -> Node -> m Node
configureBiquadFilter (LowPass f q) n = setNodeField n "type" "lowpass" >> setFrequency n f >> setQ n q
configureBiquadFilter (HighPass f q) n = setNodeField n "type" "highpass" >> setFrequency n f >> setQ n q
configureBiquadFilter (BandPass f q) n = setNodeField n "type" "bandpass" >> setFrequency n f >> setQ n q
configureBiquadFilter (LowShelf f g) n = setNodeField n "type" "lowshelf" >> setFrequency n f >> setGain n g
configureBiquadFilter (HighShelf f g) n = setNodeField n "type" "highshelf" >> setFrequency n f >> setGain n g
configureBiquadFilter (Peaking f q g) n = setNodeField n "type" "peaking" >> setFrequency n f >> setQ n q >> setGain n g
configureBiquadFilter (Notch f q) n = setNodeField n "type" "notch" >> setFrequency n f >> setQ n q
configureBiquadFilter (AllPass f q) n = setNodeField n "type" "allpass" >> setFrequency n f >> setQ n q
createConvolver :: AudioIO m => Either Float32Array FloatArraySpec -> Bool -> m Node
createConvolver bufferSpec normalize = do
ctx <- audioContext
node <- liftIO $ js_createConvolver ctx
sr <- sampleRate
bufferData <- liftIO $ instantiateArraySpec bufferSpec
nSamples <- liftIO $ js_typedArrayLength bufferData
buffer <- liftIO $ js_createAudioBuffer 1 nSamples sr ctx -- TODO (?) use a buffer spec instead of array spec (???)
liftIO $ js_copyToChannel bufferData 1 buffer
setNodeField node "buffer" buffer
setNodeField node "normalize" normalize
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
createDelay :: AudioIO m => AudioTime -> m Node
createDelay maxT = do
ctx <- audioContext
node <- liftIO $ js_createDelay ctx maxT
setValue node DelayTime maxT
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
createCompressor :: AudioIO m => Double -> Double -> Double -> AudioTime -> AudioTime -> m Node
createCompressor thr kne rat att rel = do
ctx <- audioContext
node <- liftIO $ js_createDynamicsCompressor ctx
setValue node Threshold thr
setValue node Knee kne
setValue node CompressionRatio rat
setValue node Attack att
setValue node Release rel
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
createGain :: AudioIO m => Double -> m Node
createGain g = do
ctx <- audioContext
node <- liftIO $ js_createGain ctx
setValue node Gain g
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
createChannelMerger :: AudioIO m => Int -> m Node
createChannelMerger nChnls = do
ctx <- audioContext
node <- liftIO $ js_createChannelMerger ctx nChnls
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
createChannelSplitter :: AudioIO m => Int -> m Node
createChannelSplitter nChnls = do
ctx <- audioContext
node <- liftIO $ js_createChannelSplitter ctx nChnls
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
data OversampleAmount
= NoOversampling
| X2Oversampling
| X4Oversampling
deriving (Show)
instance PToJSVal OversampleAmount where
pToJSVal NoOversampling = pToJSVal "none"
pToJSVal X2Oversampling = pToJSVal "x2"
pToJSVal X4Oversampling = pToJSVal "x4"
createWaveShaper :: AudioIO m => Either Float32Array FloatArraySpec -> OversampleAmount -> m Node
createWaveShaper curve oversample = do
ctx <- audioContext
node <- liftIO $ js_createWaveShaper ctx
curveArray <- liftIO $ instantiateArraySpec curve
setNodeField node "curve" curveArray
setNodeField node "oversample" oversample
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" False
createScriptProcessor :: AudioIO m => Int -> Int -> (JSVal -> IO ()) -> m Node
createScriptProcessor inChnls outChnls cb = do
ctx <- audioContext
node <- liftIO $ js_createScriptProcessor ctx inChnls outChnls
onaudioprocess <- liftIO $ asyncCallback1 cb
liftIO $ js_onaudioprocess node onaudioprocess
liftIO $ releaseCallback onaudioprocess
setNodeField node "isSource" True
setNodeField node "isSink" True
setNodeField node "startable" True
createAnalyser :: AudioIO m => Int -> Double -> m Node
createAnalyser fftSize smoothingTimeConstant = do
ctx <- audioContext
node <- liftIO $ js_createAnalyser ctx
setNodeField node "fftSize" fftSize
setNodeField node "smoothingTimeConstant" smoothingTimeConstant
setNodeField node "isSource" False -- technically, not true, but...
setNodeField node "isSink" True
setNodeField node "startable" False
-- | There is no function in the Web Audio API to "create" the context's
-- destination but we provide one anyway, as a convenient way to get a node
-- that, like all the other nodes, can be used in connections.
createDestination :: AudioIO m => m Node
createDestination = do
node <- Node <$> destination
setNodeField node "isSource" False
setNodeField node "isSink" True
setNodeField node "startable" False
createMediaStreamDestination :: AudioIO m => m Node
createMediaStreamDestination = do
ctx <- audioContext
node <- liftIO $ js_createMediaStreamDestination ctx
setNodeField node "isSource" False
setNodeField node "isSink" True
setNodeField node "startable" False
getSharedMediaStreamDestination :: AudioIO m => m Node
getSharedMediaStreamDestination = do
ctx <- audioContext
node <- liftIO $ js_getContextSharedMediaStreamDestination ctx
setNodeField node "isSource" False
setNodeField node "isSink" True
setNodeField node "startable" False
-- | Similarly, while the control parameters of nodes in the web audio API are distinct
-- we provide a function here to create a pseudo-node from a parameter of an
-- existing node. As with the destination (above), a node parameter, like a node,
-- can be something to which connections are made.
createParameter :: MonadIO m => Node -> ParamType -> m Node
createParameter node pType = do
let p = Node $ js_audioParam node (pToJSVal $ show pType)
setNodeField p "isSource" False
setNodeField p "isSink" True
setNodeField p "startable" False
connectNodes :: MonadIO m => Node -> Node -> m ()
connectNodes from to
| not (js_isSource from) = error $ (show from) ++ " can't be connect source."
| not (js_isSink to) = error $ (show to) ++ " can't be connect target."
| otherwise = liftIO $ js_connect from to
connectNodes' :: MonadIO m => Node -> Int -> Node -> Int -> m ()
connectNodes' fromNode fromChannel toNode toChannel
| not (js_isSource fromNode) = error $ (show fromNode) ++ " can't be connect source."
| not (js_isSink toNode) = error $ (show toNode) ++ " can't be connect target."
| otherwise = liftIO $ js_connect' fromNode toNode fromChannel toChannel
connectNodes'' :: MonadIO m => Node -> Int -> Node -> m ()
connectNodes'' fromNode fromChannel toNode
| not (js_isSource fromNode) = error $ (show fromNode) ++ " can't be connect source."
| not (js_isSink toNode) = error $ (show toNode) ++ " can't be connect target."
| otherwise = liftIO $ js_connect'' fromNode toNode fromChannel
disconnectNodes :: MonadIO m => Node -> Node -> m ()
disconnectNodes from to
| not (js_isSource from) = error $ (show from) ++ " can't be disconnect source."
| not (js_isSink to) == False = error $ (show to) ++ " can't be disconnect target."
| otherwise = liftIO $ js_disconnect from to
disconnectAll :: MonadIO m => Node -> m ()
disconnectAll x
| not (js_isSource x) = return ()
| otherwise = liftIO $ js_disconnectAll x
startNode :: MonadIO m => AudioTime -> Node -> m ()
startNode t x
| js_startable x = liftIO $ js_start x t
| otherwise = return ()
stopNode :: MonadIO m => AudioTime -> Node -> m ()
stopNode t x
| js_startable x = liftIO $ js_stop x t
| otherwise = return ()
stopNodeNow :: MonadIO m => Node -> m ()
stopNodeNow x
| js_startable x = liftIO $ js_stopNow x
| otherwise = return ()
isSourceNode :: Node -> Bool
isSourceNode n = js_isSource n && (not $ js_isSink n)
isSinkNode :: Node -> Bool
isSinkNode n = js_isSink n && (not $ js_isSource n)
-- Definitions used to set/schedule the values of fields and parameters of nodes
setNodeField :: (MonadIO m, PToJSVal a) => Node -> String -> a -> m Node
setNodeField node field val = do
liftIO $ js_setField (pToJSVal node) (pToJSVal field) (pToJSVal val)
return node
setFrequency :: AudioIO m => Node -> Double -> m Node
setFrequency node = setValue node Frequency
setGain :: AudioIO m => Node -> Double -> m Node
setGain node = setValue node Gain
setQ :: AudioIO m => Node -> Double -> m Node
setQ node = setValue node Q
setValue :: AudioIO m => Node -> ParamType -> Double -> m Node
setValue node pType value = audioTime >>= setValueAtTime node pType value
setValueAtTime :: MonadIO m => Node -> ParamType -> Double -> AudioTime -> m Node
setValueAtTime node pType value time = do
liftIO $ js_setValueAtTime node (pToJSVal $ show pType) value time
return node
linearRampToValueAtTime :: MonadIO m => Node -> ParamType -> Double -> AudioTime -> m Node
linearRampToValueAtTime node pType value time = do
liftIO $ js_linearRampToValueAtTime node (pToJSVal $ show pType) value time
return node
exponentialRampToValueAtTime :: MonadIO m => Node -> ParamType -> Double -> AudioTime -> m Node
exponentialRampToValueAtTime node pType value time = do
liftIO $ js_exponentialRampToValueAtTime node (pToJSVal $ show pType) value time
return node
setValueCurveAtTime :: MonadIO m => Node -> ParamType -> [Double] -> AudioTime -> AudioTime -> m Node
setValueCurveAtTime node pType curve startTime duration = do
curveArray <- liftIO $ toJSArray $ fmap pToJSVal curve
typedCurveArray <- liftIO $ js_typedArrayFromArray curveArray
liftIO $ js_setValueCurveAtTime node (pToJSVal $ show pType) typedCurveArray startTime duration
return node
onended :: Node -> (JSVal -> IO ()) -> IO ()
onended n cb = do
let n' = pToJSVal n
onend <- asyncCallback1 $ \_ -> cb n'
js_onended n onend
releaseCallback onend
-- JS FFI functions (required, but not exported from this module):
foreign import javascript unsafe
"$1.createConstantSource()"
js_createConstantSource :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createOscillator()"
js_createOscillator :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createBufferSource()"
js_createBufferSource :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createBiquadFilter()"
js_createBiquadFilter :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createConvolver()"
js_createConvolver :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createDelay($2)"
js_createDelay :: AudioContext -> AudioTime -> IO Node
foreign import javascript unsafe
"$1.createDynamicsCompressor()"
js_createDynamicsCompressor :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createGain()"
js_createGain :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createChannelMerger($2)"
js_createChannelMerger :: AudioContext -> Int -> IO Node
foreign import javascript unsafe
"$1.createChannelSplitter($2)"
js_createChannelSplitter :: AudioContext -> Int -> IO Node
foreign import javascript unsafe
"$1.createWaveShaper()"
js_createWaveShaper :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createScriptProcessor(void 0, $2, $3)"
js_createScriptProcessor :: AudioContext -> Int -> Int -> IO Node
foreign import javascript unsafe
"$1.createMediaStreamDestination()"
js_createMediaStreamDestination :: AudioContext -> IO Node
foreign import javascript unsafe
"if ($1.___stream == null) {\
\ $1.___stream = $1.createMediaStreamDestination(); \
\} $r = $1.___stream;"
js_getContextSharedMediaStreamDestination :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.createAnalyser()"
js_createAnalyser :: AudioContext -> IO Node
foreign import javascript unsafe
"$1.connect($2);"
js_connect :: Node -> Node -> IO ()
foreign import javascript unsafe
"$1.connect($2,$3,$4);"
js_connect' :: Node -> Node -> Int -> Int -> IO ()
foreign import javascript unsafe
"$1.connect($2,$3);"
js_connect'' :: Node -> Node -> Int -> IO ()
foreign import javascript unsafe
"$1.disconnect($2);"
js_disconnect :: Node -> Node -> IO ()
foreign import javascript unsafe
"$1.disconnect();"
js_disconnectAll :: Node -> IO ()
foreign import javascript unsafe
"$1.start($2);"
js_start :: Node -> AudioTime -> IO ()
foreign import javascript unsafe
"$1.stop($2);"
js_stop :: Node -> AudioTime -> IO ()
foreign import javascript unsafe
"$1.stop();"
js_stopNow :: Node -> IO ()
foreign import javascript unsafe
"$1.onaudioprocess = $2;"
js_onaudioprocess :: Node -> Callback (JSVal -> IO ()) -> IO ()
foreign import javascript unsafe
"$1[$2].setValueAtTime($3, $4);"
js_setValueAtTime :: Node -> JSVal -> Double -> Double -> IO ()
foreign import javascript unsafe
"$1[$2].linearRampToValueAtTime($3, $4);"
js_linearRampToValueAtTime :: Node -> JSVal -> Double -> AudioTime -> IO ()
foreign import javascript unsafe
"$1[$2].exponentialRampToValueAtTime($3, $4);"
js_exponentialRampToValueAtTime :: Node -> JSVal -> Double -> AudioTime -> IO ()
foreign import javascript unsafe
"$1[$2].setValueCurveAtTime($3, $4, $5);"
js_setValueCurveAtTime :: Node -> JSVal -> Float32Array -> Double -> AudioTime -> IO ()
foreign import javascript unsafe
"$1[$2] = $3;"
js_setField :: JSVal -> JSVal -> JSVal -> IO ()
foreign import javascript unsafe
"$1.offset.value = $2"
js_setConstantOffset :: Node -> Double -> IO ()
foreign import javascript unsafe
"$1[$2]"
js_audioParam :: Node -> JSVal -> JSVal
foreign import javascript unsafe
"$1.onended = $2;"
js_onended :: Node -> Callback (JSVal -> IO ()) -> IO ()
foreign import javascript unsafe
"$1.isSource"
js_isSource :: Node -> Bool
foreign import javascript unsafe
"$1.isSink"
js_isSink :: Node -> Bool
foreign import javascript unsafe
"$1.startable"
js_startable :: Node -> Bool