diff --git a/README.md b/README.md index 5ff0e300..db6c60d6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ There are some other examples to get you started: - **Kitchen sink** ([code](./examples/kitchen-sink) | [sound](https://purescript-wags-kitchen-sink.surge.sh/)) - **The Well-Typed Klavier** ([code](./examples/wtk) | [sound](https://twitter.com/stronglynormal/status/1382221415802408960)) -The **Atari speaks** and **Kitchen sink** examples show how to use `purescript-wags` in a [Halogen](https://github.com/purescript-halogen/purescript-halogen) app. +The examples also show how to use `purescript-wags` in a [Halogen](https://github.com/purescript-halogen/purescript-halogen) app. ## Documentation diff --git a/examples/atari-speaks/AtariSpeaks.purs b/examples/atari-speaks/AtariSpeaks.purs index e5958aa2..e7b980b6 100644 --- a/examples/atari-speaks/AtariSpeaks.purs +++ b/examples/atari-speaks/AtariSpeaks.purs @@ -136,7 +136,7 @@ handleAction = case _ of audioCtx "https://freesound.org/data/previews/100/100981_1234256-lq.mp3" let - ffiAudio = (defaultFFIAudio audioCtx unitCache) { buffers = O.singleton "atar" atar } + ffiAudio = (defaultFFIAudio audioCtx unitCache) { buffers = pure $ O.singleton "atar" atar } unsubscribe <- H.liftEffect $ subscribe diff --git a/examples/drum-machine/DrumMachine.purs b/examples/drum-machine/DrumMachine.purs index ce714d1a..202b3645 100644 --- a/examples/drum-machine/DrumMachine.purs +++ b/examples/drum-machine/DrumMachine.purs @@ -1,17 +1,22 @@ module WAGS.Example.DrumMachine where import Prelude -import Control.Comonad.Cofree (Cofree, mkCofree) +import Control.Comonad.Cofree (Cofree, deferCofree, head, mkCofree, tail) import Control.Promise (toAffE) import Data.Foldable (for_) +import Data.Identity (Identity(..)) import Data.Int (floor, toNumber) import Data.Maybe (Maybe(..)) -import Data.Tuple.Nested (type (/\)) +import Data.Newtype (unwrap) +import Data.Tuple.Nested ((/\), type (/\)) import Effect (Effect) +import Effect.Aff (launchAff_) import Effect.Aff.Class (class MonadAff) import Effect.Class (class MonadEffect) -import Effect.Class.Console as Log -import FRP.Event (subscribe) +import Effect.Ref as Ref +import FRP.Behavior (behavior) +import FRP.Event (makeEvent, subscribe) +import FRP.Event.Time (interval) import Foreign.Object as O import Halogen as H import Halogen.Aff (awaitBody, runHalogenAff) @@ -69,7 +74,9 @@ piece = tgFloor = floor (e.time / gap) crossingDivide = tgFloor /= floor ((e.time + 0.06) / gap) + crossDiff = e.time - lastCrossing + shouldReset = crossingDivide && crossDiff > 0.2 in ichange (scene shouldReset e) $> (if shouldReset then e.time else lastCrossing) @@ -123,24 +130,75 @@ render state = do [ HH.text "Stop audio" ] ] +drumCf :: Cofree Identity String +drumCf = + deferCofree \_ -> + "https://freesound.org/data/previews/321/321132_1337335-hq.mp3" + /\ Identity + ( deferCofree \_ -> + "https://freesound.org/data/previews/331/331589_5820980-hq.mp3" + /\ Identity + ( deferCofree \_ -> + "https://freesound.org/data/previews/84/84478_377011-hq.mp3" + /\ Identity + ( deferCofree \_ -> + "https://freesound.org/data/previews/270/270156_1125482-hq.mp3" + /\ Identity + ( deferCofree \_ -> + "https://freesound.org/data/previews/207/207956_19852-hq.mp3" + /\ Identity drumCf + ) + ) + ) + ) + handleAction :: forall output m. MonadEffect m => MonadAff m => Action -> H.HalogenM State Action () output m Unit handleAction = case _ of StartAudio -> do audioCtx <- H.liftEffect context unitCache <- H.liftEffect makeUnitCache - snare <- + ibuf <- H.liftAff $ toAffE $ decodeAudioDataFromUri audioCtx - "https://freesound.org/data/previews/270/270156_1125482-hq.mp3" + (head drumCf) + rf <- H.liftEffect (Ref.new (unwrap (tail drumCf))) + bf <- H.liftEffect (Ref.new ibuf) + ivlsub <- + H.liftEffect + $ subscribe (interval 1000) \_ -> do + cf <- Ref.read rf + Ref.write (unwrap (tail cf)) rf + launchAff_ do + buf <- + toAffE + $ decodeAudioDataFromUri + audioCtx + (head cf) + H.liftEffect $ Ref.write buf bf let - ffiAudio = (defaultFFIAudio audioCtx unitCache) { buffers = O.singleton "snare" snare } + ffiAudio = + (defaultFFIAudio audioCtx unitCache) + { buffers = + O.singleton "snare" + <$> ( behavior \eAToB -> + makeEvent \fB -> + subscribe eAToB \aToB -> Ref.read bf >>= fB <<< aToB + ) + } unsubscribe <- H.liftEffect $ subscribe (run (pure unit) (pure unit) { easingAlgorithm } (FFIAudio ffiAudio) piece) - (Log.info <<< show) - H.modify_ _ { unsubscribe = unsubscribe, audioCtx = Just audioCtx } + (const (pure unit)) -- (Log.info <<< show) + H.modify_ + _ + { unsubscribe = + do + unsubscribe + ivlsub + , audioCtx = Just audioCtx + } StopAudio -> do { unsubscribe, audioCtx } <- H.get H.liftEffect unsubscribe diff --git a/examples/failure/Failure.purs b/examples/failure/Failure.purs index 3dcd8628..b2c6ec03 100644 --- a/examples/failure/Failure.purs +++ b/examples/failure/Failure.purs @@ -190,14 +190,15 @@ handleAction = case _ of let ffiAudio = (defaultFFIAudio audioCtx unitCache) - { periodicWaves = O.fromFoldable [ "myWavetable" /\ myWave ] + { periodicWaves = pure $ O.fromFoldable [ "myWavetable" /\ myWave ] , buffers = - O.fromFoldable - [ "myBuffer" /\ myBuffer - , "success" /\ success - ] - , floatArrays = O.singleton "success" wicked - , recorders = O.singleton "success" recorder + pure + $ O.fromFoldable + [ "myBuffer" /\ myBuffer + , "success" /\ success + ] + , floatArrays = pure $ O.singleton "success" wicked + , recorders = pure $ O.singleton "success" recorder } unsubscribeFromWAGS <- H.liftEffect diff --git a/examples/hello-world/HelloWorld.purs b/examples/hello-world/HelloWorld.purs index a0dca608..165f5f31 100644 --- a/examples/hello-world/HelloWorld.purs +++ b/examples/hello-world/HelloWorld.purs @@ -3,19 +3,28 @@ module WAGS.Example.HelloWorld where import Prelude import Control.Comonad.Cofree (Cofree, mkCofree) +import Data.Foldable (for_) import Data.Functor.Indexed (ivoid) +import Data.Maybe (Maybe(..)) import Data.Tuple.Nested (type (/\)) import Effect (Effect) +import Effect.Aff.Class (class MonadAff) +import Effect.Class (class MonadEffect) import FRP.Event (subscribe) +import Halogen as H +import Halogen.Aff (awaitBody, runHalogenAff) +import Halogen.HTML as HH +import Halogen.HTML.Events as HE +import Halogen.VDom.Driver (runUI) import Math (pi, sin) import WAGS.Change (ichange) import WAGS.Control.Functions.Validated (iloop, (@!>)) import WAGS.Control.Types (Frame0, Scene) import WAGS.Create (icreate) +import WAGS.Interpret (AudioContext, FFIAudio(..), close, context, defaultFFIAudio, makeUnitCache) +import WAGS.Run (RunAudio, SceneI, RunEngine, run) import WAGS.Create.Optionals (CGain, CSpeaker, CSinOsc, gain, sinOsc, speaker) import WAGS.Graph.AudioUnit (TGain, TSinOsc, TSpeaker) -import WAGS.Interpret (FFIAudio(..), FFIAudio') -import WAGS.Run (RunAudio, SceneI, RunEngine, run) type SceneTemplate = CSpeaker @@ -59,11 +68,63 @@ easingAlgorithm = in fOf 20 -myRun :: FFIAudio' -> Effect (Effect Unit) -myRun ffiAudio = - subscribe - (run (pure unit) (pure unit) { easingAlgorithm } (FFIAudio ffiAudio) piece) - (const $ pure unit) - main :: Effect Unit -main = pure unit +main = + runHalogenAff do + body <- awaitBody + runUI component unit body + +type State + = { unsubscribe :: Effect Unit + , audioCtx :: Maybe AudioContext + } + +data Action + = StartAudio + | StopAudio + +component :: forall query input output m. MonadEffect m => MonadAff m => H.Component query input output m +component = + H.mkComponent + { initialState + , render + , eval: H.mkEval $ H.defaultEval { handleAction = handleAction } + } + +initialState :: forall input. input -> State +initialState _ = + { unsubscribe: pure unit + , audioCtx: Nothing + } + +render :: forall m. State -> H.ComponentHTML Action () m +render state = do + HH.div_ + [ HH.h1_ + [ HH.text "Hello world" ] + , HH.button + [ HE.onClick \_ -> StartAudio ] + [ HH.text "Start audio" ] + , HH.button + [ HE.onClick \_ -> StopAudio ] + [ HH.text "Stop audio" ] + ] + +handleAction :: forall output m. MonadEffect m => MonadAff m => Action -> H.HalogenM State Action () output m Unit +handleAction = case _ of + StartAudio -> do + audioCtx <- H.liftEffect context + unitCache <- H.liftEffect makeUnitCache + let + ffiAudio = defaultFFIAudio audioCtx unitCache + unsubscribe <- + H.liftEffect + $ subscribe + (run (pure unit) (pure unit) { easingAlgorithm } (FFIAudio ffiAudio) piece) + (const $ pure unit) + H.modify_ _ { unsubscribe = unsubscribe, audioCtx = Just audioCtx } + StopAudio -> do + { unsubscribe, audioCtx } <- H.get + H.liftEffect unsubscribe + for_ audioCtx (H.liftEffect <<< close) + H.modify_ _ { unsubscribe = pure unit, audioCtx = Nothing } \ No newline at end of file diff --git a/examples/hello-world/index.html b/examples/hello-world/index.html index 3231e46a..fa2f800f 100644 --- a/examples/hello-world/index.html +++ b/examples/hello-world/index.html @@ -1,31 +1,6 @@ -
- - diff --git a/examples/kitchen-sink/KitchenSink.purs b/examples/kitchen-sink/KitchenSink.purs index cf312f48..732a27b2 100644 --- a/examples/kitchen-sink/KitchenSink.purs +++ b/examples/kitchen-sink/KitchenSink.purs @@ -134,16 +134,17 @@ handleAction = case _ of let ffiAudio = (defaultFFIAudio audioCtx unitCache) - { periodicWaves = O.fromFoldable [ "my-wave" /\ myWave ] + { periodicWaves = pure $ O.fromFoldable [ "my-wave" /\ myWave ] , buffers = - O.fromFoldable - [ "my-buffer" /\ chimes - , "shruti" /\ shruti - , "reverb" /\ reverb - ] - , floatArrays = O.singleton "my-waveshaper" wicked - , recorders = O.singleton "my-recorder" recorder - , microphone = toNullable microphone + pure + $ O.fromFoldable + [ "my-buffer" /\ chimes + , "shruti" /\ shruti + , "reverb" /\ reverb + ] + , floatArrays = pure $ O.singleton "my-waveshaper" wicked + , recorders = pure $ O.singleton "my-recorder" recorder + , microphone = pure $ toNullable microphone } unsubscribeFromWAGS <- H.liftEffect diff --git a/examples/makenna/Makenna.purs b/examples/makenna/Makenna.purs index e58a917f..623c58a5 100644 --- a/examples/makenna/Makenna.purs +++ b/examples/makenna/Makenna.purs @@ -232,7 +232,7 @@ handleAction = case _ of let ffiAudio = (defaultFFIAudio audioCtx unitCache) - { periodicWaves = O.singleton "bday" bday + { periodicWaves = pure (O.singleton "bday" bday) } unsubscribe <- subscribe diff --git a/examples/wtk/WTK/RenderingEnv.purs b/examples/wtk/WTK/RenderingEnv.purs index 9a59a5f1..38a990aa 100644 --- a/examples/wtk/WTK/RenderingEnv.purs +++ b/examples/wtk/WTK/RenderingEnv.purs @@ -3,13 +3,14 @@ module WAGS.Example.WTK.RenderingEnv where import Prelude import Data.Int (toNumber) import Data.List (List(..), (:), filter, length, drop, zipWith) +import Data.Maybe (maybe) import Data.Set as S import Data.Tuple.Nested ((/\), type (/\)) import FRP.Event.MIDI (MIDIEvent(..)) import Math (pow) -import WAGS.Math (calcSlope) import WAGS.Example.WTK.Types (KeyUnit, MakeRenderingEnv) import WAGS.Graph.AudioUnit (OnOff(..)) +import WAGS.Math (calcSlope) keyDur :: Number keyDur = 1.6 @@ -60,7 +61,7 @@ midiEventsToOnsets = go Nil Nil go accOn accOff (_ : b) = go accOn accOff b makeRenderingEnv :: MakeRenderingEnv -makeRenderingEnv active trigger time availableKeys currentKeys = +makeRenderingEnv trigger time availableKeys currentKeys = { notesOff , onsets , newCurrentKeys @@ -71,7 +72,7 @@ makeRenderingEnv active trigger time availableKeys currentKeys = where notesOn /\ notesOffAsList = midiEventsToOnsets - (if active then (map _.value.event trigger) else Nil) + (maybe Nil (map _.value.event) trigger) notesOff = S.fromFoldable notesOffAsList diff --git a/examples/wtk/WTK/TLP.purs b/examples/wtk/WTK/TLP.purs index 9db2b973..09220486 100644 --- a/examples/wtk/WTK/TLP.purs +++ b/examples/wtk/WTK/TLP.purs @@ -146,7 +146,7 @@ createFrame _ = piece :: { makeRenderingEnv :: MakeRenderingEnv } -> Scene (SceneI Trigger Unit) RunAudio RunEngine Frame0 Unit piece { makeRenderingEnv } = createFrame - @!> iloop \{ time, trigger, active } { currentKeys, availableKeys } -> Ix.do + @!> iloop \{ time, trigger } { currentKeys, availableKeys } -> Ix.do let { notesOff , onsets @@ -154,7 +154,7 @@ piece { makeRenderingEnv } = , newAvailableKeys , futureCurrentKeys , futureAvailableKeys - } = makeRenderingEnv active trigger time availableKeys currentKeys + } = makeRenderingEnv trigger time availableKeys currentKeys ( playKeys { currentTime: time , notesOff diff --git a/examples/wtk/WTK/Types.purs b/examples/wtk/WTK/Types.purs index f1a4c684..731ad5e9 100644 --- a/examples/wtk/WTK/Types.purs +++ b/examples/wtk/WTK/Types.purs @@ -1,13 +1,15 @@ module WAGS.Example.WTK.Types where import Prelude + import Data.DateTime.Instant (Instant) import Data.List (List) +import Data.Maybe (Maybe) import Data.Set as S import Data.Tuple.Nested (type (/\)) import FRP.Event.MIDI (MIDIEventInTime) -import WAGS.Graph.AudioUnit (OnOff(..), TGain, TSinOsc, TSpeaker) import WAGS.Create.Optionals (CGain, CSinOsc, CSpeaker, gain, sinOsc, speaker) +import WAGS.Graph.AudioUnit (OnOff(..), TGain, TSinOsc, TSpeaker) data Key = K0 @@ -99,8 +101,7 @@ type KeyInfo } type MakeRenderingEnv - = Boolean -> - Trigger -> + = Maybe Trigger -> Number -> List Key -> List KeyInfo -> diff --git a/src/WAGS/Interpret.js b/src/WAGS/Interpret.js index 74f99c2f..a50737b8 100644 --- a/src/WAGS/Interpret.js +++ b/src/WAGS/Interpret.js @@ -323,7 +323,6 @@ exports.makeLoopBuf_ = function (ptr) { state.units[ptr] = { outgoing: [], incoming: [], - buffer: a, createFunction: createFunction, resumeClosure: { playbackRate: function (i) { @@ -335,19 +334,21 @@ exports.makeLoopBuf_ = function (ptr) { loopEnd: function (i) { i.loopEnd = d; }, + buffer: function (i) { + if (!state.buffers[a]) { + console.error( + "Buffer does not exist for key " + + a + + ". Using a dummy buffer. Check your code!" + ); + } + i.buffer = state.buffers[a]; + }, }, main: createFunction(), }; applyResumeClosure(state.units[ptr]); if (onOff) { - if (!state.buffers[a]) { - console.error( - "Looping buffer does not exist for key " + - a + - ". Using a dummy buffer. Check your code!" - ); - } - state.units[ptr].main.buffer = state.buffers[a]; state.units[ptr].main.start( state.writeHead + b.timeOffset, c @@ -579,13 +580,22 @@ exports.makePlayBuf_ = function (ptr) { state.units[ptr] = { outgoing: [], incoming: [], - buffer: a, bufferOffset: b, createFunction: createFunction, resumeClosure: { playbackRate: function (i) { genericStarter(i, "playbackRate", c); }, + buffer: function (i) { + if (!state.buffers[a]) { + console.error( + "Buffer does not exist for key " + + a + + ". Using a dummy buffer. Check your code!" + ); + } + i.buffer = state.buffers[a]; + }, }, main: createFunction(), }; @@ -598,7 +608,6 @@ exports.makePlayBuf_ = function (ptr) { ". Using a dummy buffer. Check your code!" ); } - state.units[ptr].main.buffer = state.buffers[a]; state.units[ptr].main.start(state.writeHead + c.timeOffset, b); } state.units[ptr].onOff = onOff; @@ -875,8 +884,8 @@ exports.setOnOff_ = function (ptr) { } else if (onOff.param === "off") { setOff_(ptr)(onOff)(state)(); } else if (onOff.param === "offOn") { - setOff_(ptr)({param: "off", timeOffset: 0.0})(state)(); - setOn_(ptr)({param: "on", timeOffset: onOff.timeOffset})(state)(); + setOff_(ptr)({ param: "off", timeOffset: 0.0 })(state)(); + setOn_(ptr)({ param: "on", timeOffset: onOff.timeOffset })(state)(); } }; }; diff --git a/src/WAGS/Interpret.purs b/src/WAGS/Interpret.purs index b3d8fc9a..e97526ca 100644 --- a/src/WAGS/Interpret.purs +++ b/src/WAGS/Interpret.purs @@ -6,13 +6,15 @@ module WAGS.Interpret , class SafeToFFI , AudioBuffer , AudioContext + , FFIAudioWithBehaviors , BrowserAudioBuffer , BrowserCamera , BrowserFloatArray , BrowserMicrophone , BrowserPeriodicWave , FFIAudio(..) - , FFIAudio' + , FFIAudioSnapshot' + , FFIAudioSnapshot(..) , FFINumericAudioParameter , MediaRecorder , audioBuffer @@ -87,6 +89,7 @@ module WAGS.Interpret ) where import Prelude + import Control.Plus (empty) import Control.Promise (Promise, toAffE) import Data.Either (Either(..)) @@ -98,6 +101,7 @@ import Data.Vec (Vec) import Data.Vec as V import Effect (Effect) import Effect.Aff (Aff) +import FRP.Behavior (Behavior) import Foreign (Foreign) import Foreign.Object (Object) import Foreign.Object as O @@ -236,7 +240,7 @@ audioBuffer i v = AudioBuffer i (map V.toArray $ V.toArray v) -- | - `buffers` - an object containing named audio buffers for playback using `PlayBuf` or `LoopBuf`. See the `atari-speaks` example to see how a buffer is used. -- | - `floatArrays` - arrays of 32=bit floats used for wave shaping. -- | - `periodicWaves` - array of periodic waves used for creating oscillator nodes. -type FFIAudio' +type FFIAudioSnapshot' = { context :: AudioContext , writeHead :: Number , units :: Foreign @@ -247,22 +251,37 @@ type FFIAudio' , periodicWaves :: Object BrowserPeriodicWave } +type FFIAudioWithBehaviors + = { context :: AudioContext + , writeHead :: Number + , units :: Foreign + , microphone :: Behavior (Nullable BrowserMicrophone) + , recorders :: Behavior (Object (MediaRecorder -> Effect Unit)) + , buffers :: Behavior (Object BrowserAudioBuffer) + , floatArrays :: Behavior (Object BrowserFloatArray) + , periodicWaves :: Behavior (Object BrowserPeriodicWave) + } + -- A default FFI audio with empty objects (ie no buffers, no microphone, etc). -defaultFFIAudio :: AudioContext -> Foreign -> FFIAudio' +defaultFFIAudio :: AudioContext -> Foreign -> FFIAudioWithBehaviors defaultFFIAudio audioCtx unitCache = { context: audioCtx , writeHead: 0.0 , units: unitCache - , microphone: null - , recorders: O.empty - , buffers: O.empty - , floatArrays: O.empty - , periodicWaves: O.empty + , microphone: pure null + , recorders: pure O.empty + , buffers: pure O.empty + , floatArrays: pure O.empty + , periodicWaves: pure O.empty } -- FFIAudio as a newtype in order to use it in typeclass instances. newtype FFIAudio - = FFIAudio FFIAudio' + = FFIAudio FFIAudioWithBehaviors + +-- FFIAudio as a newtype in order to use it in typeclass instances. +newtype FFIAudioSnapshot + = FFIAudioSnapshot FFIAudioSnapshot' -- | A class with all possible instructions for interpreting audio. -- | The class is paramaterized by two types: @@ -425,111 +444,111 @@ instance freeAudioInterpret :: AudioInterpret Unit Instruction where setPlaybackRate a b = const $ SetPlaybackRate a b setFrequency a b = const $ SetFrequency a b -foreign import connectXToY_ :: String -> String -> FFIAudio' -> Effect Unit +foreign import connectXToY_ :: String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import disconnectXFromY_ :: String -> String -> FFIAudio' -> Effect Unit +foreign import disconnectXFromY_ :: String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import destroyUnit_ :: String -> FFIAudio' -> Effect Unit +foreign import destroyUnit_ :: String -> FFIAudioSnapshot' -> Effect Unit -foreign import rebaseAllUnits_ :: Array { from :: String, to :: String } -> FFIAudio' -> Effect Unit +foreign import rebaseAllUnits_ :: Array { from :: String, to :: String } -> FFIAudioSnapshot' -> Effect Unit -foreign import makeAllpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeAllpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeBandpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeBandpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeConstant_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeConstant_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeConvolver_ :: String -> String -> FFIAudio' -> Effect Unit +foreign import makeConvolver_ :: String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import makeDelay_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeDelay_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeDynamicsCompressor_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeDynamicsCompressor_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeGain_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeGain_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeHighpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeHighpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeHighshelf_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeHighshelf_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeLoopBufWithDeferredBuffer_ :: String -> FFIAudio' -> Effect Unit +foreign import makeLoopBufWithDeferredBuffer_ :: String -> FFIAudioSnapshot' -> Effect Unit -foreign import makeLoopBuf_ :: String -> String -> FFIStringAudioParameter -> FFINumericAudioParameter -> Number -> Number -> FFIAudio' -> Effect Unit +foreign import makeLoopBuf_ :: String -> String -> FFIStringAudioParameter -> FFINumericAudioParameter -> Number -> Number -> FFIAudioSnapshot' -> Effect Unit -foreign import makeLowpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeLowpass_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeLowshelf_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeLowshelf_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeMicrophone_ :: FFIAudio' -> Effect Unit +foreign import makeMicrophone_ :: FFIAudioSnapshot' -> Effect Unit -foreign import makeNotch_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeNotch_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makePeaking_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makePeaking_ :: String -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makePeriodicOscWithDeferredOsc_ :: String -> FFIAudio' -> Effect Unit +foreign import makePeriodicOscWithDeferredOsc_ :: String -> FFIAudioSnapshot' -> Effect Unit -foreign import makePeriodicOsc_ :: String -> String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makePeriodicOsc_ :: String -> String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makePeriodicOscV_ :: String -> (Array (Array Number)) -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makePeriodicOscV_ :: String -> (Array (Array Number)) -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makePlayBufWithDeferredBuffer_ :: String -> FFIAudio' -> Effect Unit +foreign import makePlayBufWithDeferredBuffer_ :: String -> FFIAudioSnapshot' -> Effect Unit -foreign import makePlayBuf_ :: String -> String -> Number -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makePlayBuf_ :: String -> String -> Number -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeRecorder_ :: String -> String -> FFIAudio' -> Effect Unit +foreign import makeRecorder_ :: String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import makeSawtoothOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeSawtoothOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeSinOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeSinOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeSpeaker_ :: FFIAudio' -> Effect Unit +foreign import makeSpeaker_ :: FFIAudioSnapshot' -> Effect Unit -foreign import makeSquareOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeSquareOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeStereoPanner_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeStereoPanner_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeTriangleOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import makeTriangleOsc_ :: String -> FFIStringAudioParameter -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import makeWaveShaper_ :: String -> String -> String -> FFIAudio' -> Effect Unit +foreign import makeWaveShaper_ :: String -> String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import setOnOff_ :: String -> FFIStringAudioParameter -> FFIAudio' -> Effect Unit +foreign import setOnOff_ :: String -> FFIStringAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setBufferOffset_ :: String -> Number -> FFIAudio' -> Effect Unit +foreign import setBufferOffset_ :: String -> Number -> FFIAudioSnapshot' -> Effect Unit -foreign import setLoopStart_ :: String -> Number -> FFIAudio' -> Effect Unit +foreign import setLoopStart_ :: String -> Number -> FFIAudioSnapshot' -> Effect Unit -foreign import setLoopEnd_ :: String -> Number -> FFIAudio' -> Effect Unit +foreign import setLoopEnd_ :: String -> Number -> FFIAudioSnapshot' -> Effect Unit -foreign import setRatio_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setRatio_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setOffset_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setOffset_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setAttack_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setAttack_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setGain_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setGain_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setQ_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setQ_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setPan_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setPan_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setThreshold_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setThreshold_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setRelease_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setRelease_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setKnee_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setKnee_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setDelay_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setDelay_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setPlaybackRate_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setPlaybackRate_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setFrequency_ :: String -> FFINumericAudioParameter -> FFIAudio' -> Effect Unit +foreign import setFrequency_ :: String -> FFINumericAudioParameter -> FFIAudioSnapshot' -> Effect Unit -foreign import setBuffer_ :: String -> String -> FFIAudio' -> Effect Unit +foreign import setBuffer_ :: String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import setPeriodicOsc_ :: String -> String -> FFIAudio' -> Effect Unit +foreign import setPeriodicOsc_ :: String -> String -> FFIAudioSnapshot' -> Effect Unit -foreign import setPeriodicOscV_ :: String -> Array (Array Number) -> FFIAudio' -> Effect Unit +foreign import setPeriodicOscV_ :: String -> Array (Array Number) -> FFIAudioSnapshot' -> Effect Unit -instance effectfulAudioInterpret :: AudioInterpret FFIAudio (Effect Unit) where +instance effectfulAudioInterpret :: AudioInterpret FFIAudioSnapshot (Effect Unit) where connectXToY a b c = connectXToY_ (safeToFFI a) (safeToFFI b) (safeToFFI c) disconnectXFromY a b c = disconnectXFromY_ (safeToFFI a) (safeToFFI b) (safeToFFI c) destroyUnit a b = destroyUnit_ (safeToFFI a) (safeToFFI b) @@ -604,8 +623,8 @@ instance safeToFFI_Oversample :: SafeToFFI Oversample String where TwoX -> "2x" FourX -> "4x" -instance safeToFFI_FFIAudio :: SafeToFFI FFIAudio FFIAudio' where - safeToFFI (FFIAudio x) = x +instance safeToFFI_FFIAudio :: SafeToFFI FFIAudioSnapshot FFIAudioSnapshot' where + safeToFFI (FFIAudioSnapshot x) = x -- | An AudioParameter with the `transition` field stringly-typed for easier rendering in the FFI and cancelation as a boolean type FFINumericAudioParameter diff --git a/src/WAGS/Run.purs b/src/WAGS/Run.purs index f813da60..f39d8a1a 100644 --- a/src/WAGS/Run.purs +++ b/src/WAGS/Run.purs @@ -18,6 +18,7 @@ import Data.Foldable (for_) import Data.Int (floor, toNumber) import Data.List (List(..)) import Data.Maybe (Maybe(..), isNothing) +import Data.Nullable (Nullable) import Data.Time.Duration (Milliseconds) import Data.Tuple (fst, snd) import Data.Tuple.Nested ((/\), type (/\)) @@ -28,13 +29,14 @@ import FRP.Behavior (Behavior, sampleBy, sample_) import FRP.Behavior.Time (instant) import FRP.Event (Event, makeEvent, subscribe) import FRP.Event.Time (withTime, delay) -import Record as R +import Foreign (Foreign) +import Foreign.Object (Object) import WAGS.Control.Types (Frame0, Scene, oneFrame) -import WAGS.Interpret (FFIAudio(..), FFIAudio', getAudioClockTime, renderAudio) +import WAGS.Interpret (AudioContext, BrowserAudioBuffer, BrowserFloatArray, BrowserMicrophone, BrowserPeriodicWave, FFIAudio(..), FFIAudioSnapshot(..), MediaRecorder, getAudioClockTime, renderAudio) import WAGS.Rendered (Instruction) type RunAudio - = Unit /\ FFIAudio + = Unit /\ FFIAudioSnapshot type RunEngine = Instruction /\ Effect Unit @@ -55,19 +57,40 @@ run :: FFIAudio -> Scene (SceneI trigger world) - (Unit /\ FFIAudio) - (Instruction /\ Effect Unit) + RunAudio + RunEngine Frame0 res -> Event (Run res) -run trigger world' engineInfo (FFIAudio audio') scene = +run trigger world' engineInfo (FFIAudio audioWithBehaviors) scene = makeEvent \k -> do - audioClockStart <- getAudioClockTime audio'.context + audioClockStart <- getAudioClockTime audioWithBehaviors.context currentTimeoutCanceler <- Ref.new (pure unit :: Effect Unit) currentScene <- Ref.new scene currentEasingAlg <- Ref.new engineInfo.easingAlgorithm let - eventAndEnv = sampleBy (\{ world, sysTime } b -> { trigger: b, world, sysTime, active: true }) newWorld trigger + eventAndEnv = + sampleBy + ( \{ world + , sysTime + , microphone + , recorders + , buffers + , floatArrays + , periodicWaves + } b -> + { trigger: Just b + , world + , sysTime + , microphone + , recorders + , buffers + , floatArrays + , periodicWaves + } + ) + newWorld + trigger unsubscribe <- subscribe eventAndEnv \ee -> do cancelTimeout <- Ref.read currentTimeoutCanceler @@ -79,14 +102,32 @@ run trigger world' engineInfo (FFIAudio audio') scene = currentTimeoutCanceler currentEasingAlg currentScene - audio' + { context: audioWithBehaviors.context + , units: audioWithBehaviors.units + , writeHead: audioWithBehaviors.writeHead + } k pure do cancelTimeout <- Ref.read currentTimeoutCanceler cancelTimeout unsubscribe where - newWorld = (\world sysTime -> { world, sysTime }) <$> world' <*> (map unInstant instant) + newWorld = + { world: _ + , sysTime: _ + , microphone: _ + , recorders: _ + , buffers: _ + , floatArrays: _ + , periodicWaves: _ + } + <$> world' + <*> (map unInstant instant) + <*> audioWithBehaviors.microphone + <*> audioWithBehaviors.recorders + <*> audioWithBehaviors.buffers + <*> audioWithBehaviors.floatArrays + <*> audioWithBehaviors.periodicWaves -- | The information provided to `run` that tells the engine how to make certain rendering tradeoffs. type EngineInfo @@ -109,6 +150,12 @@ type EngineInfo type EasingAlgorithm = Cofree ((->) Int) Int +type NonBehavioralFFIInfo + = { context :: AudioContext + , writeHead :: Number + , units :: Foreign + } + type Run res = { instructions :: Array Instruction , res :: res @@ -116,18 +163,16 @@ type Run res -- | The input type to a scene that is handled by `run`. Given `Event trigger` and `Behavior world`, the scene will receive: -- | --- | `trigger` - the trigger. +-- | `trigger` - the trigger. If none exists (meaning we are polling) it will be Nothing. -- | `world` - the world. -- | `time` - the time of the audio context. -- | `sysTime` - the time provided by `new Date().getTime()` --- | `active` - whether this event was caused by a trigger or is a measurement of the world. This is useful to not repeat onsets from the trigger. -- | `headroom` - the amount of lookahead time. If you are programming a precise rhythmic event and need the onset to occur at a specific moment, you can use `headroom` to determine if the apex should happen now or later. type SceneI trigger world - = { trigger :: trigger + = { trigger :: Maybe trigger , world :: world , time :: Number , sysTime :: Milliseconds - , active :: Boolean , headroom :: Int } @@ -183,28 +228,40 @@ runInternal :: Monoid res => Number -> { world :: world - , trigger :: trigger + , trigger :: Maybe trigger , sysTime :: Milliseconds - , active :: Boolean + , microphone :: Nullable BrowserMicrophone + , recorders :: Object (MediaRecorder -> Effect Unit) + , buffers :: Object BrowserAudioBuffer + , floatArrays :: Object BrowserFloatArray + , periodicWaves :: Object BrowserPeriodicWave } -> - Behavior { world :: world, sysTime :: Milliseconds } -> + Behavior + { world :: world + , sysTime :: Milliseconds + , microphone :: Nullable BrowserMicrophone + , recorders :: Object (MediaRecorder -> Effect Unit) + , buffers :: Object BrowserAudioBuffer + , floatArrays :: Object BrowserFloatArray + , periodicWaves :: Object BrowserPeriodicWave + } -> Ref.Ref (Effect Unit) -> Ref.Ref EasingAlgorithm -> Ref.Ref ( Scene (SceneI trigger world) - (Unit /\ FFIAudio) - (Instruction /\ Effect Unit) + RunAudio + RunEngine Frame0 res ) -> - FFIAudio' -> + NonBehavioralFFIInfo -> (Run res -> Effect Unit) -> Effect Unit -runInternal audioClockStart worldAndTrigger world' currentTimeoutCanceler currentEasingAlg currentScene audio' reporter = do +runInternal audioClockStart fromEvents world' currentTimeoutCanceler currentEasingAlg currentScene nonBehavioralFFIInfo reporter = do easingAlgNow <- Ref.read currentEasingAlg sceneNow <- Ref.read currentScene - audioClockPriorToComputation <- getAudioClockTime audio'.context + audioClockPriorToComputation <- getAudioClockTime nonBehavioralFFIInfo.context let -- this is how far in the future we are telling the -- algorithm to calculate with respect to the audio clock @@ -216,14 +273,28 @@ runInternal audioClockStart worldAndTrigger world' currentTimeoutCanceler curren time = (audioClockPriorToComputation - audioClockStart) + headroomInSeconds - fromScene = oneFrame sceneNow (R.union worldAndTrigger { time, headroom }) - audioClockAfterComputation <- getAudioClockTime audio'.context + fromScene = + oneFrame sceneNow + ( { world: fromEvents.world + , trigger: fromEvents.trigger + , sysTime: fromEvents.sysTime + , time + , headroom + } + ) + audioClockAfterComputation <- getAudioClockTime nonBehavioralFFIInfo.context let ffi = - FFIAudio - $ audio' - { writeHead = max audioClockAfterComputation (audioClockPriorToComputation + headroomInSeconds) - } + FFIAudioSnapshot + { context: nonBehavioralFFIInfo.context + , writeHead: max audioClockAfterComputation (audioClockPriorToComputation + headroomInSeconds) + , units: nonBehavioralFFIInfo.units + , microphone: fromEvents.microphone + , recorders: fromEvents.recorders + , buffers: fromEvents.buffers + , floatArrays: fromEvents.floatArrays + , periodicWaves: fromEvents.periodicWaves + } applied = map (\f -> f (unit /\ ffi)) fromScene.instructions renderAudio (map snd applied) @@ -236,8 +307,30 @@ runInternal audioClockStart worldAndTrigger world' currentTimeoutCanceler curren reporter { instructions: map fst applied, res: fromScene.res } -- we thunk the world and move on to the next event -- note that if we did not allocate enough time, we still - -- set a timeout of 1 so that th canceler can run in case it needs to + -- set a timeout of 1 so that the canceler can run in case it needs to canceler <- - subscribe (sample_ world' (delay (max 1 remainingTimeInMs) (pure unit))) \{ world, sysTime } -> - runInternal audioClockStart { world, sysTime, trigger: worldAndTrigger.trigger, active: false } world' currentTimeoutCanceler currentEasingAlg currentScene audio' reporter + subscribe (sample_ world' (delay (max 1 remainingTimeInMs) (pure unit))) \{ world + , sysTime + , microphone + , recorders + , buffers + , floatArrays + , periodicWaves + } -> + runInternal audioClockStart + { world + , sysTime + , trigger: Nothing + , microphone + , recorders + , buffers + , floatArrays + , periodicWaves + } + world' + currentTimeoutCanceler + currentEasingAlg + currentScene + nonBehavioralFFIInfo + reporter Ref.write canceler currentTimeoutCanceler