├── .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 | 
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 | }
--------------------------------------------------------------------------------