├── .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 | 48 | -------------------------------------------------------------------------------- /examples/audio-worklet/white-noise-processor.js: -------------------------------------------------------------------------------- 1 | // white-noise-processor.js 2 | class WhiteNoiseProcessor extends AudioWorkletProcessor { 3 | static get parameterDescriptors() { 4 | return [ 5 | { 6 | name: "customGain", 7 | defaultValue: 1, 8 | minValue: 0, 9 | maxValue: 1, 10 | automationRate: "a-rate", 11 | }, 12 | ]; 13 | } 14 | 15 | process(inputs, outputs, parameters) { 16 | const output = outputs[0]; 17 | output.forEach((channel) => { 18 | for (let i = 0; i < channel.length; i++) { 19 | channel[i] = 20 | (Math.random() * 2 - 1) * 21 | (parameters["customGain"].length > 1 22 | ? parameters["customGain"][i] 23 | : parameters["customGain"][0]); 24 | // note: a parameter contains an array of 128 values (one value for each of 128 samples), 25 | // however it may contain a single value which is to be used for all 128 samples 26 | // if no automation is scheduled for the moment. 27 | } 28 | }); 29 | return true; 30 | } 31 | } 32 | 33 | registerProcessor("white-noise-processor", WhiteNoiseProcessor); 34 | -------------------------------------------------------------------------------- /examples/dupsplit/DupSplit.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.DupSplit where 2 | 3 | -- tests everything in the readme to make sure it works! 4 | import Prelude 5 | import Data.Typelevel.Num (D1) 6 | import Effect (Effect) 7 | import FRP.Behavior (Behavior) 8 | import FRP.Behavior.Audio (AudioUnit, Exporter, Run, defaultExporter, dup1, microphone, runInBrowser, sinOsc, speaker') 9 | import Math (pi, sin) 10 | 11 | sceneDup :: Number -> Behavior (AudioUnit D1) 12 | sceneDup t = pure (speaker' $ dup1 microphone (\u -> (sinOsc (5.0 + 10.0 * (sin (0.2 * t * pi))) * u))) 13 | 14 | run :: Run Unit Unit 15 | run = runInBrowser sceneDup 16 | 17 | exporter = defaultExporter :: Exporter Unit Unit 18 | 19 | main :: Effect Unit 20 | main = pure unit 21 | -------------------------------------------------------------------------------- /examples/dupsplit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/exporter/Exporter.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.Exporter where 2 | 3 | import Prelude 4 | import Data.List ((:), List(..)) 5 | import Data.NonEmpty ((:|)) 6 | import Data.Typelevel.Num (D1) 7 | import Effect (Effect) 8 | import Effect.Class.Console (log) 9 | import FRP.Behavior (Behavior) 10 | import FRP.Behavior.Audio (AudioUnit, Exporter, Run, gain', runInBrowser, sinOsc, speaker) 11 | import Math (pi, sin) 12 | 13 | scene :: Number -> Behavior (AudioUnit D1) 14 | scene time = 15 | let 16 | rad = pi * time 17 | in 18 | pure 19 | $ speaker 20 | ( (gain' 0.1 $ sinOsc (440.0 + (10.0 * sin (2.3 * rad)))) 21 | :| (gain' 0.25 $ sinOsc (235.0 + (10.0 * sin (1.7 * rad)))) 22 | : (gain' 0.2 $ sinOsc (337.0 + (10.0 * sin rad))) 23 | : (gain' 0.1 $ sinOsc (530.0 + (19.0 * (5.0 * sin rad)))) 24 | : Nil 25 | ) 26 | 27 | run :: Run Unit String 28 | run = runInBrowser scene 29 | 30 | exporter :: Exporter String Unit 31 | exporter = 32 | { acquire: pure "hello" 33 | -- this prints to the log, but it can be used for sending audio instructions 34 | -- anywhere, ie to an external MIDI device 35 | , use: 36 | \env ({ id, timeStamp, accumulator, audio }) -> do 37 | log env 38 | log $ show id 39 | log $ show timeStamp 40 | log $ show audio 41 | log $ show accumulator 42 | , release: \env -> log ("releasing " <> env) 43 | } 44 | 45 | main :: Effect Unit 46 | main = pure unit 47 | -------------------------------------------------------------------------------- /examples/exporter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/hello-world/HelloWorld.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.HelloWorld where 2 | 3 | import Prelude 4 | import Data.List ((:), List(..)) 5 | import Data.NonEmpty ((:|)) 6 | import Data.Typelevel.Num (D1) 7 | import Effect (Effect) 8 | import FRP.Behavior (Behavior) 9 | import FRP.Behavior.Audio (AudioUnit, Exporter, MediaRecorder, Run, defaultExporter, gain', mediaRecorderToUrl, recorder, runInBrowser, sinOsc, speaker) 10 | import Math (pi, sin) 11 | 12 | scene :: Number -> Behavior (AudioUnit D1) 13 | scene time = 14 | let 15 | rad = pi * time 16 | in 17 | pure 18 | $ recorder "recorder" 19 | ( speaker 20 | ( (gain' 0.1 $ sinOsc (440.0 + (10.0 * sin (2.3 * rad)))) 21 | :| (gain' 0.25 $ sinOsc (235.0 + (10.0 * sin (1.7 * rad)))) 22 | : (gain' 0.2 $ sinOsc (337.0 + (10.0 * sin rad))) 23 | : (gain' 0.1 $ sinOsc (530.0 + (19.0 * (5.0 * sin rad)))) 24 | : Nil 25 | ) 26 | ) 27 | 28 | run :: Run Unit Unit 29 | run = runInBrowser scene 30 | 31 | mr2url = mediaRecorderToUrl :: String -> (String -> Effect Unit) -> MediaRecorder -> Effect Unit 32 | 33 | exporter = defaultExporter :: Exporter Unit Unit 34 | 35 | main :: Effect Unit 36 | main = pure unit 37 | -------------------------------------------------------------------------------- /examples/hello-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/koans/.gitattributes: -------------------------------------------------------------------------------- 1 | forest.mp3 filter=lfs diff=lfs merge=lfs -text 2 | moo.js filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /examples/koans/Koans.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.Koans where 2 | 3 | import Prelude 4 | import Data.List ((:), List(..)) 5 | import Data.NonEmpty ((:|)) 6 | import Data.Tuple (Tuple(..)) 7 | import Data.Typelevel.Num (D1, D2) 8 | import Data.Vec ((+>), empty) 9 | import Effect (Effect) 10 | import FRP.Behavior (Behavior) 11 | import FRP.Behavior.Audio (AudioParameterTransition, AudioUnit, Exporter, Oversample(..), Run, allpass, bandpass, constant, convolver, defaultExporter, defaultParam, delay, dup1, dynamicsCompressor, evalPiecewise, g'add, g'bandpass, g'delay, g'gain, gain', gainT_', graph, highpass, highshelf, iirFilter, loopBuf, loopBufT, lowpass, lowshelf, merger, microphone, notch, panner, pannerMono, pannerVars', peaking, periodicOsc, play, playBuf, playBufWithOffset, playBuf_, play_, runInBrowser, sawtoothOsc, sinOsc, sinOsc_, spatialPanner, speaker, speaker', squareOsc, triangleOsc, waveShaper) 12 | import Math (pi, sin) 13 | import Type.Data.Graph (type (:/), SNil) 14 | import Type.Proxy (Proxy(..)) 15 | 16 | -- constant 17 | nothing :: Number -> Behavior (AudioUnit D1) 18 | nothing _ = pure zero 19 | 20 | -- triangle 21 | triangle :: Number -> Behavior (AudioUnit D1) 22 | triangle _ = pure $ speaker' (gain' 0.3 $ triangleOsc 420.0) 23 | 24 | triangleMul :: Number -> Behavior (AudioUnit D1) 25 | triangleMul _ = pure $ speaker' (constant 0.3 * triangleOsc 420.0) 26 | 27 | -- saw 28 | saw :: Number -> Behavior (AudioUnit D1) 29 | saw _ = pure $ speaker' (gain' 0.3 $ sawtoothOsc 420.0) 30 | 31 | -- fixed periodic wave 32 | pdfix :: Number -> Behavior (AudioUnit D1) 33 | pdfix _ = pure $ speaker' (gain' 0.3 $ periodicOsc "funtimes" 325.0) 34 | 35 | -- fixed periodic wave 36 | wsh :: Number -> Behavior (AudioUnit D1) 37 | wsh _ = pure $ speaker' (gain' 0.3 (waveShaper "waveshaperCurve" FourX $ (play "forest"))) 38 | 39 | -- square 40 | square :: Number -> Behavior (AudioUnit D1) 41 | square _ = pure $ speaker' (gain' 0.3 $ squareOsc 420.0) 42 | 43 | -- comp 44 | comp :: Number -> Behavior (AudioUnit D1) 45 | comp _ = pure $ speaker' (gain' 0.3 (dynamicsCompressor (-50.0) 40.0 12.0 0.0 0.25 $ (play "forest"))) 46 | 47 | -- verb 48 | verb :: Number -> Behavior (AudioUnit D1) 49 | verb _ = pure $ speaker' (gain' 0.3 (convolver "moo" $ (play "forest"))) 50 | 51 | -- delay 52 | wait :: Number -> Behavior (AudioUnit D1) 53 | wait time = 54 | let 55 | rad = pi * time 56 | in 57 | pure 58 | $ speaker 59 | ( delay 1.0 (gain' 0.1 $ sinOsc (440.0 + (10.0 * sin (2.3 * rad)))) 60 | :| delay 2.0 (gain' 0.25 $ sinOsc (235.0 + (10.0 * sin (1.7 * rad)))) 61 | : delay 3.0 (gain' 0.2 $ sinOsc (337.0 + (10.0 * sin rad))) 62 | : (gain' 0.1 $ sinOsc (530.0 + (19.0 * (5.0 * sin rad)))) 63 | : Nil 64 | ) 65 | 66 | -- mul 67 | ringMod :: Number -> Behavior (AudioUnit D1) 68 | ringMod _ = 69 | pure 70 | $ speaker' 71 | ( (gain' 0.5 $ sinOsc (440.0)) 72 | * (gain' 0.5 $ sinOsc (30.0)) 73 | ) 74 | 75 | -- sharp attack 76 | pwfA :: Array (Tuple Number Number) 77 | pwfA = 78 | [ Tuple 1.0 0.0 79 | , Tuple (1.06) 0.1 80 | , Tuple (1.1) 0.1 81 | , Tuple (1.15) 0.0 82 | ] 83 | 84 | atx :: Number -> Behavior (AudioUnit D1) 85 | atx t = 86 | pure 87 | ( speaker' 88 | ( gainT_' ("g0") (evalPiecewise 0.02 (pwfA) t) 89 | $ sinOsc_ ("s0") 100.0 90 | ) 91 | ) 92 | 93 | -- filters 94 | f0 :: Number -> Behavior (AudioUnit D1) 95 | f0 _ = pure $ speaker' (gain' 0.3 (lowpass 350.0 1.0 $ (play "forest"))) 96 | 97 | f1 :: Number -> Behavior (AudioUnit D1) 98 | f1 _ = pure $ speaker' (gain' 0.3 (highpass 350.0 1.0 $ (play "forest"))) 99 | 100 | f2 :: Number -> Behavior (AudioUnit D1) 101 | f2 _ = pure $ speaker' (gain' 0.3 (lowshelf 350.0 0.0 $ (play "forest"))) 102 | 103 | f3 :: Number -> Behavior (AudioUnit D1) 104 | f3 _ = pure $ speaker' (gain' 0.3 (highshelf 350.0 0.0 $ (play "forest"))) 105 | 106 | f4 :: Number -> Behavior (AudioUnit D1) 107 | f4 _ = pure $ speaker' (gain' 0.3 (bandpass 350.0 1.0 $ (play "forest"))) 108 | 109 | f5 :: Number -> Behavior (AudioUnit D1) 110 | f5 _ = pure $ speaker' (gain' 0.3 (allpass 350.0 1.0 $ (play "forest"))) 111 | 112 | f6 :: Number -> Behavior (AudioUnit D1) 113 | f6 _ = pure $ speaker' (gain' 0.3 (peaking 350.0 1.0 0.0 $ (play "forest"))) 114 | 115 | f7 :: Number -> Behavior (AudioUnit D1) 116 | f7 _ = pure $ speaker' (gain' 0.3 (notch 350.0 1.0 $ (play "forest"))) 117 | 118 | iir :: Number -> Behavior (AudioUnit D1) 119 | iir _ = 120 | pure 121 | $ speaker' 122 | ( gain' 0.3 123 | ( iirFilter 124 | (0.00020298 +> 0.0004059599 +> 0.00020298 +> empty) 125 | (1.0126964558 +> (-1.9991880801) +> 0.9873035442 +> empty) 126 | (play "forest") 127 | ) 128 | ) 129 | 130 | -- panner, merger, dup 131 | pan :: Number -> Behavior (AudioUnit D2) 132 | pan time = 133 | pure 134 | $ dup1 135 | ( (gain' 0.2 $ sinOsc 110.0) 136 | + (gain' 0.1 $ sinOsc 220.0) 137 | ) \mono -> 138 | speaker 139 | $ ( (panner (sin rad) (merger (mono +> mono +> empty))) 140 | :| Nil 141 | ) 142 | where 143 | rad = pi * time 144 | 145 | panMono :: Number -> Behavior (AudioUnit D2) 146 | panMono time = 147 | pure 148 | $ speaker' 149 | ( pannerMono (sin rad) 150 | ( (gain' 0.2 $ sinOsc 110.0) 151 | + (gain' 0.1 $ sinOsc 220.0) 152 | ) 153 | ) 154 | where 155 | rad = pi * time 156 | 157 | -- spatialPanner 158 | span :: Number -> Behavior (AudioUnit D2) 159 | span t = 160 | pure 161 | $ speaker' 162 | ( spatialPanner 163 | pannerVars' 164 | { positionX = sin (0.2 * rad) 165 | , positionY = sin (0.1 * rad) 166 | , positionZ = sin (0.3 * rad) 167 | , orientationX = sin (0.05 * rad) 168 | , orientationY = sin (0.15 * rad) 169 | , orientationZ = sin (0.19 * rad) 170 | } 171 | (play_ "f0" "forest") 172 | ) 173 | where 174 | rad = pi * t 175 | 176 | -- pb 177 | pb :: Number -> Behavior (AudioUnit D1) 178 | pb _ = pure $ speaker' (gain' 0.3 (playBuf "moo" 1.0)) 179 | 180 | -- pb 181 | pbO :: Number -> Behavior (AudioUnit D1) 182 | pbO _ = pure $ speaker' (gain' 0.3 (playBufWithOffset "moo" 1.0 1.0)) 183 | 184 | -- lb 185 | lb :: Number -> Behavior (AudioUnit D1) 186 | lb _ = pure $ speaker' (gain' 0.3 (loopBuf "moo" 1.0 0.0 0.5)) 187 | 188 | -- transitions 189 | -- lb 190 | tran :: AudioParameterTransition -> Number -> Behavior (AudioUnit D1) 191 | tran transition time = 192 | pure 193 | $ speaker' 194 | ( gain' 0.3 195 | ( loopBufT 196 | "moo" 197 | (defaultParam { param = 1.0 + 0.3 * (sin (0.2 * pi * time)), transition = transition }) 198 | 2.0 199 | 3.0 200 | ) 201 | ) 202 | 203 | -- on off 204 | onoff :: Number -> Behavior (AudioUnit D1) 205 | onoff t = 206 | pure 207 | $ speaker 208 | ( zero 209 | :| (if t < 1.0 then (pure $ gain' 0.3 (play_ "f0" "forest")) else Nil <> (if t > 3.0 then (pure $ gain' 0.3 (play_ "f1" "forest")) else Nil)) 210 | <> Nil 211 | ) 212 | 213 | -- on off 214 | onoffb :: Number -> Behavior (AudioUnit D1) 215 | onoffb t = 216 | pure 217 | $ speaker 218 | ( zero 219 | :| (if t < 9.0 then (pure $ gain' 0.3 (playBuf_ "f0" "moo" 1.0)) else Nil) 220 | <> (if t > 3.0 then (pure $ gain' 0.3 (playBuf_ "f1" "moo" 1.0)) else Nil) 221 | ) 222 | 223 | onoffb2 :: Number -> Behavior (AudioUnit D1) 224 | onoffb2 t = 225 | pure 226 | $ speaker 227 | ( zero 228 | :| (if t < 1.0 then (pure $ gain' 0.3 (playBuf_ "f0" "moo" 1.0)) else Nil) 229 | <> (if t > 3.0 then (pure $ gain' 0.3 (playBuf_ "f1" "moo" 1.0)) else Nil) 230 | ) 231 | 232 | -- feedback 233 | feedback :: Number -> Behavior (AudioUnit D1) 234 | feedback _ = 235 | pure 236 | ( speaker' 237 | $ ( graph 238 | { aggregators: 239 | { out: Tuple g'add (Proxy :: Proxy ("combine" :/ SNil)) 240 | , combine: Tuple g'add (Proxy :: Proxy ("gain" :/ "mic" :/ SNil)) 241 | , gain: Tuple (g'gain 0.5) (Proxy :: Proxy ("del" :/ SNil)) 242 | } 243 | , processors: 244 | { del: Tuple (g'delay 0.2) (Proxy :: Proxy "filt") 245 | , filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "combine") 246 | } 247 | , generators: 248 | { mic: microphone 249 | } 250 | } 251 | ) 252 | ) 253 | 254 | run :: Run Unit Unit 255 | run = runInBrowser onoffb2 256 | 257 | exporter = 258 | defaultExporter :: 259 | Exporter Unit Unit 260 | 261 | main :: Effect Unit 262 | main = pure unit 263 | -------------------------------------------------------------------------------- /examples/koans/forest.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1676d2ea31e8c959351915145588172d6c670d788da09ecf454f41ebd2fe9b88 3 | size 1376462 4 | -------------------------------------------------------------------------------- /examples/koans/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /examples/koans/moo.js: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cd53128ea63da5861ae4df088f2f07156117cfab416d4e64b97ef76097860dcb 3 | size 74148 4 | -------------------------------------------------------------------------------- /examples/metronome/Metronome.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.Metronome where 2 | 3 | import Prelude 4 | import Data.Array (range) 5 | import Data.Int (toNumber) 6 | import Data.Tuple (Tuple(..)) 7 | import Data.Typelevel.Num (D1) 8 | import Effect (Effect) 9 | import FRP.Behavior (Behavior) 10 | import FRP.Behavior.Audio (AudioUnit, Exporter, Run, defaultExporter, evalPiecewise, gain', gainT', runInBrowser, sinOsc, speaker') 11 | 12 | -- a piecewise function that creates an attack/release/sustain envelope 13 | -- at a periodicity of every 0.9 seconds 14 | pwf :: Array (Tuple Number Number) 15 | pwf = 16 | join 17 | $ map 18 | ( \i -> 19 | map 20 | ( \(Tuple f s) -> 21 | Tuple (f + 0.11 * toNumber i) s 22 | ) 23 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 24 | ) 25 | (range 0 400) 26 | 27 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 66.66667 Hz 28 | 29 | scene :: Number -> Behavior (AudioUnit D1) 30 | scene time = 31 | pure 32 | $ speaker' 33 | (gain' 0.1 (gainT' (evalPiecewise kr pwf time) $ sinOsc 440.0)) 34 | 35 | run :: Run Unit Unit 36 | run = runInBrowser scene 37 | 38 | exporter = defaultExporter :: Exporter Unit Unit 39 | 40 | main :: Effect Unit 41 | main = pure unit 42 | -------------------------------------------------------------------------------- /examples/metronome/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/midi-in/MidiIn.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.MidiIn where 2 | 3 | import Prelude 4 | import Control.Promise (Promise) 5 | import Data.Array (head) 6 | import Data.List ((:), List(..)) 7 | import Data.Map as M 8 | import Data.Maybe (fromMaybe) 9 | import Data.Tuple (snd) 10 | import Data.Typelevel.Num (D1) 11 | import Effect (Effect) 12 | import FRP.Behavior (Behavior) 13 | import FRP.Behavior.Audio (AudioUnit, Exporter, Run, defaultExporter, gain', runInBrowser_, sinOsc, speaker') 14 | import FRP.Behavior.MIDI (midi) 15 | import FRP.Event.MIDI (MIDI, MIDIAccess, MIDIEvent(..), MIDIEventInTime, getMidi, midiAccess) 16 | import Math (pi) 17 | 18 | simpleOnOff :: M.Map String (List MIDIEventInTime) -> Boolean 19 | simpleOnOff m = 20 | fromMaybe false 21 | ( do 22 | h <- head (M.toUnfoldable m) 23 | pure $ go (snd h) 24 | ) 25 | where 26 | go :: List MIDIEventInTime -> Boolean 27 | go Nil = false 28 | 29 | go ({ event: (NoteOn _ _ _) } : rest) = true 30 | 31 | go ({ event: (NoteOff _ _ _) } : rest) = false 32 | 33 | go (_ : rest) = go rest 34 | 35 | scene :: MIDI -> Number -> Behavior (AudioUnit D1) 36 | scene midiIn time = f <$> (midi midiIn) 37 | where 38 | rad = pi * time 39 | 40 | f md = 41 | speaker' 42 | (gain' (if simpleOnOff md then 0.6 else 0.0) $ sinOsc 440.0) 43 | 44 | run :: MIDIAccess -> Run Unit Unit 45 | run max = 46 | runInBrowser_ do 47 | md <- getMidi max 48 | pure (scene md) 49 | 50 | macc = midiAccess :: (Effect (Promise MIDIAccess)) 51 | 52 | exporter = defaultExporter :: Exporter Unit Unit 53 | 54 | main :: Effect Unit 55 | main = pure unit 56 | -------------------------------------------------------------------------------- /examples/midi-in/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/readme/.gitattributes: -------------------------------------------------------------------------------- 1 | forest.mp3 filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /examples/readme/Readme.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.Readme where 2 | 3 | -- tests everything in the readme to make sure it works! 4 | import Prelude 5 | import Color (rgb) 6 | import Data.Array (span, last, head, range) 7 | import Data.Int (toNumber) 8 | import Data.List ((:), List(..)) 9 | import Data.Maybe (Maybe(..), fromMaybe, maybe) 10 | import Data.NonEmpty ((:|)) 11 | import Data.Set (isEmpty) 12 | import Data.Tuple (Tuple(..), fst, snd) 13 | import Data.Typelevel.Num (D1, D2) 14 | import Data.Vec ((+>), empty) 15 | import Effect (Effect) 16 | import FRP.Behavior (Behavior) 17 | import FRP.Behavior.Audio (AV(..), AudioUnit, CanvasInfo(..), Exporter, IAudioUnit(..), Run, defaultExporter, defaultParam, dup1, g'add, g'bandpass, g'delay, g'gain, gain', gainT', graph, merger, microphone, panner, play, runInBrowser_, sinOsc, speaker, speaker') 18 | import FRP.Behavior.Mouse (buttons, position) 19 | import FRP.Event.Mouse (Mouse, getMouse) 20 | import Graphics.Painting (circle, fillColor, filled) 21 | import Math (pi, sin) 22 | import Type.Data.Graph (type (:/), SNil) 23 | import Type.Proxy (Proxy(..)) 24 | 25 | scene0 :: Number -> Behavior (AudioUnit D1) 26 | scene0 = const $ pure (speaker' $ (gain' 0.5 $ sinOsc 440.0)) 27 | 28 | scene1 :: Number -> Behavior (AudioUnit D1) 29 | scene1 = 30 | const 31 | $ pure 32 | ( speaker 33 | $ ( (gain' 0.2 $ sinOsc 110.0) 34 | :| (gain' 0.1 $ sinOsc 220.0) 35 | : microphone 36 | : Nil 37 | ) 38 | ) 39 | 40 | scene2 :: Number -> Behavior (AudioUnit D1) 41 | scene2 = 42 | const 43 | $ pure 44 | ( speaker 45 | $ ( (gain' 0.2 $ sinOsc 110.0) 46 | :| (gain' 0.1 $ sinOsc 220.0) 47 | : (gain' 0.5 $ (play "forest")) 48 | : microphone 49 | : Nil 50 | ) 51 | ) 52 | 53 | scene3 :: Number -> Behavior (AudioUnit D2) 54 | scene3 = 55 | const 56 | $ pure 57 | ( dup1 58 | ( (gain' 0.2 $ sinOsc 110.0) 59 | + (gain' 0.1 $ sinOsc 220.0) 60 | + microphone 61 | ) \mono -> 62 | speaker 63 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 64 | :| (gain' 0.5 $ (play "forest")) 65 | : Nil 66 | ) 67 | ) 68 | 69 | scene4 :: Number -> Behavior (AudioUnit D2) 70 | scene4 time = 71 | let 72 | rad = pi * time 73 | in 74 | pure 75 | $ dup1 76 | ( (gain' 0.2 $ sinOsc (110.0 + (10.0 * sin (0.2 * rad)))) 77 | + (gain' 0.1 $ sinOsc 220.0) 78 | + microphone 79 | ) \mono -> 80 | speaker 81 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 82 | :| (gain' 0.5 $ (play "forest")) 83 | : Nil 84 | ) 85 | 86 | scene5 :: Mouse -> Number -> Behavior (AudioUnit D2) 87 | scene5 mouse time = f time <$> click 88 | where 89 | f s cl = 90 | let 91 | rad = pi * s 92 | in 93 | dup1 94 | ( (gain' 0.2 $ sinOsc (110.0 + (10.0 * sin (0.2 * rad)))) 95 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then 50.0 else 0.0))) 96 | + microphone 97 | ) \mono -> 98 | speaker 99 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 100 | :| (gain' 0.5 $ (play "forest")) 101 | : Nil 102 | ) 103 | 104 | click :: Behavior Boolean 105 | click = map (not <<< isEmpty) $ buttons mouse 106 | 107 | -- a piecewise function that creates an attack/release/sustain envelope 108 | -- at a periodicity of every 0.9 seconds 109 | pwf :: Array (Tuple Number Number) 110 | pwf = 111 | join 112 | $ map 113 | ( \i -> 114 | map 115 | ( \(Tuple f s) -> 116 | Tuple (f + 0.11 * toNumber i) s 117 | ) 118 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 119 | ) 120 | (range 0 400) 121 | 122 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 66.66667 Hz 123 | 124 | scene6 :: Mouse -> Number -> Behavior (AudioUnit D2) 125 | scene6 mouse time = f time <$> click 126 | where 127 | split s = span ((s >= _) <<< fst) pwf 128 | 129 | gn s = 130 | let 131 | ht = split s 132 | 133 | left = fromMaybe (Tuple 0.0 0.0) $ last ht.init 134 | 135 | right = fromMaybe (Tuple 201.0 0.0) $ head ht.rest 136 | in 137 | -- if we are in a control cycle with a peak or trough 138 | -- we lock to that 139 | -- otherwise, we interpolate 140 | if (fst right - s) < kr then 141 | defaultParam { param = (snd right), timeOffset = (fst right - s) } 142 | else 143 | let 144 | m = (snd right - snd left) / (fst right - fst left) 145 | 146 | b = (snd right - (m * fst right)) 147 | in 148 | defaultParam { param = (m * s + b), timeOffset = 0.0 } 149 | 150 | f s cl = 151 | let 152 | rad = pi * s 153 | in 154 | dup1 155 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 156 | + (gain' 0.1 (gainT' (gn s) $ sinOsc 440.0)) 157 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then 50.0 else 0.0))) 158 | + microphone 159 | ) \mono -> 160 | speaker 161 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 162 | :| (gain' 0.5 $ (play "forest")) 163 | : Nil 164 | ) 165 | 166 | click :: Behavior Boolean 167 | click = map (not <<< isEmpty) $ buttons mouse 168 | 169 | initialOnset = { onset: Nothing } :: { onset :: Maybe Number } 170 | 171 | scene7 :: 172 | Mouse -> 173 | { onset :: Maybe Number } -> 174 | Number -> 175 | Behavior (IAudioUnit D2 { onset :: Maybe Number }) 176 | scene7 mouse acc@{ onset } time = f time <$> click 177 | where 178 | split s = span ((s >= _) <<< fst) pwf 179 | 180 | gn s = 181 | let 182 | ht = split s 183 | 184 | left = fromMaybe (Tuple 0.0 0.0) $ last ht.init 185 | 186 | right = fromMaybe (Tuple 201.0 0.0) $ head ht.rest 187 | in 188 | -- if we are in a control cycle with a peak or trough 189 | -- we lock to that 190 | -- otherwise, we interpolate 191 | if (fst right - s) < kr then 192 | defaultParam { param = (snd right), timeOffset = (fst right - s) } 193 | else 194 | let 195 | m = (snd right - snd left) / (fst right - fst left) 196 | 197 | b = (snd right - (m * fst right)) 198 | in 199 | defaultParam { param = (m * s + b), timeOffset = 0.0 } 200 | 201 | f s cl = 202 | IAudioUnit 203 | ( dup1 204 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 205 | + (gain' 0.1 (gainT' (gn s) $ sinOsc 440.0)) 206 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then (50.0 + maybe 0.0 (\t -> 10.0 * (s - t)) stTime) else 0.0))) 207 | + microphone 208 | ) \mono -> 209 | speaker 210 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 211 | :| (gain' 0.5 $ (play "forest")) 212 | : Nil 213 | ) 214 | ) 215 | (acc { onset = stTime }) 216 | where 217 | rad = pi * s 218 | 219 | stTime = case Tuple onset cl of 220 | (Tuple Nothing true) -> Just s 221 | (Tuple (Just y) true) -> Just y 222 | (Tuple _ false) -> Nothing 223 | 224 | click :: Behavior Boolean 225 | click = map (not <<< isEmpty) $ buttons mouse 226 | 227 | scene7_1 :: 228 | Mouse -> 229 | { onset :: Maybe Number } -> 230 | Number -> 231 | Behavior (IAudioUnit D2 { onset :: Maybe Number }) 232 | scene7_1 mouse acc@{ onset } time = f time <$> click 233 | where 234 | split s = span ((s >= _) <<< fst) pwf 235 | 236 | gn s = 237 | let 238 | ht = split s 239 | 240 | left = fromMaybe (Tuple 0.0 0.0) $ last ht.init 241 | 242 | right = fromMaybe (Tuple 201.0 0.0) $ head ht.rest 243 | in 244 | -- if we are in a control cycle with a peak or trough 245 | -- we lock to that 246 | -- otherwise, we interpolate 247 | if (fst right - s) < kr then 248 | defaultParam { param = (snd right), timeOffset = (fst right - s) } 249 | else 250 | let 251 | m = (snd right - snd left) / (fst right - fst left) 252 | 253 | b = (snd right - (m * fst right)) 254 | in 255 | defaultParam { param = (m * s + b), timeOffset = 0.0 } 256 | 257 | f s cl = 258 | IAudioUnit 259 | ( dup1 260 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 261 | + (gain' 0.1 (gainT' (gn s) $ sinOsc 440.0)) 262 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then (50.0 + maybe 0.0 (\t -> 10.0 * (s - t)) stTime) else 0.0))) 263 | + ( graph 264 | { aggregators: 265 | { out: Tuple g'add (Proxy :: Proxy ("combine" :/ SNil)) 266 | , combine: Tuple g'add (Proxy :: Proxy ("gain" :/ "mic" :/ SNil)) 267 | , gain: Tuple (g'gain 0.9) (Proxy :: Proxy ("del" :/ SNil)) 268 | } 269 | , processors: 270 | { del: Tuple (g'delay 0.2) (Proxy :: Proxy "filt") 271 | , filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "combine") 272 | } 273 | , generators: 274 | { mic: microphone 275 | } 276 | } 277 | ) 278 | ) \mono -> 279 | speaker 280 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 281 | :| (gain' 0.5 $ (play "forest")) 282 | : Nil 283 | ) 284 | ) 285 | (acc { onset = stTime }) 286 | where 287 | rad = pi * s 288 | 289 | stTime = case Tuple onset cl of 290 | (Tuple Nothing true) -> Just s 291 | (Tuple (Just y) true) -> Just y 292 | (Tuple _ false) -> Nothing 293 | 294 | click :: Behavior Boolean 295 | click = map (not <<< isEmpty) $ buttons mouse 296 | 297 | scene8 :: 298 | Mouse -> 299 | { onset :: Maybe Number } -> 300 | CanvasInfo -> 301 | Number -> 302 | Behavior (AV D2 { onset :: Maybe Number }) 303 | scene8 mouse acc@{ onset } (CanvasInfo { w, h, boundingClientRect: { x, y } }) time = f time <$> click <*> pos 304 | where 305 | split s = span ((s >= _) <<< fst) pwf 306 | 307 | gn s = 308 | let 309 | ht = split s 310 | 311 | left = fromMaybe (Tuple 0.0 0.0) $ last ht.init 312 | 313 | right = fromMaybe (Tuple 201.0 0.0) $ head ht.rest 314 | in 315 | -- if we are in a control cycle with a peak or trough 316 | -- we lock to that 317 | -- otherwise, we interpolate 318 | if (fst right - s) < kr then 319 | defaultParam { param = (snd right), timeOffset = (fst right - s) } 320 | else 321 | let 322 | m = (snd right - snd left) / (fst right - fst left) 323 | 324 | b = (snd right - (m * fst right)) 325 | in 326 | defaultParam { param = (m * s + b), timeOffset = 0.0 } 327 | 328 | f s cl ps = 329 | AV 330 | { audio: 331 | Just 332 | $ dup1 333 | ( (gain' 0.2 $ sinOsc (110.0 + (3.0 * sin (0.5 * rad)))) 334 | + (gain' 0.1 (gainT' (gn s) $ sinOsc 440.0)) 335 | + (gain' 0.1 $ sinOsc (220.0 + (if cl then (50.0 + maybe 0.0 (\t -> 10.0 * (s - t)) stTime) else 0.0))) 336 | + ( graph 337 | { aggregators: 338 | { out: Tuple g'add (Proxy :: Proxy ("combine" :/ SNil)) 339 | , combine: Tuple g'add (Proxy :: Proxy ("gain" :/ "mic" :/ SNil)) 340 | , gain: Tuple (g'gain 0.9) (Proxy :: Proxy ("del" :/ SNil)) 341 | } 342 | , processors: 343 | { del: Tuple (g'delay 0.2) (Proxy :: Proxy "filt") 344 | , filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "combine") 345 | } 346 | , generators: 347 | { mic: microphone 348 | } 349 | } 350 | ) 351 | ) \mono -> 352 | speaker 353 | $ ( (panner (-0.5) (merger (mono +> mono +> empty))) 354 | :| (gain' 0.5 $ (play "forest")) 355 | : Nil 356 | ) 357 | , visual: 358 | Just 359 | { painting: 360 | const 361 | $ filled 362 | (fillColor (rgb 0 0 0)) 363 | ( circle 364 | (if cl then toNumber ps.x - x else w / 2.0) 365 | (if cl then toNumber ps.y - y else h / 2.0) 366 | (if cl then 25.0 else 5.0) 367 | ) 368 | , words: mempty 369 | } 370 | , accumulator: acc { onset = stTime } 371 | } 372 | where 373 | rad = pi * s 374 | 375 | stTime = case Tuple onset cl of 376 | (Tuple Nothing true) -> Just s 377 | (Tuple (Just y') true) -> Just y' 378 | (Tuple _ false) -> Nothing 379 | 380 | click :: Behavior Boolean 381 | click = map (not <<< isEmpty) $ buttons mouse 382 | 383 | pos :: Behavior { x :: Int, y :: Int } 384 | pos = map (fromMaybe { x: 0, y: 0 }) (position mouse) 385 | 386 | run :: Run { onset :: Maybe Number } Unit 387 | run = 388 | runInBrowser_ do 389 | mouse <- getMouse 390 | pure (scene8 mouse) 391 | 392 | exporter = defaultExporter :: Exporter Unit { onset :: Maybe Number } 393 | 394 | main :: Effect Unit 395 | main = pure unit 396 | -------------------------------------------------------------------------------- /examples/readme/forest.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1676d2ea31e8c959351915145588172d6c670d788da09ecf454f41ebd2fe9b88 3 | size 1376462 4 | -------------------------------------------------------------------------------- /examples/readme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/regression/Regression.purs: -------------------------------------------------------------------------------- 1 | -- useful for regression testing 2 | module FRP.Behavior.Audio.Example.Regression where 3 | 4 | import Prelude 5 | import Data.Array (fold) 6 | import Data.List (List(..)) 7 | import Data.Map as M 8 | import Data.Maybe (fromMaybe) 9 | import Data.NonEmpty ((:|)) 10 | import Data.Profunctor (lcmap) 11 | import Data.Tuple (Tuple(..)) 12 | import Data.Typelevel.Num (D2) 13 | import Effect (Effect) 14 | import Effect.Class.Console (log) 15 | import FRP.Behavior (Behavior) 16 | import FRP.Behavior.Audio (AudioParameter, AudioUnit, Exporter, Run, defaultExporter, evalPiecewise, g'add_, g'delay_, g'gain_, graph_, playBufWithOffset_, runInBrowser, speaker) 17 | import Type.Data.Graph (SNil, type (:/)) 18 | import Type.Proxy (Proxy(..)) 19 | 20 | sounds = 21 | [ Tuple 105 1.3107256235827665 22 | , Tuple 104 1.1829251700680272 23 | , Tuple 102 1.2127891156462585 24 | ] :: 25 | Array (Tuple Int Number) 26 | 27 | kr = (20.0) / 1000.0 :: Number 28 | 29 | epwf :: Array (Tuple Number Number) -> Number -> AudioParameter 30 | epwf = evalPiecewise kr 31 | 32 | fromCloud :: String -> String 33 | fromCloud s = "https://klank-share.s3-eu-west-1.amazonaws.com/in-a-sentimental-mood/Samples/" <> s 34 | 35 | fromSounds :: Int -> Number 36 | fromSounds i = fromMaybe 0.0 (M.lookup i soundsMap) 37 | 38 | soundsMap :: M.Map Int Number 39 | soundsMap = M.fromFoldable sounds 40 | 41 | type PlayerSenOpts 42 | = { tag :: String 43 | } 44 | 45 | atT :: forall a. Number -> (Number -> a) -> (Number -> a) 46 | atT t = lcmap (_ - t) 47 | 48 | playerSen :: Int -> (Number -> PlayerSenOpts) -> Number -> List (AudioUnit D2) 49 | playerSen name' opts' time = 50 | if time + kr >= 0.0 && time < len then 51 | pure 52 | $ ( graph_ (opts.tag <> "_graph") 53 | { aggregators: 54 | { out: Tuple (g'add_ (opts.tag <> "_out")) (Proxy :: Proxy ("combine" :/ SNil)) 55 | , combine: Tuple (g'add_ (opts.tag <> "_cbn")) (Proxy :: Proxy ("gain" :/ "senn" :/ SNil)) 56 | , gain: Tuple (g'gain_ (opts.tag <> "_gnlp") 0.4) (Proxy :: Proxy ("del" :/ SNil)) 57 | } 58 | , processors: 59 | { del: Tuple (g'delay_ (opts.tag <> "_dl") 0.5) (Proxy :: Proxy "combine") 60 | } 61 | , generators: 62 | { senn: 63 | (playBufWithOffset_ (opts.tag <> "_playerSen") "moo" 1.0 0.0) 64 | } 65 | } 66 | ) 67 | else 68 | Nil 69 | where 70 | len = (fromSounds name') 71 | 72 | opts = opts' len 73 | 74 | name = "Sen-B4-" <> show name' <> "-l" 75 | 76 | data SenInfo 77 | = SenInfo Int Number 78 | 79 | playerSen_ :: Int -> (Number -> PlayerSenOpts) -> Number -> Behavior (AudioUnit D2) 80 | playerSen_ name opts time = pure $ speaker (zero :| playerSen name opts time) 81 | 82 | senSpread :: Number -> String -> Array (Number → List (AudioUnit D2)) 83 | senSpread os tg = 84 | map 85 | ( \(SenInfo x y) -> 86 | ( atT (y + os) 87 | $ playerSen x 88 | ( \l -> 89 | { tag: tg <> "sen" <> (show x) <> (show y) 90 | } 91 | ) 92 | ) 93 | ) 94 | [ SenInfo 105 0.0 95 | , SenInfo 104 0.0 96 | , SenInfo 102 0.6 97 | ] 98 | 99 | scene :: Number -> Behavior (AudioUnit D2) 100 | scene time = 101 | pure 102 | $ speaker 103 | ( zero 104 | :| fold 105 | ( map ((#) time) 106 | ( (senSpread 1.0 "A") 107 | ) 108 | ) 109 | ) 110 | 111 | run :: Run Unit Unit 112 | run = runInBrowser scene 113 | 114 | exporter = 115 | defaultExporter 116 | { use = \_ a -> log $ show a.audio 117 | } :: 118 | Exporter Unit Unit 119 | 120 | main :: Effect Unit 121 | main = pure unit 122 | -------------------------------------------------------------------------------- /examples/regression/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/stress0/Stress0.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.Audio.Example.Stress0 where 2 | 3 | -- with four oscillators and several gains, we start to hear pretty bad jank 4 | -- named units clears it up completely! 5 | import Prelude 6 | import Data.Array (range) 7 | import Data.Int (toNumber) 8 | import Data.List (List(..), (:)) 9 | import Data.NonEmpty ((:|)) 10 | import Data.Tuple (Tuple(..)) 11 | import Data.Typelevel.Num (D1) 12 | import Effect (Effect) 13 | import FRP.Behavior (Behavior) 14 | import FRP.Behavior.Audio (AudioParameter, AudioUnit, Exporter, Run, defaultExporter, evalPiecewise, gain', gainT', gainT_', gain_', runInBrowser, sinOsc, sinOsc_, speaker, speaker_) 15 | 16 | pwf0 :: Array (Tuple Number Number) 17 | pwf0 = 18 | join 19 | $ map 20 | ( \i -> 21 | map 22 | ( \(Tuple f s) -> 23 | Tuple (f + 0.11 * toNumber i) s 24 | ) 25 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 26 | ) 27 | (range 0 400) 28 | 29 | pwf1 :: Array (Tuple Number Number) 30 | pwf1 = 31 | join 32 | $ map 33 | ( \i -> 34 | map 35 | ( \(Tuple f s) -> 36 | Tuple (f + 0.13 * toNumber i) s 37 | ) 38 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 39 | ) 40 | (range 0 400) 41 | 42 | pwf2 :: Array (Tuple Number Number) 43 | pwf2 = 44 | join 45 | $ map 46 | ( \i -> 47 | map 48 | ( \(Tuple f s) -> 49 | Tuple (f + 0.15 * toNumber i) s 50 | ) 51 | [ Tuple 0.0 0.0, Tuple 0.02 0.7, Tuple 0.06 0.2 ] 52 | ) 53 | (range 0 400) 54 | 55 | pwf3 :: Array (Tuple Number Number) 56 | pwf3 = 57 | join 58 | $ map 59 | ( \i -> 60 | map 61 | ( \(Tuple f s) -> 62 | Tuple (f + 0.9 * toNumber i) s 63 | ) 64 | [ Tuple 0.0 0.0, Tuple 0.03 0.7, Tuple 0.07 0.2 ] 65 | ) 66 | (range 0 400) 67 | 68 | kr = 20.0 / 1000.0 :: Number -- the control rate in seconds, or 66.66667 Hz 69 | 70 | sceneThatHitsDeadline :: Behavior Number -> Behavior (AudioUnit D1) 71 | sceneThatHitsDeadline time = f <$> time 72 | where 73 | f s = 74 | speaker 75 | ( (gain' 0.1 (gainT' (epwf pwf0 s) $ sinOsc 440.0)) 76 | :| Nil 77 | ) 78 | 79 | epwf :: Array (Tuple Number Number) -> Number -> AudioParameter 80 | epwf = evalPiecewise kr 81 | 82 | scene :: Number -> Behavior (AudioUnit D1) 83 | scene s = 84 | pure 85 | $ speaker 86 | ( (gain' 0.1 (gainT' (epwf pwf0 s) $ sinOsc 440.0)) 87 | :| (gain' 0.1 (gainT' (epwf pwf1 s) $ sinOsc 660.0)) 88 | : (gain' 0.1 (gainT' (epwf pwf2 s) $ sinOsc 990.0)) 89 | : (gain' 0.1 (gainT' (epwf pwf3 s) $ sinOsc 220.0)) 90 | : Nil 91 | ) 92 | 93 | sceneN :: Number -> Behavior (AudioUnit D1) 94 | sceneN s = 95 | pure 96 | $ speaker_ "speaker" 97 | ( (gain_' "g0" 0.1 (gainT_' "gt0" (epwf pwf0 s) $ sinOsc_ "s0" 440.0)) 98 | :| (gain_' "g1" 0.1 (gainT_' "gt1" (epwf pwf1 s) $ sinOsc_ "s1" 660.0)) 99 | : (gain_' "g2" 0.1 (gainT_' "gt2" (epwf pwf2 s) $ sinOsc_ "s2" 990.0)) 100 | : (gain_' "g3" 0.1 (gainT_' "gt3" (epwf pwf3 s) $ sinOsc_ "s3" 220.0)) 101 | : Nil 102 | ) 103 | 104 | sceneNN :: Number -> Behavior (AudioUnit D1) 105 | sceneNN s = 106 | pure 107 | $ speaker_ "speaker" 108 | ( (gain_' "g0" 0.1 (gainT_' "gt0" (epwf pwf0 s) $ sinOsc_ "s0" 440.0)) 109 | :| (gain_' "g1" 0.1 (gainT_' "gt1" (epwf pwf1 s) $ sinOsc_ "s1" 660.0)) 110 | : (gain_' "g2" 0.1 (gainT_' "gt2" (epwf pwf2 s) $ sinOsc_ "s2" 990.0)) 111 | : (gain_' "g3" 0.1 (gainT_' "gt3" (epwf pwf3 s) $ sinOsc_ "s3" 220.0)) 112 | : (gain_' "g4" 0.05 (gainT_' "gt4" (epwf pwf1 s) $ sinOsc_ "s4" 1210.0)) 113 | : (gain_' "g5" 0.025 (gainT_' "gt5" (epwf pwf0 s) $ sinOsc_ "s5" 1580.0)) 114 | : Nil 115 | ) 116 | 117 | run :: Run Unit Unit 118 | run = runInBrowser sceneNN 119 | 120 | exporter = defaultExporter :: Exporter Unit Unit 121 | 122 | main :: Effect Unit 123 | main = pure unit 124 | -------------------------------------------------------------------------------- /examples/stress0/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-audio-behaviors", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "6.12.6", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 10 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 11 | "dev": true, 12 | "requires": { 13 | "fast-deep-equal": "^3.1.1", 14 | "fast-json-stable-stringify": "^2.0.0", 15 | "json-schema-traverse": "^0.4.1", 16 | "uri-js": "^4.2.2" 17 | } 18 | }, 19 | "ansi-escapes": { 20 | "version": "3.2.0", 21 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", 22 | "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", 23 | "dev": true 24 | }, 25 | "ansi-regex": { 26 | "version": "4.1.0", 27 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 28 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 29 | "dev": true 30 | }, 31 | "ansi-styles": { 32 | "version": "3.2.1", 33 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 34 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 35 | "dev": true, 36 | "requires": { 37 | "color-convert": "^1.9.0" 38 | } 39 | }, 40 | "aproba": { 41 | "version": "1.2.0", 42 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 43 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", 44 | "dev": true 45 | }, 46 | "arch": { 47 | "version": "2.2.0", 48 | "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", 49 | "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", 50 | "dev": true 51 | }, 52 | "asn1": { 53 | "version": "0.2.4", 54 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 55 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 56 | "dev": true, 57 | "requires": { 58 | "safer-buffer": "~2.1.0" 59 | } 60 | }, 61 | "assert-plus": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 64 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 65 | "dev": true 66 | }, 67 | "asynckit": { 68 | "version": "0.4.0", 69 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 70 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 71 | "dev": true 72 | }, 73 | "aws-sign2": { 74 | "version": "0.7.0", 75 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 76 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 77 | "dev": true 78 | }, 79 | "aws4": { 80 | "version": "1.11.0", 81 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", 82 | "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", 83 | "dev": true 84 | }, 85 | "balanced-match": { 86 | "version": "1.0.0", 87 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 88 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 89 | "dev": true 90 | }, 91 | "bcrypt-pbkdf": { 92 | "version": "1.0.2", 93 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 94 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 95 | "dev": true, 96 | "requires": { 97 | "tweetnacl": "^0.14.3" 98 | } 99 | }, 100 | "bluebird": { 101 | "version": "3.7.2", 102 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 103 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", 104 | "dev": true 105 | }, 106 | "brace-expansion": { 107 | "version": "1.1.11", 108 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 109 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 110 | "dev": true, 111 | "requires": { 112 | "balanced-match": "^1.0.0", 113 | "concat-map": "0.0.1" 114 | } 115 | }, 116 | "buffer-from": { 117 | "version": "1.1.1", 118 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 119 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 120 | "dev": true 121 | }, 122 | "byline": { 123 | "version": "5.0.0", 124 | "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", 125 | "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", 126 | "dev": true 127 | }, 128 | "cacache": { 129 | "version": "11.3.3", 130 | "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz", 131 | "integrity": "sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==", 132 | "dev": true, 133 | "requires": { 134 | "bluebird": "^3.5.5", 135 | "chownr": "^1.1.1", 136 | "figgy-pudding": "^3.5.1", 137 | "glob": "^7.1.4", 138 | "graceful-fs": "^4.1.15", 139 | "lru-cache": "^5.1.1", 140 | "mississippi": "^3.0.0", 141 | "mkdirp": "^0.5.1", 142 | "move-concurrently": "^1.0.1", 143 | "promise-inflight": "^1.0.1", 144 | "rimraf": "^2.6.3", 145 | "ssri": "^6.0.1", 146 | "unique-filename": "^1.1.1", 147 | "y18n": "^4.0.0" 148 | } 149 | }, 150 | "caseless": { 151 | "version": "0.12.0", 152 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 153 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 154 | "dev": true 155 | }, 156 | "chalk": { 157 | "version": "2.4.2", 158 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 159 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 160 | "dev": true, 161 | "requires": { 162 | "ansi-styles": "^3.2.1", 163 | "escape-string-regexp": "^1.0.5", 164 | "supports-color": "^5.3.0" 165 | } 166 | }, 167 | "chownr": { 168 | "version": "1.1.4", 169 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 170 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 171 | "dev": true 172 | }, 173 | "cli-cursor": { 174 | "version": "2.1.0", 175 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 176 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 177 | "dev": true, 178 | "requires": { 179 | "restore-cursor": "^2.0.0" 180 | } 181 | }, 182 | "color-convert": { 183 | "version": "1.9.3", 184 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 185 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 186 | "dev": true, 187 | "requires": { 188 | "color-name": "1.1.3" 189 | } 190 | }, 191 | "color-name": { 192 | "version": "1.1.3", 193 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 194 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 195 | "dev": true 196 | }, 197 | "combined-stream": { 198 | "version": "1.0.8", 199 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 200 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 201 | "dev": true, 202 | "requires": { 203 | "delayed-stream": "~1.0.0" 204 | } 205 | }, 206 | "concat-map": { 207 | "version": "0.0.1", 208 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 209 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 210 | "dev": true 211 | }, 212 | "concat-stream": { 213 | "version": "1.6.2", 214 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 215 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 216 | "dev": true, 217 | "requires": { 218 | "buffer-from": "^1.0.0", 219 | "inherits": "^2.0.3", 220 | "readable-stream": "^2.2.2", 221 | "typedarray": "^0.0.6" 222 | } 223 | }, 224 | "copy-concurrently": { 225 | "version": "1.0.5", 226 | "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", 227 | "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", 228 | "dev": true, 229 | "requires": { 230 | "aproba": "^1.1.1", 231 | "fs-write-stream-atomic": "^1.0.8", 232 | "iferr": "^0.1.5", 233 | "mkdirp": "^0.5.1", 234 | "rimraf": "^2.5.4", 235 | "run-queue": "^1.0.0" 236 | } 237 | }, 238 | "core-util-is": { 239 | "version": "1.0.2", 240 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 241 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 242 | "dev": true 243 | }, 244 | "cross-spawn": { 245 | "version": "7.0.3", 246 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 247 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 248 | "dev": true, 249 | "requires": { 250 | "path-key": "^3.1.0", 251 | "shebang-command": "^2.0.0", 252 | "which": "^2.0.1" 253 | }, 254 | "dependencies": { 255 | "which": { 256 | "version": "2.0.2", 257 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 258 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 259 | "dev": true, 260 | "requires": { 261 | "isexe": "^2.0.0" 262 | } 263 | } 264 | } 265 | }, 266 | "cyclist": { 267 | "version": "1.0.1", 268 | "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", 269 | "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", 270 | "dev": true 271 | }, 272 | "dashdash": { 273 | "version": "1.14.1", 274 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 275 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 276 | "dev": true, 277 | "requires": { 278 | "assert-plus": "^1.0.0" 279 | } 280 | }, 281 | "delayed-stream": { 282 | "version": "1.0.0", 283 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 284 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 285 | "dev": true 286 | }, 287 | "duplexify": { 288 | "version": "3.7.1", 289 | "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", 290 | "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", 291 | "dev": true, 292 | "requires": { 293 | "end-of-stream": "^1.0.0", 294 | "inherits": "^2.0.1", 295 | "readable-stream": "^2.0.0", 296 | "stream-shift": "^1.0.0" 297 | } 298 | }, 299 | "ecc-jsbn": { 300 | "version": "0.1.2", 301 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 302 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 303 | "dev": true, 304 | "requires": { 305 | "jsbn": "~0.1.0", 306 | "safer-buffer": "^2.1.0" 307 | } 308 | }, 309 | "emoji-regex": { 310 | "version": "7.0.3", 311 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 312 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 313 | "dev": true 314 | }, 315 | "end-of-stream": { 316 | "version": "1.4.4", 317 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 318 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 319 | "dev": true, 320 | "requires": { 321 | "once": "^1.4.0" 322 | } 323 | }, 324 | "env-paths": { 325 | "version": "2.2.1", 326 | "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", 327 | "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", 328 | "dev": true 329 | }, 330 | "escape-string-regexp": { 331 | "version": "1.0.5", 332 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 333 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 334 | "dev": true 335 | }, 336 | "execa": { 337 | "version": "2.1.0", 338 | "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", 339 | "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", 340 | "dev": true, 341 | "requires": { 342 | "cross-spawn": "^7.0.0", 343 | "get-stream": "^5.0.0", 344 | "is-stream": "^2.0.0", 345 | "merge-stream": "^2.0.0", 346 | "npm-run-path": "^3.0.0", 347 | "onetime": "^5.1.0", 348 | "p-finally": "^2.0.0", 349 | "signal-exit": "^3.0.2", 350 | "strip-final-newline": "^2.0.0" 351 | } 352 | }, 353 | "extend": { 354 | "version": "3.0.2", 355 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 356 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 357 | "dev": true 358 | }, 359 | "extsprintf": { 360 | "version": "1.3.0", 361 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 362 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 363 | "dev": true 364 | }, 365 | "fast-deep-equal": { 366 | "version": "3.1.3", 367 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 368 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 369 | "dev": true 370 | }, 371 | "fast-json-stable-stringify": { 372 | "version": "2.1.0", 373 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 374 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 375 | "dev": true 376 | }, 377 | "figgy-pudding": { 378 | "version": "3.5.2", 379 | "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", 380 | "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", 381 | "dev": true 382 | }, 383 | "filesize": { 384 | "version": "4.2.1", 385 | "resolved": "https://registry.npmjs.org/filesize/-/filesize-4.2.1.tgz", 386 | "integrity": "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA==", 387 | "dev": true 388 | }, 389 | "flush-write-stream": { 390 | "version": "1.1.1", 391 | "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", 392 | "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", 393 | "dev": true, 394 | "requires": { 395 | "inherits": "^2.0.3", 396 | "readable-stream": "^2.3.6" 397 | } 398 | }, 399 | "forever-agent": { 400 | "version": "0.6.1", 401 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 402 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 403 | "dev": true 404 | }, 405 | "form-data": { 406 | "version": "2.3.3", 407 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 408 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 409 | "dev": true, 410 | "requires": { 411 | "asynckit": "^0.4.0", 412 | "combined-stream": "^1.0.6", 413 | "mime-types": "^2.1.12" 414 | } 415 | }, 416 | "from2": { 417 | "version": "2.3.0", 418 | "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", 419 | "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", 420 | "dev": true, 421 | "requires": { 422 | "inherits": "^2.0.1", 423 | "readable-stream": "^2.0.0" 424 | } 425 | }, 426 | "fs-minipass": { 427 | "version": "1.2.7", 428 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", 429 | "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", 430 | "dev": true, 431 | "requires": { 432 | "minipass": "^2.6.0" 433 | } 434 | }, 435 | "fs-write-stream-atomic": { 436 | "version": "1.0.10", 437 | "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", 438 | "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", 439 | "dev": true, 440 | "requires": { 441 | "graceful-fs": "^4.1.2", 442 | "iferr": "^0.1.5", 443 | "imurmurhash": "^0.1.4", 444 | "readable-stream": "1 || 2" 445 | } 446 | }, 447 | "fs.realpath": { 448 | "version": "1.0.0", 449 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 450 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 451 | "dev": true 452 | }, 453 | "get-stream": { 454 | "version": "5.2.0", 455 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 456 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 457 | "dev": true, 458 | "requires": { 459 | "pump": "^3.0.0" 460 | } 461 | }, 462 | "getpass": { 463 | "version": "0.1.7", 464 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 465 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 466 | "dev": true, 467 | "requires": { 468 | "assert-plus": "^1.0.0" 469 | } 470 | }, 471 | "glob": { 472 | "version": "7.1.6", 473 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 474 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 475 | "dev": true, 476 | "requires": { 477 | "fs.realpath": "^1.0.0", 478 | "inflight": "^1.0.4", 479 | "inherits": "2", 480 | "minimatch": "^3.0.4", 481 | "once": "^1.3.0", 482 | "path-is-absolute": "^1.0.0" 483 | } 484 | }, 485 | "glpk.js": { 486 | "version": "3.2.0", 487 | "resolved": "https://registry.npmjs.org/glpk.js/-/glpk.js-3.2.0.tgz", 488 | "integrity": "sha512-Xxti+nm4ulLryEx41BcgAp5sPA0lLhbSF8RCNqsy1k8G2TV35Q6ChshvCiHOXfITiwp1u7/iCAmwTbvAyRghSw==" 489 | }, 490 | "graceful-fs": { 491 | "version": "4.2.6", 492 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 493 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", 494 | "dev": true 495 | }, 496 | "har-schema": { 497 | "version": "2.0.0", 498 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 499 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 500 | "dev": true 501 | }, 502 | "har-validator": { 503 | "version": "5.1.5", 504 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 505 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 506 | "dev": true, 507 | "requires": { 508 | "ajv": "^6.12.3", 509 | "har-schema": "^2.0.0" 510 | } 511 | }, 512 | "has-flag": { 513 | "version": "3.0.0", 514 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 515 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 516 | "dev": true 517 | }, 518 | "http-signature": { 519 | "version": "1.2.0", 520 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 521 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 522 | "dev": true, 523 | "requires": { 524 | "assert-plus": "^1.0.0", 525 | "jsprim": "^1.2.2", 526 | "sshpk": "^1.7.0" 527 | } 528 | }, 529 | "iferr": { 530 | "version": "0.1.5", 531 | "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", 532 | "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", 533 | "dev": true 534 | }, 535 | "imurmurhash": { 536 | "version": "0.1.4", 537 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 538 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 539 | "dev": true 540 | }, 541 | "inflight": { 542 | "version": "1.0.6", 543 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 544 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 545 | "dev": true, 546 | "requires": { 547 | "once": "^1.3.0", 548 | "wrappy": "1" 549 | } 550 | }, 551 | "inherits": { 552 | "version": "2.0.4", 553 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 554 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 555 | "dev": true 556 | }, 557 | "is-fullwidth-code-point": { 558 | "version": "2.0.0", 559 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 560 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 561 | "dev": true 562 | }, 563 | "is-plain-obj": { 564 | "version": "2.1.0", 565 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 566 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 567 | "dev": true 568 | }, 569 | "is-stream": { 570 | "version": "2.0.0", 571 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 572 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", 573 | "dev": true 574 | }, 575 | "is-typedarray": { 576 | "version": "1.0.0", 577 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 578 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 579 | "dev": true 580 | }, 581 | "isarray": { 582 | "version": "1.0.0", 583 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 584 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 585 | "dev": true 586 | }, 587 | "isexe": { 588 | "version": "2.0.0", 589 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 590 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 591 | "dev": true 592 | }, 593 | "isstream": { 594 | "version": "0.1.2", 595 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 596 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 597 | "dev": true 598 | }, 599 | "jsbn": { 600 | "version": "0.1.1", 601 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 602 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 603 | "dev": true 604 | }, 605 | "json-schema": { 606 | "version": "0.2.3", 607 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 608 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", 609 | "dev": true 610 | }, 611 | "json-schema-traverse": { 612 | "version": "0.4.1", 613 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 614 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 615 | "dev": true 616 | }, 617 | "json-stringify-safe": { 618 | "version": "5.0.1", 619 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 620 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 621 | "dev": true 622 | }, 623 | "jsprim": { 624 | "version": "1.4.1", 625 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 626 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 627 | "dev": true, 628 | "requires": { 629 | "assert-plus": "1.0.0", 630 | "extsprintf": "1.3.0", 631 | "json-schema": "0.2.3", 632 | "verror": "1.10.0" 633 | } 634 | }, 635 | "log-symbols": { 636 | "version": "3.0.0", 637 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", 638 | "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", 639 | "dev": true, 640 | "requires": { 641 | "chalk": "^2.4.2" 642 | } 643 | }, 644 | "log-update": { 645 | "version": "3.4.0", 646 | "resolved": "https://registry.npmjs.org/log-update/-/log-update-3.4.0.tgz", 647 | "integrity": "sha512-ILKe88NeMt4gmDvk/eb615U/IVn7K9KWGkoYbdatQ69Z65nj1ZzjM6fHXfcs0Uge+e+EGnMW7DY4T9yko8vWFg==", 648 | "dev": true, 649 | "requires": { 650 | "ansi-escapes": "^3.2.0", 651 | "cli-cursor": "^2.1.0", 652 | "wrap-ansi": "^5.0.0" 653 | } 654 | }, 655 | "lru-cache": { 656 | "version": "5.1.1", 657 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 658 | "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 659 | "dev": true, 660 | "requires": { 661 | "yallist": "^3.0.2" 662 | } 663 | }, 664 | "merge-stream": { 665 | "version": "2.0.0", 666 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 667 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 668 | "dev": true 669 | }, 670 | "mime-db": { 671 | "version": "1.46.0", 672 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", 673 | "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", 674 | "dev": true 675 | }, 676 | "mime-types": { 677 | "version": "2.1.29", 678 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", 679 | "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", 680 | "dev": true, 681 | "requires": { 682 | "mime-db": "1.46.0" 683 | } 684 | }, 685 | "mimic-fn": { 686 | "version": "2.1.0", 687 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 688 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 689 | "dev": true 690 | }, 691 | "minimatch": { 692 | "version": "3.0.4", 693 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 694 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 695 | "dev": true, 696 | "requires": { 697 | "brace-expansion": "^1.1.7" 698 | } 699 | }, 700 | "minimist": { 701 | "version": "1.2.5", 702 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 703 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 704 | "dev": true 705 | }, 706 | "minipass": { 707 | "version": "2.9.0", 708 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", 709 | "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", 710 | "dev": true, 711 | "requires": { 712 | "safe-buffer": "^5.1.2", 713 | "yallist": "^3.0.0" 714 | } 715 | }, 716 | "minizlib": { 717 | "version": "1.3.3", 718 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", 719 | "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", 720 | "dev": true, 721 | "requires": { 722 | "minipass": "^2.9.0" 723 | } 724 | }, 725 | "mississippi": { 726 | "version": "3.0.0", 727 | "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", 728 | "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", 729 | "dev": true, 730 | "requires": { 731 | "concat-stream": "^1.5.0", 732 | "duplexify": "^3.4.2", 733 | "end-of-stream": "^1.1.0", 734 | "flush-write-stream": "^1.0.0", 735 | "from2": "^2.1.0", 736 | "parallel-transform": "^1.1.0", 737 | "pump": "^3.0.0", 738 | "pumpify": "^1.3.3", 739 | "stream-each": "^1.1.0", 740 | "through2": "^2.0.0" 741 | } 742 | }, 743 | "mkdirp": { 744 | "version": "0.5.5", 745 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 746 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 747 | "dev": true, 748 | "requires": { 749 | "minimist": "^1.2.5" 750 | } 751 | }, 752 | "move-concurrently": { 753 | "version": "1.0.1", 754 | "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", 755 | "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", 756 | "dev": true, 757 | "requires": { 758 | "aproba": "^1.1.1", 759 | "copy-concurrently": "^1.0.0", 760 | "fs-write-stream-atomic": "^1.0.8", 761 | "mkdirp": "^0.5.1", 762 | "rimraf": "^2.5.4", 763 | "run-queue": "^1.0.3" 764 | } 765 | }, 766 | "ms": { 767 | "version": "2.1.3", 768 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 769 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 770 | "dev": true 771 | }, 772 | "npm-run-path": { 773 | "version": "3.1.0", 774 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", 775 | "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", 776 | "dev": true, 777 | "requires": { 778 | "path-key": "^3.0.0" 779 | } 780 | }, 781 | "oauth-sign": { 782 | "version": "0.9.0", 783 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 784 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 785 | "dev": true 786 | }, 787 | "once": { 788 | "version": "1.4.0", 789 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 790 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 791 | "dev": true, 792 | "requires": { 793 | "wrappy": "1" 794 | } 795 | }, 796 | "onetime": { 797 | "version": "5.1.2", 798 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 799 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 800 | "dev": true, 801 | "requires": { 802 | "mimic-fn": "^2.1.0" 803 | } 804 | }, 805 | "p-finally": { 806 | "version": "2.0.1", 807 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", 808 | "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", 809 | "dev": true 810 | }, 811 | "parallel-transform": { 812 | "version": "1.2.0", 813 | "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", 814 | "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", 815 | "dev": true, 816 | "requires": { 817 | "cyclist": "^1.0.1", 818 | "inherits": "^2.0.3", 819 | "readable-stream": "^2.1.5" 820 | } 821 | }, 822 | "path-is-absolute": { 823 | "version": "1.0.1", 824 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 825 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 826 | "dev": true 827 | }, 828 | "path-key": { 829 | "version": "3.1.1", 830 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 831 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 832 | "dev": true 833 | }, 834 | "performance-now": { 835 | "version": "2.1.0", 836 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 837 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 838 | "dev": true 839 | }, 840 | "process-nextick-args": { 841 | "version": "2.0.1", 842 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 843 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 844 | "dev": true 845 | }, 846 | "promise-inflight": { 847 | "version": "1.0.1", 848 | "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", 849 | "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", 850 | "dev": true 851 | }, 852 | "psl": { 853 | "version": "1.8.0", 854 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 855 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", 856 | "dev": true 857 | }, 858 | "pump": { 859 | "version": "3.0.0", 860 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 861 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 862 | "dev": true, 863 | "requires": { 864 | "end-of-stream": "^1.1.0", 865 | "once": "^1.3.1" 866 | } 867 | }, 868 | "pumpify": { 869 | "version": "1.5.1", 870 | "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", 871 | "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", 872 | "dev": true, 873 | "requires": { 874 | "duplexify": "^3.6.0", 875 | "inherits": "^2.0.3", 876 | "pump": "^2.0.0" 877 | }, 878 | "dependencies": { 879 | "pump": { 880 | "version": "2.0.1", 881 | "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", 882 | "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", 883 | "dev": true, 884 | "requires": { 885 | "end-of-stream": "^1.1.0", 886 | "once": "^1.3.1" 887 | } 888 | } 889 | } 890 | }, 891 | "punycode": { 892 | "version": "2.1.1", 893 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 894 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 895 | "dev": true 896 | }, 897 | "purescript": { 898 | "version": "0.14.0", 899 | "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.14.0.tgz", 900 | "integrity": "sha512-iTA1k8mwxHqrJp/K296g6omVfve5RCjnB5D6Up93PVtZdP66mCIqlOYC4hrLBHg2hgkk27xePDt5NUXKvH4ApQ==", 901 | "dev": true, 902 | "requires": { 903 | "purescript-installer": "^0.2.0" 904 | } 905 | }, 906 | "purescript-installer": { 907 | "version": "0.2.5", 908 | "resolved": "https://registry.npmjs.org/purescript-installer/-/purescript-installer-0.2.5.tgz", 909 | "integrity": "sha512-fQAWWP5a7scuchXecjpU4r4KEgSPuS6bBnaP01k9f71qqD28HaJ2m4PXHFkhkR4oATAxTPIGCtmTwtVoiBOHog==", 910 | "dev": true, 911 | "requires": { 912 | "arch": "^2.1.1", 913 | "byline": "^5.0.0", 914 | "cacache": "^11.3.2", 915 | "chalk": "^2.4.2", 916 | "env-paths": "^2.2.0", 917 | "execa": "^2.0.3", 918 | "filesize": "^4.1.2", 919 | "is-plain-obj": "^2.0.0", 920 | "log-symbols": "^3.0.0", 921 | "log-update": "^3.2.0", 922 | "minimist": "^1.2.0", 923 | "mkdirp": "^0.5.1", 924 | "ms": "^2.1.2", 925 | "once": "^1.4.0", 926 | "pump": "^3.0.0", 927 | "request": "^2.88.0", 928 | "rimraf": "^2.6.3", 929 | "tar": "^4.4.6", 930 | "which": "^1.3.1", 931 | "zen-observable": "^0.8.14" 932 | } 933 | }, 934 | "purty": { 935 | "version": "6.2.0", 936 | "resolved": "https://registry.npmjs.org/purty/-/purty-6.2.0.tgz", 937 | "integrity": "sha512-JfT8kJHSyxuOFQtRiH2x55SiPxXZsSdedQlZap8JehQ7KzB49B5C9cWwVybtSB36BdADQcxPvtw8D52z4EPnBw==", 938 | "dev": true 939 | }, 940 | "qs": { 941 | "version": "6.5.2", 942 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 943 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", 944 | "dev": true 945 | }, 946 | "readable-stream": { 947 | "version": "2.3.7", 948 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 949 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 950 | "dev": true, 951 | "requires": { 952 | "core-util-is": "~1.0.0", 953 | "inherits": "~2.0.3", 954 | "isarray": "~1.0.0", 955 | "process-nextick-args": "~2.0.0", 956 | "safe-buffer": "~5.1.1", 957 | "string_decoder": "~1.1.1", 958 | "util-deprecate": "~1.0.1" 959 | } 960 | }, 961 | "request": { 962 | "version": "2.88.2", 963 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 964 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 965 | "dev": true, 966 | "requires": { 967 | "aws-sign2": "~0.7.0", 968 | "aws4": "^1.8.0", 969 | "caseless": "~0.12.0", 970 | "combined-stream": "~1.0.6", 971 | "extend": "~3.0.2", 972 | "forever-agent": "~0.6.1", 973 | "form-data": "~2.3.2", 974 | "har-validator": "~5.1.3", 975 | "http-signature": "~1.2.0", 976 | "is-typedarray": "~1.0.0", 977 | "isstream": "~0.1.2", 978 | "json-stringify-safe": "~5.0.1", 979 | "mime-types": "~2.1.19", 980 | "oauth-sign": "~0.9.0", 981 | "performance-now": "^2.1.0", 982 | "qs": "~6.5.2", 983 | "safe-buffer": "^5.1.2", 984 | "tough-cookie": "~2.5.0", 985 | "tunnel-agent": "^0.6.0", 986 | "uuid": "^3.3.2" 987 | } 988 | }, 989 | "restore-cursor": { 990 | "version": "2.0.0", 991 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 992 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 993 | "dev": true, 994 | "requires": { 995 | "onetime": "^2.0.0", 996 | "signal-exit": "^3.0.2" 997 | }, 998 | "dependencies": { 999 | "mimic-fn": { 1000 | "version": "1.2.0", 1001 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 1002 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", 1003 | "dev": true 1004 | }, 1005 | "onetime": { 1006 | "version": "2.0.1", 1007 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 1008 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 1009 | "dev": true, 1010 | "requires": { 1011 | "mimic-fn": "^1.0.0" 1012 | } 1013 | } 1014 | } 1015 | }, 1016 | "rimraf": { 1017 | "version": "2.7.1", 1018 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 1019 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 1020 | "dev": true, 1021 | "requires": { 1022 | "glob": "^7.1.3" 1023 | } 1024 | }, 1025 | "run-queue": { 1026 | "version": "1.0.3", 1027 | "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", 1028 | "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", 1029 | "dev": true, 1030 | "requires": { 1031 | "aproba": "^1.1.1" 1032 | } 1033 | }, 1034 | "safe-buffer": { 1035 | "version": "5.1.2", 1036 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1037 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 1038 | "dev": true 1039 | }, 1040 | "safer-buffer": { 1041 | "version": "2.1.2", 1042 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1043 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1044 | "dev": true 1045 | }, 1046 | "shebang-command": { 1047 | "version": "2.0.0", 1048 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1049 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1050 | "dev": true, 1051 | "requires": { 1052 | "shebang-regex": "^3.0.0" 1053 | } 1054 | }, 1055 | "shebang-regex": { 1056 | "version": "3.0.0", 1057 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1058 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1059 | "dev": true 1060 | }, 1061 | "signal-exit": { 1062 | "version": "3.0.3", 1063 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1064 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 1065 | "dev": true 1066 | }, 1067 | "spago": { 1068 | "version": "0.19.1", 1069 | "resolved": "https://registry.npmjs.org/spago/-/spago-0.19.1.tgz", 1070 | "integrity": "sha512-OD/yopJZ9Ub+XsFtayDeLAWLT4kLdMxosJEyyp8W5OkyJVVSbCrvYacsO7iq3lSuHJmmNny/TEZdyb7uSyupng==", 1071 | "dev": true, 1072 | "requires": { 1073 | "request": "^2.88.0", 1074 | "tar": "^4.4.8" 1075 | } 1076 | }, 1077 | "sshpk": { 1078 | "version": "1.16.1", 1079 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1080 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1081 | "dev": true, 1082 | "requires": { 1083 | "asn1": "~0.2.3", 1084 | "assert-plus": "^1.0.0", 1085 | "bcrypt-pbkdf": "^1.0.0", 1086 | "dashdash": "^1.12.0", 1087 | "ecc-jsbn": "~0.1.1", 1088 | "getpass": "^0.1.1", 1089 | "jsbn": "~0.1.0", 1090 | "safer-buffer": "^2.0.2", 1091 | "tweetnacl": "~0.14.0" 1092 | } 1093 | }, 1094 | "ssri": { 1095 | "version": "6.0.1", 1096 | "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", 1097 | "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", 1098 | "dev": true, 1099 | "requires": { 1100 | "figgy-pudding": "^3.5.1" 1101 | } 1102 | }, 1103 | "stream-each": { 1104 | "version": "1.2.3", 1105 | "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", 1106 | "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", 1107 | "dev": true, 1108 | "requires": { 1109 | "end-of-stream": "^1.1.0", 1110 | "stream-shift": "^1.0.0" 1111 | } 1112 | }, 1113 | "stream-shift": { 1114 | "version": "1.0.1", 1115 | "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", 1116 | "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", 1117 | "dev": true 1118 | }, 1119 | "string-width": { 1120 | "version": "3.1.0", 1121 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 1122 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 1123 | "dev": true, 1124 | "requires": { 1125 | "emoji-regex": "^7.0.1", 1126 | "is-fullwidth-code-point": "^2.0.0", 1127 | "strip-ansi": "^5.1.0" 1128 | } 1129 | }, 1130 | "string_decoder": { 1131 | "version": "1.1.1", 1132 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1133 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1134 | "dev": true, 1135 | "requires": { 1136 | "safe-buffer": "~5.1.0" 1137 | } 1138 | }, 1139 | "strip-ansi": { 1140 | "version": "5.2.0", 1141 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1142 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1143 | "dev": true, 1144 | "requires": { 1145 | "ansi-regex": "^4.1.0" 1146 | } 1147 | }, 1148 | "strip-final-newline": { 1149 | "version": "2.0.0", 1150 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 1151 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", 1152 | "dev": true 1153 | }, 1154 | "supports-color": { 1155 | "version": "5.5.0", 1156 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1157 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1158 | "dev": true, 1159 | "requires": { 1160 | "has-flag": "^3.0.0" 1161 | } 1162 | }, 1163 | "tar": { 1164 | "version": "4.4.13", 1165 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", 1166 | "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", 1167 | "dev": true, 1168 | "requires": { 1169 | "chownr": "^1.1.1", 1170 | "fs-minipass": "^1.2.5", 1171 | "minipass": "^2.8.6", 1172 | "minizlib": "^1.2.1", 1173 | "mkdirp": "^0.5.0", 1174 | "safe-buffer": "^5.1.2", 1175 | "yallist": "^3.0.3" 1176 | } 1177 | }, 1178 | "through2": { 1179 | "version": "2.0.5", 1180 | "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", 1181 | "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", 1182 | "dev": true, 1183 | "requires": { 1184 | "readable-stream": "~2.3.6", 1185 | "xtend": "~4.0.1" 1186 | } 1187 | }, 1188 | "tough-cookie": { 1189 | "version": "2.5.0", 1190 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1191 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1192 | "dev": true, 1193 | "requires": { 1194 | "psl": "^1.1.28", 1195 | "punycode": "^2.1.1" 1196 | } 1197 | }, 1198 | "tunnel-agent": { 1199 | "version": "0.6.0", 1200 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1201 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1202 | "dev": true, 1203 | "requires": { 1204 | "safe-buffer": "^5.0.1" 1205 | } 1206 | }, 1207 | "tweetnacl": { 1208 | "version": "0.14.5", 1209 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1210 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1211 | "dev": true 1212 | }, 1213 | "typedarray": { 1214 | "version": "0.0.6", 1215 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1216 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 1217 | "dev": true 1218 | }, 1219 | "unique-filename": { 1220 | "version": "1.1.1", 1221 | "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", 1222 | "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", 1223 | "dev": true, 1224 | "requires": { 1225 | "unique-slug": "^2.0.0" 1226 | } 1227 | }, 1228 | "unique-slug": { 1229 | "version": "2.0.2", 1230 | "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", 1231 | "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", 1232 | "dev": true, 1233 | "requires": { 1234 | "imurmurhash": "^0.1.4" 1235 | } 1236 | }, 1237 | "uri-js": { 1238 | "version": "4.4.1", 1239 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1240 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1241 | "dev": true, 1242 | "requires": { 1243 | "punycode": "^2.1.0" 1244 | } 1245 | }, 1246 | "util-deprecate": { 1247 | "version": "1.0.2", 1248 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1249 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1250 | "dev": true 1251 | }, 1252 | "uuid": { 1253 | "version": "3.4.0", 1254 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1255 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 1256 | "dev": true 1257 | }, 1258 | "verror": { 1259 | "version": "1.10.0", 1260 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1261 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1262 | "dev": true, 1263 | "requires": { 1264 | "assert-plus": "^1.0.0", 1265 | "core-util-is": "1.0.2", 1266 | "extsprintf": "^1.2.0" 1267 | } 1268 | }, 1269 | "which": { 1270 | "version": "1.3.1", 1271 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1272 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1273 | "dev": true, 1274 | "requires": { 1275 | "isexe": "^2.0.0" 1276 | } 1277 | }, 1278 | "wrap-ansi": { 1279 | "version": "5.1.0", 1280 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", 1281 | "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", 1282 | "dev": true, 1283 | "requires": { 1284 | "ansi-styles": "^3.2.0", 1285 | "string-width": "^3.0.0", 1286 | "strip-ansi": "^5.0.0" 1287 | } 1288 | }, 1289 | "wrappy": { 1290 | "version": "1.0.2", 1291 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1292 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1293 | "dev": true 1294 | }, 1295 | "xtend": { 1296 | "version": "4.0.2", 1297 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 1298 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 1299 | "dev": true 1300 | }, 1301 | "y18n": { 1302 | "version": "4.0.1", 1303 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", 1304 | "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", 1305 | "dev": true 1306 | }, 1307 | "yallist": { 1308 | "version": "3.1.1", 1309 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 1310 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 1311 | "dev": true 1312 | }, 1313 | "zen-observable": { 1314 | "version": "0.8.15", 1315 | "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", 1316 | "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", 1317 | "dev": true 1318 | } 1319 | } 1320 | } 1321 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-audio-behaviors", 3 | "version": "0.0.0", 4 | "description": "Somewhat broken, flaky, unready for production experiment in using [`purescript-behaviors`](https://github.com/paf31/purescript-behaviors) for web audio.", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "postinstall": "spago install", 10 | "test": "spago -x test.dhall test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mikesol/purescript-audio-behaviors.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/mikesol/purescript-audio-behaviors/issues" 21 | }, 22 | "homepage": "https://github.com/mikesol/purescript-audio-behaviors#readme", 23 | "dependencies": { 24 | "glpk.js": "^3.2.0" 25 | }, 26 | "devDependencies": { 27 | "purescript": "^0.14.0", 28 | "purty": "^6.2.0", 29 | "spago": "^0.19.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages.dhall: -------------------------------------------------------------------------------- 1 | {- 2 | Welcome to your new Dhall package-set! 3 | 4 | Below are instructions for how to edit this file for most use 5 | cases, so that you don't need to know Dhall to use it. 6 | 7 | ## Warning: Don't Move This Top-Level Comment! 8 | 9 | Due to how `dhall format` currently works, this comment's 10 | instructions cannot appear near corresponding sections below 11 | because `dhall format` will delete the comment. However, 12 | it will not delete a top-level comment like this one. 13 | 14 | ## Use Cases 15 | 16 | Most will want to do one or both of these options: 17 | 1. Override/Patch a package's dependency 18 | 2. Add a package not already in the default package set 19 | 20 | This file will continue to work whether you use one or both options. 21 | Instructions for each option are explained below. 22 | 23 | ### Overriding/Patching a package 24 | 25 | Purpose: 26 | - Change a package's dependency to a newer/older release than the 27 | default package set's release 28 | - Use your own modified version of some dependency that may 29 | include new API, changed API, removed API by 30 | using your custom git repo of the library rather than 31 | the package set's repo 32 | 33 | Syntax: 34 | Replace the overrides' "{=}" (an empty record) with the following idea 35 | The "//" or "⫽" means "merge these two records and 36 | when they have the same value, use the one on the right:" 37 | ------------------------------- 38 | let overrides = 39 | { packageName = 40 | upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" } 41 | , packageName = 42 | upstream.packageName // { version = "v4.0.0" } 43 | , packageName = 44 | upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" } 45 | } 46 | ------------------------------- 47 | 48 | Example: 49 | ------------------------------- 50 | let overrides = 51 | { halogen = 52 | upstream.halogen // { version = "master" } 53 | , halogen-vdom = 54 | upstream.halogen-vdom // { version = "v4.0.0" } 55 | } 56 | ------------------------------- 57 | 58 | ### Additions 59 | 60 | Purpose: 61 | - Add packages that aren't already included in the default package set 62 | 63 | Syntax: 64 | Replace the additions' "{=}" (an empty record) with the following idea: 65 | ------------------------------- 66 | let additions = 67 | { package-name = 68 | { dependencies = 69 | [ "dependency1" 70 | , "dependency2" 71 | ] 72 | , repo = 73 | "https://example.com/path/to/git/repo.git" 74 | , version = 75 | "tag ('v4.0.0') or branch ('master')" 76 | } 77 | , package-name = 78 | { dependencies = 79 | [ "dependency1" 80 | , "dependency2" 81 | ] 82 | , repo = 83 | "https://example.com/path/to/git/repo.git" 84 | , version = 85 | "tag ('v4.0.0') or branch ('master')" 86 | } 87 | , etc. 88 | } 89 | ------------------------------- 90 | 91 | Example: 92 | ------------------------------- 93 | let additions = 94 | { benchotron = 95 | { dependencies = 96 | [ "arrays" 97 | , "exists" 98 | , "profunctor" 99 | , "strings" 100 | , "quickcheck" 101 | , "lcg" 102 | , "transformers" 103 | , "foldable-traversable" 104 | , "exceptions" 105 | , "node-fs" 106 | , "node-buffer" 107 | , "node-readline" 108 | , "datetime" 109 | , "now" 110 | ] 111 | , repo = 112 | "https://github.com/hdgarrood/purescript-benchotron.git" 113 | , version = 114 | "v7.0.0" 115 | } 116 | } 117 | ------------------------------- 118 | -} 119 | let upstream = 120 | https://github.com/purescript/package-sets/releases/download/psc-0.14.0-20210311/packages.dhall sha256:3da8be2b7b4a0e7de6186591167b363023695accffb98a8639e9e7d06e2070d6 121 | 122 | let overrides = {=} 123 | 124 | let additions = 125 | { typelevel-graph = 126 | { dependencies = [ "typelevel-peano" ] 127 | , repo = "https://github.com/mikesol/purescript-typelevel-graph.git" 128 | , version = "main" 129 | } 130 | , typelevel-peano = 131 | { dependencies = 132 | [ "arrays" 133 | , "console" 134 | , "effect" 135 | , "prelude" 136 | , "psci-support" 137 | , "typelevel-prelude" 138 | , "unsafe-coerce" 139 | ] 140 | , repo = "https://github.com/csicar/purescript-typelevel-peano.git" 141 | , version = "v1.0.1" 142 | } 143 | , event = 144 | { dependencies = 145 | [ "console" 146 | , "effect" 147 | , "filterable" 148 | , "nullable" 149 | , "unsafe-reference" 150 | , "js-timers" 151 | , "now" 152 | ] 153 | , repo = "https://github.com/mikesol/purescript-event.git" 154 | , version = "master" 155 | } 156 | , behaviors = 157 | { dependencies = 158 | [ "psci-support" 159 | , "effect" 160 | , "ordered-collections" 161 | , "filterable" 162 | , "nullable" 163 | , "event" 164 | , "web-html" 165 | , "web-events" 166 | , "web-uievents" 167 | ] 168 | , repo = "https://github.com/mikesol/purescript-behaviors.git" 169 | , version = "master" 170 | } 171 | , painting = 172 | { dependencies = 173 | [ "canvas" 174 | , "colors" 175 | , "console" 176 | , "effect" 177 | , "foldable-traversable" 178 | , "foreign-object" 179 | , "psci-support" 180 | , "web-html" 181 | ] 182 | , repo = "https://github.com/mikesol/purescript-painting.git" 183 | , version = "main" 184 | } 185 | } 186 | 187 | in upstream // overrides // additions 188 | -------------------------------------------------------------------------------- /spago.dhall: -------------------------------------------------------------------------------- 1 | { name = "purescript-audio-behaviors" 2 | , dependencies = 3 | [ "aff-promise" 4 | , "behaviors" 5 | , "canvas" 6 | , "console" 7 | , "debug" 8 | , "drawing" 9 | , "effect" 10 | , "foreign-object" 11 | , "heterogeneous" 12 | , "painting" 13 | , "psci-support" 14 | , "sized-vectors" 15 | , "typelevel-graph" 16 | , "typelevel-prelude" 17 | ] 18 | , packages = ./packages.dhall 19 | , sources = [ "src/**/*.purs" ] 20 | } 21 | -------------------------------------------------------------------------------- /src/FRP/Behavior/Audio.js: -------------------------------------------------------------------------------- 1 | ("use strict"); 2 | exports.isNaN = isNaN; 3 | 4 | exports.unsafeParseInt = function unsafeParseInt(input, base) { 5 | return parseInt(input, base); 6 | }; 7 | 8 | exports.makeAudioContext = function () { 9 | return new (window.AudioContext || window.webkitAudioContext)(); 10 | }; 11 | 12 | exports.makeAudioTrack = function (s) { 13 | return function () { 14 | var o = new Audio(s); 15 | o.crossOrigin = "anonymous"; 16 | return o; 17 | }; 18 | }; 19 | exports.decodeAudioDataFromBase64EncodedString = function (ctx) { 20 | return function (s) { 21 | return function () { 22 | { 23 | function base64ToArrayBuffer(base64) { 24 | var binaryString = window.atob(base64); 25 | var len = binaryString.length; 26 | var bytes = new Uint8Array(len); 27 | for (var i = 0; i < len; i++) { 28 | bytes[i] = binaryString.charCodeAt(i); 29 | } 30 | return bytes.buffer; 31 | } 32 | return ctx.decodeAudioData(base64ToArrayBuffer(s)); 33 | } 34 | }; 35 | }; 36 | }; 37 | exports.decodeAudioDataFromUri = function (ctx) { 38 | return function (s) { 39 | return function () { 40 | { 41 | return fetch(s) 42 | .then(function (b) { 43 | return b.arrayBuffer(); 44 | }) 45 | .then(function (b) { 46 | return ctx.decodeAudioData(b); 47 | }); 48 | } 49 | }; 50 | }; 51 | }; 52 | exports.audioWorkletAddModule = function (ctx) { 53 | return function (s) { 54 | return function () { 55 | { 56 | return ctx.audioWorklet.addModule(s); 57 | } 58 | }; 59 | }; 60 | }; 61 | exports.makeAudioBuffer = function (ctx) { 62 | return function (b) { 63 | return function () { 64 | var myArrayBuffer = ctx.createBuffer( 65 | b.value1.length, 66 | b.value1[0].length, 67 | b.value0 68 | ); 69 | for ( 70 | var channel = 0; 71 | channel < myArrayBuffer.numberOfChannels; 72 | channel++ 73 | ) { 74 | var nowBuffering = myArrayBuffer.getChannelData(channel); 75 | for (var i = 0; i < myArrayBuffer.length; i++) { 76 | nowBuffering[i] = b.value1[channel][i]; 77 | } 78 | } 79 | return myArrayBuffer; 80 | }; 81 | }; 82 | }; 83 | 84 | exports.makePeriodicWaveImpl = function (ctx) { 85 | return function (real_) { 86 | return function (imag_) { 87 | return function () { 88 | var real = new Float32Array(real_.length); 89 | var imag = new Float32Array(imag_.length); 90 | for (var i = 0; i < real_.length; i++) { 91 | real[i] = real_[i]; 92 | } 93 | for (var i = 0; i < imag_.length; i++) { 94 | imag[i] = imag_[i]; 95 | } 96 | return ctx.createPeriodicWave(real, imag, { 97 | disableNormalization: true, 98 | }); 99 | }; 100 | }; 101 | }; 102 | }; 103 | 104 | exports.makeFloatArray = function (fa) { 105 | return function () { 106 | var r = new Float32Array(fa.length); 107 | for (var i = 0; i < fa.length; i++) { 108 | r[i] = fa[i]; 109 | } 110 | return r; 111 | }; 112 | }; 113 | 114 | var getMainFromGenerator = function (g) { 115 | return g.main; 116 | }; 117 | 118 | var getSideEffectFromGenerator = function (g) { 119 | return g.se; 120 | }; 121 | 122 | var genericSetter = function (predicates, generators, c, n, timeToSet) { 123 | if (predicates.isImmediately(c.value3)) { 124 | getMainFromGenerator(generators[c.value0])[n].value = c.value1; 125 | } else { 126 | getMainFromGenerator(generators[c.value0])[n][ 127 | predicates.isNoRamp(c.value3) 128 | ? "setValueAtTime" 129 | : predicates.isLinearRamp(c.value3) 130 | ? "linearRampToValueAtTime" 131 | : predicates.isExponentialRamp(c.value3) 132 | ? "exponentialRampToValueAtTime" 133 | : "linearRampToValueAtTime" 134 | ](c.value1, timeToSet + c.value2); 135 | } 136 | }; 137 | 138 | exports.touchAudio = function (predicates) { 139 | return function (timeToSet) { 140 | return function (instructions) { 141 | return function (context) { 142 | return function (audioInfo) { 143 | return function (incoming) { 144 | return function () { 145 | // should never happen 146 | if (timeToSet < context.currentTime) { 147 | console.warn( 148 | "Programming error: we are setting in the past", 149 | timeToSet, 150 | context.currentTime 151 | ); 152 | timeToSet = context.currentTime; 153 | } 154 | var nu = []; 155 | var lb = {}; 156 | var generators = incoming.generators.slice(); 157 | var recorders = incoming.recorders; 158 | var old = incoming.generators.slice(); 159 | for (var i = 0; i < instructions.length; i++) { 160 | var c = instructions[i]; 161 | if (predicates.isDisconnectFrom(c)) { 162 | getMainFromGenerator(generators[c.value0]).disconnect( 163 | getMainFromGenerator(generators[c.value1]) 164 | ); 165 | var se = getSideEffectFromGenerator(generators[c.value1]); 166 | if (se) { 167 | getMainFromGenerator(generators[c.value0]).disconnect(se); 168 | } 169 | } else if (predicates.isConnectTo(c)) { 170 | if (predicates.isNothing(c.value2)) { 171 | getMainFromGenerator(generators[c.value0]).connect( 172 | getMainFromGenerator(generators[c.value1]) 173 | ); 174 | var se = getSideEffectFromGenerator(generators[c.value1]); 175 | if (se) { 176 | getMainFromGenerator(generators[c.value0]).connect(se); 177 | } 178 | } else { 179 | getMainFromGenerator(generators[c.value0]).connect( 180 | getMainFromGenerator(generators[c.value1]), 181 | c.value2.value0.value0, 182 | c.value2.value0.value1 183 | ); 184 | } 185 | } else if (predicates.isShuffle(c)) { 186 | generators[c.value1] = old[c.value0]; 187 | } else if (predicates.isNewUnit(c)) { 188 | nu.push(c); 189 | generators[c.value0] = { 190 | main: predicates.isSpeaker(c.value1) 191 | ? context.createGain() 192 | : predicates.isRecorder(c.value1) 193 | ? context.createGain() 194 | : predicates.isMicrophone(c.value1) 195 | ? context.createMediaStreamSource( 196 | audioInfo.microphones[ 197 | Object.keys(audioInfo.microphones)[0] 198 | ] 199 | ) 200 | : predicates.isPlay(c.value1) 201 | ? context.createMediaElementSource( 202 | audioInfo.tracks[c.value3.value0] 203 | ) 204 | : predicates.isPlayBuf(c.value1) 205 | ? context.createBufferSource() 206 | : predicates.isLoopBuf(c.value1) 207 | ? context.createBufferSource() 208 | : predicates.isIIRFilter(c.value1) 209 | ? context.createIIRFilter( 210 | c.value6.value0.value0, 211 | c.value6.value0.value1 212 | ) 213 | : predicates.isLowpass(c.value1) 214 | ? context.createBiquadFilter() 215 | : predicates.isBandpass(c.value1) 216 | ? context.createBiquadFilter() 217 | : predicates.isLowshelf(c.value1) 218 | ? context.createBiquadFilter() 219 | : predicates.isHighshelf(c.value1) 220 | ? context.createBiquadFilter() 221 | : predicates.isNotch(c.value1) 222 | ? context.createBiquadFilter() 223 | : predicates.isAllpass(c.value1) 224 | ? context.createBiquadFilter() 225 | : predicates.isPeaking(c.value1) 226 | ? context.createBiquadFilter() 227 | : predicates.isHighpass(c.value1) 228 | ? context.createBiquadFilter() 229 | : predicates.isConvolver(c.value1) 230 | ? context.createConvolver() 231 | : predicates.isDynamicsCompressor(c.value1) 232 | ? context.createDynamicsCompressor() 233 | : predicates.isSawtoothOsc(c.value1) 234 | ? context.createOscillator() 235 | : predicates.isTriangleOsc(c.value1) 236 | ? context.createOscillator() 237 | : predicates.isPeriodicOsc(c.value1) 238 | ? context.createOscillator() 239 | : predicates.isWaveShaper(c.value1) 240 | ? context.createWaveShaper() 241 | : predicates.isDup(c.value1) 242 | ? context.createGain() 243 | : predicates.isStereoPanner(c.value1) 244 | ? context.createStereoPanner() 245 | : predicates.isPanner(c.value1) 246 | ? context.createPanner() 247 | : predicates.isSinOsc(c.value1) 248 | ? context.createOscillator() 249 | : predicates.isSquareOsc(c.value1) 250 | ? context.createOscillator() 251 | : predicates.isMul(c.value1) 252 | ? (function () { 253 | var nConnections = 0; 254 | for (var j = 0; j < instructions.length; j++) { 255 | // this hack is necessary because 256 | // custom audio worklets need explicit 257 | // channel assignments. maybe make explicit everywhere? 258 | var d = instructions[j]; 259 | if ( 260 | predicates.isConnectTo(d) && 261 | d.value1 == c.value0 262 | ) { 263 | d.value2 = predicates.justly( 264 | predicates.tupply(0)(nConnections) 265 | ); 266 | nConnections += 1; 267 | } 268 | } 269 | return new AudioWorkletNode(context, "ps-aud-mul", { 270 | numberOfInputs: nConnections, 271 | numberOfOutputs: 1, 272 | }); 273 | })() 274 | : predicates.isAudioWorkletGenerator(c.value1) || 275 | predicates.isAudioWorkletProcessor(c.value1) || 276 | predicates.isAudioWorkletAggregator(c.value1) 277 | ? (function () { 278 | var initialParams = {}; 279 | for (var j = 0; j < instructions.length; j++) { 280 | var d = instructions[j]; 281 | if ( 282 | predicates.isSetCustomParam(d) && 283 | d.value0 == c.value0 284 | ) { 285 | initialParams[d.value1] = d.value2; 286 | } 287 | } 288 | if (predicates.isAudioWorkletAggregator(c.value1)) { 289 | var nConnections = 0; 290 | for (var j = 0; j < instructions.length; j++) { 291 | // this hack is necessary because 292 | // custom audio worklets need explicit 293 | // channel assignments. maybe make explicit everywhere? 294 | var d = instructions[j]; 295 | if ( 296 | predicates.isConnectTo(d) && 297 | d.value1 == c.value0 298 | ) { 299 | d.value2 = predicates.justly( 300 | predicates.tupply(0)(nConnections) 301 | ); 302 | nConnections += 1; 303 | } 304 | } 305 | } 306 | return new AudioWorkletNode( 307 | context, 308 | c.value3.value0, 309 | { 310 | numberOfInputs: predicates.isAudioWorkletGenerator( 311 | c.value1 312 | ) 313 | ? 0 314 | : predicates.isAudioWorkletProcessor(c.value1) 315 | ? 1 316 | : 2, 317 | numberOfOutputs: 1, 318 | parameterData: initialParams, 319 | } 320 | ); 321 | })() 322 | : predicates.isAdd(c.value1) 323 | ? context.createGain() 324 | : predicates.isDelay(c.value1) 325 | ? context.createDelay(10.0) // magic number for 10 seconds...make tweakable? 326 | : predicates.isConstant(c.value1) 327 | ? context.createConstantSource() 328 | : predicates.isGain(c.value1) 329 | ? context.createGain() 330 | : predicates.isSplitRes(c.value1) 331 | ? context.createGain() 332 | : predicates.isDupRes(c.value1) 333 | ? context.createGain() 334 | : predicates.isSplitter(c.value1) 335 | ? context.createChannelSplitter(c.value2.value0) 336 | : predicates.isMerger(c.value1) 337 | ? context.createChannelMerger(c.value2.value0) 338 | : null, 339 | }; 340 | if (predicates.isSpeaker(c.value1)) { 341 | generators[c.value0].se = context.destination; 342 | } else if (predicates.isRecorder(c.value1)) { 343 | var mediaRecorderSideEffectFn = 344 | audioInfo.recorders[c.value3.value0]; 345 | var dest = context.createMediaStreamDestination(); 346 | var mediaRecorder = new MediaRecorder(dest.stream); 347 | recorders = recorders.concat(mediaRecorder); 348 | mediaRecorderSideEffectFn(mediaRecorder)(); 349 | mediaRecorder.start(); 350 | generators[c.value0].se = dest; 351 | } 352 | } else if (predicates.isSetFrequency(c)) { 353 | genericSetter( 354 | predicates, 355 | generators, 356 | c, 357 | "frequency", 358 | timeToSet 359 | ); 360 | } else if (predicates.isSetPan(c)) { 361 | genericSetter(predicates, generators, c, "pan", timeToSet); 362 | } else if (predicates.isSetGain(c)) { 363 | genericSetter(predicates, generators, c, "gain", timeToSet); 364 | } else if (predicates.isSetQ(c)) { 365 | genericSetter(predicates, generators, c, "Q", timeToSet); 366 | } else if (predicates.isSetBuffer(c)) { 367 | var myArrayBuffer = context.createBuffer( 368 | c.value2.length, 369 | c.value2[0].length, 370 | c.value1 371 | ); 372 | for ( 373 | var channel = 0; 374 | channel < myArrayBuffer.numberOfChannels; 375 | channel++ 376 | ) { 377 | var nowBuffering = myArrayBuffer.getChannelData(channel); 378 | for (var i = 0; i < myArrayBuffer.length; i++) { 379 | nowBuffering[i] = c.value2[channel][i]; 380 | } 381 | } 382 | getMainFromGenerator( 383 | generators[c.value0] 384 | ).buffer = myArrayBuffer; 385 | } else if (predicates.isSetDelay(c)) { 386 | genericSetter( 387 | predicates, 388 | generators, 389 | c, 390 | "delayTime", 391 | timeToSet 392 | ); 393 | } else if (predicates.isSetOffset(c)) { 394 | genericSetter(predicates, generators, c, "offset", timeToSet); 395 | } else if (predicates.isSetLoopStart(c)) { 396 | lb[c.value0] = c.value1; 397 | getMainFromGenerator(generators[c.value0]).loopStart = 398 | c.value1; 399 | } else if (predicates.isSetLoopEnd(c)) { 400 | getMainFromGenerator(generators[c.value0]).loopEnd = c.value1; 401 | } else if (predicates.isSetOversample(c)) { 402 | getMainFromGenerator(generators[c.value0]).oversample = 403 | c.value1; 404 | } else if (predicates.isSetCurve(c)) { 405 | var curve = new Float32Array(c.value1.length); 406 | for (var i = 0; i < c.value1.length; i++) { 407 | curve[i] = c.value1[i]; 408 | } 409 | 410 | getMainFromGenerator(generators[c.value0]).curve = curve; 411 | } else if (predicates.isSetPlaybackRate(c)) { 412 | genericSetter( 413 | predicates, 414 | generators, 415 | c, 416 | "playbackRate", 417 | timeToSet 418 | ); 419 | } else if (predicates.isSetThreshold(c)) { 420 | genericSetter( 421 | predicates, 422 | generators, 423 | c, 424 | "threshold", 425 | timeToSet 426 | ); 427 | } else if (predicates.isSetKnee(c)) { 428 | genericSetter(predicates, generators, c, "knee", timeToSet); 429 | } else if (predicates.isSetRatio(c)) { 430 | genericSetter(predicates, generators, c, "ratio", timeToSet); 431 | } else if (predicates.isSetAttack(c)) { 432 | genericSetter(predicates, generators, c, "attack", timeToSet); 433 | } else if (predicates.isSetRelease(c)) { 434 | genericSetter( 435 | predicates, 436 | generators, 437 | c, 438 | "release", 439 | timeToSet 440 | ); 441 | } else if (predicates.isSetCustomParam(c)) { 442 | getMainFromGenerator(generators[c.value0]) 443 | .parameters.get(c.value1) 444 | .linearRampToValueAtTime(c.value2, timeToSet + c.value3); 445 | } else if (predicates.isStop(c)) { 446 | getMainFromGenerator(generators[c.value0]).stop(); 447 | } else if (predicates.isFree(c)) { 448 | delete generators[c.value0]; 449 | } else if (predicates.isSetConeInnerAngle(c)) { 450 | getMainFromGenerator(generators[c.value0]).coneInnerAngle = 451 | c.value1; 452 | } else if (predicates.isSetConeOuterAngle(c)) { 453 | getMainFromGenerator(generators[c.value0]).coneOuterAngle = 454 | c.value1; 455 | } else if (predicates.isSetConeOuterGain(c)) { 456 | getMainFromGenerator(generators[c.value0]).coneOuterGain = 457 | c.value1; 458 | } else if (predicates.isSetDistanceModel(c)) { 459 | getMainFromGenerator(generators[c.value0]).distanceModel = 460 | c.value1; 461 | } else if (predicates.isSetMaxDistance(c)) { 462 | getMainFromGenerator(generators[c.value0]).maxDistance = 463 | c.value1; 464 | } else if (predicates.isSetOrientationX(c)) { 465 | genericSetter( 466 | predicates, 467 | generators, 468 | c, 469 | "orientationX", 470 | timeToSet 471 | ); 472 | } else if (predicates.isSetOrientationY(c)) { 473 | genericSetter( 474 | predicates, 475 | generators, 476 | c, 477 | "orientationY", 478 | timeToSet 479 | ); 480 | } else if (predicates.isSetOrientationZ(c)) { 481 | genericSetter( 482 | predicates, 483 | generators, 484 | c, 485 | "orientationZ", 486 | timeToSet 487 | ); 488 | } else if (predicates.isSetPanningModel(c)) { 489 | getMainFromGenerator(generators[c.value0]).panningModel = 490 | c.value1; 491 | } else if (predicates.isSetPositionX(c)) { 492 | genericSetter( 493 | predicates, 494 | generators, 495 | c, 496 | "positionX", 497 | timeToSet 498 | ); 499 | } else if (predicates.isSetPositionY(c)) { 500 | genericSetter( 501 | predicates, 502 | generators, 503 | c, 504 | "positionY", 505 | timeToSet 506 | ); 507 | } else if (predicates.isSetPositionZ(c)) { 508 | genericSetter( 509 | predicates, 510 | generators, 511 | c, 512 | "positionZ", 513 | timeToSet 514 | ); 515 | } else if (predicates.isSetRefDistance(c)) { 516 | getMainFromGenerator(generators[c.value0]).refDistance = 517 | c.value1; 518 | } else if (predicates.isSetRolloffFactor(c)) { 519 | getMainFromGenerator(generators[c.value0]).rolloffFactor = 520 | c.value1; 521 | } 522 | } 523 | for (var i = 0; i < nu.length; i++) { 524 | var c = nu[i]; 525 | if (predicates.isLoopBuf(c.value1)) { 526 | getMainFromGenerator(generators[c.value0]).loop = true; 527 | getMainFromGenerator(generators[c.value0]).buffer = 528 | audioInfo.buffers[c.value3.value0]; 529 | getMainFromGenerator(generators[c.value0]).start( 530 | predicates.isNothing(c.value4) 531 | ? 0.0 532 | : timeToSet + c.value4.value0, 533 | lb[c.value0] 534 | ); 535 | } else if (predicates.isWaveShaper(c.value1)) { 536 | getMainFromGenerator(generators[c.value0]).curve = 537 | audioInfo.floatArrays[c.value3.value0]; 538 | } else if (predicates.isConvolver(c.value1)) { 539 | getMainFromGenerator(generators[c.value0]).buffer = 540 | audioInfo.buffers[c.value3.value0]; 541 | } else if (predicates.isPlayBuf(c.value1)) { 542 | getMainFromGenerator(generators[c.value0]).loop = false; 543 | getMainFromGenerator(generators[c.value0]).buffer = 544 | audioInfo.buffers[c.value3.value0]; 545 | getMainFromGenerator(generators[c.value0]).start( 546 | predicates.isNothing(c.value4) 547 | ? 0.0 548 | : timeToSet + c.value4.value0, 549 | c.value5.value0 550 | ); 551 | } else if (predicates.isPlay(c.value1)) { 552 | // todo - if the same element is resumed via play it won't 553 | // work in the current setup 554 | // this is because there is a 1-to-1 relationship between source 555 | // and media element 556 | // the current workaround is to create multiple media elements. 557 | // todo - add delay somehow... 558 | audioInfo.tracks[c.value3.value0].play(); 559 | } else if (predicates.isConstant(c.value1)) { 560 | getMainFromGenerator(generators[c.value0]).start( 561 | predicates.isNothing(c.value4) 562 | ? 0.0 563 | : timeToSet + c.value4.value0 564 | ); 565 | } else if (predicates.isLowpass(c.value1)) { 566 | getMainFromGenerator(generators[c.value0]).type = "lowpass"; 567 | } else if (predicates.isBandpass(c.value1)) { 568 | getMainFromGenerator(generators[c.value0]).type = "bandpass"; 569 | } else if (predicates.isLowshelf(c.value1)) { 570 | getMainFromGenerator(generators[c.value0]).type = "lowshelf"; 571 | } else if (predicates.isHighshelf(c.value1)) { 572 | getMainFromGenerator(generators[c.value0]).type = "highshelf"; 573 | } else if (predicates.isNotch(c.value1)) { 574 | getMainFromGenerator(generators[c.value0]).type = "notch"; 575 | } else if (predicates.isAllpass(c.value1)) { 576 | getMainFromGenerator(generators[c.value0]).type = "allpass"; 577 | } else if (predicates.isPeaking(c.value1)) { 578 | getMainFromGenerator(generators[c.value0]).type = "peaking"; 579 | } else if (predicates.isHighpass(c.value1)) { 580 | getMainFromGenerator(generators[c.value0]).type = "highpass"; 581 | } else if (predicates.isSinOsc(c.value1)) { 582 | getMainFromGenerator(generators[c.value0]).type = "sine"; 583 | getMainFromGenerator(generators[c.value0]).start( 584 | predicates.isNothing(c.value4) 585 | ? 0.0 586 | : timeToSet + c.value4.value0 587 | ); 588 | } else if (predicates.isSquareOsc(c.value1)) { 589 | getMainFromGenerator(generators[c.value0]).type = "square"; 590 | getMainFromGenerator(generators[c.value0]).start( 591 | predicates.isNothing(c.value4) 592 | ? 0.0 593 | : timeToSet + c.value4.value0 594 | ); 595 | } else if (predicates.isTriangleOsc(c.value1)) { 596 | getMainFromGenerator(generators[c.value0]).type = "triangle"; 597 | getMainFromGenerator(generators[c.value0]).start( 598 | predicates.isNothing(c.value4) 599 | ? 0.0 600 | : timeToSet + c.value4.value0 601 | ); 602 | } else if (predicates.isSawtoothOsc(c.value1)) { 603 | getMainFromGenerator(generators[c.value0]).type = "sawtooth"; 604 | getMainFromGenerator(generators[c.value0]).start( 605 | predicates.isNothing(c.value4) 606 | ? 0.0 607 | : timeToSet + c.value4.value0 608 | ); 609 | } else if (predicates.isPeriodicOsc(c.value1)) { 610 | getMainFromGenerator(generators[c.value0]).setPeriodicWave( 611 | audioInfo.periodicWaves[c.value3.value0] 612 | ); 613 | getMainFromGenerator(generators[c.value0]).start( 614 | predicates.isNothing(c.value4) 615 | ? 0.0 616 | : timeToSet + c.value4.value0 617 | ); 618 | } else if (predicates.isSplitRes(c.value1)) { 619 | getMainFromGenerator( 620 | generators[c.value0] 621 | ).gain.linearRampToValueAtTime(1.0, timeToSet); 622 | } else if (predicates.isDupRes(c.value1)) { 623 | getMainFromGenerator( 624 | generators[c.value0] 625 | ).gain.linearRampToValueAtTime(1.0, timeToSet); 626 | } 627 | } 628 | return { generators: generators, recorders: recorders }; 629 | }; 630 | }; 631 | }; 632 | }; 633 | }; 634 | }; 635 | }; 636 | 637 | exports.stopMediaRecorder = function (mediaRecorder) { 638 | return function () { 639 | mediaRecorder.stop(); 640 | }; 641 | }; 642 | 643 | exports.getAudioClockTime = function (ctx) { 644 | return function () { 645 | return ctx.currentTime; 646 | }; 647 | }; 648 | 649 | exports.getBoundingClientRect = function (canvas) { 650 | return function () { 651 | var o = canvas.getBoundingClientRect(); 652 | return { 653 | x: o.left, 654 | y: o.top, 655 | width: o.right - o.left, 656 | height: o.bottom - o.top, 657 | }; 658 | }; 659 | }; 660 | 661 | exports.isTypeSupported = function (mimeType) { 662 | return function () { 663 | return MediaRecorder.isTypeSupported(mimeType); 664 | }; 665 | }; 666 | 667 | exports.mediaRecorderToUrl = function (mimeType) { 668 | return function (handler) { 669 | return function (mediaRecorder) { 670 | var chunks = []; 671 | return function () { 672 | mediaRecorder.ondataavailable = function (evt) { 673 | chunks.push(evt.data); 674 | }; 675 | 676 | mediaRecorder.onstop = function () { 677 | var blob = new Blob(chunks, { type: mimeType }); 678 | handler(URL.createObjectURL(blob))(); 679 | chunks = null; 680 | }; 681 | }; 682 | }; 683 | }; 684 | }; 685 | -------------------------------------------------------------------------------- /src/FRP/Behavior/MIDI.purs: -------------------------------------------------------------------------------- 1 | module FRP.Behavior.MIDI 2 | ( midi 3 | ) where 4 | 5 | import Prelude 6 | 7 | import Data.List (List) 8 | import Data.Map (Map) 9 | import FRP.Behavior (Behavior, behavior) 10 | import FRP.Event.MIDI (MIDI, withMidi, MIDIEventInTime) 11 | 12 | -- | A `Behavior` which reports the current midi program 13 | midi :: MIDI -> Behavior (Map String (List MIDIEventInTime)) 14 | midi m = behavior \e -> map (\{ value, midi: midi_ } -> value midi_) (withMidi m e) 15 | -------------------------------------------------------------------------------- /src/FRP/Event/MIDI.js: -------------------------------------------------------------------------------- 1 | exports.midiAccess = function () { 2 | return navigator.requestMIDIAccess(); 3 | }; 4 | 5 | exports.getData_ = function (nothing) { 6 | return function (just) { 7 | return function (e) { 8 | return function () { 9 | return e.data ? just(e.data) : nothing; 10 | }; 11 | }; 12 | }; 13 | }; 14 | 15 | exports.getTimeStamp_ = function (nothing) { 16 | return function (just) { 17 | return function (e) { 18 | return function () { 19 | return e.timeStamp ? just(e.timeStamp) : nothing; 20 | }; 21 | }; 22 | }; 23 | }; 24 | 25 | exports.toTargetMap = function (midiAccess) { 26 | return function () { 27 | var o = {}; 28 | var a = Array.from(midiAccess.inputs); 29 | for (var i = 0; i < a.length; i++) { 30 | o[a[i][0]] = a[i][1]; 31 | } 32 | return o; 33 | }; 34 | }; 35 | exports.toMIDIEvent_ = function (NoteOff) { 36 | return function (NoteOn) { 37 | return function (Polytouch) { 38 | return function (ControlChange) { 39 | return function (ProgramChange) { 40 | return function (Aftertouch) { 41 | return function (Pitchwheel) { 42 | return function (Nothing) { 43 | return function (Just) { 44 | return function (a) { 45 | return function () { 46 | return a[0] >= 128 && a[0] <= 143 47 | ? Just(NoteOff(a[0] - 128)(a[1])(a[2])) 48 | : a[0] >= 144 && a[0] <= 159 49 | ? Just(NoteOn(a[0] - 144)(a[1])(a[2])) 50 | : a[0] >= 160 && a[0] <= 175 51 | ? Just(Polytouch(a[0] - 160)(a[1])(a[2])) 52 | : a[0] >= 176 && a[0] <= 191 53 | ? Just(ControlChange(a[0] - 176)(a[1])(a[2])) 54 | : a[0] >= 192 && a[0] <= 207 55 | ? Just(ProgramChange(a[0] - 192)(a[1])) 56 | : a[0] >= 208 && a[0] <= 223 57 | ? Just(Aftertouch(a[0] - 208)(a[1])) 58 | : a[0] >= 224 && a[0] <= 239 59 | ? Just(Pitchwheel(a[0] - 224)(a[1])) 60 | : Nothing; 61 | }; 62 | }; 63 | }; 64 | }; 65 | }; 66 | }; 67 | }; 68 | }; 69 | }; 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/FRP/Event/MIDI.purs: -------------------------------------------------------------------------------- 1 | module FRP.Event.MIDI 2 | ( MIDI(..) 3 | , MIDIAccess(..) 4 | , MIDIEvent(..) 5 | , MIDIEventInTime(..) 6 | , getMidi 7 | , disposeMidi 8 | , withMidi 9 | , midiAccess 10 | ) where 11 | 12 | import Prelude 13 | import Control.Promise (Promise) 14 | import Data.ArrayBuffer.Types (ArrayBuffer) 15 | import Data.Foldable (traverse_) 16 | import Data.List (List(..), (:)) 17 | import Data.Map (Map) 18 | import Data.Map as M 19 | import Data.Maybe (Maybe(..), fromMaybe, maybe) 20 | import Data.Newtype (wrap) 21 | import Data.Traversable (sequence) 22 | import Data.Tuple (Tuple(..)) 23 | import Effect (Effect) 24 | import Effect.Ref as Ref 25 | import FRP.Event (Event, makeEvent, subscribe) 26 | import Foreign.Object as O 27 | import Web.Event.Event as WE 28 | import Web.Event.EventTarget (EventTarget, addEventListener, eventListener, removeEventListener) 29 | import Web.Internal.FFI (unsafeReadProtoTagged) 30 | 31 | data MIDIEvent 32 | = NoteOff Int Int Int 33 | | NoteOn Int Int Int 34 | | Polytouch Int Int Int 35 | | ControlChange Int Int Int 36 | | ProgramChange Int Int 37 | | Aftertouch Int Int 38 | | Pitchwheel Int Int 39 | 40 | type MIDIEventInTime 41 | = { timeStamp :: Number 42 | , event :: MIDIEvent 43 | } 44 | 45 | foreign import data MIDIAccess :: Type 46 | 47 | foreign import data MIDIMessageEvent :: Type 48 | 49 | foreign import midiAccess :: Effect (Promise MIDIAccess) 50 | 51 | foreign import toTargetMap :: MIDIAccess -> Effect (O.Object EventTarget) 52 | 53 | foreign import toMIDIEvent_ :: 54 | (Int -> Int -> Int -> MIDIEvent) -> 55 | (Int -> Int -> Int -> MIDIEvent) -> 56 | (Int -> Int -> Int -> MIDIEvent) -> 57 | (Int -> Int -> Int -> MIDIEvent) -> 58 | (Int -> Int -> MIDIEvent) -> 59 | (Int -> Int -> MIDIEvent) -> 60 | (Int -> Int -> MIDIEvent) -> 61 | Maybe MIDIEvent -> 62 | (MIDIEvent -> Maybe MIDIEvent) -> 63 | ArrayBuffer -> 64 | Effect (Maybe MIDIEvent) 65 | 66 | foreign import getData_ :: (Maybe ArrayBuffer) -> (ArrayBuffer -> Maybe ArrayBuffer) -> MIDIMessageEvent -> Effect (Maybe ArrayBuffer) 67 | 68 | foreign import getTimeStamp_ :: (Maybe Number) -> (Number -> Maybe Number) -> MIDIMessageEvent -> Effect (Maybe Number) 69 | 70 | getData :: MIDIMessageEvent -> Effect (Maybe ArrayBuffer) 71 | getData = getData_ Nothing Just 72 | 73 | getTimeStamp :: MIDIMessageEvent -> Effect (Maybe Number) 74 | getTimeStamp = getTimeStamp_ Nothing Just 75 | 76 | newtype MIDI 77 | = MIDI 78 | { midi :: Ref.Ref (Map String (List MIDIEventInTime)) 79 | , dispose :: Effect Unit 80 | } 81 | 82 | toMIDIEvent :: ArrayBuffer -> Effect (Maybe MIDIEvent) 83 | toMIDIEvent = 84 | toMIDIEvent_ 85 | NoteOff 86 | NoteOn 87 | Polytouch 88 | ControlChange 89 | ProgramChange 90 | Aftertouch 91 | Pitchwheel 92 | Nothing 93 | Just 94 | 95 | fromEvent :: WE.Event -> Maybe MIDIMessageEvent 96 | fromEvent = unsafeReadProtoTagged "MIDIMessageEvent" 97 | 98 | getMidi :: MIDIAccess -> Effect MIDI 99 | getMidi midiAccess_ = do 100 | targetMap <- toTargetMap midiAccess_ >>= pure <<< M.fromFoldable <<< (O.toUnfoldable :: O.Object EventTarget -> List (Tuple String EventTarget)) 101 | midi <- Ref.new M.empty 102 | let 103 | makeListener inputName = 104 | eventListener \e -> do 105 | fromEvent e 106 | # traverse_ \me -> do 107 | data__ <- getData me 108 | timeStamp__ <- getTimeStamp me 109 | midiEvent__ <- maybe (pure Nothing) toMIDIEvent data__ 110 | let 111 | toAdd_ = 112 | ( do 113 | timeStamp_ <- timeStamp__ 114 | midiEvent_ <- midiEvent__ 115 | pure 116 | { timeStamp: timeStamp_ 117 | , event: midiEvent_ 118 | } 119 | ) 120 | case toAdd_ of 121 | Nothing -> pure unit 122 | Just toAdd -> 123 | ( do 124 | _ <- 125 | Ref.modify 126 | ( \mmap -> 127 | M.union 128 | ( M.singleton inputName 129 | ( toAdd 130 | : ( fromMaybe Nil 131 | $ M.lookup inputName mmap 132 | ) 133 | ) 134 | ) 135 | mmap 136 | ) 137 | midi 138 | pure unit 139 | ) 140 | listeners <- 141 | sequence 142 | $ M.mapMaybeWithKey 143 | ( \k v -> 144 | Just 145 | ( do 146 | listener <- makeListener k 147 | _ <- 148 | addEventListener 149 | (wrap "midimessage") 150 | listener 151 | false 152 | v 153 | pure (Tuple listener v) 154 | ) 155 | ) 156 | targetMap 157 | let 158 | dispose = do 159 | _ <- 160 | sequence 161 | $ M.mapMaybeWithKey 162 | ( \k (Tuple l v) -> 163 | Just 164 | ( removeEventListener 165 | (wrap "midimessage") 166 | l 167 | false 168 | v 169 | ) 170 | ) 171 | listeners 172 | pure unit 173 | pure (MIDI { midi, dispose }) 174 | 175 | disposeMidi :: MIDI -> Effect Unit 176 | disposeMidi (MIDI { dispose }) = dispose 177 | 178 | -- | Create an event which also returns the current state of MIDI. 179 | withMidi :: 180 | forall a. 181 | MIDI -> 182 | Event a -> 183 | Event { value :: a, midi :: Map String (List MIDIEventInTime) } 184 | withMidi (MIDI { midi }) e = 185 | makeEvent \k -> 186 | e 187 | `subscribe` 188 | \value -> do 189 | midi_ <- Ref.read midi 190 | k { value, midi: midi_ } 191 | -------------------------------------------------------------------------------- /test.dhall: -------------------------------------------------------------------------------- 1 | let conf = ./spago.dhall 2 | 3 | in conf 4 | // { sources = conf.sources # [ "test/**/*.purs" ] 5 | , dependencies = conf.dependencies # [ "spec" ] 6 | } 7 | -------------------------------------------------------------------------------- /test/Basic.purs: -------------------------------------------------------------------------------- 1 | module Test.Basic where 2 | 3 | import Prelude 4 | import Control.Monad.Error.Class (class MonadThrow) 5 | import Data.Array (replicate) 6 | import Data.List (List(..), (:)) 7 | import Data.List as DL 8 | import Data.Map as DM 9 | import Data.Maybe (Maybe(..)) 10 | import Data.NonEmpty ((:|)) 11 | import Data.Set (fromFoldable) 12 | import Data.Set as DS 13 | import Data.Tuple (Tuple(..), snd) 14 | import Data.Typelevel.Num (D1, d3) 15 | import Data.Vec as V 16 | import Effect.Aff (Error) 17 | import Effect.Class (class MonadEffect) 18 | import FRP.Behavior.Audio (class AsProcessorObject, class IsValidAudioGraph, AudioGraph, AudioGraphProcessor, AudioParameterTransition(..), AudioUnit, AudioUnit'(..), Status(..), asProcessor, asProcessorObject, audioToPtr, dup1, g'add, g'bandpass, g'delay, gain, gain', graph, merger, microphone, sinOsc, speaker', split3, toObject) 19 | import Foreign.Object as O 20 | import Prim.Boolean (False, True) 21 | import Prim.RowList (class RowToList) 22 | import Test.Spec (SpecT, describe, it) 23 | import Test.Spec.Assertions (shouldEqual) 24 | import Type.Data.Boolean (BProxy(..)) 25 | import Type.Data.Graph (type (:/), SNil) 26 | import Type.Proxy (Proxy(..)) 27 | 28 | isValidAudioGraph1 :: BProxy True 29 | isValidAudioGraph1 = 30 | BProxy :: 31 | forall b ch. 32 | IsValidAudioGraph 33 | ( generators :: Record ( hello :: AudioUnit ch ) 34 | ) 35 | ch 36 | b => 37 | BProxy b 38 | 39 | isValidAudioGraph2 :: BProxy False 40 | isValidAudioGraph2 = 41 | BProxy :: 42 | forall b ch. 43 | IsValidAudioGraph 44 | () 45 | ch 46 | b => 47 | BProxy b 48 | 49 | isValidAudioGraph3 :: BProxy True 50 | isValidAudioGraph3 = 51 | BProxy :: 52 | forall b ch. 53 | IsValidAudioGraph 54 | ( processors :: Record ( goodbye :: Tuple (AudioGraphProcessor) (Proxy "hello") ) 55 | , generators :: Record ( hello :: AudioUnit ch ) 56 | ) 57 | ch 58 | b => 59 | BProxy b 60 | 61 | isValidAudioGraph4 :: BProxy False 62 | isValidAudioGraph4 = 63 | BProxy :: 64 | forall b ch. 65 | IsValidAudioGraph 66 | ( processors :: Record ( goodbye :: Tuple (AudioGraphProcessor) (Proxy "notThere") ) 67 | , generators :: Record ( hello :: AudioUnit ch ) 68 | ) 69 | ch 70 | b => 71 | BProxy b 72 | 73 | basicTestSuite :: ∀ eff m. Monad m ⇒ Bind eff ⇒ MonadEffect eff ⇒ MonadThrow Error eff ⇒ SpecT eff Unit m Unit 74 | basicTestSuite = do 75 | describe "complex split with merge" do 76 | it "should correctly split" do 77 | let 78 | tree = 79 | audioToPtr 80 | $ split3 81 | ( merger 82 | $ V.replicate d3 83 | ( ( gain 1.0 84 | ( (sinOsc 440.0) 85 | :| (DL.fromFoldable (replicate 2 (sinOsc 441.0))) 86 | ) 87 | ) 88 | + ( gain 0.9 89 | ( (sinOsc 442.0) 90 | :| (DL.fromFoldable (replicate 2 (sinOsc 443.0))) 91 | ) 92 | ) 93 | ) 94 | ) 95 | ( \v -> 96 | speaker' 97 | ( gain' 0.5 98 | $ ( merger 99 | (map (gain' 0.3) v) 100 | ) 101 | ) 102 | ) 103 | tree 104 | `shouldEqual` 105 | { flat: 106 | ( DM.fromFoldable 107 | [ (Tuple 0 { au: (Splitter' 3), chan: 3, head: 1, name: Nothing, next: (fromFoldable (5 : 7 : 9 : Nil)), prev: (fromFoldable (10 : Nil)), ptr: 0, status: On }) 108 | , (Tuple 1 { au: Speaker', chan: 3, head: 1, name: Nothing, next: (fromFoldable Nil), prev: (fromFoldable (2 : Nil)), ptr: 1, status: On }) 109 | , (Tuple 2 { au: (Gain' { forceSet: false, param: 0.5, timeOffset: 0.0, transition: LinearRamp }), chan: 3, head: 2, name: Nothing, next: (fromFoldable (1 : Nil)), prev: (fromFoldable (3 : Nil)), ptr: 2, status: On }) 110 | , (Tuple 3 { au: (Merger' (8 : 6 : 4 : Nil)), chan: 3, head: 3, name: Nothing, next: (fromFoldable (2 : Nil)), prev: (fromFoldable (4 : 6 : 8 : Nil)), ptr: 3, status: On }) 111 | , (Tuple 4 { au: (Gain' { forceSet: false, param: 0.3, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 4, name: Nothing, next: (fromFoldable (3 : Nil)), prev: (fromFoldable (5 : Nil)), ptr: 4, status: On }) 112 | , (Tuple 5 { au: (SplitRes' 0), chan: 1, head: 5, name: Nothing, next: (fromFoldable (4 : Nil)), prev: (fromFoldable Nil), ptr: 5, status: On }) 113 | , (Tuple 6 { au: (Gain' { forceSet: false, param: 0.3, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 6, name: Nothing, next: (fromFoldable (3 : Nil)), prev: (fromFoldable (7 : Nil)), ptr: 6, status: On }) 114 | , (Tuple 7 { au: (SplitRes' 1), chan: 1, head: 7, name: Nothing, next: (fromFoldable (6 : Nil)), prev: (fromFoldable Nil), ptr: 7, status: On }) 115 | , (Tuple 8 { au: (Gain' { forceSet: false, param: 0.3, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 8, name: Nothing, next: (fromFoldable (3 : Nil)), prev: (fromFoldable (9 : Nil)), ptr: 8, status: On }) 116 | , (Tuple 9 { au: (SplitRes' 2), chan: 1, head: 9, name: Nothing, next: (fromFoldable (8 : Nil)), prev: (fromFoldable Nil), ptr: 9, status: On }) 117 | , (Tuple 10 { au: (Merger' (29 : 20 : 11 : Nil)), chan: 3, head: 10, name: Nothing, next: (fromFoldable (0 : Nil)), prev: (fromFoldable (11 : 20 : 29 : Nil)), ptr: 10, status: On }) 118 | , (Tuple 11 { au: Add', chan: 1, head: 11, name: Nothing, next: (fromFoldable (10 : Nil)), prev: (fromFoldable (12 : 16 : Nil)), ptr: 11, status: On }) 119 | , (Tuple 12 { au: (Gain' { forceSet: false, param: 1.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 12, name: Nothing, next: (fromFoldable (11 : Nil)), prev: (fromFoldable (13 : 14 : 15 : Nil)), ptr: 12, status: On }) 120 | , (Tuple 13 { au: (SinOsc' { forceSet: false, param: 440.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 13, name: Nothing, next: (fromFoldable (12 : Nil)), prev: (fromFoldable Nil), ptr: 13, status: On }) 121 | , (Tuple 14 { au: (SinOsc' { forceSet: false, param: 441.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 14, name: Nothing, next: (fromFoldable (12 : Nil)), prev: (fromFoldable Nil), ptr: 14, status: On }) 122 | , (Tuple 15 { au: (SinOsc' { forceSet: false, param: 441.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 15, name: Nothing, next: (fromFoldable (12 : Nil)), prev: (fromFoldable Nil), ptr: 15, status: On }) 123 | , (Tuple 16 { au: (Gain' { forceSet: false, param: 0.9, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 16, name: Nothing, next: (fromFoldable (11 : Nil)), prev: (fromFoldable (17 : 18 : 19 : Nil)), ptr: 16, status: On }) 124 | , (Tuple 17 { au: (SinOsc' { forceSet: false, param: 442.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 17, name: Nothing, next: (fromFoldable (16 : Nil)), prev: (fromFoldable Nil), ptr: 17, status: On }) 125 | , (Tuple 18 { au: (SinOsc' { forceSet: false, param: 443.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 18, name: Nothing, next: (fromFoldable (16 : Nil)), prev: (fromFoldable Nil), ptr: 18, status: On }) 126 | , (Tuple 19 { au: (SinOsc' { forceSet: false, param: 443.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 19, name: Nothing, next: (fromFoldable (16 : Nil)), prev: (fromFoldable Nil), ptr: 19, status: On }) 127 | , (Tuple 20 { au: Add', chan: 1, head: 20, name: Nothing, next: (fromFoldable (10 : Nil)), prev: (fromFoldable (21 : 25 : Nil)), ptr: 20, status: On }) 128 | , (Tuple 21 { au: (Gain' { forceSet: false, param: 1.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 21, name: Nothing, next: (fromFoldable (20 : Nil)), prev: (fromFoldable (22 : 23 : 24 : Nil)), ptr: 21, status: On }) 129 | , (Tuple 22 { au: (SinOsc' { forceSet: false, param: 440.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 22, name: Nothing, next: (fromFoldable (21 : Nil)), prev: (fromFoldable Nil), ptr: 22, status: On }) 130 | , (Tuple 23 { au: (SinOsc' { forceSet: false, param: 441.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 23, name: Nothing, next: (fromFoldable (21 : Nil)), prev: (fromFoldable Nil), ptr: 23, status: On }) 131 | , (Tuple 24 { au: (SinOsc' { forceSet: false, param: 441.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 24, name: Nothing, next: (fromFoldable (21 : Nil)), prev: (fromFoldable Nil), ptr: 24, status: On }) 132 | , (Tuple 25 { au: (Gain' { forceSet: false, param: 0.9, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 25, name: Nothing, next: (fromFoldable (20 : Nil)), prev: (fromFoldable (26 : 27 : 28 : Nil)), ptr: 25, status: On }) 133 | , (Tuple 26 { au: (SinOsc' { forceSet: false, param: 442.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 26, name: Nothing, next: (fromFoldable (25 : Nil)), prev: (fromFoldable Nil), ptr: 26, status: On }) 134 | , (Tuple 27 { au: (SinOsc' { forceSet: false, param: 443.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 27, name: Nothing, next: (fromFoldable (25 : Nil)), prev: (fromFoldable Nil), ptr: 27, status: On }) 135 | , (Tuple 28 { au: (SinOsc' { forceSet: false, param: 443.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 28, name: Nothing, next: (fromFoldable (25 : Nil)), prev: (fromFoldable Nil), ptr: 28, status: On }) 136 | , (Tuple 29 { au: Add', chan: 1, head: 29, name: Nothing, next: (fromFoldable (10 : Nil)), prev: (fromFoldable (30 : 34 : Nil)), ptr: 29, status: On }) 137 | , (Tuple 30 { au: (Gain' { forceSet: false, param: 1.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 30, name: Nothing, next: (fromFoldable (29 : Nil)), prev: (fromFoldable (31 : 32 : 33 : Nil)), ptr: 30, status: On }) 138 | , (Tuple 31 { au: (SinOsc' { forceSet: false, param: 440.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 31, name: Nothing, next: (fromFoldable (30 : Nil)), prev: (fromFoldable Nil), ptr: 31, status: On }) 139 | , (Tuple 32 { au: (SinOsc' { forceSet: false, param: 441.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 32, name: Nothing, next: (fromFoldable (30 : Nil)), prev: (fromFoldable Nil), ptr: 32, status: On }) 140 | , (Tuple 33 { au: (SinOsc' { forceSet: false, param: 441.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 33, name: Nothing, next: (fromFoldable (30 : Nil)), prev: (fromFoldable Nil), ptr: 33, status: On }) 141 | , (Tuple 34 { au: (Gain' { forceSet: false, param: 0.9, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 34, name: Nothing, next: (fromFoldable (29 : Nil)), prev: (fromFoldable (35 : 36 : 37 : Nil)), ptr: 34, status: On }) 142 | , (Tuple 35 { au: (SinOsc' { forceSet: false, param: 442.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 35, name: Nothing, next: (fromFoldable (34 : Nil)), prev: (fromFoldable Nil), ptr: 35, status: On }) 143 | , (Tuple 36 { au: (SinOsc' { forceSet: false, param: 443.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 36, name: Nothing, next: (fromFoldable (34 : Nil)), prev: (fromFoldable Nil), ptr: 36, status: On }) 144 | , (Tuple 37 { au: (SinOsc' { forceSet: false, param: 443.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 37, name: Nothing, next: (fromFoldable (34 : Nil)), prev: (fromFoldable Nil), ptr: 37, status: On }) 145 | ] 146 | ) 147 | , len: 38 148 | , p: { au: (Splitter' 3), chan: 3, head: 1, name: Nothing, next: (fromFoldable (5 : 7 : 9 : Nil)), prev: (fromFoldable (10 : Nil)), ptr: 0, status: On } 149 | } 150 | describe "Audio tree" do 151 | it "should correctly split" do 152 | let 153 | tree = 154 | audioToPtr 155 | (speaker' $ dup1 microphone (\u -> (sinOsc 42.0 * u))) 156 | tree 157 | `shouldEqual` 158 | { flat: 159 | ( DM.fromFoldable 160 | [ (Tuple 0 { au: Speaker', chan: 1, head: 0, name: Nothing, next: (fromFoldable Nil), prev: (fromFoldable (2 : Nil)), ptr: 0, status: On }) 161 | , (Tuple 1 { au: Dup', chan: 1, head: 2, name: Nothing, next: (fromFoldable (4 : Nil)), prev: (fromFoldable (5 : Nil)), ptr: 1, status: On }) 162 | , (Tuple 2 { au: Mul', chan: 1, head: 2, name: Nothing, next: (fromFoldable (0 : Nil)), prev: (fromFoldable (3 : 4 : Nil)), ptr: 2, status: On }) 163 | , (Tuple 3 { au: (SinOsc' { forceSet: false, param: 42.0, timeOffset: 0.0, transition: LinearRamp }), chan: 1, head: 3, name: Nothing, next: (fromFoldable (2 : Nil)), prev: (fromFoldable Nil), ptr: 3, status: On }) 164 | , (Tuple 4 { au: DupRes', chan: 1, head: 4, name: Nothing, next: (fromFoldable (2 : Nil)), prev: (fromFoldable Nil), ptr: 4, status: On }) 165 | , (Tuple 5 { au: Microphone', chan: 1, head: 5, name: Nothing, next: (fromFoldable (1 : Nil)), prev: (fromFoldable Nil), ptr: 5, status: On }) 166 | ] 167 | ) 168 | , len: 6 169 | , p: { au: Speaker', chan: 1, head: 0, name: Nothing, next: (fromFoldable Nil), prev: (fromFoldable (2 : Nil)), ptr: 0, status: On } 170 | } 171 | describe "To object" do 172 | it "should correctly transform a simple object" do 173 | let 174 | g = 175 | ( toObject 176 | { generators: { mic: microphone } 177 | } 178 | ) :: 179 | AudioGraph D1 180 | O.size g.generators `shouldEqual` 1 181 | it "should correctly transform a processor" do 182 | (snd <$> (asProcessor $ Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "mic"))) `shouldEqual` Just "mic" 183 | (snd <$> (asProcessor $ Tuple (g'bandpass 440.0 1.0) "a string")) `shouldEqual` Nothing 184 | it "should correctly transform a processors object" do 185 | let 186 | apo :: forall t tl. RowToList t tl => AsProcessorObject tl t => (Record t) -> O.Object (Tuple (AudioGraphProcessor) String) 187 | apo r = asProcessorObject (Proxy :: Proxy tl) r 188 | 189 | g = 190 | apo 191 | { filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "mic") 192 | } 193 | O.size g `shouldEqual` 1 194 | it "should correctly transform a complex object" do 195 | let 196 | g = 197 | toObject 198 | { processors: 199 | { filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "mic") 200 | } 201 | , generators: { mic: microphone } 202 | } :: 203 | AudioGraph D1 204 | O.size g.generators `shouldEqual` 1 205 | O.size g.processors `shouldEqual` 1 206 | (snd <$> O.values g.processors) `shouldEqual` [ "mic" ] 207 | it "should correctly transform a very complex object" do 208 | let 209 | g = 210 | toObject 211 | { aggregators: 212 | { combine: Tuple g'add (Proxy :: Proxy ("filt" :/ "sosc" :/ SNil)) 213 | } 214 | , processors: 215 | { filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "mic") 216 | } 217 | , generators: 218 | { mic: microphone 219 | , sosc: sinOsc 440.0 220 | } 221 | } :: 222 | AudioGraph D1 223 | O.size g.generators `shouldEqual` 2 224 | O.size g.processors `shouldEqual` 1 225 | (snd <$> O.values g.processors) `shouldEqual` [ "mic" ] 226 | (snd <$> O.values g.aggregators) `shouldEqual` [ DS.fromFoldable [ "filt", "sosc" ] ] 227 | describe "Audio tree" do 228 | it "should correctly compile" do 229 | let 230 | tree = 231 | audioToPtr 232 | ( speaker' 233 | $ ( graph 234 | { aggregators: 235 | { combine: Tuple g'add (Proxy :: Proxy ("filt" :/ "sosc" :/ SNil)) 236 | } 237 | , processors: 238 | { filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "mic") 239 | } 240 | , generators: 241 | { mic: microphone 242 | , sosc: sinOsc 440.0 243 | } 244 | } 245 | ) 246 | ) 247 | tree 248 | `shouldEqual` 249 | { flat: 250 | ( DM.fromFoldable 251 | [ (Tuple 0 { au: Speaker', chan: 1, head: 0, name: Nothing, next: (fromFoldable Nil), prev: (fromFoldable (2 : Nil)), ptr: 0, status: On }) 252 | , ( Tuple 1 253 | { au: 254 | ( Bandpass' 255 | ( { param: 440.0, forceSet: false, timeOffset: 0.0, transition: LinearRamp 256 | } 257 | ) 258 | ({ transition: LinearRamp, forceSet: false, param: 1.0, timeOffset: 0.0 }) 259 | ) 260 | , chan: 1 261 | , head: 1 262 | , name: Nothing 263 | , next: (fromFoldable (2 : Nil)) 264 | , prev: (fromFoldable (3 : Nil)) 265 | , ptr: 1 266 | , status: On 267 | } 268 | ) 269 | , ( Tuple 2 270 | { au: Add' 271 | , chan: 1 272 | , head: 2 273 | , name: Nothing 274 | , next: (fromFoldable (0 : Nil)) 275 | , prev: (fromFoldable (1 : 4 : Nil)) 276 | , ptr: 2 277 | , status: On 278 | } 279 | ) 280 | , ( Tuple 3 281 | { au: Microphone' 282 | , chan: 1 283 | , head: 3 284 | , name: Nothing 285 | , next: (fromFoldable (1 : Nil)) 286 | , prev: (fromFoldable Nil) 287 | , ptr: 3 288 | , status: On 289 | } 290 | ) 291 | , ( Tuple 4 292 | { au: 293 | ( SinOsc' 294 | ( { param: 440.0 295 | , forceSet: false 296 | , timeOffset: 0.0 297 | , transition: LinearRamp 298 | } 299 | ) 300 | ) 301 | , chan: 1 302 | , head: 4 303 | , name: Nothing 304 | , next: (fromFoldable (2 : Nil)) 305 | , prev: (fromFoldable Nil) 306 | , ptr: 4 307 | , status: On 308 | } 309 | ) 310 | ] 311 | ) 312 | , len: 5 313 | , p: { au: Speaker', chan: 1, head: 0, name: Nothing, next: (fromFoldable Nil), prev: (fromFoldable (2 : Nil)), ptr: 0, status: On } 314 | } 315 | it "should correctly compile feedback" do 316 | let 317 | tree = 318 | audioToPtr 319 | ( speaker' 320 | $ ( graph 321 | { aggregators: 322 | { combine: Tuple g'add (Proxy :: Proxy ("del" :/ "mic" :/ SNil)) 323 | } 324 | , processors: 325 | { filt: Tuple (g'bandpass 440.0 1.0) (Proxy :: Proxy "mic") 326 | , del: Tuple (g'delay 0.2) (Proxy :: Proxy "filt") 327 | } 328 | , generators: 329 | { mic: microphone 330 | } 331 | } 332 | ) 333 | ) 334 | tree 335 | `shouldEqual` 336 | { flat: 337 | ( DM.fromFoldable 338 | [ ( Tuple 0 339 | { au: Speaker' 340 | , chan: 1 341 | , head: 0 342 | , name: Nothing 343 | , next: (fromFoldable Nil) 344 | , prev: 345 | (fromFoldable (3 : Nil)) 346 | , ptr: 0 347 | , status: On 348 | } 349 | ) 350 | , ( Tuple 1 351 | { au: (Bandpass' ({ transition: LinearRamp, forceSet: false, param: 440.0, timeOffset: 0.0 }) ({ transition: LinearRamp, forceSet: false, param: 1.0, timeOffset: 0.0 })) 352 | , chan: 1 353 | , head: 1 354 | , name: Nothing 355 | , next: 356 | (fromFoldable (2 : Nil)) 357 | , prev: (fromFoldable (4 : Nil)) 358 | , ptr: 1 359 | , status: On 360 | } 361 | ) 362 | , ( Tuple 2 363 | { au: (Delay' ({ transition: LinearRamp, forceSet: false, param: 0.2, timeOffset: 0.0 })) 364 | , chan: 1 365 | , head: 366 | 2 367 | , name: Nothing 368 | , next: (fromFoldable (3 : Nil)) 369 | , prev: (fromFoldable (1 : Nil)) 370 | , ptr: 2 371 | , status: On 372 | } 373 | ) 374 | , ( Tuple 3 375 | { au: Add' 376 | , chan: 1 377 | , head: 3 378 | , name: Nothing 379 | , next: (fromFoldable (0 : Nil)) 380 | , prev: 381 | (fromFoldable (2 : 4 : Nil)) 382 | , ptr: 3 383 | , status: On 384 | } 385 | ) 386 | , ( Tuple 4 387 | { au: Microphone' 388 | , chan: 1 389 | , head: 4 390 | , name: Nothing 391 | , next: 392 | ( fromFoldable 393 | ( 1 : 3 394 | : Nil 395 | ) 396 | ) 397 | , prev: (fromFoldable Nil) 398 | , ptr: 4 399 | , status: On 400 | } 401 | ) 402 | ] 403 | ) 404 | , len: 5 405 | , p: { au: Speaker', chan: 1, head: 0, name: Nothing, next: (fromFoldable Nil), prev: (fromFoldable (3 : Nil)), ptr: 0, status: On } 406 | } 407 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Effect (Effect) 5 | import Effect.Aff (launchAff_) 6 | import Test.Basic (basicTestSuite) 7 | import Test.Spec.Reporter.Console (consoleReporter) 8 | import Test.Spec.Runner (runSpec) 9 | 10 | main ∷ Effect Unit 11 | main = 12 | launchAff_ 13 | $ runSpec [ consoleReporter ] do 14 | basicTestSuite 15 | --------------------------------------------------------------------------------