├── .github └── workflows │ └── basic.yml ├── .gitignore ├── README.md ├── custom-units └── ps-aud-mul.js ├── examples.dhall ├── examples ├── audio-worklet │ ├── AudioWorklet.purs │ ├── add-processor.js │ ├── gain-processor.js │ ├── index.html │ └── white-noise-processor.js ├── dupsplit │ ├── DupSplit.purs │ └── index.html ├── exporter │ ├── Exporter.purs │ └── index.html ├── hello-world │ ├── HelloWorld.purs │ └── index.html ├── koans │ ├── .gitattributes │ ├── Koans.purs │ ├── forest.mp3 │ ├── index.html │ └── moo.js ├── metronome │ ├── Metronome.purs │ └── index.html ├── midi-in │ ├── MidiIn.purs │ └── index.html ├── readme │ ├── .gitattributes │ ├── Readme.purs │ ├── forest.mp3 │ └── index.html ├── regression │ ├── Regression.purs │ └── index.html └── stress0 │ ├── Stress0.purs │ └── index.html ├── package-lock.json ├── package.json ├── packages.dhall ├── spago.dhall ├── src └── FRP │ ├── Behavior │ ├── Audio.js │ ├── Audio.purs │ └── MIDI.purs │ └── Event │ ├── MIDI.js │ └── MIDI.purs ├── test.dhall └── test ├── Basic.purs └── Main.purs /.github/workflows/basic.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [12.x, 14.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install -g purescript spago 21 | - run: npm install 22 | - run: npm test 23 | env: 24 | CI: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components/ 2 | /node_modules/ 3 | /.pulp-cache/ 4 | /output/ 5 | /generated-docs/ 6 | /.psc-package/ 7 | /.psc* 8 | /.purs* 9 | /.psa* 10 | /.spago 11 | /dist 12 | /ffi/ 13 | .vscode 14 | .venv 15 | examples/hello-world/index.js 16 | examples/hello-world/ps-aud-mul.js 17 | examples/regression/index.js 18 | examples/regression/moo.js 19 | examples/regression/ps-aud-mul.js 20 | examples/midi-in/index.js 21 | examples/midi-in/ps-aud-mul.js 22 | examples/exporter/index.js 23 | examples/exporter/ps-aud-mul.js 24 | examples/dupsplit/index.js 25 | examples/dupsplit/ps-aud-mul.js 26 | examples/audio-worklet/index.js 27 | examples/audio-worklet/ps-aud-mul.js 28 | examples/metronome/index.js 29 | examples/metronome/ps-aud-mul.js 30 | examples/readme/index.js 31 | examples/readme/ps-aud-mul.js 32 | examples/stress0/index.js 33 | examples/stress0/ps-aud-mul.js 34 | examples/koans/index.js 35 | examples/koans/ps-aud-mul.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-audio-behaviors 2 | 3 | > UPDATE. This repo is archived and is no longer being maintained. I've since created [`purescript-wags`](https://github.com/mikesol/purescript-wags), which is faster and more ergonomic. Please use that! 4 | 5 | [`purescript-behaviors`](https://github.com/mikesol/purescript-behaviors) for web audio. 6 | 7 | ## Demo 8 | 9 | Check out [klank.dev](https://github.com/mikesol/klank.dev), where the `klank-studio` directory has examples of this being used in the browser. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | spago install 15 | ``` 16 | 17 | ## Build 18 | 19 | ```bash 20 | spago build 21 | ``` 22 | 23 | ## Main idea 24 | 25 | This library uses the [behaviors pattern](https://wiki.haskell.org/Functional_Reactive_Programming) pioneered by Conal Elliott and Paul Hudak. You describe the way audio should behave at a given time, and the function is sampled at regular intervals to build the audio graph. 26 | 27 | For example, consider the following behavior, taken from [`HelloWorld.purs`](./examples/hello-world/HelloWorld.purs): 28 | 29 | ```haskell 30 | scene :: Number -> Behavior (AudioUnit D1) 31 | scene time = let 32 | rad = pi * time 33 | in 34 | pure $ speaker 35 | ( (gain' 0.1 $ sinOsc (440.0 + (10.0 * sin (2.3 * rad)))) 36 | :| (gain' 0.25 $ sinOsc (235.0 + (10.0 * sin (1.7 * rad)))) 37 | : (gain' 0.2 $ sinOsc (337.0 + (10.0 * sin rad))) 38 | : (gain' 0.1 $ sinOsc (530.0 + (19.0 * (5.0 * sin rad)))) 39 | : Nil 40 | ) 41 | ``` 42 | 43 | Here, there are four sine wave oscillators whose frequencies modulate subtly based on time, creating an eerie Theremin effect. Under the hood, this library samples the function to know what the frequencies should be played at any given time and makes sure they are rendered to the speaker. 44 | 45 | ## Building a scene 46 | 47 | The main unit of work in `purescript-audio-behaviors` is the **scene**. A scene, like the one above, is a function of time, where the input time comes from the audio clock at regular intervals. 48 | 49 | In this section, we'll build a scene from the ground up. In doing so, we'll accomplish several things: 50 | 51 | 1. Getting a static sound to play. 52 | 1. Adding sound via the microphone. 53 | 1. Adding playback from an `audio` tag. 54 | 1. Going from mono to stereo. 55 | 1. Getting the sound to change as a function of time. 56 | 1. Getting the sound to change as a function of a mouse input event. 57 | 1. Making sure that certain sounds occur at a precise time. 58 | 1. Remembering when events happened. 59 | 1. Working with feedback. 60 | 1. Adding visuals. 61 | 62 | ### Getting a static sound to play 63 | 64 | Let's start with a sine wave at A440 playing at a volume of `0.5` (where `1.0` is the loudest volume). 65 | 66 | ```haskell 67 | scene :: AudioUnit D1 68 | scene = speaker' $ (gain' 0.5 $ sinOsc 440.0) 69 | ``` 70 | 71 | For simple audio graphs, we do not need to use behaviors and can just use the `AudioUnit ch` type, where `ch` is the number of channels prefixed by `D`. As the example above is mono, `D1` is the number of channels. 72 | 73 | ### Adding sound via the microphone 74 | 75 | Let's add our voice to the mix! We'll put it above a nice low drone. 76 | 77 | ```haskell 78 | scene :: AudioUnit D1 79 | scene = 80 | speaker 81 | $ ( (gain' 0.2 $ sinOsc 110.0) 82 | :| (gain' 0.1 $ sinOsc 220.0) 83 | : microphone 84 | : Nil 85 | ) 86 | ``` 87 | 88 | Make sure to wear headphones to avoid feedback! 89 | 90 | ### Adding playback from an audio tag 91 | 92 | Let's add some soothing jungle sounds to the mix. We use the function `play` to add an audio element. This function assumes that you provide an audio element with the appropriate tag to the toplevel `runInBrowser` function. In this case, the tag is `"forest"`. 93 | 94 | ```haskell 95 | -- assuming we have passed in an object 96 | -- with { forest: new Audio("my-recording.mp3") } 97 | -- to `runInBrowser` 98 | scene :: AudioUnit D1 99 | scene = 100 | speaker 101 | $ ( (gain' 0.2 $ sinOsc 110.0) 102 | :| (gain' 0.1 $ sinOsc 220.0) 103 | : (gain' 0.5 $ (playBuf "forest" 1.0)) 104 | : microphone 105 | : Nil 106 | ) 107 | ``` 108 | 109 | ### Going from mono to stereo 110 | 111 | To go from mono to stereo, there is a class of functions called `dupX`, `splitX` and `merger`. In the example below, we use `dup1` to duplicate a mono sound and then `merge` it into two stereo tracks. 112 | 113 | If you want to make two separate audio units, then you can use a normal let block. If, on the other hand, you want to use the same underlying unit, use `dupX`. When in doubt, use `dupX`, as you'll rarely need to duplicate an identical audio source. 114 | 115 | ```haskell 116 | scene :: AudioUnit D2 117 | scene = 118 | dup1 119 | ( (gain' 0.2 $ sinOsc 110.0) 120 | + (gain' 0.1 $ sinOsc 220.0) 121 | + microphone 122 | ) \mono -> 123 | speaker 124 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 125 | :| (gain' 0.5 $ (playBuf "forest" 1.0)) 126 | : Nil 127 | ) 128 | ``` 129 | 130 | ### Getting the sound to change as a function of time 131 | 132 | Up until this point, our audio hasn't reacted to many behaviors. Let's fix that! One behavior to react to is the passage of time. Let's add a slow undulation to the lowest pitch in the drone that is based on the passage of time 133 | 134 | ```haskell 135 | scene :: Number -> AudioUnit D2 136 | scene time = 137 | let 138 | rad = pi * time 139 | in 140 | dup1 141 | ( (gain' 0.2 $ sinOsc (110.0 + (10.0 * sin (0.2 * rad)))) 142 | + (gain' 0.1 $ sinOsc 220.0) 143 | + microphone 144 | ) \mono -> 145 | speaker 146 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 147 | :| (gain' 0.5 $ (playBuf "forest" 1.0)) 148 | : Nil 149 | ) 150 | ``` 151 | 152 | ### Getting the sound to change as a function of a mouse input event 153 | 154 | The next snippet of code uses the mouse to modulate the pitch of the higher note by roughly a major third. 155 | 156 | ```haskell 157 | scene :: Mouse -> Number -> Behavior (AudioUnit D2) 158 | scene mouse time = f time <$> click 159 | where 160 | f s cl = 161 | let 162 | rad = pi * s 163 | in 164 | dup1 165 | ( (gain' 0.2 $ sinOsc (110.0 + (10.0 * sin (0.2 * rad)))) 166 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then 50.0 else 0.0))) 167 | + microphone 168 | ) \mono -> 169 | speaker 170 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 171 | :| (gain' 0.5 $ (playBuf "forest" 1.0)) 172 | : Nil 173 | ) 174 | 175 | click :: Behavior Boolean 176 | click = map (not <<< isEmpty) $ buttons mouse 177 | ``` 178 | 179 | ### Making sure that certain sounds occur at a precise time 180 | 181 | Great audio is all about timing, but so far, we have been locked to scheduling events at multiples of the control rate. The most commonly used control rate for this library is 50Hz (meaning one event every 0.02 seconds), which is too slow to accurately depict complex rhythmic events. 182 | 183 | To fix the control rate problem, parameters that can change in time like _frequency_ or _gain_ have an optional second parameter that specifies the offset, in seconds, from the current quantized value in the control rate. The type of this parameter is `AudioParameter`, and it has several other values that can be set to precisely control how values change over time. 184 | 185 | Using `AudioParameter` directly is an advanced feature that will be discussed below. The most common way to use `AudioParameter` is through the function `evalPiecewise`, which accepts the control rate in seconds (in our case, `0.02`), a piecewise function in the form `Array (Tuple time value)` where `time` and `value` are both `Number`s, and the current time. 186 | 187 | Let's add a small metronome on the inside of our sound. We will have it beat every `0.9` seconds, and we use the function `gainT'` instead of `gain` to accept the `AudioParameter` output by `epwf`. 188 | 189 | ```haskell 190 | -- a piecewise function that creates an attack/release/sustain envelope 191 | -- at a periodicity of every 0.9 seconds 192 | pwf :: Array (Tuple Number Number) 193 | pwf = 194 | join 195 | $ map 196 | ( \i -> 197 | map 198 | ( \(Tuple f s) -> 199 | Tuple (f + 0.11 * toNumber i) s 200 | ) 201 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 202 | ) 203 | (range 0 400) 204 | 205 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 50 Hz 206 | 207 | epwf = evalPiecewise kr :: Array (Tuple Number Number) -> Number -> AudioParameter 208 | 209 | scene :: Mouse -> Number -> Behavior (AudioUnit D2) 210 | scene mouse time = f time <$> click 211 | where 212 | f s cl = 213 | let 214 | rad = pi * s 215 | in 216 | dup1 217 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 218 | + (gain' 0.1 (gainT' (epwf pwf s) $ sinOsc 440.0)) 219 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then 50.0 else 0.0))) 220 | + microphone 221 | ) \mono -> 222 | speaker 223 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 224 | :| (gain' 0.5 $ (playBuf "forest" 1.0)) 225 | : Nil 226 | ) 227 | 228 | click :: Behavior Boolean 229 | click = map (not <<< isEmpty) $ buttons mouse 230 | ``` 231 | 232 | ### Remembering when events happened 233 | 234 | Sometimes, you don't just want to react to an event like a mouse click. You want to remember when the event happened in time. For example, imagine that we modulate a pitch whenever a button is clicked, like in the example below. When you click the mouse, the pitch should continue slowly rising until the mouse button is released. 235 | 236 | To accomplish this, or anything where memory needs to be retained, the scene accepts an arbitrary accumulator as its first parameter. You can think of it as a [fold](https://pursuit.purescript.org/packages/purescript-foldable-traversable/4.1.1/docs/Data.Foldable#v:fold) over time. 237 | 238 | To make the accumulator useful, the scene should return the accumulator as well. The constructor `IAudioUnit` allows for this: it accepts an audio unit as well as an accumulator. 239 | 240 | ```haskell 241 | pwf :: Array (Tuple Number Number) 242 | pwf = 243 | join 244 | $ map 245 | ( \i -> 246 | map 247 | ( \(Tuple f s) -> 248 | Tuple (f + 0.11 * toNumber i) s 249 | ) 250 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 251 | ) 252 | (range 0 400) 253 | 254 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 50 Hz 255 | 256 | epwf = evalPiecewise kr 257 | 258 | initialOnset = { onset: Nothing } :: { onset :: Maybe Number } 259 | 260 | scene :: 261 | forall a. 262 | Mouse -> 263 | { onset :: Maybe Number | a } -> 264 | Number -> 265 | Behavior (IAudioUnit D2 { onset :: Maybe Number | a }) 266 | scene mouse acc@{ onset } time = f time <$> click 267 | where 268 | f s cl = 269 | IAudioUnit 270 | ( dup1 271 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 272 | + (gain' 0.1 (gainT' (epwf pwf s) $ sinOsc 440.0)) 273 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then (50.0 + maybe 0.0 (\t -> 10.0 * (s - t)) stTime) else 0.0))) 274 | + microphone 275 | ) \mono -> 276 | speaker 277 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 278 | :| (gain' 0.5 $ (playBuf "forest" 1.0)) 279 | : Nil 280 | ) 281 | ) 282 | (acc { onset = stTime }) 283 | where 284 | rad = pi * s 285 | 286 | stTime = case Tuple onset cl of 287 | (Tuple Nothing true) -> Just s 288 | (Tuple (Just y) true) -> Just y 289 | (Tuple _ false) -> Nothing 290 | 291 | click :: Behavior Boolean 292 | click = map (not <<< isEmpty) $ buttons mouse 293 | ``` 294 | 295 | Because the accumulator object is global for an entire audio graph, it's a good idea to use row polymorphism in the accumulator object. While using keys like `onset` is fine for small projects, if you're a library developer, you'll want to make sure to use keys more like namespaces. That is, you'll want to make sure that they do not conflict with other vendors' keys and with users' keys. A good practice is to use something like `{ myLibrary :: { param1 :: Number } | a }`. 296 | 297 | #### Working with feedback 298 | 299 | Our microphone has been pretty boring up until now. Let's create a feedback loop to spice things up. 300 | 301 | A feedback loop is created when one uses the processed output of an audio node as an input to itself. One classic physical feedback loop is echo between two walls: the delayed audio bounces back and forth, causing really interesting and surprising effects. 302 | 303 | Because audio functions like `gain` consume other audio functions like `sinOsc`, there is no way to create a loop by composing these functions. Instead, to create a feedback loop, we need to use the `graph` function to create an audio graph. 304 | 305 | An audio graph is a row with three keys: `accumulators`, `processors` and `generators`. `generators` can be any function that creates audio (including `graph` itself). `processors` are unary audio operators like filters and convolution. All of the audio functions that do this, like `highpass` and `waveShaper`, have graph analogues with `g'` prepended, ie `g'highpass` and `g'waveShaper`. `aggregators` are _n_-ary audio operators like `g'add`, `g'mul` and `g'gain` (gain is just addition composed with multiplication of a constant, and the special `gain` function does this in an efficient way). 306 | 307 | The audio graph must respect certain rules: it must be fully connected, it must have a unique terminal node, it must have at least one generator, it must have no orphan nodes, it must not have duplicate edges between nodes, etc. Violating any of these rules will result in a type error at compile-time. 308 | 309 | The graph structure is represented using _incoming_ edges, so processors have only one incoming edge whereas accumulators have an arbitrary number of incoming edges, as we see below. Play it and you'll hear an echo effect! 310 | 311 | ```haskell 312 | pwf :: Array (Tuple Number Number) 313 | pwf = 314 | join 315 | $ map 316 | ( \i -> 317 | map 318 | ( \(Tuple f s) -> 319 | Tuple (f + 0.11 * toNumber i) s 320 | ) 321 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 322 | ) 323 | (range 0 400) 324 | 325 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 50 Hz 326 | 327 | epwf = evalPiecewise kr 328 | 329 | initialOnset = { onset: Nothing } :: { onset :: Maybe Number } 330 | 331 | scene :: 332 | forall a. 333 | Mouse -> 334 | { onset :: Maybe Number | a } -> 335 | Number -> 336 | Behavior (IAudioUnit D2 { onset :: Maybe Number | a }) 337 | scene mouse acc@{ onset } time = f time <$> click 338 | where 339 | f s cl = 340 | IAudioUnit 341 | ( dup1 342 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 343 | + (gain' 0.1 (gainT' (epwf pwf s) $ sinOsc 440.0)) 344 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then (50.0 + maybe 0.0 (\t -> 10.0 * (s - t)) stTime) else 0.0))) 345 | + ( graph 346 | { aggregators: 347 | { out: Tuple g'add (SLProxy :: SLProxy ("combine" :/ SNil)) 348 | , combine: Tuple g'add (SLProxy :: SLProxy ("gain" :/ "mic" :/ SNil)) 349 | , gain: Tuple (g'gain 0.9) (SLProxy :: SLProxy ("del" :/ SNil)) 350 | } 351 | , processors: 352 | { del: Tuple (g'delay 0.2) (SProxy :: SProxy "filt") 353 | , filt: Tuple (g'bandpass 440.0 1.0) (SProxy :: SProxy "combine") 354 | } 355 | , generators: 356 | { mic: microphone 357 | } 358 | } 359 | ) 360 | ) \mono -> 361 | speaker 362 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 363 | :| (gain' 0.5 $ (playBuf "forest" 1.0)) 364 | : Nil 365 | ) 366 | ) 367 | (acc { onset = stTime }) 368 | where 369 | rad = pi * s 370 | 371 | stTime = case Tuple onset cl of 372 | (Tuple Nothing true) -> Just s 373 | (Tuple (Just y) true) -> Just y 374 | (Tuple _ false) -> Nothing 375 | 376 | click :: Behavior Boolean 377 | click = map (not <<< isEmpty) $ buttons mouse 378 | ``` 379 | 380 | #### Adding visuals 381 | 382 | Let's add a little dot that gets bigger when we click. We'll do that using the `AV` constructor that accepts a [Drawing](https://pursuit.purescript.org/packages/purescript-drawing/4.0.0/docs/Graphics.Drawing#t:Drawing). 383 | 384 | ```haskell 385 | pwf :: Array (Tuple Number Number) 386 | pwf = 387 | join 388 | $ map 389 | ( \i -> 390 | map 391 | ( \(Tuple f s) -> 392 | Tuple (f + 0.11 * toNumber i) s 393 | ) 394 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 395 | ) 396 | (range 0 400) 397 | 398 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 50 Hz 399 | 400 | epwf = evalPiecewise kr 401 | 402 | initialOnset = { onset: Nothing } :: { onset :: Maybe Number } 403 | 404 | scene :: 405 | forall a. 406 | Mouse -> 407 | { onset :: Maybe Number | a } -> 408 | CanvasInfo -> 409 | Number -> 410 | Behavior (AV D2 { onset :: Maybe Number | a }) 411 | scene mouse acc@{ onset } (CanvasInfo { w, h }) time = f time <$> click 412 | where 413 | f s cl = 414 | AV 415 | { audio: 416 | Just 417 | $ dup1 418 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 419 | + (gain' 0.1 (gainT' (gn s) $ sinOsc 440.0)) 420 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then (50.0 + maybe 0.0 (\t -> 10.0 * (s - t)) stTime) else 0.0))) 421 | + ( graph 422 | { aggregators: 423 | { out: Tuple g'add (SLProxy :: SLProxy ("combine" :/ SNil)) 424 | , combine: Tuple g'add (SLProxy :: SLProxy ("gain" :/ "mic" :/ SNil)) 425 | , gain: Tuple (g'gain 0.9) (SLProxy :: SLProxy ("del" :/ SNil)) 426 | } 427 | , processors: 428 | { del: Tuple (g'delay 0.2) (SProxy :: SProxy "filt") 429 | , filt: Tuple (g'bandpass 440.0 1.0) (SProxy :: SProxy "combine") 430 | } 431 | , generators: 432 | { mic: microphone 433 | } 434 | } 435 | ) 436 | ) \mono -> 437 | speaker 438 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 439 | :| (gain' 0.5 $ (play "forest")) 440 | : Nil 441 | ) 442 | , visual: 443 | Just 444 | { painting: 445 | const 446 | $ filled 447 | (fillColor (rgb 0 0 0)) 448 | ( circle 449 | (if cl then toNumber ps.x - x else w / 2.0) 450 | (if cl then toNumber ps.y - y else h / 2.0) 451 | (if cl then 25.0 else 5.0) 452 | ) 453 | , words: mempty 454 | } 455 | , accumulator: acc { onset = stTime } 456 | } 457 | where 458 | rad = pi * s 459 | 460 | stTime = case Tuple onset cl of 461 | (Tuple Nothing true) -> Just s 462 | (Tuple (Just y) true) -> Just y 463 | (Tuple _ false) -> Nothing 464 | 465 | click :: Behavior Boolean 466 | click = map (not <<< isEmpty) $ buttons mouse 467 | ``` 468 | 469 | ### Conclusion 470 | 471 | We started with a simple sound and built all the way up to a complex, precisely-timed stereo structure with feedback that responds to mouse events both visually and sonically. These examples also exist in [Readme.purs](./examples/readme/Readme.purs). 472 | 473 | From here, the only thing left is to make some noise! There are many more audio units in the library, such as filters, compressors and convolvers. Almost the whole [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) is exposed. 474 | 475 | To see a list of exported audio units, you can check out [`Audio.purs`](./src/FRP/Behavior/Audio.purs). In a future version of this, we will refactor things so that all of the audio units are in one package. 476 | 477 | ## MIDI 478 | 479 | The file [src/FRP/Behavior/MIDI.purs](./src/FRP/Behavior/MIDI.purs) exposes one function - `midi` - that can be used in conjunction with `getMidi` [src/FRP/Event/MIDI.purs](./src/FRP/Event/MIDI.purs) to incorporate [realtime MIDI data](https://twitter.com/stronglynormal/status/1316756584786276352) into the audio graph. For an example of how this is done, check out [examples/midi-in](./examples/midi-in). 480 | 481 | ## Interacting with the browser 482 | 483 | In simple setups, you'll interact with the browser in a ` 4 | 43 |
44 | 45 | 46 | 47 |