├── .gitignore ├── LICENSE ├── README.md ├── assets └── github-repo-splash.png ├── dist ├── audio │ ├── event.js │ ├── index.js │ ├── keyed.js │ ├── node.js │ └── property.js ├── dom │ ├── attribute.js │ ├── element.js │ ├── event.js │ └── index.js ├── index.js ├── music │ ├── index.js │ ├── note.js │ ├── notes.js │ └── time.js ├── plugins │ ├── index.js │ └── web-socket.js ├── program │ ├── effect.js │ ├── index.js │ ├── instrument.js │ └── worker.js └── runtime │ ├── debugger.js │ ├── virtual-audio.js │ └── virtual-dom.js ├── docs ├── index.html ├── main.6d9c0726.js └── main.6d9c0726.js.map ├── examples ├── audio.js ├── index.html ├── main.js └── view.js ├── package-lock.json ├── package.json └── src ├── action.js ├── audio ├── event.js ├── index.js ├── keyed.js ├── node.js └── property.js ├── dom ├── attribute.js ├── element.js ├── event.js └── index.js ├── effect.js ├── index.js ├── music ├── index.js ├── note.js ├── notes.json └── time.js ├── plugins ├── index.js └── web-socket.js ├── program ├── index.js ├── instrument.js └── worker.js ├── runtime ├── debugger.js ├── virtual-audio.js └── virtual-dom.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX garbage 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Dependency directories 22 | node_modules/ 23 | jspm_packages/ 24 | 25 | # Optional npm cache directory 26 | .npm 27 | 28 | # Optional REPL history 29 | .node_repl_history 30 | 31 | # Output of 'npm pack' 32 | *.tgz 33 | 34 | # dotenv environment variables file 35 | .env 36 | .env.test 37 | 38 | # parcel-bundler cache (https://parceljs.org/) 39 | .cache 40 | 41 | # 42 | .dev -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Andrew Thompson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Flow Framework 2 | > An Elm-inspired framework for Web Audio applications. Flow is positioned as an 3 | alternative to frameworks like React, with a tight integration with the Web 4 | Audio API. 5 | 6 | --- 7 | 8 | ![](/assets/github-repo-splash.png) 9 | 10 | ## Motivation 11 | A number of projects exist to provide complete frameworks for building Web Audio 12 | applications such as BRAID[1](#braid), 13 | WAAX[2](#waax), and 14 | Flocking[3](#flocking). All of these examples 15 | encourage a tight coupling between the UI and the audio graph: 16 | 17 | - BRAID relies on global variables and callbacks attached to UI elements to 18 | directly manipulate audio nodes. 19 | - WAAX uses Web Components to provide a `.connect` method for UI elements. These 20 | elements can be connected directly to audio node parameters to control their 21 | value. 22 | - Similarly, Flocking UI elements directly manipulate audio params. 23 | 24 | While such an approach is acceptable for small applications. It becomes increasingly 25 | difficult to manage application and audio graph state as an application grows. 26 | Relying on global variables as done in BRAID is simply not a scalalbe solution 27 | for serious applications, and the tight coupling between UI and audio found in 28 | WAAX and Flocking can make it difficult to determine how and when application 29 | state is being changed. 30 | 31 | Flow takes a different approach in line with more modern frontend frameworks. We 32 | argue for strict separation of audio and view code, instead choosing to generate 33 | both from a single, immutable model. This prevents one going out of sync with 34 | the other while also ensuring that a refactor of the view won't impact how the 35 | app handles audio. 36 | 37 | Flow also provides a declarative API for Web Audio development that is 38 | signficantly clearer than the vanilla Web Audio API. Connections, for example, 39 | are much more clearly expressed with Flow's audio library: 40 | 41 | ```javascript 42 | // Flow 43 | osc([], [ 44 | gain([], [ 45 | dac() 46 | ]) 47 | ]) 48 | 49 | // Web Audio API 50 | const osc = context.createOscillator() 51 | const amp = context.createGain() 52 | 53 | osc.connect(amp) 54 | amp.connect(context.destination) 55 | ``` 56 | 57 | It is also much easier to define reusable _sub graphs_ such as combining the 58 | above oscillator and gain node into a reusable synth node: 59 | 60 | ```javascript 61 | // Flow 62 | const synth = (freq, vol, connections) => 63 | osc([ Prop.frequency(freq) ], [ 64 | gain([ Prop.gain(vol) ], [ 65 | ...connections 66 | ]) 67 | ]) 68 | 69 | // Web Audio API 70 | const synth = (freq, vol) => { 71 | const osc = context.createOscillator() 72 | const amp = context.createGain() 73 | 74 | osc.frequency.value = freq 75 | amp.gain.value = vol 76 | 77 | osc.connect(amp) 78 | 79 | return { 80 | connect (node) { 81 | amp.connect(node) 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## Features 88 | 89 | - [x] Declarative audio API 90 | - [x] "Anonymous" audio nodes 91 | - [x] Clear unidirectional dataflow thanks to the MVU architecture 92 | - [x] Predictable model updates thanks to Actions and single update function 93 | - [x] Managed side effects thanks to Effects 94 | - [x] Plugin system allowing custom events 95 | - [ ] TODO: Time travel debugger 96 | - [ ] TODO: Plugin system allowing custom audio nodes and DOM elements 97 | 98 | ## Installation 99 | First, install the library from npm: 100 | 101 | ``` 102 | npm i @flow-lang/framework 103 | ```` 104 | 105 | Then make sure your HTMl has an element for Flow to mount to: 106 | 107 | ```html 108 | 109 |
110 | 111 | 112 | ``` 113 | 114 | Finally, create a `main.js` to init your application: 115 | 116 | ```javascript 117 | import { Program, DOM, Audio, Music } from '@flow-lang/framework' 118 | 119 | ... 120 | 121 | const App = Program.instrument(init, update, audio, view listen) 122 | 123 | App.use(DOM.Event) 124 | App.use(Audio.Event) 125 | App.start({ 126 | root: document.querySelector('#app'), 127 | context: new AudioContext() 128 | }) 129 | ``` 130 | 131 | ## Docs and Reference 132 | An official guide, complete with examples and a full API reference can be found 133 | [here](https://flow-lang.github.io/). 134 | 135 | ## Example 136 | It's not uncommon for frameworks to have a simple counter application to 137 | demonstrate how they work. Flow is no different, but this time it's a counter 138 | with an audio twist! 139 | 140 | First, import everything we need from Flow 141 | 142 | ```javascript 143 | import { Program, DOM, Audio, Music } from '@flow-lang/framework' 144 | ``` 145 | 146 | The init function is called once when we call `App.start` and is used to generate 147 | the initial model for our application. For our counter app we need to keep track 148 | of the current count, and we're also going to define some notes of a chord to 149 | trigger. 150 | 151 | The `Music.Note` library contains some handy utilities for converting to and from 152 | different formats. Here we're converting note names to frequency values. 153 | 154 | ```javascript 155 | function init () { 156 | const voices = [ 'C3', 'E3', 'G3', 'B3', 'D4', 'G4', 'B4' ] 157 | 158 | return { 159 | count: 0, 160 | voices: voices.map(Music.Note.ntof) 161 | } 162 | } 163 | ``` 164 | 165 | Update is called by the runtime whenever a new model needs to be created. Here, 166 | we switch on our two actions (Increment and Decrement) to increase or decrease 167 | the counter. 168 | 169 | Because we're also going to use the count to trigger notes in a 170 | chord we need to make sure the counter doesn't dip below 0 or increase above the 171 | number of notes we have defined. 172 | 173 | ```javascript 174 | const Increment = 0 175 | const Decrement = 1 176 | 177 | function update ({ action }, model) { 178 | switch (action) { 179 | case Increment: 180 | if (model.count < model.voices.length) { 181 | return { ...model, count: model.count + 1 } 182 | } else { 183 | return { ...model } 184 | } 185 | 186 | case Decrement: 187 | if (model.count > 0) { 188 | return { ...model, count: model.count - 1 } 189 | } else { 190 | return { ...model } 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | We've declared our two actions as constants in a sort of enum but this isn't 197 | necessary. You may wish to use strings instead of numbers (perhaps for better 198 | debugging) or not to use constants at all. 199 | 200 | The audio function is called every time the model updates. Here, we map over 201 | the voices array and map each note into an oscillator. By comparing the current 202 | index to the model's count we can conditionally turn off some voices by setting 203 | the gain to 0. 204 | 205 | ```javascript 206 | function audio ({ count, voices ) { 207 | return voices.map((note, i) => 208 | Audio.Node.oscillator([ Audio.Property.frequency(note) ], [ 209 | Audio.Node.gain([ Audio.Property.gain(i < count ? 0.1 : 0) ], [ 210 | Audio.Node.dac() 211 | ]) 212 | ]) 213 | ) 214 | } 215 | ``` 216 | 217 | As with the audio function, the view function is also called whenever the model 218 | changes. 219 | 220 | ```javascript 221 | function view ({ count, voices }) { 222 | return DOM.Element.div([], [ 223 | DOM.Element.button([ DOM.Attribute.id('incr') ], [ '+' ]), 224 | DOM.Element.div([], [ count.toString() ]), 225 | DOM.Element.button([ DOM.Attribute.id('decr') ], [ '-' ]) 226 | ]) 227 | } 228 | ``` 229 | 230 | Instead of attaching event listeners to DOM nodes directly, the listen function 231 | serves as the single place to define all event listeners. Here we attach click 232 | event listeners to the two buttons and return an appropriate action. 233 | 234 | ```javascript 235 | function listen (model) { 236 | return [ 237 | DOM.Event.click('#incr', e => ({ action: Increment })), 238 | DOM.Event.click('#decr', e => ({ action: Decrement })), 239 | ] 240 | } 241 | ``` 242 | 243 | With everything defined we can create a new application. The `instrument` program 244 | is a complete Flow application with an audio, view, and listen function. 245 | 246 | The Flow runtime needs to know how to setup and handle different types of events, 247 | so we tell our application to use the DOM.Event plugin. 248 | 249 | Finally we start the application, supplying a DOM node to inject our view into 250 | and an audio context used to create our audio nodes. 251 | 252 | ```javascript 253 | const App = Program.instrument(init, update, audio, view, listen) 254 | 255 | App.use(DOM.Event) 256 | App.start({ 257 | root: document.querySelector('#app'), 258 | context: new AudioContext() 259 | }) 260 | ``` 261 | 262 | You can see the code for this example as one piece in 263 | [examples/counter/](/examples/counter/). Along with this counter example there 264 | are a few other example applications including: 265 | 266 | - [x] A step sequencer: [source](/examples/step-sequencer) 267 | - [ ] TODO A polyphonic synth: ~~[source]()~~ 268 | 269 | ## References 270 | - [1] BRAID: : A Web Audio Instrument Builder with Embedded 271 | Code Blocks – [paper](https://pdfs.semanticscholar.org/d92e/9fe43966b903c514613feaf281d2a40a6cc0.pdf) 272 | - [2] WAAX: Web Audio API eXtension – [paper](http://nime.org/proceedings/2013/nime2013_119.pdf) 273 | - [3] Flocking: A Framework for Declarative Music-Making on the Web – [paper](https://pdfs.semanticscholar.org/bcbf/b66bc4ced14beafcf9e7463c37692faa9a29.pdf) -------------------------------------------------------------------------------- /assets/github-repo-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flow-lang/flow-framework/21652e9670328d06d0c464d60d3a3a30f0ec468a/assets/github-repo-splash.png -------------------------------------------------------------------------------- /dist/audio/event.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c=n.targetTime-n.lookahead){var r=n.targetTime-i;switch(e(n.handler(i+r)),n.type){case"repeat":n.targetTime=i+r+n.time;break;case"once":n.stop()}}},25)}},{key:"update",value:function(e){var t=e.time,n=e.handler;t&&(this.time=t),n&&(this.handler=n)}},{key:"stop",value:function(){clearInterval(this.timerID)}}]),t}(); 3 | },{}]},{},["q1Vi"], null) -------------------------------------------------------------------------------- /dist/audio/keyed.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:[],connections:arguments.length>2&&void 0!==arguments[2]?arguments[2]:[]}};exports.node=e;var r=function(e){return{type:"RefNode",key:e}};exports.ref=r;var o=function(r,o){return e("AudioBufferSourceNode",r,o)};exports.audioBufferSource=o;var t=function(){return e("AudioDestinationNode")};exports.audioDestination=t;var n=function(r,o){return e("AudioScheduledSourceNode",r,o)};exports.audioScheduledSource=n;var a=function(r,o){return e("BiquadFilterNode",r,o)};exports.biquadFilter=a;var s=function(r,o){return e("ChannelMergerNode",r,o)};exports.channelMerger=s;var i=function(r,o){return e("ChannelSplitterNode",r,o)};exports.channelSplitter=i;var u=function(r,o){return e("ConstantSourceNode",r,o)};exports.constantSource=u;var p=function(r,o){return e("ConvolverNode",r,o)};exports.convolver=p;var c=function(r,o){return e("DelayNode",r,o)};exports.delay=c;var d=function(r,o){return e("DynamicsCompressorNode",r,o)};exports.dynamicsCompressor=d;var l=function(r,o){return e("GainNode",r,o)};exports.gain=l;var x=function(r,o){return e("IIRFilterNode",r,o)};exports.iirFilter=x;var v=function(r,o){return e("OscillatorNode",r,o)};exports.oscillator=v;var f=function(r,o){return e("PannerNode",r,o)};exports.panner=f;var S=function(r,o){return e("StereoPannerNode",r,o)};exports.stereoPanner=S;var h=function(r,o){return e("WaveShaperNode",r,o)};exports.waveShaper=h;var N=d;exports.compressor=N;var m=t;exports.dac=m;var y=a;exports.filter=y;var g=u;exports.num=g;var C=v;exports.osc=C;var F={node:e,ref:r,audioBufferSource:o,audioDestination:t,audioScheduledSource:n,biquadFilter:a,channelMerger:s,channelSplitter:i,constantSource:u,convolver:p,delay:c,dynamicsCompressor:d,gain:l,iirFilter:x,oscillator:v,panner:f,stereoPanner:S,waveShaper:h,compressor:N,dac:m,filter:y,num:g,osc:C};exports.default=F; 3 | },{}],"//G6":[function(require,module,exports) { 4 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=exports.osc=exports.num=exports.filter=exports.compressor=exports.waveShaper=exports.stereoPanner=exports.panner=exports.oscillator=exports.mediaStreamAudioSource=exports.mediaStreamAudioDestination=exports.mediaElementAudioSource=exports.iirFilter=exports.gain=exports.dynamicsCompressor=exports.delay=exports.convolver=exports.constantSource=exports.channelSplitter=exports.channelMerger=exports.biquadFilter=exports.audioScheduledSource=exports.audioBufferSource=exports.analyser=exports.key=exports.keyed=void 0;var e=require("./node");function r(e,r){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var t=Object.getOwnPropertySymbols(e);r&&(t=t.filter(function(r){return Object.getOwnPropertyDescriptor(e,r).enumerable})),o.push.apply(o,t)}return o}function o(e){for(var o=1;o2&&void 0!==arguments[2]?arguments[2]:[],a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:[];return o({key:r},(0,e.node)(t,n,a))};exports.keyed=n;var a=function(e,r){return o({key:e},r)};exports.key=a;var i=function(e,r,o){return n(e,"AnalyserNode",r,o)};exports.analyser=i;var u=function(e,r,o){return n(e,"AudioBufferSourceNode",r,o)};exports.audioBufferSource=u;var s=function(e,r,o){return n(e,"AudioScheduledSourceNode",r,o)};exports.audioScheduledSource=s;var c=function(e,r,o){return n(e,"BiquadFilterNode",r,o)};exports.biquadFilter=c;var p=function(e,r,o){return n(e,"ChannelMergerNode",r,o)};exports.channelMerger=p;var d=function(e,r,o){return n(e,"ChannelSplitterNode",r,o)};exports.channelSplitter=d;var l=function(e,r,o){return n(e,"ConstantSourceNode",r,o)};exports.constantSource=l;var x=function(e,r,o){return n(e,"ConvolverNode",r,o)};exports.convolver=x;var f=function(e,r,o){return n(e,"DelayNode",r,o)};exports.delay=f;var v=function(e,r,o){return n(e,"DynamicsCompressorNode",r,o)};exports.dynamicsCompressor=v;var S=function(e,r,o){return n(e,"GainNode",r,o)};exports.gain=S;var m=function(e,r,o){return n(e,"IIRFilterNode",r,o)};exports.iirFilter=m;var y=function(e,r,o){return n(e,"MediaElementAudioSourceNode",r,o)};exports.mediaElementAudioSource=y;var h=function(e,r,o){return n(e,"MediaStreamAudioDestinationNode",r,o)};exports.mediaStreamAudioDestination=h;var b=function(e,r,o){return n(e,"MediaStreamAudioSourceNode",r,o)};exports.mediaStreamAudioSource=b;var N=function(e,r,o){return n(e,"OscillatorNode",r,o)};exports.oscillator=N;var O=function(e,r,o){return n(e,"PannerNode",r,o)};exports.panner=O;var g=function(e,r,o){return n(e,"StereoPannerNode",r,o)};exports.stereoPanner=g;var A=function(e,r,o){return n(e,"WaveShaperNode",r,o)};exports.waveShaper=A;var P=v;exports.compressor=P;var j=c;exports.filter=j;var w=l;exports.num=w;var D=N;exports.osc=D;var k={keyed:n,key:a,analyser:i,audioBufferSource:u,audioScheduledSource:s,biquadFilter:c,channelMerger:p,channelSplitter:d,constantSource:l,convolver:x,delay:f,dynamicsCompressor:v,gain:S,iirFilter:m,mediaElementAudioSource:y,mediaStreamAudioDestination:h,mediaStreamAudioSource:b,oscillator:N,panner:O,stereoPanner:g,waveShaper:A,compressor:P,filter:j,num:w,osc:D};exports.default=k; 5 | },{"./node":"whCT"}]},{},["//G6"], null) -------------------------------------------------------------------------------- /dist/audio/node.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:[],connections:arguments.length>2&&void 0!==arguments[2]?arguments[2]:[]}};exports.node=e;var r=function(e){return{type:"RefNode",key:e}};exports.ref=r;var o=function(r,o){return e("AudioBufferSourceNode",r,o)};exports.audioBufferSource=o;var t=function(){return e("AudioDestinationNode")};exports.audioDestination=t;var n=function(r,o){return e("AudioScheduledSourceNode",r,o)};exports.audioScheduledSource=n;var a=function(r,o){return e("BiquadFilterNode",r,o)};exports.biquadFilter=a;var s=function(r,o){return e("ChannelMergerNode",r,o)};exports.channelMerger=s;var i=function(r,o){return e("ChannelSplitterNode",r,o)};exports.channelSplitter=i;var u=function(r,o){return e("ConstantSourceNode",r,o)};exports.constantSource=u;var p=function(r,o){return e("ConvolverNode",r,o)};exports.convolver=p;var c=function(r,o){return e("DelayNode",r,o)};exports.delay=c;var d=function(r,o){return e("DynamicsCompressorNode",r,o)};exports.dynamicsCompressor=d;var l=function(r,o){return e("GainNode",r,o)};exports.gain=l;var x=function(r,o){return e("IIRFilterNode",r,o)};exports.iirFilter=x;var v=function(r,o){return e("OscillatorNode",r,o)};exports.oscillator=v;var f=function(r,o){return e("PannerNode",r,o)};exports.panner=f;var S=function(r,o){return e("StereoPannerNode",r,o)};exports.stereoPanner=S;var h=function(r,o){return e("WaveShaperNode",r,o)};exports.waveShaper=h;var N=d;exports.compressor=N;var m=t;exports.dac=m;var y=a;exports.filter=y;var g=u;exports.num=g;var C=v;exports.osc=C;var F={node:e,ref:r,audioBufferSource:o,audioDestination:t,audioScheduledSource:n,biquadFilter:a,channelMerger:s,channelSplitter:i,constantSource:u,convolver:p,delay:c,dynamicsCompressor:d,gain:l,iirFilter:x,oscillator:v,panner:f,stereoPanner:S,waveShaper:h,compressor:N,dac:m,filter:y,num:g,osc:C};exports.default=F; 3 | },{}]},{},["whCT"], null) -------------------------------------------------------------------------------- /dist/audio/property.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:[],children:arguments.length>2&&void 0!==arguments[2]?arguments[2]:[]}};exports.element=r;var t=function(t,e){return r("a",t,e)};exports.a=t;var e=function(t,e){return r("abbr",t,e)};exports.abbr=e;var o=function(t,e){return r("address",t,e)};exports.address=o;var n=function(t,e){return r("area",t,e)};exports.area=n;var s=function(t,e){return r("article",t,e)};exports.article=s;var p=function(t,e){return r("aside",t,e)};exports.aside=p;var u=function(t,e){return r("audio",t,e)};exports.audio=u;var a=function(t,e){return r("b",t,e)};exports.b=a;var x=function(t,e){return r("base",t,e)};exports.base=x;var i=function(t,e){return r("bdi",t,e)};exports.bdi=i;var c=function(t,e){return r("bdo",t,e)};exports.bdo=c;var f=function(t,e){return r("blockquote",t,e)};exports.blockquote=f;var v=function(t,e){return r("body",t,e)};exports.body=v;var d=function(t,e){return r("br",t,e)};exports.br=d;var l=function(t,e){return r("button",t,e)};exports.button=l;var m=function(t,e){return r("canvas",t,e)};exports.canvas=m;var b=function(t,e){return r("caption",t,e)};exports.caption=b;var h=function(t,e){return r("cite",t,e)};exports.cite=h;var g=function(t,e){return r("code",t,e)};exports.code=g;var y=function(t,e){return r("col",t,e)};exports.col=y;var k=function(t,e){return r("colgroup",t,e)};exports.colgroup=k;var q=function(t,e){return r("data",t,e)};exports.data=q;var j=function(t,e){return r("datalist",t,e)};exports.datalist=j;var _=function(t,e){return r("dd",t,e)};exports.dd=_;var w=function(t,e){return r("del",t,e)};exports.del=w;var M=function(t,e){return r("details",t,e)};exports.details=M;var O=function(t,e){return r("dfn",t,e)};exports.dfn=O;var P=function(t,e){return r("dialog",t,e)};exports.dialog=P;var z=function(t,e){return r("div",t,e)};exports.div=z;var A=function(t,e){return r("dl",t,e)};exports.dl=A;var B=function(t,e){return r("dt",t,e)};exports.dt=B;var C=function(t,e){return r("em",t,e)};exports.em=C;var D=function(t,e){return r("embed",t,e)};exports.embed=D;var E=function(t,e){return r("fieldset",t,e)};exports.fieldset=E;var F=function(t,e){return r("figure",t,e)};exports.figure=F;var G=function(t,e){return r("footer",t,e)};exports.footer=G;var H=function(t,e){return r("form",t,e)};exports.form=H;var I=function(t,e){return r("h1",t,e)};exports.h1=I;var J=function(t,e){return r("h2",t,e)};exports.h2=J;var K=function(t,e){return r("h3",t,e)};exports.h3=K;var L=function(t,e){return r("h4",t,e)};exports.h4=L;var N=function(t,e){return r("h5",t,e)};exports.h5=N;var Q=function(t,e){return r("h6",t,e)};exports.h6=Q;var R=function(t,e){return r("head",t,e)};exports.head=R;var S=function(t,e){return r("header",t,e)};exports.header=S;var T=function(t,e){return r("hgroup",t,e)};exports.hgroup=T;var U=function(t,e){return r("hr",t,e)};exports.hr=U;var V=function(t,e){return r("html",t,e)};exports.html=V;var W=function(t,e){return r("i",t,e)};exports.i=W;var X=function(t,e){return r("iframe",t,e)};exports.iframe=X;var Y=function(t,e){return r("img",t,e)};exports.img=Y;var Z=function(t,e){return r("input",t,e)};exports.input=Z;var $=function(t,e){return r("ins",t,e)};exports.ins=$;var rr=function(t,e){return r("kbd",t,e)};exports.kbd=rr;var tr=function(t,e){return r("keygen",t,e)};exports.keygen=tr;var er=function(t,e){return r("label",t,e)};exports.label=er;var or=function(t,e){return r("legend",t,e)};exports.legend=or;var nr=function(t,e){return r("li",t,e)};exports.li=nr;var sr=function(t,e){return r("link",t,e)};exports.link=sr;var pr=function(t,e){return r("main",t,e)};exports.main=pr;var ur=function(t,e){return r("map",t,e)};exports.map=ur;var ar=function(t,e){return r("mark",t,e)};exports.mark=ar;var xr=function(t,e){return r("menu",t,e)};exports.menu=xr;var ir=function(t,e){return r("menuitem",t,e)};exports.menuitem=ir;var cr=function(t,e){return r("meta",t,e)};exports.meta=cr;var fr=function(t,e){return r("meter",t,e)};exports.meter=fr;var vr=function(t,e){return r("nav",t,e)};exports.nav=vr;var dr=function(t,e){return r("noscript",t,e)};exports.noscript=dr;var lr=function(t,e){return r("object",t,e)};exports.object=lr;var mr=function(t,e){return r("ol",t,e)};exports.ol=mr;var br=function(t,e){return r("optgroup",t,e)};exports.optgroup=br;var hr=function(t,e){return r("option",t,e)};exports.option=hr;var gr=function(t,e){return r("output",t,e)};exports.output=gr;var yr=function(t,e){return r("p",t,e)};exports.p=yr;var kr=function(t,e){return r("param",t,e)};exports.param=kr;var qr=function(t,e){return r("pre",t,e)};exports.pre=qr;var jr=function(t,e){return r("progress",t,e)};exports.progress=jr;var _r=function(t,e){return r("q",t,e)};exports.q=_r;var wr=function(t,e){return r("rb",t,e)};exports.rb=wr;var Mr=function(t,e){return r("rp",t,e)};exports.rp=Mr;var Or=function(t,e){return r("rt",t,e)};exports.rt=Or;var Pr=function(t,e){return r("rtc",t,e)};exports.rtc=Pr;var zr=function(t,e){return r("ruby",t,e)};exports.ruby=zr;var Ar=function(t,e){return r("s",t,e)};exports.s=Ar;var Br=function(t,e){return r("samp",t,e)};exports.samp=Br;var Cr=function(t,e){return r("script",t,e)};exports.script=Cr;var Dr=function(t,e){return r("section",t,e)};exports.section=Dr;var Er=function(t,e){return r("select",t,e)};exports.select=Er;var Fr=function(t,e){return r("small",t,e)};exports.small=Fr;var Gr=function(t,e){return r("source",t,e)};exports.source=Gr;var Hr=function(t,e){return r("span",t,e)};exports.span=Hr;var Ir=function(t,e){return r("strong",t,e)};exports.strong=Ir;var Jr=function(t,e){return r("style",t,e)};exports.style=Jr;var Kr=function(t,e){return r("sub",t,e)};exports.sub=Kr;var Lr=function(t,e){return r("summary",t,e)};exports.summary=Lr;var Nr=function(t,e){return r("sup",t,e)};exports.sup=Nr;var Qr=function(t,e){return r("table",t,e)};exports.table=Qr;var Rr=function(t,e){return r("tbody",t,e)};exports.tbody=Rr;var Sr=function(t,e){return r("td",t,e)};exports.td=Sr;var Tr=function(t,e){return r("template",t,e)};exports.template=Tr;var Ur=function(r){return"".concat(r)};exports.text=Ur;var Vr=function(t,e){return r("textarea",t,e)};exports.textarea=Vr;var Wr=function(t,e){return r("tfoot",t,e)};exports.tfoot=Wr;var Xr=function(t,e){return r("th",t,e)};exports.th=Xr;var Yr=function(t,e){return r("thead",t,e)};exports.thead=Yr;var Zr=function(t,e){return r("time",t,e)};exports.time=Zr;var $r=function(t,e){return r("title",t,e)};exports.title=$r;var rt=function(t,e){return r("tr",t,e)};exports.tr=rt;var tt=function(t,e){return r("track",t,e)};exports.track=tt;var et=function(t,e){return r("u",t,e)};exports.u=et;var ot=function(t,e){return r("ul",t,e)};exports.ul=ot;var nt=function(t,e){return r("var",t,e)};exports.var_=nt;var st=function(t,e){return r("video",t,e)};exports.video=st;var pt=function(t,e){return r("wbr",t,e)};exports.wbr=pt;var ut={element:r,a:t,abbr:e,address:o,area:n,article:s,aside:p,audio:u,b:a,base:x,bdi:i,bdo:c,blockquote:f,body:v,br:d,button:l,canvas:m,caption:b,cite:h,code:g,col:y,colgroup:k,data:q,datalist:j,dd:_,del:w,details:M,dfn:O,dialog:P,div:z,dl:A,dt:B,em:C,embed:D,fieldset:E,figure:F,footer:G,form:H,h1:I,h2:J,h3:K,h4:L,h5:N,h6:Q,head:R,header:S,hgroup:T,hr:U,html:V,i:W,iframe:X,img:Y,input:Z,ins:$,kbd:rr,keygen:tr,label:er,legend:or,li:nr,link:sr,main:pr,map:ur,mark:ar,menu:xr,menuitem:ir,meta:cr,meter:fr,nav:vr,noscript:dr,object:lr,ol:mr,optgroup:br,option:hr,output:gr,p:yr,param:kr,pre:qr,progress:jr,q:_r,rb:wr,rp:Mr,rt:Or,rtc:Pr,ruby:zr,s:Ar,samp:Br,script:Cr,section:Dr,select:Er,small:Fr,source:Gr,span:Hr,strong:Ir,style:Jr,sub:Kr,summary:Lr,sup:Nr,table:Qr,tbody:Rr,td:Sr,template:Tr,text:Ur,textarea:Vr,tfoot:Wr,th:Xr,thead:Yr,time:Zr,title:$r,tr:rt,track:tt,u:et,ul:ot,var_:nt,video:st,wbr:pt};exports.default=ut; 3 | },{}]},{},["RgX7"], null) -------------------------------------------------------------------------------- /dist/dom/event.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c3&&void 0!==arguments[3]?arguments[3]:{}}};exports.event=o;var n=function(e,r,t){return o(e,"focus",r,t)};exports.focus=n;var u=function(e,r,t){return o(e,"blur",r,t)};exports.blur=u;var c=function(e,r,t){return o(e,"reset",r,t)};exports.reset=c;var s=function(e,r,t){return o(e,"submit",r,t)};exports.submit=s;var a=function(e,r,t){return o(e,"fullscreenchange",r,t)};exports.fullscreenchange=a;var i=function(e,r,t){return o(e,"fullscreenerror",r,t)};exports.fullscreenerror=i;var p=function(e,r,t){return o(e,"resize",r,t)};exports.resize=p;var l=function(e,r,t){return o(e,"scroll",r,t)};exports.scroll=l;var v=function(e,r,t){return o(e,"cut",r,t)};exports.cut=v;var x=function(e,r,t){return o(e,"copy",r,t)};exports.copy=x;var d=function(e,r,t){return o(e,"paste",r,t)};exports.paste=d;var h=function(e,r,t){return o(e,"keydown",r,t)};exports.keydown=h;var f=function(e,r,t){return o(e,"keypress",r,t)};exports.keypress=f;var g=function(e,r,t){return o(e,"keyup",r,t)};exports.keyup=g;var m=function(e,r,t){return o(e,"auxclick",r,t)};exports.auxclick=m;var y=function(e,r,t){return o(e,"click",r,t)};exports.click=y;var b=function(e,r,t){return o(e,"contextmenu",r,t)};exports.contextmenu=b;var k=function(e,r,t){return o(e,"dblclick",r,t)};exports.dblclick=k;var w=function(e,r,t){return o(e,"mousedown",r,t)};exports.mousedown=w;var C=function(e,r,t){return o(e,"mouseenter",r,t)};exports.mouseenter=C;var O=function(e,r,t){return o(e,"mouseleave",r,t)};exports.mouseleave=O;var $=function(e,r,t){return o(e,"mousemove",r,t)};exports.mousemove=$;var _=function(e,r,t){return o(e,"mouseover",r,t)};exports.mouseover=_;var S=function(e,r,t){return o(e,"mouseout",r,t)};exports.mouseout=S;var j=function(e,r,t){return o(e,"mouseup",r,t)};exports.mouseup=j;var P=function(e,r,t){return o(e,"pointerlockchange",r,t)};exports.pointerlockchange=P;var E=function(e,r,t){return o(e,"pointerlockerror",r,t)};exports.pointerlockerror=E;var D=function(e,r,t){return o(e,"select",r,t)};exports.select=D;var z=function(e,r,t){return o(e,"wheel",r,t)};exports.wheel=z;var R=function(e,r,t){return o(e,"drag",r,t)};exports.drag=R;var V=function(e,r,t){return o(e,"dragend",r,t)};exports.dragend=V;var G=function(e,r,t){return o(e,"dragenter",r,t)};exports.dragenter=G;var M=function(e,r,t){return o(e,"dragstart",r,t)};exports.dragstart=M;var N=function(e,r,t){return o(e,"dragleave",r,t)};exports.dragleave=N;var T=function(e,r,t){return o(e,"dragover",r,t)};exports.dragover=T;var L=function(e,r,t){return o(e,"drop",r,t)};exports.drop=L;var H=function(e,r,t){return o(e,"broadcast",r,t)};exports.broadcast=H;var q=function(e,r,t){return o(e,"CheckboxStateChange",r,t)};exports.CheckboxStateChange=q;var A=function(e,r,t){return o(e,"hashchange",r,t)};exports.hashchange=A;var B=function(e,r,t){return o(e,"input",r,t)};exports.input=B;var F=function(e,r,t){return o(e,"RadioStateChange",r,t)};exports.RadioStateChange=F;var I=function(e,r,t){return o(e,"readystatechange",r,t)};exports.readystatechange=I;var J=function(e,r,t){return o(e,"ValueChange",r,t)};exports.ValueChange=J;var K=function(e,r,t){return o(e,"compassneedscalibration",r,t)};exports.compassneedscalibration=K;var Q=function(e,r,t){return o(e,"devicelight",r,t)};exports.devicelight=Q;var U=function(e,r,t){return o(e,"devicemotion",r,t)};exports.devicemotion=U;var W=function(e,r,t){return o(e,"deviceorientation",r,t)};exports.deviceorientation=W;var X=function(e,r,t){return o(e,"deviceproximity",r,t)};exports.deviceproximity=X;var Y=function(e,r,t){return o(e,"orientationchange",r,t)};exports.orientationchange=Y;var Z=function(e,r,t){return o(e,"userproximity",r,t)};exports.userproximity=Z;var ee=function(e,r,t){return o(e,"touchcancel",r,t)};exports.touchcancel=ee;var re=function(e,r,t){return o(e,"touchend",r,t)};exports.touchend=re;var te=function(e,r,t){return o(e,"touchenter",r,t)};exports.touchenter=te;var oe=function(e,r,t){return o(e,"touchleave",r,t)};exports.touchleave=oe;var ne=function(e,r,t){return o(e,"touchmove",r,t)};exports.touchmove=ne;var ue=function(e,r,t){return o(e,"touchstart",r,t)};exports.touchstart=ue;var ce=function(e,r,t){return o(e,"pointerover",r,t)};exports.pointerover=ce;var se=function(e,r,t){return o(e,"pointerenter",r,t)};exports.pointerenter=se;var ae=function(e,r,t){return o(e,"pointerdown",r,t)};exports.pointerdown=ae;var ie=function(e,r,t){return o(e,"pointermove",r,t)};exports.pointermove=ie;var pe=function(e,r,t){return o(e,"pointerup",r,t)};exports.pointerup=pe;var le=function(e,r,t){return o(e,"pointercancel",r,t)};exports.pointercancel=le;var ve=function(e,r,t){return o(e,"pointerout",r,t)};exports.pointerout=ve;var xe=function(e,r,t){return o(e,"pointerleave",r,t)};exports.pointerleave=xe;var de=function(e,r,t){return o(e,"gotpointercapture",r,t)};exports.gotpointercapture=de;var he=function(e,r,t){return o(e,"lostpointercapture",r,t)};exports.lostpointercapture=he;var fe={event:o,focus:n,blur:u,reset:c,submit:s,fullscreenchange:a,fullscreenerror:i,resize:p,scroll:l,cut:v,copy:x,paste:d,keydown:h,keypress:f,keyup:g,auxclick:m,click:y,contextmenu:b,dblclick:k,mousedown:w,mouseenter:C,mouseleave:O,mousemove:$,mouseover:_,mouseout:S,mouseup:j,pointerlockchange:P,pointerlockerror:E,select:D,wheel:z,drag:R,dragend:V,dragenter:G,dragstart:M,dragleave:N,dragover:T,drop:L,broadcast:H,CheckboxStateChange:q,hashchange:A,input:B,RadioStateChange:F,readystatechange:I,ValueChange:J,compassneedscalibration:K,devicelight:Q,devicemotion:U,deviceorientation:W,deviceproximity:X,orientationchange:Y,userproximity:Z,touchcancel:ee,touchend:re,touchenter:te,touchleave:oe,touchmove:ne,touchstart:ue,pointerover:ce,pointerenter:se,pointerdown:ae,pointermove:ie,pointerup:pe,pointercancel:le,pointerout:ve,pointerleave:xe,gotpointercapture:de,lostpointercapture:he,__pluginType:"event",__pluginName:"Html.Event",__eventType:"DOM",__install:function(e){var r=this,t=e.$dispatch;this.$dispatch=t,ge.forEach(function(e){r.$events[e]=[],r.$handlers[e]=function(t){r.$events[e].forEach(function(e){var o=e.selector,n=e.handler;(r.$isGlobal(o)||t.target.matches(o))&&n(t)})},window.addEventListener(e,r.$handlers[e])})},__update:function(e){var t=this;e=e.map(function(e){var o=e.opts.specific||t.$isGlobal(e.selector)?e.selector:"".concat(e.selector,", ").concat(e.selector," > *");return r({},e,{handler:function(r){return t.$dispatch(e.handler(r))},selector:o})});var o=function(r){t.$events[r]=e.filter(function(e){return e.eventName===r})};for(var n in this.$events)o(n)},__uninstall:function(){var e=this;ge.forEach(function(r){window.removeEventListener(r,e.$handlers[r])})},$dispatch:null,$events:{},$handlers:{},$isGlobal:function(e){return"document"===e||"window"===e}};exports.default=fe;var ge=["focus","blur","reset","submit","fullscreenchange","fullscreenerror","resize","scroll","cut","copy","paste","keydown","keypress","keyup","auxclick","click","contextmenu","dblclick","mousedown","mouseenter","mouseleave","mousemove","mouseover","mouseout","mouseup","pointerlockchange","pointerlockerror","select","wheel","drag","dragend","dragenter","dragstart","dragleave","dragover","drop","broadcast","CheckboxStateChange","hashchange","input","RadioStateChange","readystatechange","ValueChange","compassneedscalibration","devicelight","devicemotion","deviceorientation","deviceproximity","orientationchange","userproximity","touchcancel","touchend","touchenter","touchleave","touchmove","touchstart","pointerover","pointerenter","pointerdown","pointermove","pointerup","pointercancel","pointerout","pointerleave","gotpointercapture","lostpointercapture"]; 3 | },{}]},{},["8q2w"], null) -------------------------------------------------------------------------------- /dist/music/index.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c0&&void 0!==arguments[0]?arguments[0]:new s,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};o(this,t),this.$context=e,this.$nodes={$:this.$context.createGain()},this.$nodes.$.gain.linearRampToValueAtTime(1,this.$context.currentTime+1),this.$nodes.$.connect(this.$context.destination),this.vPrev={},n.autostart&&this.resume()}return c(t,null,[{key:"prepare",value:function(){return function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return t.forEach(function(o,r){"RefNode"!==o.type&&(n[o.key]=o),o.connections&&e(o.connections,n,a+1),a>0&&(t[r]={type:"RefNode",key:o.key})}),n}(function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return t.forEach(function(t,a){"RefNode"!==t.type&&(t.key||(t.key="".concat(n,"_").concat(a)),t.connections&&t.connections.length>0&&e(t.connections,t.key))}),t}(arguments.length>0&&void 0!==arguments[0]?arguments[0]:[]))}},{key:"diff",value:function(e,t){for(var n={created:[],updated:[],removed:[]},a=function(){var t=r[o],a=e[t.key];if(a)if(a.type!==t.type)n.updated.push({type:"node",key:t.key,data:t}),t.connections.forEach(function(e){n.created.push({type:"connection",key:t.key,data:e.key.split(".")})});else{for(var c=0;c0&&void 0!==arguments[0]?arguments[0]:[],a=t.prepare(n),o=t.diff(this.vPrev,a);o.removed.forEach(function(t){switch(t.type){case"node":e._destroyNode(t.key);break;case"property":e._removeProperty(t.key,t.data);break;case"connection":e._disconnect(t.key,t.data)}}),o.created.forEach(function(t){switch(t.type){case"node":e._createNode(t.key,t.data);break;case"property":e._setProperty(t.key,t.data);break;case"connection":i(function(){return e._connect(t.key,t.data)})}}),o.updated.forEach(function(t){switch(t.type){case"node":e._destroyNode(t.key),e._createNode(t.key,t.data);break;case"property":e._setProperty(t.key,t.data)}}),this.vPrev=a}},{key:"suspend",value:function(){this.$nodes.$.gain.value=0,this.$context.suspend()}},{key:"resume",value:function(){this.$context.resume(),this.$nodes.$.gain.linearRampToValueAtTime(1,this.$context.currentTime+.1)}},{key:"_createNode",value:function(e,t){var n=this,a=t.type,o=t.properties,r=null;switch(a){case"AnalyserNode":r=this.$context.createAnalyser();break;case"AudioBufferSourceNode":r=this.$context.createBufferSource();break;case"AudioDestinationNode":r=this.$nodes.$;break;case"BiquadFilterNode":r=this.$context.createBiquadFilter();break;case"ChannelMergerNode":r=this.$context.createChannelMerger();break;case"ChannelSplitterNode":r=this.$context.createChannelSplitter();break;case"ConstantSourceNode":r=this.$context.createConstantSource();break;case"ConvolverNode":r=this.$context.createConvolver();break;case"DelayNode":var c=o.find(function(e){return"maxDelayTime"===e.label});r=this.$context.createDelay(c&&c.value||1);break;case"DynamicsCompressorNode":r=this.$context.createDynamicsCompressor();break;case"GainNode":r=this.$context.createGain();break;case"IIRFilterNode":var i=o.find(function(e){return"feedforward"===e.label}),s=o.find(function(e){return"feedback"===e.label});r=this.$context.createIIRFilter(i&&i.value||[0],s&&s.value||[1]);break;case"MediaElementAudioSourceNode":var d=o.find(function(e){return"mediaElement"===e.label});r=this.$context.createMediaElementSource(document.querySelector(d.value));break;case"MediaStreamAudioDestinationNode":r=this.$context.createMediaStreamDestination();break;case"OscillatorNode":r=this.$context.createOscillator();break;case"PannerNode":r=this.$context.createPanner();break;case"StereoPannerNode":r=this.$context.createStereoPanner();break;case"WaveShaperNode":r=this.$context.createWaveShaper();break;default:console.warn("Invalide node type of: ".concat(a,". Defaulting to GainNode to avoid crashing the AudioContext.")),r=this.$context.createGain()}this.$nodes[e]=r,o.forEach(function(t){return n._setProperty(e,t)}),r.start&&r.start()}},{key:"_destroyNode",value:function(e){var t=this.$nodes[e];t.stop&&t.stop(),t.disconnect(),delete this.$nodes[e]}},{key:"_setProperty",value:function(e,t){var n=t.type,a=t.label,o=t.value,r=this.$nodes[e];switch(n){case"NodeProperty":r[a]=o;break;case"AudioParam":r[a].linearRampToValueAtTime(o,this.$context.currentTime+.05);break;case"ScheduledAudioParam":r[a][o.method](o.target,o.time)}}},{key:"_removeProperty",value:function(e,t){var n=t.type,a=t.label,o=(t.value,this.$nodes[e]);switch(n){case"NodeProperty":break;case"AudioParam":o[a].value=o[a].linearRampToValueAtTime(o[a].default,this.$context.currentTime+.05)}}},{key:"_connect",value:function(t,n){var a=e(n,2),o=a[0],r=a[1],c=void 0===r?null:r;o&&this.$nodes[t].connect(c?this.$nodes[o][c]:this.$nodes[o])}},{key:"_disconnect",value:function(t,n){var a=e(n,2),o=a[0],r=a[1],c=void 0===r?null:r;o&&this.$nodes[t].disconnect(c?this.$nodes[o][c]:this.$nodes[o])}}]),t}();exports.default=d; 3 | },{}],"FO+Z":[function(require,module,exports) { 4 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.defer=void 0;var e=function(e){return setTimeout(e,0)};exports.defer=e; 5 | },{}],"OKjx":[function(require,module,exports) { 6 | "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=a;var n=t(require("../runtime/virtual-audio")),e=require("../utils");function t(n){return n&&n.__esModule?n:{default:n}}function o(n){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(n){return typeof n}:function(n){return n&&"function"==typeof Symbol&&n.constructor===Symbol&&n!==Symbol.prototype?"symbol":typeof n})(n)}function r(n,e){return l(n)||u(n,e)||i()}function i(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function u(n,e){var t=[],o=!0,r=!1,i=void 0;try{for(var u,l=n[Symbol.iterator]();!(o=(u=l.next()).done)&&(t.push(u.value),!e||t.length!==e);o=!0);}catch(a){r=!0,i=a}finally{try{o||null==l.return||l.return()}finally{if(r)throw i}}return t}function l(n){if(Array.isArray(n))return n}function a(t,i,u,l){var a,c,f,s=!1,d={audio:[],event:[]},p=function(n){return(0,e.defer)(function(){var e=i(n,c);Array.isArray(e)?y(e):y([e])})},y=function(n){var e=r(n,2),t=e[0],i=e[1],a=void 0===i?void 0:i;if(s&&console.time("Total update time"),s&&console.group("$update"),null==t)console.warn("Your update function returned undefined or null, the model will remain unchainged. Did you forget to handle all of your Actions?");else if(JSON.stringify(c)!==JSON.stringify(t)){c=t,s&&console.time("$audio");var y=u(c);f.update(y),s&&console.timeEnd("$audio"),s&&console.time("$events");var v=l(c);d.event.forEach(function(n){n.__update(v.filter(function(e){return e.__eventType===n.__eventType}))}),s&&console.timeEnd("$events")}a&&("object"===o(a)?a.run(p,c):a(p,c)),s&&console.groupEnd("$update"),s&&console.timeEnd("Total update time")};return{use:function(n){console.log("Registering ".concat(n.__pluginName," plugin.")),function(n){switch(n.__pluginType){case"audio":break;case"event":d.event.push(n)}}(n)},start:function(e){var o=e.context,r=(e.root,e.flags);for(var i in(s=r&&r.debug||s)&&console.log("Starting Program..."),a=o,f=new n.default(a),s&&console.log("Installing plugins..."),d)d[i].forEach(function(n){s&&console.log("Installing ".concat(n.__pluginName," plugin.")),n.__install({$context:a,$root:void 0,$dispatch:p})});s&&console.log("Running initial update..."),y([t(r,a.currentTime,void 0)])},send:function(n){p(n)},destroy:function(){f.update([]),d.event.forEach(function(n){return n.__destroy?n.__destroy():n.__update([])})}}} 7 | },{"../runtime/virtual-audio":"geg8","../utils":"FO+Z"}]},{},["OKjx"], null) -------------------------------------------------------------------------------- /dist/runtime/virtual-audio.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c0&&void 0!==arguments[0]?arguments[0]:new s,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};o(this,t),this.$context=e,this.$nodes={$:this.$context.createGain()},this.$nodes.$.gain.linearRampToValueAtTime(1,this.$context.currentTime+1),this.$nodes.$.connect(this.$context.destination),this.vPrev={},n.autostart&&this.resume()}return c(t,null,[{key:"prepare",value:function(){return function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;return t.forEach(function(o,r){"RefNode"!==o.type&&(n[o.key]=o),o.connections&&e(o.connections,n,a+1),a>0&&(t[r]={type:"RefNode",key:o.key})}),n}(function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return t.forEach(function(t,a){"RefNode"!==t.type&&(t.key||(t.key="".concat(n,"_").concat(a)),t.connections&&t.connections.length>0&&e(t.connections,t.key))}),t}(arguments.length>0&&void 0!==arguments[0]?arguments[0]:[]))}},{key:"diff",value:function(e,t){for(var n={created:[],updated:[],removed:[]},a=function(){var t=r[o],a=e[t.key];if(a)if(a.type!==t.type)n.updated.push({type:"node",key:t.key,data:t}),t.connections.forEach(function(e){n.created.push({type:"connection",key:t.key,data:e.key.split(".")})});else{for(var c=0;c0&&void 0!==arguments[0]?arguments[0]:[],a=t.prepare(n),o=t.diff(this.vPrev,a);o.removed.forEach(function(t){switch(t.type){case"node":e._destroyNode(t.key);break;case"property":e._removeProperty(t.key,t.data);break;case"connection":e._disconnect(t.key,t.data)}}),o.created.forEach(function(t){switch(t.type){case"node":e._createNode(t.key,t.data);break;case"property":e._setProperty(t.key,t.data);break;case"connection":i(function(){return e._connect(t.key,t.data)})}}),o.updated.forEach(function(t){switch(t.type){case"node":e._destroyNode(t.key),e._createNode(t.key,t.data);break;case"property":e._setProperty(t.key,t.data)}}),this.vPrev=a}},{key:"suspend",value:function(){this.$nodes.$.gain.value=0,this.$context.suspend()}},{key:"resume",value:function(){this.$context.resume(),this.$nodes.$.gain.linearRampToValueAtTime(1,this.$context.currentTime+.1)}},{key:"_createNode",value:function(e,t){var n=this,a=t.type,o=t.properties,r=null;switch(a){case"AnalyserNode":r=this.$context.createAnalyser();break;case"AudioBufferSourceNode":r=this.$context.createBufferSource();break;case"AudioDestinationNode":r=this.$nodes.$;break;case"BiquadFilterNode":r=this.$context.createBiquadFilter();break;case"ChannelMergerNode":r=this.$context.createChannelMerger();break;case"ChannelSplitterNode":r=this.$context.createChannelSplitter();break;case"ConstantSourceNode":r=this.$context.createConstantSource();break;case"ConvolverNode":r=this.$context.createConvolver();break;case"DelayNode":var c=o.find(function(e){return"maxDelayTime"===e.label});r=this.$context.createDelay(c&&c.value||1);break;case"DynamicsCompressorNode":r=this.$context.createDynamicsCompressor();break;case"GainNode":r=this.$context.createGain();break;case"IIRFilterNode":var i=o.find(function(e){return"feedforward"===e.label}),s=o.find(function(e){return"feedback"===e.label});r=this.$context.createIIRFilter(i&&i.value||[0],s&&s.value||[1]);break;case"MediaElementAudioSourceNode":var d=o.find(function(e){return"mediaElement"===e.label});r=this.$context.createMediaElementSource(document.querySelector(d.value));break;case"MediaStreamAudioDestinationNode":r=this.$context.createMediaStreamDestination();break;case"OscillatorNode":r=this.$context.createOscillator();break;case"PannerNode":r=this.$context.createPanner();break;case"StereoPannerNode":r=this.$context.createStereoPanner();break;case"WaveShaperNode":r=this.$context.createWaveShaper();break;default:console.warn("Invalide node type of: ".concat(a,". Defaulting to GainNode to avoid crashing the AudioContext.")),r=this.$context.createGain()}this.$nodes[e]=r,o.forEach(function(t){return n._setProperty(e,t)}),r.start&&r.start()}},{key:"_destroyNode",value:function(e){var t=this.$nodes[e];t.stop&&t.stop(),t.disconnect(),delete this.$nodes[e]}},{key:"_setProperty",value:function(e,t){var n=t.type,a=t.label,o=t.value,r=this.$nodes[e];switch(n){case"NodeProperty":r[a]=o;break;case"AudioParam":r[a].linearRampToValueAtTime(o,this.$context.currentTime+.05);break;case"ScheduledAudioParam":r[a][o.method](o.target,o.time)}}},{key:"_removeProperty",value:function(e,t){var n=t.type,a=t.label,o=(t.value,this.$nodes[e]);switch(n){case"NodeProperty":break;case"AudioParam":o[a].value=o[a].linearRampToValueAtTime(o[a].default,this.$context.currentTime+.05)}}},{key:"_connect",value:function(t,n){var a=e(n,2),o=a[0],r=a[1],c=void 0===r?null:r;o&&this.$nodes[t].connect(c?this.$nodes[o][c]:this.$nodes[o])}},{key:"_disconnect",value:function(t,n){var a=e(n,2),o=a[0],r=a[1],c=void 0===r?null:r;o&&this.$nodes[t].disconnect(c?this.$nodes[o][c]:this.$nodes[o])}}]),t}();exports.default=d; 3 | },{}]},{},["geg8"], null) -------------------------------------------------------------------------------- /dist/runtime/virtual-dom.js: -------------------------------------------------------------------------------- 1 | parcelRequire=function(e,r,t,n){var i,o="function"==typeof parcelRequire&&parcelRequire,u="function"==typeof require&&require;function f(t,n){if(!r[t]){if(!e[t]){var i="function"==typeof parcelRequire&&parcelRequire;if(!n&&i)return i(t,!0);if(o)return o(t,!0);if(u&&"string"==typeof t)return u(t);var c=new Error("Cannot find module '"+t+"'");throw c.code="MODULE_NOT_FOUND",c}p.resolve=function(r){return e[t][1][r]||r},p.cache={};var l=r[t]=new f.Module(t);e[t][0].call(l.exports,p,l,l.exports,this)}return r[t].exports;function p(e){return f(p.resolve(e))}}f.isParcelRequire=!0,f.Module=function(e){this.id=e,this.bundle=f,this.exports={}},f.modules=e,f.cache=r,f.parent=o,f.register=function(r,t){e[r]=[function(e,r){r.exports=t},{}]};for(var c=0;c3&&void 0!==arguments[3]?arguments[3]:0;r=r||this.vPrev;var a=(o=o||this.$root).childNodes[i];if(r&&a)if(e)if(t(r)!==t(e))this._replace(o,a,e);else if(n.isText(r)&&n.isText(e))r!==e&&this._replace(o,a,e);else if(n.isVirtualNode(r)&&n.isVirtualNode(e)){if(r.tag!==e.tag)this._replace(o,a,e);else for(var l=0;lFlow example
2 | -------------------------------------------------------------------------------- /examples/audio.js: -------------------------------------------------------------------------------- 1 | import { Node as N, Keyed as K, Property as P } from '../src/audio' 2 | 3 | const voice = (step, { type }) => ({ note, steps }) => { 4 | const amp = steps[step] ? 0.2 : 0 5 | 6 | return N.oscillator([P.frequency(note), P.type(type)], [ 7 | N.gain([P.gain(amp)], [ 8 | N.ref('delay'), 9 | N.ref('master'), 10 | ]), 11 | ]) 12 | } 13 | 14 | export default ({ sequencer, synth }) => { 15 | const voices = sequencer.rows.map(voice(sequencer.step, synth)) 16 | 17 | return [ 18 | ...voices, 19 | K.delay('delay', [P.delayTime(synth.delayTime)], [ 20 | N.gain([P.gain(synth.delayAmount)], [ 21 | N.biquadFilter([P.type('lowpass'), P.frequency(400)], [ 22 | N.ref('delay'), 23 | N.ref('master') 24 | ]) 25 | ]) 26 | ]), 27 | K.gain('master', [P.gain(synth.masterGain)], [ 28 | N.biquadFilter([P.type('lowpass'), P.frequency(synth.cutoff)], [ 29 | N.dac() 30 | ]) 31 | ]) 32 | ] 33 | } -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Flow example 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import { Program, Effect, Action, DOM, Audio } from '../src' 2 | import { Note, Time } from '../src/music' 3 | 4 | import audio from './audio' 5 | import view from './view' 6 | 7 | // Main ------------------------------------------------------------------------ 8 | const App = Program.instrument(init, update, audio, view, listen) 9 | 10 | App.use(DOM.Event) 11 | App.use(Audio.Event) 12 | 13 | App.start({ 14 | root: document.querySelector('#app'), 15 | context: new AudioContext(), 16 | flags: { 17 | debug: true 18 | } 19 | }) 20 | 21 | // Model ----------------------------------------------------------------------- 22 | function init(_, time) { 23 | const row = (note, name, length) => ({ note, name, steps: Array(length).fill(false) }) 24 | 25 | const numSteps = 8 26 | const notes = ['C5', 'B4', 'A4', 'G4', 'F4', 'E4', 'D4', 'C4'] 27 | const bpm = 150 28 | 29 | return { 30 | currentTime: time, 31 | sequencer: { 32 | rows: notes.map(note => row(Note.ntof(note), note, numSteps)), 33 | running: false, 34 | step: 0, 35 | stepCount: numSteps, 36 | stepInterval: Time.sec(bpm, Time.Eighth), 37 | tempo: bpm, 38 | }, 39 | synth: { 40 | type: 'sine', 41 | delayTime: 1, 42 | delayAmount: 0.2, 43 | masterGain: 0, 44 | cutoff: 2000 45 | }, 46 | } 47 | } 48 | 49 | // Update ---------------------------------------------------------------------- 50 | // Here we declare all of our actions. It's not strictly necessary to do this, 51 | // but doing so allows us to take advantage of auto-completion and helps to 52 | // prevent string-typing our application. 53 | // Typos and misspellings are common when working with raw strings! 54 | // Sequencer Actions ----------------------------------------------------------- 55 | const PLAY = 'play' 56 | const STOP = 'stop' 57 | const TICK = 'tick' 58 | const ADD_STEP = 'add-step' 59 | const RMV_STEP = 'rmv-step' 60 | const TGL_STEP = 'tgl-step' 61 | const RESET_STEPS = 'reset-steps' 62 | // Synth Actions --------------------------------------------------------------- 63 | const MUTE_TOGGLE = 'mute-toggle' 64 | const CHANGE_WAVEFORM = 'change-waveform' 65 | const CHANGE_DELAY = 'change-delay' 66 | const UPDATE_CUTOFF = 'update-cutoff' 67 | 68 | function update({ action, payload }, model) { 69 | switch (action) { 70 | case PLAY: { 71 | const sequencer = { 72 | ...model.sequencer, 73 | running: true, 74 | // The sequencer will immediately tick to the next step after hitting 75 | // play so setting this to one lower now means we'll start the sequence 76 | // on the step we're currently on. 77 | // We also check if the sequencer is already running to prevent this 78 | // messing with the current step if you hit the play button multiple 79 | // times. 80 | step: model.sequencer.running 81 | ? model.sequencer.step 82 | : model.sequencer.step - 1 83 | } 84 | 85 | return [{ ...model, sequencer }] 86 | } 87 | 88 | case STOP: { 89 | const sequencer = { ...model.sequencer, running: false } 90 | 91 | return [{ ...model, sequencer }] 92 | } 93 | 94 | case TICK: { 95 | const { time } = payload 96 | const step = (model.sequencer.step + 1) % model.sequencer.stepCount 97 | const sequencer = { ...model.sequencer, step } 98 | 99 | return [{ ...model, currentTime: time, sequencer }] 100 | } 101 | 102 | case ADD_STEP: { 103 | const stepCount = model.sequencer.stepCount + 1 104 | const rows = model.sequencer.rows.map(row => ({ 105 | ...row, steps: [...row.steps, false] 106 | })) 107 | const sequencer = { ...model.sequencer, rows, stepCount } 108 | 109 | return [{ ...model, sequencer }] 110 | } 111 | 112 | case RMV_STEP: { 113 | // There must always be at least four step sin the sequencer. 114 | const stepCount = model.sequencer.stepCount > 4 115 | ? model.sequencer.stepCount - 1 116 | : model.sequencer.stepCount 117 | const rows = model.sequencer.rows.map(row => ({ 118 | ...row, steps: row.steps.slice(0, stepCount) 119 | })) 120 | const sequencer = { ...model.sequencer, rows, stepCount } 121 | 122 | return [{ ...model, sequencer }] 123 | } 124 | 125 | case TGL_STEP: { 126 | const { note, step } = payload 127 | const rows = model.sequencer.rows.map(row => 128 | row.name == note 129 | ? { ...row, steps: row.steps.map((a, i) => step == i ? !a : a) } 130 | : row 131 | ) 132 | const sequencer = { ...model.sequencer, rows } 133 | 134 | return [{ ...model, sequencer }] 135 | } 136 | 137 | case RESET_STEPS: { 138 | const rows = model.sequencer.rows.map(row => 139 | ({ ...row, steps: row.steps.map(() => false) }) 140 | ) 141 | const sequencer = { ...model.sequencer, rows } 142 | 143 | return [{ ...model, sequencer }] 144 | } 145 | 146 | case MUTE_TOGGLE: { 147 | return [{ 148 | ...model, synth: { 149 | ...model.synth, masterGain: model.synth.masterGain == 1 ? 0 : 1 150 | } 151 | }] 152 | } 153 | 154 | case CHANGE_WAVEFORM: { 155 | const { type } = payload 156 | const synth = { ...model.synth, type } 157 | 158 | return [{ ...model, synth }] 159 | } 160 | 161 | case CHANGE_DELAY: { 162 | const { time } = payload 163 | const synth = { ...model.synth, delayTime: time === 'long' ? 1 : 0.2 } 164 | 165 | return [{ ...model, synth }] 166 | } 167 | 168 | case UPDATE_CUTOFF: { 169 | const { freq } = payload 170 | const synth = { ...model.synth, cutoff: freq } 171 | 172 | return [{ ...model, synth }] 173 | } 174 | 175 | // This should serve as a handy reminder in case we forget to catch an 176 | // action in this switch statement while also not crashing that app by just 177 | // returning the model unchanged. 178 | default: { 179 | console.warn(`Unhandled action: ${action}`) 180 | return [model] 181 | } 182 | } 183 | } 184 | 185 | // Listen ---------------------------------------------------------------------- 186 | function throttle(delay, f) { 187 | let t = Date.now() 188 | return function (e) { 189 | if ((t + delay - Date.now()) < 0) { 190 | t = Date.now() 191 | return f(e) 192 | } 193 | } 194 | } 195 | 196 | function listen(model) { 197 | const listeners = [ 198 | DOM.Event.click('#play', () => Action(PLAY)), 199 | DOM.Event.click('#stop', () => Action(STOP)), 200 | DOM.Event.click('#add-step', () => Action(ADD_STEP)), 201 | DOM.Event.click('#rmv-step', () => Action(RMV_STEP)), 202 | DOM.Event.click('#reset-steps', () => Action(RESET_STEPS)), 203 | DOM.Event.click('#mute-toggle', () => Action(MUTE_TOGGLE)), 204 | DOM.Event.click('[data-step]', ({ target }) => { 205 | const { note, step } = target.dataset 206 | return Action(TGL_STEP, { note, step }) 207 | }), 208 | DOM.Event.click('[data-waveform]', ({ target }) => { 209 | const { waveform } = target.dataset 210 | return Action(CHANGE_WAVEFORM, { type: waveform }) 211 | }), 212 | DOM.Event.click('[data-delay]', ({ target }) => { 213 | const { delay } = target.dataset 214 | return Action(CHANGE_DELAY, { time: delay }) 215 | }), 216 | DOM.Event.input('[data-cutoff]', throttle(100, ({ target }) => { 217 | return Action(UPDATE_CUTOFF, { freq: Number(target.value) }) 218 | })), 219 | DOM.Event.keydown('window', ({ key }) => { 220 | return key == ' ' 221 | ? Action(model.sequencer.running ? STOP : PLAY) 222 | : {} 223 | }) 224 | ] 225 | 226 | // We only need to listen for audio timing events when the sequencer is 227 | // actually running, so we can just conditionally push this listener when we 228 | // need to. 229 | if (model.sequencer.running) { 230 | listeners.push( 231 | Audio.Event.every('tick', model.sequencer.stepInterval, time => 232 | Action(TICK, { time }) 233 | ) 234 | ) 235 | } 236 | 237 | return listeners 238 | } 239 | -------------------------------------------------------------------------------- /examples/view.js: -------------------------------------------------------------------------------- 1 | import { Element as E, Attribute as A } from '../src/dom' 2 | 3 | // Utils ----------------------------------------------------------------------- 4 | const combineTailwindCategories = categories => 5 | A.className(categories.filter(c => c !== '').join(' ')) 6 | 7 | // Basic components ------------------------------------------------------------ 8 | const button = (id, colour, attributes, children) => { 9 | // These are all the different parts of the Tailwind css library. They're not 10 | // essential to the code but they make everything look pretty. 11 | const typography = 'text-white' 12 | const background = `bg-${colour}-600 hover:bg-${colour}-800` 13 | const borders = 'border-4 border-gray-900' 14 | const spacing = 'p-2 mr-4 my-2' 15 | const classes = combineTailwindCategories([ 16 | typography, background, borders, spacing 17 | ]) 18 | 19 | return E.button([...attributes, classes, A.id(id)], [ 20 | ...children 21 | ]) 22 | } 23 | 24 | const slider = (attrs) => { 25 | return E.div([A.className('flex')], [ 26 | E.span([A.className('mr-4 my-2 p-2')], ['Frequency']), 27 | E.input([ 28 | A.className('flex-1'), 29 | A.type('range'), 30 | ...attrs 31 | ]) 32 | ]) 33 | } 34 | 35 | const sequencerDisplay = (rows, highlightedColumn) => { 36 | // These are all the different parts of the Tailwind css library. They're not 37 | // essential to the code but they make everything look pretty. 38 | const layout = 'overflow-x-scroll' 39 | const borders = 'border-4 border-gray-900' 40 | const spacing = 'my-4' 41 | const sizing = 'w-auto' 42 | const classes = combineTailwindCategories([ 43 | layout, borders, spacing, sizing 44 | ]) 45 | 46 | return E.div([classes], [ 47 | ...rows.map(sequencerRow(highlightedColumn)) 48 | ]) 49 | } 50 | 51 | const sequencerRow = (highlightedColumn) => ({ name, steps }) => { 52 | // These are all the different parts of the Tailwind css library. They're not 53 | // essential to the code but they make everything look pretty. 54 | const layout = 'flex' 55 | const flexbox = 'items-center' 56 | const classes = combineTailwindCategories([ 57 | layout, flexbox 58 | ]) 59 | 60 | return E.div([classes], [ 61 | E.span([A.className('pl-2 pr-6 font-bold')], [name]), 62 | ...steps.map(sequencerStep(name, highlightedColumn)) 63 | ]) 64 | } 65 | 66 | const sequencerStep = (note, highlightedColumn) => (active, i) => { 67 | // These are all the different parts of the Tailwind css library. They're not 68 | // essential to the code but they make everything look pretty. 69 | const typography = 'text-white' 70 | const background = `bg-gray-${active ? '900' : '600'} hover:bg-gray-800` 71 | const borders = 'border-4 border-gray-900' 72 | const spacing = 'py-4 px-6' 73 | const classes = combineTailwindCategories([ 74 | typography, background, borders, spacing 75 | ]) 76 | 77 | return E.div([A.className(`p-2 bg-${highlightedColumn == i ? 'gray-300' : 'transparent'}`)], [ 78 | E.button([ 79 | A.dataCustom('step', `${i}`), 80 | A.dataCustom('note', note), 81 | classes 82 | ]) 83 | ]) 84 | } 85 | 86 | // Complex components ---------------------------------------------------------- 87 | 88 | 89 | // Export ---------------------------------------------------------------------- 90 | export default ({ sequencer, synth }) => { 91 | // These are all the different parts of the Tailwind css library. They're not 92 | // essential to the code but they make everything look pretty. 93 | const layout = 'container' 94 | const typography = 'font-mono' 95 | const spacing = 'mx-auto py-6 px-4' 96 | const classes = combineTailwindCategories([ 97 | layout, typography, spacing 98 | ]) 99 | 100 | return E.main([classes], [ 101 | // Title and info ---------------------------------------------------------- 102 | E.section([], [ 103 | E.h1([A.className('text-2xl font-bold')], ['Flow.js']) 104 | ]), 105 | // Sequencer controls ------------------------------------------------------ 106 | E.section([], [ 107 | button('play', 'gray', [], ['play']), 108 | button('stop', 'gray', [], ['stop']), 109 | button('add-step', 'gray', [], ['add step']), 110 | button('rmv-step', 'gray', [], ['remove step']), 111 | button('reset-steps', 'orange', [], ['reset steps']), 112 | ]), 113 | // Sequencer steps --------------------------------------------------------- 114 | E.section([], [ 115 | `${sequencer.step}`, 116 | sequencerDisplay(sequencer.rows, sequencer.step), 117 | ]), 118 | // Synth controls ---------------------------------------------------------- 119 | E.section([], [ 120 | E.h2([A.className('text-lg font-bold')], ['Synth controls:']), 121 | button('mute-toggle', 'gray', [], [ 122 | synth.masterGain == 1 ? 'mute' : 'unmute' 123 | ]) 124 | ]), 125 | E.section([], [ 126 | E.h2([A.className('text-lg font-bold')]['Filter']), 127 | slider([A.dataCustom('cutoff'), A.min(50), A.max(4000), A.value(synth.cutoff)]), 128 | ]), 129 | E.section([], [ 130 | E.h2([A.className('text-lg font-bold')], ['Waveform:']), 131 | button('', 'blue', [A.dataCustom('waveform', 'sine')], ['sine']), 132 | button('', 'green', [A.dataCustom('waveform', 'triangle')], ['triangle']), 133 | button('', 'red', [A.dataCustom('waveform', 'sawtooth')], ['sawtooth']), 134 | button('', 'yellow', [A.dataCustom('waveform', 'square')], ['square']), 135 | ]), 136 | E.section([], [ 137 | E.h2([A.className('text-lg font-bold')], ['Delay time:']), 138 | button('delay-short', 'purple', [A.dataCustom('delay', 'short')], ['short']), 139 | button('delay-long', 'purple', [A.dataCustom('delay', 'long')], ['long']), 140 | ]) 141 | ]) 142 | } 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flow-lang/framework", 3 | "version": "1.2.0", 4 | "description": "An Elm-inspired framework for interactive Web Audio applications.", 5 | "browser": "dist/index.js", 6 | "directories": { 7 | "lib": "src", 8 | "doc": "docs", 9 | "example": "examples", 10 | "test": "tests" 11 | }, 12 | "scripts": { 13 | "build": "parcel build src/index.js src/**/* -d dist --no-source-maps", 14 | "dev": "parcel .dev/src/index.html -d .dev/build", 15 | "examples": "parcel examples/index.html -d .dev/build" 16 | }, 17 | "keywords": [ 18 | "Elm", 19 | "MVU", 20 | "Web Audio API", 21 | "framework" 22 | ], 23 | "author": "Andrew Thompson ", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "parcel-bundler": "^1.12.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/action.js: -------------------------------------------------------------------------------- 1 | export default (action, payload = null) => ({ action, payload }) -------------------------------------------------------------------------------- /src/audio/event.js: -------------------------------------------------------------------------------- 1 | export const event = (type, id, time, handler) => ({ __eventType: 'audio', type, id, time, handler }) 2 | 3 | export const every = (id, time, handler) => event('repeat', id, time, handler) 4 | export const at = (id, time, handler) => event('once', id, time, handler) 5 | 6 | export default { 7 | // Core ====================================================================== 8 | every, 9 | at, 10 | // Plugin data =============================================================== 11 | // The runtime needs to know what type of plugin to install 12 | __pluginType: 'event', 13 | // In the future, __pluginName will be used to stop duplicate plugins being 14 | // registered. 15 | __pluginName: 'Audio.Event', 16 | // The event type should match the __eventType of any event objects you want 17 | // this plugin to handle. 18 | __eventType: 'audio', 19 | // Install is called after a program has been started. It is always passed an 20 | // object with $context, $root, and $dispatch but a plugin may choose to ignore 21 | // any or all of these fields. 22 | __install({ $context, $dispatch }) { 23 | this.$context = $context 24 | this.$dispatch = $dispatch 25 | }, 26 | // Update is called every time the model is updated, and it receives a filtered 27 | // list of all the new event listeners. The list is filtered based on the 28 | // __eventType defined above. 29 | __update(newEvents) { 30 | const oldEvents = Object.keys(this.$events) 31 | 32 | for (let i = 0; i < Math.max(oldEvents.length, newEvents.length); i++) { 33 | const newEvent = newEvents[i] 34 | const oldEvent = oldEvents[i] 35 | 36 | if (newEvent) { 37 | if (this.$events[newEvent.id]) { 38 | this.$events[newEvent.id].update(newEvent) 39 | } else { 40 | this.$events[newEvent.id] = new Event(newEvent, this.$dispatch, this.$context) 41 | } 42 | } else { 43 | this.$events[oldEvent].stop() 44 | 45 | delete this.$events[oldEvent] 46 | } 47 | } 48 | }, 49 | // 50 | $context: null, 51 | $dispatch: null, 52 | $events: {} 53 | } 54 | 55 | // 56 | class Event { 57 | constructor({ type, time, handler }, $dispatch, $context) { 58 | this.type = type 59 | this.time = time 60 | this.handler = handler 61 | 62 | this.interval = 25 // ms 63 | this.lookahead = 0.1 // seconds 64 | 65 | this.start($dispatch, $context) 66 | } 67 | 68 | start($dispatch, $context) { 69 | this.targetTime = $context.currentTime + this.time 70 | 71 | if (this.type === 'repeat') $dispatch(this.handler($context.currentTime)) 72 | 73 | this.timerID = setInterval(() => { 74 | const currentTime = $context.currentTime 75 | 76 | if (currentTime >= this.targetTime - this.lookahead) { 77 | const diff = this.targetTime - currentTime 78 | 79 | $dispatch(this.handler({ 80 | type: 'Time', 81 | value: currentTime + diff 82 | })) 83 | 84 | switch (this.type) { 85 | case 'repeat': 86 | this.targetTime = currentTime + diff + this.time 87 | break 88 | case 'once': 89 | this.stop() 90 | } 91 | } 92 | }, 25) 93 | } 94 | 95 | update({ time, handler }) { 96 | if (time) this.time = time 97 | if (handler) this.handler = handler 98 | } 99 | 100 | stop() { 101 | clearInterval(this.timerID) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/audio/index.js: -------------------------------------------------------------------------------- 1 | export { default as Node } from './node' 2 | export { default as Keyed } from './keyed' 3 | export { default as Property } from './property' 4 | export { default as Event } from './event' 5 | -------------------------------------------------------------------------------- /src/audio/keyed.js: -------------------------------------------------------------------------------- 1 | import { node } from './node' 2 | 3 | export const keyed = (key, type, properties = [], connections = []) => ({ key, ...node(type, properties, connections) }) 4 | export const key = (key, node) => ({ key, ...node }) 5 | 6 | export const analyser = (key, properties, connections) => keyed(key, 'AnalyserNode', properties, connections) 7 | export const audioBufferSource = (key, properties, connections) => keyed(key, 'AudioBufferSourceNode', properties, connections) 8 | export const audioScheduledSource = (key, properties, connections) => keyed(key, 'AudioScheduledSourceNode', properties, connections) 9 | export const biquadFilter = (key, properties, connections) => keyed(key, 'BiquadFilterNode', properties, connections) 10 | export const channelMerger = (key, properties, connections) => keyed(key, 'ChannelMergerNode', properties, connections) 11 | export const channelSplitter = (key, properties, connections) => keyed(key, 'ChannelSplitterNode', properties, connections) 12 | export const constantSource = (key, properties, connections) => keyed(key, 'ConstantSourceNode', properties, connections) 13 | export const convolver = (key, properties, connections) => keyed(key, 'ConvolverNode', properties, connections) 14 | export const delay = (key, properties, connections) => keyed(key, 'DelayNode', properties, connections) 15 | export const dynamicsCompressor = (key, properties, connections) => keyed(key, 'DynamicsCompressorNode', properties, connections) 16 | export const gain = (key, properties, connections) => keyed(key, 'GainNode', properties, connections) 17 | export const iirFilter = (key, properties, connections) => keyed(key, 'IIRFilterNode', properties, connections) 18 | export const mediaElementAudioSource = (key, properties, connections) => keyed(key, 'MediaElementAudioSourceNode', properties, connections) 19 | export const mediaStreamAudioDestination = (key, properties, connections) => keyed(key, 'MediaStreamAudioDestinationNode', properties, connections) 20 | export const mediaStreamAudioSource = (key, properties, connections) => keyed(key, 'MediaStreamAudioSourceNode', properties, connections) 21 | export const oscillator = (key, properties, connections) => keyed(key, 'OscillatorNode', properties, connections) 22 | export const panner = (key, properties, connections) => keyed(key, 'PannerNode', properties, connections) 23 | export const stereoPanner = (key, properties, connections) => keyed(key, 'StereoPannerNode', properties, connections) 24 | export const waveShaper = (key, properties, connections) => keyed(key, 'WaveShaperNode', properties, connections) 25 | 26 | export const compressor = dynamicsCompressor 27 | export const filter = biquadFilter 28 | export const num = constantSource 29 | export const osc = oscillator 30 | 31 | export default { 32 | // Core 33 | keyed, 34 | key, 35 | // Keyed Web Audio nodes 36 | analyser, 37 | audioBufferSource, 38 | audioScheduledSource, 39 | biquadFilter, 40 | channelMerger, 41 | channelSplitter, 42 | constantSource, 43 | convolver, 44 | delay, 45 | dynamicsCompressor, 46 | gain, 47 | iirFilter, 48 | mediaElementAudioSource, 49 | mediaStreamAudioDestination, 50 | mediaStreamAudioSource, 51 | oscillator, 52 | panner, 53 | stereoPanner, 54 | waveShaper, 55 | // Aliases 56 | compressor, 57 | filter, 58 | num, 59 | osc 60 | } 61 | -------------------------------------------------------------------------------- /src/audio/node.js: -------------------------------------------------------------------------------- 1 | export const node = (type, properties = [], connections = []) => ({ type, properties, connections }) 2 | export const ref = key => ({ type: 'RefNode', key }) 3 | 4 | // export const analyser = (properties, connections) => node('AnalyserNode', properties, connections) 5 | export const audioBufferSource = (properties, connections) => node('AudioBufferSourceNode', properties, connections) 6 | export const audioDestination = () => node('AudioDestinationNode') 7 | export const audioScheduledSource = (properties, connections) => node('AudioScheduledSourceNode', properties, connections) 8 | export const biquadFilter = (properties, connections) => node('BiquadFilterNode', properties, connections) 9 | export const channelMerger = (properties, connections) => node('ChannelMergerNode', properties, connections) 10 | export const channelSplitter = (properties, connections) => node('ChannelSplitterNode', properties, connections) 11 | export const constantSource = (properties, connections) => node('ConstantSourceNode', properties, connections) 12 | export const convolver = (properties, connections) => node('ConvolverNode', properties, connections) 13 | export const delay = (properties, connections) => node('DelayNode', properties, connections) 14 | export const dynamicsCompressor = (properties, connections) => node('DynamicsCompressorNode', properties, connections) 15 | export const gain = (properties, connections) => node('GainNode', properties, connections) 16 | export const iirFilter = (properties, connections) => node('IIRFilterNode', properties, connections) 17 | // export const mediaElementAudioSource = (properties, connections) => node('MediaElementAudioSourceNode', properties, connections) 18 | // export const mediaStreamAudioDestination = (properties, connections) => node('MediaStreamAudioDestinationNode', properties, connections) 19 | // export const mediaStreamAudioSource = (properties, connections) => node('MediaStreamAudioSourceNode', properties, connections) 20 | export const oscillator = (properties, connections) => node('OscillatorNode', properties, connections) 21 | export const panner = (properties, connections) => node('PannerNode', properties, connections) 22 | export const stereoPanner = (properties, connections) => node('StereoPannerNode', properties, connections) 23 | export const waveShaper = (properties, connections) => node('WaveShaperNode', properties, connections) 24 | 25 | export const compressor = dynamicsCompressor 26 | export const dac = audioDestination 27 | export const filter = biquadFilter 28 | export const num = constantSource 29 | export const osc = oscillator 30 | 31 | export default { 32 | // Core 33 | node, 34 | ref, 35 | // Web Audio nodes 36 | // analyser, 37 | audioBufferSource, 38 | audioDestination, 39 | audioScheduledSource, 40 | biquadFilter, 41 | channelMerger, 42 | channelSplitter, 43 | constantSource, 44 | convolver, 45 | delay, 46 | dynamicsCompressor, 47 | gain, 48 | iirFilter, 49 | // mediaElementAudioSource, 50 | // mediaStreamAudioDestination, 51 | // mediaStreamAudioSource, 52 | oscillator, 53 | panner, 54 | stereoPanner, 55 | waveShaper, 56 | // Aliases 57 | compressor, 58 | dac, 59 | filter, 60 | num, 61 | osc 62 | } 63 | -------------------------------------------------------------------------------- /src/audio/property.js: -------------------------------------------------------------------------------- 1 | export const property = (type, label, value) => ({ type, label, value }) 2 | export const scheduledProperty = (method, prop, time) => property('ScheduledAudioParam', prop.label, { method, target: prop.value, time }) 3 | 4 | export const setValueAtTime = (property, time) => scheduledProperty('setValueAtTime', property, time) 5 | export const linearRampToValueAtTime = (property, time) => scheduledProperty('linearRampToValueAtTime', property, time) 6 | export const exponentialRampToValueAtTime = (property, time) => scheduledProperty('exponentialRampToValueAtTime', property, time) 7 | 8 | export const setValuesAtTime = (property, valuesAndTimes) => valuesAndTimes.map(([ value, time ]) => setValueAtTime(property(value), time)) 9 | export const linearRampToValuesAtTime = (property, valuesAndTimes) => valuesAndTimes.map(([ value, time ]) => linearRampToValueAtTime(property(value), time)) 10 | export const exponentialRampToValuesAtTime = (property, valuesAndTimes) => valuesAndTimes.map(([ value, time ]) => exponentialRampToValueAtTime(property(value), time)) 11 | 12 | export const attack = value => property('AudioParam', 'attack', value) 13 | export const coneInnerAngle = value => property('NodeProperty', 'coneInnerAngle', value) 14 | export const coneOuterAngle = value => property('NodeProperty', 'coneOuterAngle', value) 15 | export const coneOuterGain = value => property('NodeProperty', 'coneOuterGain', value) 16 | export const curve = value => property('NodeProperty', 'curve', value) 17 | export const delayTime = value => property('AudioParam', 'delayTime', value) 18 | export const detune = value => property('AudioParam', 'detune', value) 19 | export const distanceModel = value => property('NodeProperty', 'distanceModel', value) 20 | export const feedback = value => property('ConstructorProperty', 'feedback', value) 21 | export const feedforward = value => property('ConstructorProperty', 'feedforward', value) 22 | export const fftSize = value => property('NodeProperty', 'fftSize', value) 23 | export const frequency = value => property('AudioParam', 'frequency', value) 24 | export const frequencyBinCount = value => property('NodeProperty', 'frequencyBinCount', value) 25 | export const gain = value => property('AudioParam', 'gain', value) 26 | export const gainAmount = gain 27 | export const knee = value => property('AudioParam', 'knee', value) 28 | export const loop = value => property('NodeProperty', 'loop', value) 29 | export const loopEnd = value => property('NodeProperty', 'loopEnd', value) 30 | export const loopStart = value => property('NodeProperty', 'loopStart', value) 31 | export const maxChannelCount = value => property('NodeProperty', 'maxChannelCount', value) 32 | export const maxDecibels = value => property('NodeProperty', 'maxDecibels', value) 33 | export const maxDelayTime = value => property('ConstructorProperty', 'maxDelayTime', value) 34 | export const maxDistance = value => property('NodeProperty', 'maxDistance', value) 35 | export const mediaElement = value => property('ConstructorProperty', 'mediaElement', value) 36 | export const mediaStream = value => property('ConstructorProperty', 'mediaStream', value) 37 | export const minDecibels = value => property('NodeProperty', 'minDecibels', value) 38 | export const normalize = value => property('NodeProperty', 'normalize', value) 39 | export const offset = value => property('AudioParam', 'offset', value) 40 | export const orientationX = value => property('AudioParam', 'orientationX', value) 41 | export const orientationY = value => property('AudioParam', 'orientationY', value) 42 | export const orientationZ = value => property('AudioParam', 'orientationZ', value) 43 | export const oversample = value => property('NodeProperty', 'oversample', value) 44 | export const pan = value => property('AudioParam', 'pan', value) 45 | export const panningModel = value => property('NodeProperty', 'panningModel', value) 46 | export const playbackRate = value => property('AudioParam', 'playbackRate', value) 47 | export const positionX = value => property('AudioParam', 'positionX', value) 48 | export const positionY = value => property('AudioParam', 'positionY', value) 49 | export const positionZ = value => property('AudioParam', 'positionZ', value) 50 | export const q = value => property('AudioParam', 'Q', value) 51 | export const ratio = value => property('AudioParam', 'ratio', value) 52 | export const reduction = value => property('AudioParam', 'reduction', value) 53 | export const refDistance = value => property('NodeProperty', 'refDistance', value) 54 | export const release = value => property('AudioParam', 'release', value) 55 | export const rolloffFactor = value => property('NodeProperty', 'rolloffFactor', value) 56 | export const smoothingTimeConstant = value => property('NodeProperty', 'smoothingTimeConstant', value) 57 | export const stream = value => property('MediaStream', 'stream', value) 58 | export const threshold = value => property('AudioParam', 'threshold', value) 59 | export const type = value => property('NodeProperty', 'type', value) 60 | 61 | export default { 62 | // Core 63 | property, 64 | scheduledProperty, 65 | // Scheduled properties 66 | setValueAtTime, 67 | linearRampToValueAtTime, 68 | exponentialRampToValueAtTime, 69 | setValuesAtTime, 70 | linearRampToValuesAtTime, 71 | exponentialRampToValuesAtTime, 72 | // Web Audio properties 73 | attack, 74 | coneInnerAngle, 75 | coneOuterAngle, 76 | coneOuterGain, 77 | curve, 78 | delayTime, 79 | detune, 80 | distanceModel, 81 | feedback, 82 | feedforward, 83 | fftSize, 84 | frequency, 85 | frequencyBinCount, 86 | gain, 87 | gainAmount, 88 | knee, 89 | loop, 90 | loopEnd, 91 | loopStart, 92 | maxChannelCount, 93 | maxDecibels, 94 | maxDelayTime, 95 | maxDistance, 96 | mediaElement, 97 | mediaStream, 98 | minDecibels, 99 | normalize, 100 | offset, 101 | orientationX, 102 | orientationY, 103 | orientationZ, 104 | oversample, 105 | pan, 106 | panningModel, 107 | playbackRate, 108 | positionX, 109 | positionY, 110 | positionZ, 111 | q, 112 | ratio, 113 | reduction, 114 | refDistance, 115 | release, 116 | rolloffFactor, 117 | smoothingTimeConstant, 118 | stream, 119 | threshold, 120 | type 121 | } 122 | -------------------------------------------------------------------------------- /src/dom/attribute.js: -------------------------------------------------------------------------------- 1 | export const attribute = (name, value) => ({ name, value }) 2 | 3 | export const accept = value => attribute('accept', value) 4 | export const acceptCharset = value => attribute('accept-charset', value) 5 | export const accesskey = value => attribute('accesskey', value) 6 | export const action = value => attribute('action', value) 7 | export const align = value => attribute('align', value) 8 | export const allow = value => attribute('allow', value) 9 | export const alt = value => attribute('alt', value) 10 | export const async = value => attribute('async', value) 11 | export const autocapitalize = value => attribute('autocapitalize', value) 12 | export const autocomplete = value => attribute('autocomplete', value) 13 | export const autofocus = value => attribute('autofocus', value) 14 | export const autoplay = value => attribute('autoplay', value) 15 | export const buffered = value => attribute('buffered', value) 16 | export const challenge = value => attribute('challenge', value) 17 | export const charset = value => attribute('charset', value) 18 | export const checked = value => attribute('checked', value) 19 | export const cite = value => attribute('cite', value) 20 | export const className = value => attribute('class', value) 21 | export const code = value => attribute('code', value) 22 | export const codebase = value => attribute('codebase', value) 23 | export const color = value => attribute('color', value) 24 | export const cols = value => attribute('cols', value) 25 | export const colspan = value => attribute('colspan', value) 26 | export const content = value => attribute('content', value) 27 | export const contenteditable = value => attribute('contenteditable', value) 28 | export const contextmenu = value => attribute('contextmenu', value) 29 | export const controls = value => attribute('controls', value) 30 | export const coords = value => attribute('coords', value) 31 | export const crossorigin = value => attribute('crossorigin', value) 32 | export const csp = value => attribute('csp', value) 33 | export const data = value => attribute('data', value) 34 | export const dataCustom = (name, value) => attribute(`data-${name}`, value) 35 | export const datetime = value => attribute('datetime', value) 36 | export const decoding = value => attribute('decoding', value) 37 | export const default_ = value => attribute('default', value) 38 | export const defer = value => attribute('defer', value) 39 | export const dir = value => attribute('dir', value) 40 | export const dirname = value => attribute('dirname', value) 41 | export const disabled = value => attribute('disabled', value) 42 | export const download = value => attribute('download', value) 43 | export const draggable = value => attribute('draggable', value) 44 | export const dropzone = value => attribute('dropzone', value) 45 | export const enctype = value => attribute('enctype', value) 46 | export const for_ = value => attribute('for', value) 47 | export const form = value => attribute('form', value) 48 | export const formaction = value => attribute('formaction', value) 49 | export const headers = value => attribute('headers', value) 50 | export const height = value => attribute('height', value) 51 | export const hidden = value => attribute('hidden', value) 52 | export const high = value => attribute('high', value) 53 | export const href = value => attribute('href', value) 54 | export const hreflang = value => attribute('hreflang', value) 55 | export const httpEquiv = value => attribute('http-equiv', value) 56 | export const icon = value => attribute('icon', value) 57 | export const id = value => attribute('id', value) 58 | export const importance = value => attribute('importance', value) 59 | export const integrity = value => attribute('integrity', value) 60 | export const ismap = value => attribute('ismap', value) 61 | export const itemprop = value => attribute('itemprop', value) 62 | export const keytype = value => attribute('keytype', value) 63 | export const kind = value => attribute('kind', value) 64 | export const label = value => attribute('label', value) 65 | export const lang = value => attribute('lang', value) 66 | export const language = value => attribute('language', value) 67 | export const lazyload = value => attribute('lazyload', value) 68 | export const list = value => attribute('list', value) 69 | export const loop = value => attribute('loop', value) 70 | export const low = value => attribute('low', value) 71 | export const manifest = value => attribute('manifest', value) 72 | export const max = value => attribute('max', value) 73 | export const maxlength = value => attribute('maxlength', value) 74 | export const minlength = value => attribute('minlength', value) 75 | export const media = value => attribute('media', value) 76 | export const method = value => attribute('method', value) 77 | export const min = value => attribute('min', value) 78 | export const multiple = value => attribute('multiple', value) 79 | export const muted = value => attribute('muted', value) 80 | export const name = value => attribute('name', value) 81 | export const novalidate = value => attribute('novalidate', value) 82 | export const open = value => attribute('open', value) 83 | export const optimum = value => attribute('optimum', value) 84 | export const pattern = value => attribute('pattern', value) 85 | export const ping = value => attribute('ping', value) 86 | export const placeholder = value => attribute('placeholder', value) 87 | export const poster = value => attribute('poster', value) 88 | export const preload = value => attribute('preload', value) 89 | export const radiogroup = value => attribute('radiogroup', value) 90 | export const readonly = value => attribute('readonly', value) 91 | export const referrerpolicy = value => attribute('referrerpolicy', value) 92 | export const rel = value => attribute('rel', value) 93 | export const required = value => attribute('required', value) 94 | export const reversed = value => attribute('reversed', value) 95 | export const rows = value => attribute('rows', value) 96 | export const rowspan = value => attribute('rowspan', value) 97 | export const sandbox = value => attribute('sandbox', value) 98 | export const scope = value => attribute('scope', value) 99 | export const scoped = value => attribute('scoped', value) 100 | export const selected = value => attribute('selected', value) 101 | export const shape = value => attribute('shape', value) 102 | export const size = value => attribute('size', value) 103 | export const sizes = value => attribute('sizes', value) 104 | export const slot = value => attribute('slot', value) 105 | export const span = value => attribute('span', value) 106 | export const spellcheck = value => attribute('spellcheck', value) 107 | export const src = value => attribute('src', value) 108 | export const srcdoc = value => attribute('srcdoc', value) 109 | export const srclang = value => attribute('srclang', value) 110 | export const srcset = value => attribute('srcset', value) 111 | export const start = value => attribute('start', value) 112 | export const step = value => attribute('step', value) 113 | export const style = value => attribute('style', value) 114 | export const summary = value => attribute('summary', value) 115 | export const tabindex = value => attribute('tabindex', value) 116 | export const target = value => attribute('target', value) 117 | export const title = value => attribute('title', value) 118 | export const translate = value => attribute('translate', value) 119 | export const type = value => attribute('type', value) 120 | export const usemap = value => attribute('usemap', value) 121 | export const value = value => attribute('value', value) 122 | export const width = value => attribute('width', value) 123 | export const wrap = value => attribute('wrap', value) 124 | 125 | export default { 126 | // Core 127 | attribute, 128 | // HTML attributes 129 | accept, 130 | acceptCharset, 131 | accesskey, 132 | action, 133 | align, 134 | allow, 135 | alt, 136 | async, 137 | autocapitalize, 138 | autocomplete, 139 | autofocus, 140 | autoplay, 141 | buffered, 142 | challenge, 143 | charset, 144 | checked, 145 | cite, 146 | className, 147 | code, 148 | codebase, 149 | color, 150 | cols, 151 | colspan, 152 | content, 153 | contenteditable, 154 | contextmenu, 155 | controls, 156 | coords, 157 | crossorigin, 158 | csp, 159 | data, 160 | dataCustom, 161 | datetime, 162 | decoding, 163 | default_, 164 | defer, 165 | dir, 166 | dirname, 167 | disabled, 168 | download, 169 | draggable, 170 | dropzone, 171 | enctype, 172 | for_, 173 | form, 174 | formaction, 175 | headers, 176 | height, 177 | hidden, 178 | high, 179 | href, 180 | hreflang, 181 | httpEquiv, 182 | icon, 183 | id, 184 | importance, 185 | integrity, 186 | ismap, 187 | itemprop, 188 | keytype, 189 | kind, 190 | label, 191 | lang, 192 | language, 193 | lazyload, 194 | list, 195 | loop, 196 | low, 197 | manifest, 198 | max, 199 | maxlength, 200 | minlength, 201 | media, 202 | method, 203 | min, 204 | multiple, 205 | muted, 206 | name, 207 | novalidate, 208 | open, 209 | optimum, 210 | pattern, 211 | ping, 212 | placeholder, 213 | poster, 214 | preload, 215 | radiogroup, 216 | readonly, 217 | referrerpolicy, 218 | rel, 219 | required, 220 | reversed, 221 | rows, 222 | rowspan, 223 | sandbox, 224 | scope, 225 | scoped, 226 | selected, 227 | shape, 228 | size, 229 | sizes, 230 | slot, 231 | span, 232 | spellcheck, 233 | src, 234 | srcdoc, 235 | srclang, 236 | srcset, 237 | start, 238 | step, 239 | style, 240 | summary, 241 | tabindex, 242 | target, 243 | title, 244 | translate, 245 | type, 246 | usemap, 247 | value, 248 | width, 249 | wrap 250 | } 251 | -------------------------------------------------------------------------------- /src/dom/element.js: -------------------------------------------------------------------------------- 1 | export const element = (tag, attrs = [], children = []) => ({ tag, attrs, children }) 2 | 3 | export const a = (attrs, children) => element('a', attrs, children) 4 | export const abbr = (attrs, children) => element('abbr', attrs, children) 5 | export const address = (attrs, children) => element('address', attrs, children) 6 | export const area = (attrs, children) => element('area', attrs, children) 7 | export const article = (attrs, children) => element('article', attrs, children) 8 | export const aside = (attrs, children) => element('aside', attrs, children) 9 | export const audio = (attrs, children) => element('audio', attrs, children) 10 | export const b = (attrs, children) => element('b', attrs, children) 11 | export const base = (attrs, children) => element('base', attrs, children) 12 | export const bdi = (attrs, children) => element('bdi', attrs, children) 13 | export const bdo = (attrs, children) => element('bdo', attrs, children) 14 | export const blockquote = (attrs, children) => element('blockquote', attrs, children) 15 | export const body = (attrs, children) => element('body', attrs, children) 16 | export const br = (attrs, children) => element('br', attrs, children) 17 | export const button = (attrs, children) => element('button', attrs, children) 18 | export const canvas = (attrs, children) => element('canvas', attrs, children) 19 | export const caption = (attrs, children) => element('caption', attrs, children) 20 | export const cite = (attrs, children) => element('cite', attrs, children) 21 | export const code = (attrs, children) => element('code', attrs, children) 22 | export const col = (attrs, children) => element('col', attrs, children) 23 | export const colgroup = (attrs, children) => element('colgroup', attrs, children) 24 | export const data = (attrs, children) => element('data', attrs, children) 25 | export const datalist = (attrs, children) => element('datalist', attrs, children) 26 | export const dd = (attrs, children) => element('dd', attrs, children) 27 | export const del = (attrs, children) => element('del', attrs, children) 28 | export const details = (attrs, children) => element('details', attrs, children) 29 | export const dfn = (attrs, children) => element('dfn', attrs, children) 30 | export const dialog = (attrs, children) => element('dialog', attrs, children) 31 | export const div = (attrs, children) => element('div', attrs, children) 32 | export const dl = (attrs, children) => element('dl', attrs, children) 33 | export const dt = (attrs, children) => element('dt', attrs, children) 34 | export const em = (attrs, children) => element('em', attrs, children) 35 | export const embed = (attrs, children) => element('embed', attrs, children) 36 | export const fieldset = (attrs, children) => element('fieldset', attrs, children) 37 | export const figure = (attrs, children) => element('figure', attrs, children) 38 | export const footer = (attrs, children) => element('footer', attrs, children) 39 | export const form = (attrs, children) => element('form', attrs, children) 40 | export const h1 = (attrs, children) => element('h1', attrs, children) 41 | export const h2 = (attrs, children) => element('h2', attrs, children) 42 | export const h3 = (attrs, children) => element('h3', attrs, children) 43 | export const h4 = (attrs, children) => element('h4', attrs, children) 44 | export const h5 = (attrs, children) => element('h5', attrs, children) 45 | export const h6 = (attrs, children) => element('h6', attrs, children) 46 | export const head = (attrs, children) => element('head', attrs, children) 47 | export const header = (attrs, children) => element('header', attrs, children) 48 | export const hgroup = (attrs, children) => element('hgroup', attrs, children) 49 | export const hr = (attrs, children) => element('hr', attrs, children) 50 | export const html = (attrs, children) => element('html', attrs, children) 51 | export const i = (attrs, children) => element('i', attrs, children) 52 | export const iframe = (attrs, children) => element('iframe', attrs, children) 53 | export const img = (attrs, children) => element('img', attrs, children) 54 | export const input = (attrs, children) => element('input', attrs, children) 55 | export const ins = (attrs, children) => element('ins', attrs, children) 56 | export const kbd = (attrs, children) => element('kbd', attrs, children) 57 | export const keygen = (attrs, children) => element('keygen', attrs, children) 58 | export const label = (attrs, children) => element('label', attrs, children) 59 | export const legend = (attrs, children) => element('legend', attrs, children) 60 | export const li = (attrs, children) => element('li', attrs, children) 61 | export const link = (attrs, children) => element('link', attrs, children) 62 | export const main = (attrs, children) => element('main', attrs, children) 63 | export const map = (attrs, children) => element('map', attrs, children) 64 | export const mark = (attrs, children) => element('mark', attrs, children) 65 | export const menu = (attrs, children) => element('menu', attrs, children) 66 | export const menuitem = (attrs, children) => element('menuitem', attrs, children) 67 | export const meta = (attrs, children) => element('meta', attrs, children) 68 | export const meter = (attrs, children) => element('meter', attrs, children) 69 | export const nav = (attrs, children) => element('nav', attrs, children) 70 | export const noscript = (attrs, children) => element('noscript', attrs, children) 71 | export const object = (attrs, children) => element('object', attrs, children) 72 | export const ol = (attrs, children) => element('ol', attrs, children) 73 | export const optgroup = (attrs, children) => element('optgroup', attrs, children) 74 | export const option = (attrs, children) => element('option', attrs, children) 75 | export const output = (attrs, children) => element('output', attrs, children) 76 | export const p = (attrs, children) => element('p', attrs, children) 77 | export const param = (attrs, children) => element('param', attrs, children) 78 | export const pre = (attrs, children) => element('pre', attrs, children) 79 | export const progress = (attrs, children) => element('progress', attrs, children) 80 | export const q = (attrs, children) => element('q', attrs, children) 81 | export const rb = (attrs, children) => element('rb', attrs, children) 82 | export const rp = (attrs, children) => element('rp', attrs, children) 83 | export const rt = (attrs, children) => element('rt', attrs, children) 84 | export const rtc = (attrs, children) => element('rtc', attrs, children) 85 | export const ruby = (attrs, children) => element('ruby', attrs, children) 86 | export const s = (attrs, children) => element('s', attrs, children) 87 | export const samp = (attrs, children) => element('samp', attrs, children) 88 | export const script = (attrs, children) => element('script', attrs, children) 89 | export const section = (attrs, children) => element('section', attrs, children) 90 | export const select = (attrs, children) => element('select', attrs, children) 91 | export const small = (attrs, children) => element('small', attrs, children) 92 | export const source = (attrs, children) => element('source', attrs, children) 93 | export const span = (attrs, children) => element('span', attrs, children) 94 | export const strong = (attrs, children) => element('strong', attrs, children) 95 | export const style = (attrs, children) => element('style', attrs, children) 96 | export const sub = (attrs, children) => element('sub', attrs, children) 97 | export const summary = (attrs, children) => element('summary', attrs, children) 98 | export const sup = (attrs, children) => element('sup', attrs, children) 99 | export const table = (attrs, children) => element('table', attrs, children) 100 | export const tbody = (attrs, children) => element('tbody', attrs, children) 101 | export const td = (attrs, children) => element('td', attrs, children) 102 | export const template = (attrs, children) => element('template', attrs, children) 103 | export const text = s => `${s}` 104 | export const textarea = (attrs, children) => element('textarea', attrs, children) 105 | export const tfoot = (attrs, children) => element('tfoot', attrs, children) 106 | export const th = (attrs, children) => element('th', attrs, children) 107 | export const thead = (attrs, children) => element('thead', attrs, children) 108 | export const time = (attrs, children) => element('time', attrs, children) 109 | export const title = (attrs, children) => element('title', attrs, children) 110 | export const tr = (attrs, children) => element('tr', attrs, children) 111 | export const track = (attrs, children) => element('track', attrs, children) 112 | export const u = (attrs, children) => element('u', attrs, children) 113 | export const ul = (attrs, children) => element('ul', attrs, children) 114 | export const var_ = (attrs, children) => element('var', attrs, children) 115 | export const video = (attrs, children) => element('video', attrs, children) 116 | export const wbr = (attrs, children) => element('wbr', attrs, children) 117 | 118 | export default { 119 | // Core 120 | element, 121 | // HTML elements 122 | a, 123 | abbr, 124 | address, 125 | area, 126 | article, 127 | aside, 128 | audio, 129 | b, 130 | base, 131 | bdi, 132 | bdo, 133 | blockquote, 134 | body, 135 | br, 136 | button, 137 | canvas, 138 | caption, 139 | cite, 140 | code, 141 | col, 142 | colgroup, 143 | data, 144 | datalist, 145 | dd, 146 | del, 147 | details, 148 | dfn, 149 | dialog, 150 | div, 151 | dl, 152 | dt, 153 | em, 154 | embed, 155 | fieldset, 156 | figure, 157 | footer, 158 | form, 159 | h1, 160 | h2, 161 | h3, 162 | h4, 163 | h5, 164 | h6, 165 | head, 166 | header, 167 | hgroup, 168 | hr, 169 | html, 170 | i, 171 | iframe, 172 | img, 173 | input, 174 | ins, 175 | kbd, 176 | keygen, 177 | label, 178 | legend, 179 | li, 180 | link, 181 | main, 182 | map, 183 | mark, 184 | menu, 185 | menuitem, 186 | meta, 187 | meter, 188 | nav, 189 | noscript, 190 | object, 191 | ol, 192 | optgroup, 193 | option, 194 | output, 195 | p, 196 | param, 197 | pre, 198 | progress, 199 | q, 200 | rb, 201 | rp, 202 | rt, 203 | rtc, 204 | ruby, 205 | s, 206 | samp, 207 | script, 208 | section, 209 | select, 210 | small, 211 | source, 212 | span, 213 | strong, 214 | style, 215 | sub, 216 | summary, 217 | sup, 218 | table, 219 | tbody, 220 | td, 221 | template, 222 | text, 223 | textarea, 224 | tfoot, 225 | th, 226 | thead, 227 | time, 228 | title, 229 | tr, 230 | track, 231 | u, 232 | ul, 233 | var_, 234 | video, 235 | wbr 236 | } 237 | -------------------------------------------------------------------------------- /src/dom/event.js: -------------------------------------------------------------------------------- 1 | export const event = (selector, eventName, handler, opts = {}) => ({ 2 | __eventType: 'DOM', selector, eventName, handler, opts 3 | }) 4 | 5 | // MOST COMMON EVENTS ------------------------------------------------------- 6 | // Focus events 7 | export const focus = (selector, handler, opts) => event(selector, 'focus', handler, opts) 8 | export const blur = (selector, handler, opts) => event(selector, 'blur', handler, opts) 9 | 10 | // Form events 11 | export const reset = (selector, handler, opts) => event(selector, 'reset', handler, opts) 12 | export const submit = (selector, handler, opts) => event(selector, 'submit', handler, opts) 13 | 14 | // View events 15 | export const fullscreenchange = (selector, handler, opts) => event(selector, 'fullscreenchange', handler, opts) 16 | export const fullscreenerror = (selector, handler, opts) => event(selector, 'fullscreenerror', handler, opts) 17 | export const resize = (selector, handler, opts) => event(selector, 'resize', handler, opts) 18 | export const scroll = (selector, handler, opts) => event(selector, 'scroll', handler, opts) 19 | 20 | // Clipboard events 21 | export const cut = (selector, handler, opts) => event(selector, 'cut', handler, opts) 22 | export const copy = (selector, handler, opts) => event(selector, 'copy', handler, opts) 23 | export const paste = (selector, handler, opts) => event(selector, 'paste', handler, opts) 24 | 25 | // Keyboard events 26 | export const keydown = (selector, handler, opts) => event(selector, 'keydown', handler, opts) 27 | export const keypress = (selector, handler, opts) => event(selector, 'keypress', handler, opts) 28 | export const keyup = (selector, handler, opts) => event(selector, 'keyup', handler, opts) 29 | 30 | // Mouse events 31 | export const auxclick = (selector, handler, opts) => event(selector, 'auxclick', handler, opts) 32 | export const click = (selector, handler, opts) => event(selector, 'click', handler, opts) 33 | export const contextmenu = (selector, handler, opts) => event(selector, 'contextmenu', handler, opts) 34 | export const dblclick = (selector, handler, opts) => event(selector, 'dblclick', handler, opts) 35 | export const mousedown = (selector, handler, opts) => event(selector, 'mousedown', handler, opts) 36 | export const mouseenter = (selector, handler, opts) => event(selector, 'mouseenter', handler, opts) 37 | export const mouseleave = (selector, handler, opts) => event(selector, 'mouseleave', handler, opts) 38 | export const mousemove = (selector, handler, opts) => event(selector, 'mousemove', handler, opts) 39 | 40 | export const mouseover = (selector, handler, opts) => event(selector, 'mouseover', handler, opts) 41 | export const mouseout = (selector, handler, opts) => event(selector, 'mouseout', handler, opts) 42 | export const mouseup = (selector, handler, opts) => event(selector, 'mouseup', handler, opts) 43 | export const pointerlockchange = (selector, handler, opts) => event(selector, 'pointerlockchange', handler, opts) 44 | export const pointerlockerror = (selector, handler, opts) => event(selector, 'pointerlockerror', handler, opts) 45 | export const select = (selector, handler, opts) => event(selector, 'select', handler, opts) 46 | export const wheel = (selector, handler, opts) => event(selector, 'wheel', handler, opts) 47 | 48 | // Drag & Drop events 49 | export const drag = (selector, handler, opts) => event(selector, 'drag', handler, opts) 50 | export const dragend = (selector, handler, opts) => event(selector, 'dragend', handler, opts) 51 | export const dragenter = (selector, handler, opts) => event(selector, 'dragenter', handler, opts) 52 | export const dragstart = (selector, handler, opts) => event(selector, 'dragstart', handler, opts) 53 | export const dragleave = (selector, handler, opts) => event(selector, 'dragleave', handler, opts) 54 | export const dragover = (selector, handler, opts) => event(selector, 'dragover', handler, opts) 55 | export const drop = (selector, handler, opts) => event(selector, 'drop', handler, opts) 56 | 57 | // Value change events 58 | export const broadcast = (selector, handler, opts) => event(selector, 'broadcast', handler, opts) 59 | export const CheckboxStateChange = (selector, handler, opts) => event(selector, 'CheckboxStateChange', handler, opts) 60 | export const hashchange = (selector, handler, opts) => event(selector, 'hashchange', handler, opts) 61 | export const input = (selector, handler, opts) => event(selector, 'input', handler, opts) 62 | export const RadioStateChange = (selector, handler, opts) => event(selector, 'RadioStateChange', handler, opts) 63 | export const readystatechange = (selector, handler, opts) => event(selector, 'readystatechange', handler, opts) 64 | export const ValueChange = (selector, handler, opts) => event(selector, 'ValueChange', handler, opts) 65 | 66 | // LESS COMMON EVENTS ------------------------------------------------------- 67 | // Sensor events 68 | export const compassneedscalibration = (selector, handler, opts) => event(selector, 'compassneedscalibration', handler, opts) 69 | export const devicelight = (selector, handler, opts) => event(selector, 'devicelight', handler, opts) 70 | export const devicemotion = (selector, handler, opts) => event(selector, 'devicemotion', handler, opts) 71 | export const deviceorientation = (selector, handler, opts) => event(selector, 'deviceorientation', handler, opts) 72 | export const deviceproximity = (selector, handler, opts) => event(selector, 'deviceproximity', handler, opts) 73 | export const orientationchange = (selector, handler, opts) => event(selector, 'orientationchange', handler, opts) 74 | export const userproximity = (selector, handler, opts) => event(selector, 'userproximity', handler, opts) 75 | 76 | // Touch events 77 | export const touchcancel = (selector, handler, opts) => event(selector, 'touchcancel', handler, opts) 78 | export const touchend = (selector, handler, opts) => event(selector, 'touchend', handler, opts) 79 | export const touchenter = (selector, handler, opts) => event(selector, 'touchenter', handler, opts) 80 | export const touchleave = (selector, handler, opts) => event(selector, 'touchleave', handler, opts) 81 | export const touchmove = (selector, handler, opts) => event(selector, 'touchmove', handler, opts) 82 | export const touchstart = (selector, handler, opts) => event(selector, 'touchstart', handler, opts) 83 | 84 | // Pointer events 85 | export const pointerover = (selector, handler, opts) => event(selector, 'pointerover', handler, opts) 86 | export const pointerenter = (selector, handler, opts) => event(selector, 'pointerenter', handler, opts) 87 | export const pointerdown = (selector, handler, opts) => event(selector, 'pointerdown', handler, opts) 88 | export const pointermove = (selector, handler, opts) => event(selector, 'pointermove', handler, opts) 89 | export const pointerup = (selector, handler, opts) => event(selector, 'pointerup', handler, opts) 90 | export const pointercancel = (selector, handler, opts) => event(selector, 'pointercancel', handler, opts) 91 | export const pointerout = (selector, handler, opts) => event(selector, 'pointerout', handler, opts) 92 | export const pointerleave = (selector, handler, opts) => event(selector, 'pointerleave', handler, opts) 93 | export const gotpointercapture = (selector, handler, opts) => event(selector, 'gotpointercapture', handler, opts) 94 | export const lostpointercapture = (selector, handler, opts) => event(selector, 'lostpointercapture', handler, opts) 95 | 96 | // 97 | export default { 98 | // Core ====================================================================== 99 | event, 100 | // Focus events 101 | focus, 102 | blur, 103 | // Form events 104 | reset, 105 | submit, 106 | // View events 107 | fullscreenchange, 108 | fullscreenerror, 109 | resize, 110 | scroll, 111 | // Clipboard events 112 | cut, 113 | copy, 114 | paste, 115 | // Keyboard events 116 | keydown, 117 | keypress, 118 | keyup, 119 | // Mouse events 120 | auxclick, 121 | click, 122 | contextmenu, 123 | dblclick, 124 | mousedown, 125 | mouseenter, 126 | mouseleave, 127 | mousemove, 128 | mouseover, 129 | mouseout, 130 | mouseup, 131 | pointerlockchange, 132 | pointerlockerror, 133 | select, 134 | wheel, 135 | // Drag & Drop events 136 | drag, 137 | dragend, 138 | dragenter, 139 | dragstart, 140 | dragleave, 141 | dragover, 142 | drop, 143 | // Value change events 144 | broadcast, 145 | CheckboxStateChange, 146 | hashchange, 147 | input, 148 | RadioStateChange, 149 | readystatechange, 150 | ValueChange, 151 | // Sensor events 152 | compassneedscalibration, 153 | devicelight, 154 | devicemotion, 155 | deviceorientation, 156 | deviceproximity, 157 | orientationchange, 158 | userproximity, 159 | // Touch events 160 | touchcancel, 161 | touchend, 162 | touchenter, 163 | touchleave, 164 | touchmove, 165 | touchstart, 166 | // Pointer events 167 | pointerover, 168 | pointerenter, 169 | pointerdown, 170 | pointermove, 171 | pointerup, 172 | pointercancel, 173 | pointerout, 174 | pointerleave, 175 | gotpointercapture, 176 | lostpointercapture, 177 | // Plugin data =============================================================== 178 | // The runtime needs to know what type of plugin to install 179 | __pluginType: 'event', 180 | // In the future, __pluginName will be used to stop duplicate plugins being 181 | // registered. 182 | __pluginName: 'Html.Event', 183 | // The event type should match the __eventType of any event objects you want 184 | // this plugin to handle. 185 | __eventType: 'DOM', 186 | // Install is called after a program has been started. It is always passed an 187 | // object with $context, $root, and $dispatch but a plugin may choose to ignore 188 | // any or all of these fields. 189 | __install ({ $dispatch }) { 190 | this.$dispatch = $dispatch 191 | 192 | events.forEach(event => { 193 | this.$events[event] = [] 194 | this.$handlers[event] = e => { 195 | this.$events[event].forEach(({ selector, handler }) => { 196 | if (this.$isGlobal(selector) || e.target.matches(selector)) { 197 | handler(e) 198 | } 199 | }) 200 | } 201 | 202 | window.addEventListener(event, this.$handlers[event]) 203 | }) 204 | }, 205 | // Update is called every time the model is updated, and it receives a filtered 206 | // list of all the new event listeners. The list is filtered based on the 207 | // __eventType defined above. 208 | __update (newEvents) { 209 | newEvents = newEvents.map(event => { 210 | const handler = e => this.$dispatch(event.handler(e)) 211 | const selector = event.opts.specific || this.$isGlobal(event.selector) 212 | ? event.selector 213 | : `${event.selector}, ${event.selector} > *` 214 | 215 | return { ...event, handler, selector } 216 | }) 217 | 218 | for (const event in this.$events) { 219 | this.$events[event] = newEvents.filter(({ eventName }) => 220 | eventName === event 221 | ) 222 | } 223 | }, 224 | // Uninstall is called if a Program's destroy method is invoked. This should 225 | // clean up any event listeners so they're not left firing once the program 226 | // has been closed. 227 | __uninstall () { 228 | events.forEach(event => { 229 | window.removeEventListener(event, this.$handlers[event]) 230 | }) 231 | }, 232 | // 233 | $dispatch: null, 234 | $events: {}, 235 | $handlers: {}, 236 | $isGlobal (selector) { 237 | return selector === 'document' || selector === 'window' 238 | } 239 | } 240 | 241 | const events = [ 242 | // Focus events 243 | 'focus', 'blur', 244 | // Form events 245 | 'reset', 'submit', 246 | // View events 247 | 'fullscreenchange', 'fullscreenerror', 'resize', 'scroll', 248 | // Clipboard events 249 | 'cut', 'copy', 'paste', 250 | // Keyboard events 251 | 'keydown', 'keypress', 'keyup', 252 | // Mouse events 253 | 'auxclick', 'click', 'contextmenu', 'dblclick', 'mousedown', 'mouseenter', 254 | 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'pointerlockchange', 255 | 'pointerlockerror', 'select', 'wheel', 256 | // Drag & Drop events 257 | 'drag', 'dragend', 'dragenter', 'dragstart', 'dragleave', 'dragover', 'drop', 258 | // Value change events 259 | 'broadcast', 'CheckboxStateChange', 'hashchange', 'input', 'RadioStateChange', 260 | 'readystatechange', 'ValueChange', 261 | // Sensor events 262 | 'compassneedscalibration', 'devicelight', 'devicemotion', 'deviceorientation', 263 | 'deviceproximity', 'orientationchange', 'userproximity', 264 | // Touch events 265 | 'touchcancel', 'touchend', 'touchenter', 'touchleave', 'touchmove', 'touchstart', 266 | // Pointer events 267 | 'pointerover', 'pointerenter', 'pointerdown', 'pointermove', 'pointerup', 268 | 'pointercancel', 'pointerout', 'pointerleave', 'gotpointercapture', 269 | 'lostpointercapture' 270 | ] 271 | -------------------------------------------------------------------------------- /src/dom/index.js: -------------------------------------------------------------------------------- 1 | export { default as Element } from './element' 2 | export { default as Attribute } from './attribute' 3 | export { default as Event } from './event' 4 | -------------------------------------------------------------------------------- /src/effect.js: -------------------------------------------------------------------------------- 1 | export const none = () => {} 2 | export const batch = (...effects) => ($dispatch, $model) => 3 | (effects || []).forEach(effect => effect($dispatch, $model)) 4 | 5 | export default { 6 | none, 7 | batch 8 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // 2 | import instrument from './program/instrument' 3 | // 4 | import Effect_ from './effect' 5 | import Action_ from './action' 6 | // 7 | import Node from './audio/node' 8 | import Property from './audio/property' 9 | import Keyed from './audio/keyed' 10 | import AudioEvent from './audio/event' 11 | // 12 | import Note from './music/note' 13 | import Time from './music/time' 14 | // 15 | import Element from './dom/element' 16 | import Attribute from './dom/attribute' 17 | import DOMEvent from './dom/event' 18 | 19 | export const Effect = Effect_ 20 | export const Action = Action_ 21 | 22 | export const Program = { instrument } 23 | export const Audio = { Node, Property, Keyed, Event: AudioEvent } 24 | export const DOM = { Element, Attribute, Event: DOMEvent } 25 | export const Music = { Note, Time } 26 | 27 | export default { 28 | Program, 29 | Effect, 30 | Action, 31 | Audio, 32 | DOM, 33 | Music 34 | } 35 | -------------------------------------------------------------------------------- /src/music/index.js: -------------------------------------------------------------------------------- 1 | export { default as Note } from './note' 2 | export { default as Time } from './time' 3 | -------------------------------------------------------------------------------- /src/music/note.js: -------------------------------------------------------------------------------- 1 | import Notes from './notes.json' 2 | 3 | // MIDI note conversions ======================================================= 4 | export const mton = note => Object.keys(Notes).find(name => Notes[name] === note) 5 | export const mtof = note => 440 * Math.pow(2, (note - 69) / 12) 6 | 7 | // Note name conversions ======================================================= 8 | export const ntom = note => note.length === 3 9 | ? Notes[note[0].toUpperCase() + note[1].toLowerCase() + note[2]] 10 | : Notes[note[0].toUpperCase() + note[1]] 11 | export const ntof = note => mtof(ntom(note)) 12 | 13 | // Frequency conversions ======================================================= 14 | export const ftom = note => 12 * Math.log2(note / 440) + 69 15 | export const fton = note => mton(ftom(note)) 16 | 17 | // 18 | export default { 19 | // MIDI note names and numbers 20 | Notes, 21 | // MIDI note conversions 22 | mton, 23 | mtof, 24 | // Note name conversions 25 | ntom, 26 | ntof, 27 | // Frequency conversions 28 | ftom, 29 | fton 30 | } 31 | -------------------------------------------------------------------------------- /src/music/notes.json: -------------------------------------------------------------------------------- 1 | { 2 | "A0": 21, 3 | "A#0": 22, 4 | "Bb0": 22, 5 | "B0": 23, 6 | "C1": 24, 7 | "C#1": 25, 8 | "Db1": 25, 9 | "D1": 26, 10 | "D#1": 27, 11 | "Eb1": 27, 12 | "E1": 28, 13 | "F1": 29, 14 | "F#1": 30, 15 | "Gb1": 30, 16 | "G1": 31, 17 | "G#1": 32, 18 | "Ab1": 32, 19 | "A1": 33, 20 | "A#1": 34, 21 | "Bb1": 34, 22 | "B1": 35, 23 | "C2": 36, 24 | "C#2": 37, 25 | "Db2": 37, 26 | "D2": 38, 27 | "D#2": 39, 28 | "Eb2": 39, 29 | "E2": 40, 30 | "F2": 41, 31 | "F#2": 42, 32 | "Gb2": 42, 33 | "G2": 43, 34 | "G#2": 44, 35 | "Ab2": 44, 36 | "A2": 45, 37 | "A#2": 46, 38 | "Bb2": 46, 39 | "B2": 47, 40 | "C3": 48, 41 | "C#3": 49, 42 | "Cb3": 49, 43 | "D3": 50, 44 | "D#3": 51, 45 | "Eb3": 51, 46 | "E3": 52, 47 | "F3": 53, 48 | "F#3": 54, 49 | "Gb3": 54, 50 | "G3": 55, 51 | "G#3": 56, 52 | "Ab3": 56, 53 | "A3": 57, 54 | "A#3": 58, 55 | "Bb3": 58, 56 | "B3": 59, 57 | "C4": 60, 58 | "C#4": 61, 59 | "Db4": 61, 60 | "D4": 62, 61 | "D#4": 63, 62 | "Eb4": 63, 63 | "E4": 64, 64 | "F4": 65, 65 | "F#4": 66, 66 | "Gb4": 66, 67 | "G4": 67, 68 | "G#4": 68, 69 | "Ab4": 68, 70 | "A4": 69, 71 | "A#4": 70, 72 | "Bb4": 70, 73 | "B4": 71, 74 | "C5": 72, 75 | "C#5": 73, 76 | "Db5": 73, 77 | "D5": 74, 78 | "D#5": 75, 79 | "Eb5": 75, 80 | "E5": 76, 81 | "F5": 77, 82 | "F#5": 78, 83 | "Gb5": 78, 84 | "G5": 79, 85 | "G#5": 80, 86 | "Ab5": 80, 87 | "A5": 81, 88 | "A#5": 82, 89 | "Bb5": 82, 90 | "B5": 83, 91 | "C6": 84, 92 | "C#6": 85, 93 | "Db6": 85, 94 | "D6": 86, 95 | "D#6": 87, 96 | "Eb6": 87, 97 | "E6": 88, 98 | "F6": 89, 99 | "F#6": 90, 100 | "Gb6": 90, 101 | "G6": 91, 102 | "G#6": 92, 103 | "Ab6": 92, 104 | "A6": 93, 105 | "A#6": 94, 106 | "Bb6": 94, 107 | "B6": 95, 108 | "C7": 96, 109 | "C#7": 97, 110 | "Db7": 97, 111 | "D7": 98, 112 | "D#7": 99, 113 | "Eb7": 99, 114 | "E7": 100, 115 | "F7": 101, 116 | "F#7": 102, 117 | "Gb7": 102, 118 | "G7": 103, 119 | "G#7": 104, 120 | "Ab7": 104, 121 | "A7": 105, 122 | "A#7": 106, 123 | "Bb7": 106, 124 | "B7": 107, 125 | "C8": 108 126 | } -------------------------------------------------------------------------------- /src/music/time.js: -------------------------------------------------------------------------------- 1 | // Tempos ====================================================================== 2 | export const pretissimo = 200 3 | export const presto = 168 4 | export const vivace = 140 5 | export const allegro = 120 6 | export const allegroModera = 112 7 | export const moderato = 108 8 | export const andante = 78 9 | export const adagietto = 70 10 | export const adagio = 66 11 | export const larghetto = 60 12 | export const largo = 50 13 | export const lento = 40 14 | export const larghissimo = 20 15 | 16 | // Time ratios ================================================================= 17 | export const Whole = 0.25 18 | export const HalfDotted = 1 / 3 19 | export const Half = 0.5 20 | export const HalfTripplet = 0.75 21 | export const QuarterDotted = 1 / 3 * 2 22 | export const Quarter = 1 23 | export const QuarterTripplet = 1.5 24 | export const EighthDotted = 1 + 1 / 3 25 | export const Eighth = 2 26 | export const EighthTripplet = 3 27 | export const SixteenthDotted = 2 + 1 / 3 * 2 28 | export const Sixteenth = 4 29 | export const SixteenthTripplet = 6 30 | export const ThirtysecondDotted = 5 + 1 / 3 31 | export const Thirtysecond = 8 32 | export const ThirtysecondTripplet = 12 33 | 34 | // Conversions ================================================================= 35 | export const ms = (bpm, time) => 1 / (bpm / 60 * time * 0.001) 36 | export const sec = (bpm, time) => ms(bpm, time) / 1000 37 | 38 | // Time Arithmetic ============================================================= 39 | export const from = value => ({ type: 'Time', value }) 40 | export const offset = (time, ...offsets) => { 41 | if (typeof time === 'object' && time.type === 'Time') { 42 | return offsets.reduce(({ type, value, __debug_value }, n) => { 43 | return __debug_value 44 | ? { type, value: value + n, __debug_value: __debug_value + n } 45 | : { type, value: value + n } 46 | }, time) 47 | } 48 | 49 | if (typeof time === 'number') { 50 | return offsets.reduce((t, n) => t + n, time) 51 | } 52 | 53 | return { 54 | type: 'Time', 55 | value: 0 56 | } 57 | } 58 | 59 | // 60 | export default { 61 | // Tempos 62 | pretissimo, 63 | presto, 64 | vivace, 65 | allegro, 66 | allegroModera, 67 | moderato, 68 | andante, 69 | adagietto, 70 | adagio, 71 | larghetto, 72 | largo, 73 | lento, 74 | larghissimo, 75 | // Time ratios 76 | Whole, 77 | HalfDotted, 78 | Half, 79 | HalfTripplet, 80 | QuarterDotted, 81 | Quarter, 82 | QuarterTripplet, 83 | EighthDotted, 84 | Eighth, 85 | EighthTripplet, 86 | SixteenthDotted, 87 | Sixteenth, 88 | SixteenthTripplet, 89 | ThirtysecondDotted, 90 | Thirtysecond, 91 | ThirtysecondTripplet, 92 | // Conversions 93 | ms, 94 | sec, 95 | // Time Arithmetic 96 | from, 97 | offset 98 | } 99 | -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | export { default as WebSockets } from './web-socket' 2 | -------------------------------------------------------------------------------- /src/plugins/web-socket.js: -------------------------------------------------------------------------------- 1 | /* global WebSocket */ 2 | export const event = (handler, eventName) => ({ __eventType: 'ws', eventName, handler }) 3 | 4 | export const onopen = handler => event(handler, 'open') 5 | export const onerror = handler => event(handler, 'error') 6 | export const onmessage = handler => event(handler, 'message') 7 | export const onclose = handler => event(handler, 'close') 8 | 9 | export const effect = handler => ({ __effectType: 'ws', run: handler }) 10 | 11 | export default ({ url, protocols = [] }) => { 12 | return { 13 | event, 14 | onopen, 15 | onerror, 16 | onmessage, 17 | onclose, 18 | send (data) { 19 | return effect(() => this.$ws.send(data)) 20 | }, 21 | close () { 22 | return effect(() => this.$ws.close()) 23 | }, 24 | // Plugin data =============================================================== 25 | // The runtime needs to know what type of plugin to install 26 | __pluginType: 'event', 27 | // In the future, __pluginName will be used to stop duplicate plugins being 28 | // registered. 29 | __pluginName: 'WebSocket', 30 | // The event type should match the __eventType of any event objects you want 31 | // this plugin to handle. 32 | __eventType: 'ws', 33 | // Install is called after a program has been started. It is always passed an 34 | // object with $context, $root, and $dispatch but a plugin may choose to ignore 35 | // any or all of these fields. 36 | __install ({ $dispatch }) { 37 | this.$ws = new WebSocket(url, protocols) 38 | this.$dispatch = $dispatch 39 | 40 | Object.keys(this.$events).forEach(event => { 41 | this.$ws.addEventListener(event, e => { 42 | this.$events[event].forEach(({ handler }) => { 43 | handler(e) 44 | }) 45 | }) 46 | }) 47 | }, 48 | // Update is called every time the model is updated, and it receives a filtered 49 | // list of all the new event listeners. The list is filtered based on the 50 | // __eventType defined above. 51 | __update (newEvents) { 52 | newEvents = newEvents.map(event => ({ 53 | ...event, 54 | handler: e => this.$dispatch(event.handler(e)) 55 | })) 56 | 57 | for (const event in this.$events) { 58 | this.$events[event] = newEvents.filter(({ eventName }) => 59 | eventName === event 60 | ) 61 | } 62 | }, 63 | // 64 | $dispatch: null, 65 | $ws: null, 66 | $events: { 67 | open: [], 68 | error: [], 69 | message: [], 70 | close: [] 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/program/index.js: -------------------------------------------------------------------------------- 1 | export { default as instrument } from './instrument' 2 | export { default as worker } from './worker' -------------------------------------------------------------------------------- /src/program/instrument.js: -------------------------------------------------------------------------------- 1 | import VirtualAudioGraph from '../runtime/virtual-audio' 2 | import VirtualDOM from '../runtime/virtual-dom' 3 | 4 | import * as Debugger from '../runtime/debugger' 5 | import * as Time from '../music/time' 6 | import * as Utils from '../utils' 7 | 8 | export default function instrument(init, update, audio, view, listen) { 9 | // Debug mode prints a load of information to the console at every step of 10 | // the programs lifecycle. Generally not recommended as this is going to 11 | // really hurt performance. Useful to see which part of your program is a 12 | // bottleneck though. 13 | let DEBUG_MODE = false 14 | 15 | // Plugins can be registered to include new audio nodes, HTML components or 16 | // different event listeners, so let's keep track of all the plugins a dev 17 | // has added. 18 | let $plugins = { 19 | audio: [], 20 | html: [], 21 | event: [] 22 | } 23 | 24 | let $context // An instance of an AudioContext 25 | let $root // The root DOM node to attach our view to 26 | 27 | let $model // To complete state of our program 28 | let $debugger 29 | 30 | let $audio // The VirtualAudioGraph 31 | let $view // The VirtualDOM 32 | 33 | // $dispatch is how side side effects, events, and calls to send() can update 34 | // the model. 35 | const $dispatch = action => Utils.defer(() => { 36 | if (!action) return 37 | if ($context.state == 'suspended') $context.resume() 38 | action.now = Time.from($context.currentTime) 39 | 40 | if (DEBUG_MODE && action.action?.startsWith('__')) { 41 | const [debuggerModel, effect, shouldUpdate] = Debugger.update(action, $debugger) 42 | $debugger = debuggerModel 43 | 44 | if ($debugger.running) { 45 | const [appAction, model] = $debugger.history[$debugger.pointer] 46 | const calculateRelativeTiming = v => { 47 | if (typeof v === 'object' && v.type === 'Time') { 48 | return { ...v, __debug_value: $context.currentTime + (v.value - appAction.now.value) } 49 | } 50 | 51 | if (Array.isArray(v)) { 52 | return v 53 | } 54 | 55 | if (typeof v === 'object') { 56 | return Utils.mapObject(v, calculateRelativeTiming) 57 | } 58 | 59 | return v 60 | } 61 | const appModel = Utils.mapObject(model, calculateRelativeTiming) 62 | 63 | $update([appModel]) 64 | 65 | } else if (shouldUpdate) { 66 | $update([$model]) 67 | } 68 | 69 | effect && effect($dispatch, $debugger) 70 | } else if (!DEBUG_MODE || !$debugger.running) { 71 | const [model, effect = undefined] = update(action, $model) || [] 72 | 73 | if (DEBUG_MODE) { 74 | const result = Debugger.update( 75 | Debugger.PushAction(action, model), 76 | $debugger 77 | ) 78 | 79 | $debugger = result[0] 80 | } 81 | 82 | $update([model, effect]) 83 | } 84 | }) 85 | 86 | // The internal update function. This takes the result of the users __update 87 | // function and takes care of turning that new model into a new audio graph, 88 | // dom, and event listeners. This is also where Effects get called. 89 | const $update = ([model, effect = undefined]) => { 90 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('Total update time') 91 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.group('$update') 92 | 93 | // If the model supplied is invalid, show a warning in the console to let 94 | // the developer know. We don't do a simple falsey check (!model) because 95 | // its reasnoable for a simple model to be an empty string or 0. 96 | if (typeof model === 'undefined' || model === null) { 97 | console.warn('Your update function returned undefined or null, ' + 98 | 'the model will remain unchainged. ' + 99 | 'Did you forget to handle all of your Actions?' 100 | ) 101 | 102 | } else { 103 | $model = model 104 | 105 | // Audio ----------------------------------------------------------------- 106 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('$audio') 107 | const graph = audio($model) 108 | 109 | $audio.update(graph) 110 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('$audio') 111 | // DOM ------------------------------------------------------------------- 112 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('$view') 113 | const dom = view($model) 114 | 115 | $view.update( 116 | DEBUG_MODE 117 | ? (dom.children.push(Debugger.view($debugger)), dom) 118 | : dom 119 | ) 120 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('$view') 121 | // Events ---------------------------------------------------------------- 122 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('$events') 123 | const events = DEBUG_MODE 124 | ? [...listen($model), ...Debugger.listen($debugger)] 125 | : listen($model) 126 | 127 | // Each plugin creates events with its own __eventType property. So for each 128 | // plugin, filter the events list and only pass in the ones that plugin was 129 | // designed to handle. 130 | $plugins.event.forEach(plugin => { 131 | plugin.__update( 132 | events.filter(event => 133 | event.__eventType === plugin.__eventType 134 | ) 135 | ) 136 | }) 137 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('$events') 138 | } 139 | 140 | // Effects are functions that can have side effects, or do stateful things. 141 | // to return an action back to the runtime they receive the $dispatch function. 142 | if (effect) { 143 | if (typeof effect === 'object') { 144 | effect.run($dispatch, $model) 145 | } else { 146 | effect($dispatch, $model) 147 | } 148 | } 149 | 150 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.groupEnd('$update') 151 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('Total update time') 152 | } 153 | 154 | const __registerPlugin = plugin => { 155 | switch (plugin.__pluginType) { 156 | case 'audio': 157 | break 158 | case 'html': 159 | break 160 | case 'event': 161 | $plugins.event.push(plugin) 162 | break 163 | } 164 | } 165 | 166 | return { 167 | // Before a Program has been started, plugins can be registered to expand 168 | // its functionality. They get installed when the start method is called 169 | // so new plugins can't be registered after that. 170 | use(plugin) { 171 | console.log(`Registering ${plugin.__pluginName} plugin.`) 172 | __registerPlugin(plugin) 173 | }, 174 | 175 | // Creating a new Program isn't enough, it must be started before anything can 176 | // happen. Calling start will setup the virtual audio graph and virtual DOM with 177 | // their context and root node respectively. It'll also install any plugins that 178 | // were registered prior to calling start, before finally creating the first model 179 | // and performing the first render. 180 | start({ context, root, flags }) { 181 | DEBUG_MODE = flags && flags.debug || DEBUG_MODE 182 | 183 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log('Starting Program...') 184 | $context = context 185 | $root = root 186 | 187 | $audio = new VirtualAudioGraph($context) 188 | $view = new VirtualDOM($root) 189 | 190 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log('Installing plugins...') 191 | for (const pluginType in $plugins) { 192 | $plugins[pluginType].forEach(plugin => { 193 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log(`Installing ${plugin.__pluginName} plugin.`) 194 | plugin.__install({ $context, $root, $dispatch }) 195 | }) 196 | } 197 | 198 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log('Running initial update...') 199 | const initialModel = init(flags, Time.from($context.currentTime), $root) 200 | 201 | $debugger = Debugger.init($context.currentTime, initialModel) 202 | $update([initialModel]) 203 | }, 204 | 205 | // Use this to send an Action to the runtime from some external javascript. 206 | send(action) { 207 | $dispatch(action) 208 | }, 209 | 210 | // This should go at least some of the way towards tearing down a currently 211 | // running Flow application. In normal circumstances you shouldn't ever need 212 | // to call this, but it is necessary for the interactive playground to work 213 | // correctly. 214 | destroy() { 215 | $audio.update([]) 216 | $view.update('') 217 | $plugins.event.forEach(plugin => plugin.__destroy 218 | ? plugin.__destroy() 219 | : plugin.__update([]) 220 | ) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/program/worker.js: -------------------------------------------------------------------------------- 1 | import VirtualAudioGraph from '../runtime/virtual-audio' 2 | 3 | import { defer } from '../utils' 4 | 5 | export default function instrument (init, update, audio, listen) { 6 | // Debug mode prints a load of information to the console at every step of 7 | // the programs lifecycle. Generally not recommended as this is going to 8 | // really hurt performance. Useful to see which part of your program is a 9 | // bottleneck though. 10 | let DEBUG_MODE = false 11 | 12 | // Plugins can be registered to include new audio nodes, HTML components or 13 | // different event listeners, so let's keep track of all the plugins a dev 14 | // has added. 15 | let $plugins = { 16 | audio: [], 17 | event: [] 18 | } 19 | 20 | let $context // An instance of an AudioContext 21 | let $root // The root DOM node to attach our view to 22 | 23 | let $model // To complete state of our program 24 | 25 | let $audio // The VirtualAudioGraph 26 | 27 | // $dispatch is how side side effects, events, and calls to send() can update 28 | // the model. 29 | const $dispatch = action => defer(() => { 30 | const result = update(action, $model) 31 | 32 | // When we need to invoke additional side effects, calls to __update can 33 | // reutrn an array like [ model, sideEffect ]. It's tedious to wrap every 34 | // new model in an array if we don't need effects, however, so we allow 35 | // __update to return anything. Anything that isn't an array is treated 36 | // like a new model and wrapped here. 37 | Array.isArray(result) ? $update(result) : $update([ result ]) 38 | }) 39 | 40 | // The internal update function. This takes the result of the users __update 41 | // function and takes care of turning that new model into a new audio graph, 42 | // dom, and event listeners. This is also where Effects get called. 43 | const $update = ([ model, effect = undefined ]) => { 44 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('Total update time') 45 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.group('$update') 46 | 47 | // If the model supplied is invalid, show a warning in the console to let 48 | // the developer know. We don't do a simple falsey check (!model) because 49 | // its reasnoable for a simple model to be an empty string or 0. 50 | if (typeof model === 'undefined' || model === null) { 51 | console.warn('Your update function returned undefined or null, ' + 52 | 'the model will remain unchainged. ' + 53 | 'Did you forget to handle all of your Actions?' 54 | ) 55 | 56 | // A fairly naive check to see if the model has changed between updates. There 57 | // is probably a performance cost to calling JSON.stringify like this, but it's 58 | // potentially quicker than generating new virtual audio graphs, dom trees, and 59 | // event listeners just to diff something that hasn't changed. 60 | } else if (JSON.stringify($model) !== JSON.stringify(model)) { 61 | $model = model 62 | 63 | // Audio ----------------------------------------------------------------- 64 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('$audio') 65 | const graph = audio($model) 66 | 67 | $audio.update(graph) 68 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('$audio') 69 | // Events ---------------------------------------------------------------- 70 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.time('$events') 71 | const events = listen($model) 72 | 73 | // Each plugin creates events with its own __eventType property. So for each 74 | // plugin, filter the events list and only pass in the ones that plugin was 75 | // designed to handle. 76 | $plugins.event.forEach(plugin => { 77 | plugin.__update( 78 | events.filter(event => 79 | event.__eventType === plugin.__eventType 80 | ) 81 | ) 82 | }) 83 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('$events') 84 | } 85 | 86 | // Effects are functions that can have side effects, or do stateful things. 87 | // to return an action back to the runtime they receive the $dispatch function. 88 | if (effect) { 89 | if (typeof effect === 'object') { 90 | effect.run($dispatch, $model) 91 | } else { 92 | effect($dispatch, $model) 93 | } 94 | } 95 | 96 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.groupEnd('$update') 97 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.timeEnd('Total update time') 98 | } 99 | 100 | const __registerPlugin = plugin => { 101 | switch (plugin.__pluginType) { 102 | case 'audio': 103 | break 104 | case 'event': 105 | $plugins.event.push(plugin) 106 | break 107 | } 108 | } 109 | 110 | return { 111 | // Before a Program has been started, plugins can be registered to expand 112 | // its functionality. They get installed when the start method is called 113 | // so new plugins can't be registered after that. 114 | use (plugin) { 115 | console.log(`Registering ${plugin.__pluginName} plugin.`) 116 | __registerPlugin(plugin) 117 | }, 118 | 119 | // Creating a new Program isn't enough, it must be started before anything can 120 | // happen. Calling start will setup the virtual audio graph and virtual DOM with 121 | // their context and root node respectively. It'll also install any plugins that 122 | // were registered prior to calling start, before finally creating the first model 123 | // and performing the first render. 124 | start ({ context, root, flags }) { 125 | DEBUG_MODE = flags && flags.debug || DEBUG_MODE 126 | 127 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log('Starting Program...') 128 | $context = context 129 | 130 | $audio = new VirtualAudioGraph($context) 131 | 132 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log('Installing plugins...') 133 | for (const pluginType in $plugins) { 134 | $plugins[pluginType].forEach(plugin => { 135 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log(`Installing ${plugin.__pluginName} plugin.`) 136 | plugin.__install({ $context, $root, $dispatch }) 137 | }) 138 | } 139 | 140 | /* DEBUG STATEMENT */ if (DEBUG_MODE) console.log('Running initial update...') 141 | $update([ init(flags, $context.currentTime, $root) ]) 142 | }, 143 | 144 | // Use this to send an Action to the runtime from some external javascript. 145 | send (action) { 146 | $dispatch(action) 147 | }, 148 | 149 | // This should go at least some of the way towards tearing down a currently 150 | // running Flow application. In normal circumstances you shouldn't ever need 151 | // to call this, but it is necessary for the interactive playground to work 152 | // correctly. 153 | destroy () { 154 | $audio.update([]) 155 | $plugins.event.forEach(plugin => plugin.__destroy 156 | ? plugin.__destroy() 157 | : plugin.__update([]) 158 | ) 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/runtime/debugger.js: -------------------------------------------------------------------------------- 1 | import Action from '../action' 2 | 3 | import * as Html from '../dom/element' 4 | import * as Attr from '../dom/attribute' 5 | import * as Event from '../dom/event' 6 | import * as Audio from '../audio/event' 7 | import * as Time from '../music/time' 8 | 9 | export const init = (now, appModel) => ({ 10 | running: false, 11 | playing: false, 12 | history: [[{ action: '__init', now }, appModel]], 13 | pointer: 0, 14 | now: Time.from(now) 15 | }) 16 | 17 | export const ToggleRunning = () => Action('__Toggle-Running') 18 | export const TogglePlaying = () => Action('__Toggle-Playing') 19 | export const MovePointer = i => Action('__Move-Pointer', i) 20 | export const PushAction = (action, appModel) => Action('__Push-Action', { action, appModel }) 21 | export const ExportHistory = () => Action('__Export-History') 22 | export const ImportHistory = () => Action('__Import-History') 23 | export const LoadJsonHistory = history => Action('__Load-Json', { history }) 24 | 25 | 26 | const exportHistory = (_, model) => { 27 | const history = JSON.stringify(model.history, null, 2) 28 | const el = document.createElement('a') 29 | el.setAttribute('href', `data:application/json;charset=utf-8,${encodeURIComponent(history)}`) 30 | el.setAttribute('download', 'history.json') 31 | 32 | el.style.display = 'none' 33 | document.body.appendChild(el) 34 | el.click() 35 | document.body.removeChild(el) 36 | } 37 | 38 | const importHistory = ($dispatch, _) => { 39 | const reader = new FileReader() 40 | reader.onload = () => { 41 | try { 42 | const history = JSON.parse(reader.result) 43 | $dispatch(LoadJsonHistory(history)) 44 | } catch (e) { 45 | console.error(e) 46 | } 47 | } 48 | 49 | const el = document.createElement('input') 50 | el.setAttribute('type', 'file') 51 | el.setAttribute('accept', 'application/json') 52 | 53 | document.body.appendChild(el) 54 | el.addEventListener('change', ({ target }) => { 55 | if (target.files) reader.readAsText(target.files[0]) 56 | document.body.removeChild(el) 57 | }) 58 | 59 | el.click() 60 | } 61 | 62 | export const update = ({ action, now, payload }, model) => { 63 | switch (action) { 64 | case '__Toggle-Running': 65 | return [{ ...model, now, running: !model.running }, undefined, false] 66 | 67 | case '__Toggle-Playing': { 68 | return [{ ...model, now, running: model.running || !model.playing, playing: !model.playing }, undefined, false] 69 | } 70 | 71 | case '__Move-Pointer': { 72 | return [{ ...model, now, pointer: payload }, undefined, true] 73 | } 74 | 75 | case '__Push-Action': { 76 | const { action, appModel } = payload 77 | const history = model.pointer < model.history.length - 1 78 | ? [...model.history.slice(0, model.pointer + 1), [action, appModel]] 79 | : [...model.history, [action, appModel]] 80 | 81 | return [{ ...model, history, pointer: history.length - 1 }, undefined, false] 82 | } 83 | 84 | case '__Export-History': 85 | return [model, exportHistory, false] 86 | 87 | case '__Import-History': 88 | return [model, importHistory, false] 89 | 90 | case '__Load-Json': { 91 | const { history } = payload 92 | 93 | return [{ ...model, history, pointer: 0 }, undefined, false] 94 | } 95 | } 96 | } 97 | 98 | export const view = model => { 99 | const css = ` 100 | background: #EEEEEE; 101 | font-family: monospace; 102 | position: fixed; 103 | bottom: 25px; 104 | right: 25px; 105 | padding: 10px; 106 | -webkit-box-shadow: 10px 10px 10px 0px rgba(0,0,0,0.1); 107 | -moz-box-shadow: 10px 10px 10px 0px rgba(0,0,0,0.1); 108 | box-shadow: 10px 10px 10px 0px rgba(0,0,0,0.1); 109 | ` 110 | 111 | return Html.div([Attr.style(css), Attr.id('__Debugger')], [ 112 | Html.div([Attr.style(`margin-bottom: 5px`)], [ 113 | Html.span([], [ 114 | Html.text('Toggle debugger: ') 115 | ]), 116 | Html.button([Attr.id('__Toggle')], [ 117 | Html.text(`[${model.running ? 'x' : ' '}]`) 118 | ]), 119 | ]), 120 | 121 | Html.div([Attr.style(`margin-bottom: 5px`)], [ 122 | Html.span([], [ 123 | Html.text('Toggle playing: ') 124 | ]), 125 | Html.button([Attr.id('__Toggle-Playing')], [ 126 | Html.text(`[${model.playing ? 'x' : ' '}]`) 127 | ]), 128 | ]), 129 | 130 | Html.div([ 131 | Attr.style(` 132 | margin: 5px 0; 133 | `) 134 | ], [ 135 | Html.span([], [ 136 | Html.text(`${model.pointer + 1}/${model.history.length}`) 137 | ]), 138 | Html.input([ 139 | Attr.id('__Move'), 140 | Attr.type('range'), 141 | Attr.min(1), 142 | Attr.max(model.history.length), 143 | Attr.value(model.pointer + 1), 144 | Attr.style(` 145 | vertical-align: middle; 146 | margin: 0 10px; 147 | `) 148 | ]), 149 | Html.button([Attr.id('__Move-Back')], [ 150 | Html.text('-') 151 | ]), 152 | Html.span([], [ 153 | Html.text(' | ') 154 | ]), 155 | Html.button([Attr.id('__Move-Forward')], [ 156 | Html.text('+') 157 | ]) 158 | ]), 159 | 160 | Html.details([ 161 | Attr.style(` 162 | margin: 5px 0; 163 | `) 164 | ], [ 165 | Html.summary([], [ 166 | Html.text(`Action: ${model.history[model.pointer][0].action}`) 167 | ]), 168 | Html.p([ 169 | Attr.style(` 170 | max-height: 200px; 171 | width: 300px; 172 | overflow-y: scroll; 173 | overflow-x: auto; 174 | white-space: pre-wrap; 175 | padding-top: 5px; 176 | `) 177 | ], [ 178 | Html.text( 179 | JSON.stringify(model.history[model.pointer][1], null, 2) 180 | ) 181 | ]) 182 | ]), 183 | 184 | Html.div([ 185 | Attr.style(` 186 | display: flex; 187 | justify-content: space-between; 188 | margin-top: 5px; 189 | `) 190 | ], [ 191 | Html.button([Attr.id('__Export')], [ 192 | Html.text('Export') 193 | ]), 194 | Html.button([Attr.id('__Import')], [ 195 | Html.text('Import') 196 | ]) 197 | ]) 198 | ]) 199 | } 200 | 201 | function throttle(delay, f) { 202 | let t = Date.now() 203 | return function (e) { 204 | if ((t + delay - Date.now()) < 0) { 205 | t = Date.now() 206 | return f(e) 207 | } 208 | } 209 | } 210 | 211 | export const listen = model => { 212 | return [ 213 | Event.click('#__Toggle', () => ToggleRunning()), 214 | Event.click('#__Toggle-Playing', () => TogglePlaying()), 215 | Event.input('#__Move', throttle(100, e => MovePointer(Number(e.target.value) - 1))), 216 | Event.click('#__Move-Back', () => MovePointer( 217 | model.pointer - 1 < 0 218 | ? 0 219 | : model.pointer - 1 220 | )), 221 | Event.click('#__Move-Forward', () => MovePointer( 222 | model.pointer + 1 >= model.history.length 223 | ? model.history.length - 1 224 | : model.pointer + 1 225 | )), 226 | Event.click('#__Export', () => ExportHistory()), 227 | Event.click('#__Import', () => ImportHistory()), 228 | (() => { 229 | if (model.playing && model.history[model.pointer + 1]) { 230 | const now = model.history[model.pointer][0].now 231 | const then = model.history[model.pointer + 1][0].now 232 | return Audio.every('playback', then.value - now.value, () => MovePointer(model.pointer + 1)) 233 | } 234 | })() 235 | ].filter(listener => listener !== undefined) 236 | } 237 | 238 | -------------------------------------------------------------------------------- /src/runtime/virtual-audio.js: -------------------------------------------------------------------------------- 1 | const defer = f => setTimeout(f, 0) 2 | const AudioContext = window.AudioContext || window.webkitAudioContext 3 | 4 | export default class VirtualAudioGraph { 5 | // Static Methods ============================================================ 6 | // 7 | static prepare(graph = []) { 8 | // The first step in preparing the graph is to key each virtual node. 9 | // This is how we perform a diff between graphs and calculate what has 10 | // changed each update. 11 | const key = (graph, base = '') => { 12 | graph.forEach((node, i) => { 13 | // RefNodes always have a key, and they also 14 | // cannot have connections or properties 15 | // so we can just return early and move on. 16 | if (node.type === 'RefNode') return 17 | // Assign the node a key if it didn't already have one. 18 | // This is how we track changes to the graph in a slightly 19 | // more organised way 20 | if (!node.key) node.key = `${base}_${i}` 21 | 22 | // Recursively assign keys to this nodes connections. 23 | if (node.connections && node.connections.length > 0) { 24 | key(node.connections, node.key) 25 | } 26 | }) 27 | 28 | return graph 29 | } 30 | 31 | // It is often most natural to represent the audio graph as a list 32 | // of trees, using RefNodes to "jump" between chains of node 33 | // connections. This isns't the easiest data structure to deal with 34 | // however, so the next step in preparation is the flatten the graph 35 | // into a single array. 36 | const flatten = (graph, nodes = {}, depth = 0) => { 37 | graph.forEach((node, i) => { 38 | // Don't push RefNodes to the flat graph. 39 | if (node.type !== 'RefNode') nodes[node.key] = node 40 | if (node.connections) flatten(node.connections, nodes, depth + 1) 41 | // If we're deeper than the root of the graph, replace 42 | // this node with a reference to itself by key. 43 | if (depth > 0) graph[i] = { type: 'RefNode', key: node.key } 44 | }) 45 | 46 | return nodes 47 | } 48 | 49 | return flatten(key(graph)) 50 | } 51 | 52 | // 53 | static diff(oldNodes, newNodes) { 54 | const patches = { created: [], updated: [], removed: [] } 55 | 56 | for (const newNode of Object.values(newNodes)) { 57 | const oldNode = oldNodes[newNode.key] 58 | 59 | // A node with newNode.key does not exist in the old graph, so this must 60 | // mean we've created a brand new node. 61 | if (!oldNode) { 62 | patches.created.push({ type: 'node', key: newNode.key, data: newNode }) 63 | 64 | newNode.connections.forEach(connection => { 65 | patches.created.push({ type: 'connection', key: newNode.key, data: connection.key.split('.') }) 66 | }) 67 | 68 | // A node with the same key exists in both graphs, but the type has changed 69 | // (eg osc -> gain) so we need to recreate the node. 70 | } else if (oldNode.type !== newNode.type) { 71 | patches.updated.push({ type: 'node', key: newNode.key, data: newNode }) 72 | 73 | newNode.connections.forEach(connection => { 74 | patches.created.push({ type: 'connection', key: newNode.key, data: connection.key.split('.') }) 75 | }) 76 | 77 | // A node with the same key exists in both graphs and the node hasn't 78 | // fundamentally changed, so now we check whether properties or connections 79 | // have changed. 80 | } else { 81 | // Checking properties... 82 | for (let j = 0; j < Math.max(oldNode.properties.length, newNode.properties.length); j++) { 83 | const oldProp = oldNode.properties[j] 84 | const newProp = newNode.properties[j] 85 | 86 | // 87 | if (!oldProp) { 88 | patches.created.push({ type: 'property', key: oldNode.key, data: newProp }) 89 | } else if (!newProp) { 90 | patches.removed.push({ type: 'property', key: oldNode.key, data: oldProp }) 91 | } else if (oldProp.label !== newProp.label) { 92 | patches.removed.push({ type: 'property', key: oldNode.key, data: oldProp }) 93 | patches.created.push({ type: 'property', key: oldNode.key, data: newProp }) 94 | } else if (oldProp.value !== newProp.value) { 95 | patches.updated.push({ type: 'property', key: oldNode.key, data: newProp }) 96 | } 97 | } 98 | 99 | // Checking connections... 100 | for (let j = 0; j < Math.max(oldNode.connections.length, newNode.connections.length); j++) { 101 | const oldConnection = oldNode.connections[j] 102 | const newConnection = newNode.connections[j] 103 | 104 | // 105 | if (!oldConnection) { 106 | patches.created.push({ type: 'connection', key: oldNode.key, data: newConnection.key.split('.') }) 107 | } else if (!newConnection) { 108 | patches.removed.push({ type: 'connection', key: oldNode.key, data: oldConnection.key.split('.') }) 109 | } else if (oldConnection.key !== newConnection.key) { 110 | patches.removed.push({ type: 'connection', key: oldNode.key, data: oldConnection.key.split('.') }) 111 | patches.created.push({ type: 'connection', key: oldNode.key, data: newConnection.key.split('.') }) 112 | } 113 | } 114 | } 115 | 116 | delete oldNodes[newNode.key] 117 | } 118 | 119 | for (const oldNode of Object.values(oldNodes)) { 120 | patches.removed.push({ type: 'node', key: oldNode.key, data: oldNode }) 121 | } 122 | 123 | return patches 124 | } 125 | 126 | // Constructor =============================================================== 127 | // 128 | constructor(context = new AudioContext(), opts = {}) { 129 | // Borrowing a convetion from virtual dom libraries, the $ sign //is used to 130 | // indicate "real" Web Audio bits, and the v- prefix is used to indicate 131 | // virtual elements. 132 | 133 | // $context is a reference to the `AudioContext` either passed in or created 134 | // on construction. 135 | this.$context = context 136 | // A reference to the real graph of audio nodes 137 | this.$nodes = { 138 | $: this.$context.createGain() 139 | } 140 | 141 | // Schedule the master gain to fade in as soon as the audio context is 142 | // resumed (if it was suspended) or immediately (if the context was created) 143 | // in response to some user action. 144 | this.$nodes.$.gain.linearRampToValueAtTime(1, this.$context.currentTime + 1) 145 | this.$nodes.$.connect(this.$context.destination) 146 | 147 | // We keep track of the prebious graph so we can perform a diff and work out 148 | // what has changed between updates. 149 | this.vPrev = {} 150 | 151 | // In most modern browsers an Audio Context starts in a suspended state and 152 | // requires some user interaction before it can be resumed. Still, we can 153 | // attempt to resume the context ourselves in the developer passes in the 154 | // `autostart` option. 155 | if (opts.autostart) this.resume() 156 | } 157 | 158 | // Public Methods ============================================================ 159 | // 160 | update(vGraph = []) { 161 | // The accompanying library of virtual node functions 162 | // encourages a nested tree-like approach to describing 163 | // audio graphs. This isn't the easiest structure to deal 164 | // with, however, so a preparation step serves to wrestle 165 | // the graph into a more suitable shape. 166 | const vCurr = VirtualAudioGraph.prepare(vGraph) 167 | 168 | // A diff tracks everything that has been removed, created, 169 | // and updated between updates. We perform this step so we 170 | // only touch the audio nodes that need to be changed in some 171 | // way. 172 | const diff = VirtualAudioGraph.diff(this.vPrev, vCurr) 173 | 174 | // Remove nodes and properties from the graph. 175 | diff.removed.forEach(patch => { 176 | switch (patch.type) { 177 | case 'node': 178 | this._destroyNode(patch.key) 179 | break 180 | case 'property': 181 | this._removeProperty(patch.key, patch.data) 182 | break 183 | case 'connection': 184 | this._disconnect(patch.key, patch.data) 185 | break 186 | } 187 | }) 188 | 189 | // Create new nodes and add new properties to 190 | // the graph. 191 | diff.created.forEach(patch => { 192 | switch (patch.type) { 193 | case 'node': 194 | this._createNode(patch.key, patch.data) 195 | break 196 | case 'property': 197 | this._setProperty(patch.key, patch.data) 198 | break 199 | case 'connection': 200 | defer(() => this._connect(patch.key, patch.data)) 201 | break 202 | } 203 | }) 204 | 205 | // Update existing nodes and properties in the 206 | // graph. 207 | diff.updated.forEach(patch => { 208 | switch (patch.type) { 209 | case 'node': 210 | this._destroyNode(patch.key) 211 | this._createNode(patch.key, patch.data) 212 | break 213 | case 'property': 214 | this._setProperty(patch.key, patch.data) 215 | break 216 | case 'connection': 217 | // Connections can't be updated 218 | break 219 | } 220 | }) 221 | 222 | // Store the current graph for next time. 223 | this.vPrev = vCurr 224 | } 225 | 226 | // A thin wrapper of the `AudioContext.suspend()` method. This 227 | // bassically exists so developers don't have to reach in and 228 | // touch the "real" audio context directly. 229 | suspend() { 230 | this.$nodes.$.gain.value = 0 231 | this.$context.suspend() 232 | } 233 | 234 | // A thin wrapper of the `AudioContext.resume()` method. This 235 | // bassically exists so developers don't have to reach in and 236 | // touch the "real" audio context directly. 237 | resume() { 238 | this.$context.resume() 239 | this.$nodes.$.gain.linearRampToValueAtTime(1, this.$context.currentTime + 0.1) 240 | } 241 | 242 | // Private Methods =========================================================== 243 | // 244 | _createNode(key, { type, properties }) { 245 | let $node = null 246 | 247 | // 248 | switch (type) { 249 | case 'AnalyserNode': 250 | $node = this.$context.createAnalyser() 251 | break 252 | case 'AudioBufferSourceNode': 253 | $node = this.$context.createBufferSource() 254 | break 255 | case 'AudioDestinationNode': 256 | $node = this.$nodes.$ 257 | break 258 | case 'BiquadFilterNode': 259 | $node = this.$context.createBiquadFilter() 260 | break 261 | case 'ChannelMergerNode': 262 | $node = this.$context.createChannelMerger() 263 | break 264 | case 'ChannelSplitterNode': 265 | $node = this.$context.createChannelSplitter() 266 | break 267 | case 'ConstantSourceNode': 268 | $node = this.$context.createConstantSource() 269 | break 270 | case 'ConvolverNode': 271 | $node = this.$context.createConvolver() 272 | break 273 | case 'DelayNode': 274 | const maxDelayTime = properties.find(({ label }) => label === 'maxDelayTime') 275 | $node = this.$context.createDelay((maxDelayTime && maxDelayTime.value) || 1) 276 | break 277 | case 'DynamicsCompressorNode': 278 | $node = this.$context.createDynamicsCompressor() 279 | break 280 | case 'GainNode': 281 | $node = this.$context.createGain() 282 | break 283 | case 'IIRFilterNode': 284 | const feedforward = properties.find(({ label }) => label === 'feedforward') 285 | const feedback = properties.find(({ label }) => label === 'feedback') 286 | $node = this.$context.createIIRFilter( 287 | (feedforward && feedforward.value) || [0], 288 | (feedback && feedback.value) || [1] 289 | ) 290 | break 291 | case 'MediaElementAudioSourceNode': 292 | const mediaElement = properties.find(({ label }) => label === 'mediaElement') 293 | $node = this.$context.createMediaElementSource( 294 | document.querySelector(mediaElement.value) 295 | ) 296 | break 297 | case 'MediaStreamAudioDestinationNode': 298 | $node = this.$context.createMediaStreamDestination() 299 | break 300 | // TODO: How should I handle creating / grabbing the media stream? 301 | // case 'MediaStreamAudioSourceNode': 302 | // $node = this.$context.createMediaStreamSource( 303 | 304 | // ) 305 | // break 306 | case 'OscillatorNode': 307 | $node = this.$context.createOscillator() 308 | break 309 | case 'PannerNode': 310 | $node = this.$context.createPanner() 311 | break 312 | case 'StereoPannerNode': 313 | $node = this.$context.createStereoPanner() 314 | break 315 | case 'WaveShaperNode': 316 | $node = this.$context.createWaveShaper() 317 | break 318 | // 319 | default: 320 | console.warn(`Invalide node type of: ${type}. Defaulting to GainNode to avoid crashing the AudioContext.`) 321 | $node = this.$context.createGain() 322 | } 323 | 324 | this.$nodes[key] = $node 325 | 326 | // 327 | properties.forEach(prop => this._setProperty(key, prop)) 328 | 329 | // Certain nodes like oscillators must be started before they will produce 330 | // noise. We make the assumption that these nodes should always start 331 | // immediately after they have been created, so if a `start` method exists we 332 | // call it. 333 | if ($node.start) $node.start() 334 | } 335 | 336 | // 337 | _destroyNode(key) { 338 | const $node = this.$nodes[key] 339 | 340 | // Certain nodes like oscillators can be stopped. It probably doesn't make 341 | // much of a difference calling this method, but we do just in case! 342 | if ($node.stop) $node.stop() 343 | 344 | // Calling disconnect with no arguments will disconnect this node from 345 | // everything. 346 | $node.disconnect() 347 | 348 | // Finally remove the node from the graph and let the GC do its job. 349 | delete this.$nodes[key] 350 | } 351 | 352 | // 353 | _setProperty(key, { type, label, value }) { 354 | const $node = this.$nodes[key] 355 | 356 | switch (type) { 357 | case 'NodeProperty': 358 | $node[label] = value 359 | break 360 | case 'AudioParam': 361 | $node[label].linearRampToValueAtTime(value, this.$context.currentTime + 0.05) 362 | break 363 | case 'ScheduledAudioParam': { 364 | const time = typeof value.time === 'object' && value.time.type === 'Time' 365 | ? value.time.__debug_value || value.time.value 366 | : value.time 367 | $node[label][value.method](value.target, time) 368 | break 369 | } 370 | } 371 | } 372 | 373 | // 374 | _removeProperty(key, { type, label, value }) { 375 | const $node = this.$nodes[key] 376 | 377 | switch (type) { 378 | case 'NodeProperty': 379 | break 380 | case 'AudioParam': 381 | $node[label].value = $node[label].linearRampToValueAtTime($node[label].default, this.$context.currentTime + 0.05) 382 | break 383 | case 'ScheduledAudioParam': 384 | // TODO: work out how to cancel scheduled updates 385 | break 386 | } 387 | } 388 | 389 | // 390 | _connect(a, [b, param = null]) { 391 | if (b) this.$nodes[a].connect(param ? this.$nodes[b][param] : this.$nodes[b]) 392 | } 393 | 394 | // 395 | _disconnect(a, [b, param = null]) { 396 | if (b) this.$nodes[a].disconnect(param ? this.$nodes[b][param] : this.$nodes[b]) 397 | } 398 | } -------------------------------------------------------------------------------- /src/runtime/virtual-dom.js: -------------------------------------------------------------------------------- 1 | import { defer } from '../utils' 2 | 3 | export default class VirtualDOM { 4 | // Static Methods ============================================================ 5 | // 6 | static isText(node) { 7 | return typeof node === 'string' || 8 | typeof node === 'number' || 9 | typeof node === 'boolean' 10 | } 11 | 12 | // 13 | static isVirtualNode(node) { 14 | return typeof node === 'object' && node.attrs && node.children 15 | } 16 | 17 | // 18 | static isComponent(node) { 19 | return typeof node === 'function' 20 | } 21 | 22 | // Constructor =============================================================== 23 | // 24 | constructor($root) { 25 | this.$root = $root 26 | this.vPrev = null 27 | 28 | // Remove any children on the root DOM node when a new VDOM is created, this 29 | // ensures nothing interferes with the diffing process. 30 | while (this.$root.firstChild) { 31 | this.$root.removeChild(this.$root.firstChild) 32 | } 33 | } 34 | 35 | // Public Methods ============================================================ 36 | // 37 | update(vCurr, vPrev, $root, index = 0) { 38 | // When update is first called it is just given the new virtual tree to diff, 39 | // but it is then called recursively on each child element with the appropriate 40 | // previous tree and root node. Because of this, we default to the original 41 | // previous tree and the global root DOM node. We don't use the default 42 | // assignment in the function arguments (like with index = 0) because I don't 43 | // think you can access `this` from function arguments. 44 | vPrev = vPrev || this.vPrev 45 | $root = $root || this.$root 46 | 47 | const $el = $root.childNodes[index] 48 | 49 | // There is no previous tree. 50 | if (!vPrev || !$el) { 51 | this._append($root, vCurr) 52 | 53 | // There is no new tree. This probably means we've 54 | // removed a node (or the entire tree) from the previous 55 | // tree so let's remove it from the dom. 56 | } else if (!vCurr) { 57 | $el && this._remove($el) 58 | 59 | // There is a type mismatch between the previous and current 60 | // trees. For example the old tree was an object representing 61 | // a dom node, and now it is a string representing a text node. 62 | } else if (typeof vPrev !== typeof vCurr) { 63 | this._replace($root, $el, vCurr) 64 | 65 | // The current nodes are the same type AND they are both text-like. 66 | // This means we can do a simple equality comparison and replace 67 | // them if necessary. 68 | } else if (VirtualDOM.isText(vPrev) && VirtualDOM.isText(vCurr)) { 69 | if (vPrev !== vCurr) { 70 | this._replace($root, $el, vCurr) 71 | } 72 | 73 | // The current nodes are the same type AND they are both virtual nodes. 74 | // Here we perform a more involved diff to determine what and how 75 | // to update. 76 | } else if (VirtualDOM.isVirtualNode(vPrev) && VirtualDOM.isVirtualNode(vCurr)) { 77 | // We can safely assume if the tag has changed that the overall 78 | // structure of the sub tree has changed too, and so we can 79 | // replace the node and its children without performing any further 80 | // diff. 81 | if (vPrev.tag !== vCurr.tag) { 82 | this._replace($root, $el, vCurr) 83 | 84 | // Otherwise... 85 | } else { 86 | // First we diff the attributes on the node. 87 | for (let i = 0; i < vPrev.attrs.length || i < vCurr.attrs.length; i++) { 88 | const prevAttr = vPrev.attrs[i] 89 | const currAttr = vCurr.attrs[i] 90 | 91 | // This is somewhat ineffecient. If the order of attributes 92 | // is different but both the previous and current node has 93 | // the same attributes, they will be removed and then re-added. 94 | if (!currAttr) { 95 | $el.removeAttribute(prevAttr.name) 96 | } else { 97 | $el[currAttr.name] = currAttr.value 98 | $el.setAttribute(currAttr.name, currAttr.value) 99 | } 100 | } 101 | } 102 | 103 | // Then iterate each of this node's children and recursively call 104 | // the render function. 105 | for (let i = 0; i < Math.max(vPrev.children.length, vCurr.children.length); i++) { 106 | this.update(vCurr.children[i], vPrev.children[i], $el, i) 107 | } 108 | 109 | // Components are functions that return some virtual dom node(s), so they 110 | // need to be handled differently. Currently we don't actually support 111 | // components, but this is the foundation to do so. 112 | } else if (VirtualDOM.isComponent(vPrev) && VirtualDOM.isComponent(vCurr)) { 113 | 114 | } 115 | 116 | // Update the previous tree so we can perform the right diff on subsequent 117 | // calls to update. 118 | this.vPrev = vCurr 119 | } 120 | 121 | // Private Methods =========================================================== 122 | // 123 | _create(node) { 124 | if (VirtualDOM.isText(node)) { 125 | return document.createTextNode(`${node}`) 126 | } 127 | 128 | // VirtualDOM.isVirtualNode performs the necessary checks to make sure the 129 | // object has the necessary fields (type, attrs, children) so we can safely 130 | // assume everything exists if it passes that check. 131 | if (VirtualDOM.isVirtualNode(node)) { 132 | // We assume the tag is valid, some checks 133 | // may be useful here... 134 | const $el = document.createElement(node.tag) 135 | 136 | for (const attr of node.attrs) { 137 | $el[attr.name] = attr.value 138 | $el.setAttribute(attr.name, attr.value) 139 | } 140 | 141 | // Recursively create all the children 142 | // of this node 143 | for (const elem of node.children) { 144 | $el.appendChild(this._create(elem)) 145 | } 146 | 147 | return $el 148 | } 149 | 150 | // In the future we might want to handle custom omponent functions, but 151 | // ight now we just return an empty node. 152 | if (VirtualDOM.isComponent(node)) { 153 | return document.createTextNode('') 154 | } 155 | 156 | // If some invalid object was passed in, just create a blank DOM node. 157 | return document.createTextNode('') 158 | } 159 | 160 | // 161 | _replace($root, $el, node) { 162 | $root.replaceChild(this._create(node), $el) 163 | } 164 | 165 | // 166 | _append($root, node) { 167 | $root.appendChild(this._create(node)) 168 | } 169 | 170 | // 171 | _remove($el) { 172 | defer(() => $el.remove()) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // Defer pushes a function to the back of the Browser's event queue. It essentially 2 | // tells the browser to "do f when you next have time." 3 | export const defer = f => setTimeout(f, 0) 4 | 5 | export const mapObject = (obj, f) => { 6 | return Object.fromEntries( 7 | Object.keys(obj).map(k => { 8 | return [k, f(obj[k])] 9 | }) 10 | ) 11 | } --------------------------------------------------------------------------------