├── styles ├── external-node.mcss ├── codemirror.css ├── center-tab.mcss ├── source-node.mcss ├── modulator-node.mcss ├── processor-node.mcss ├── main-params.mcss ├── raw-editor.mcss ├── global-controller-node.mcss ├── node-spawner.mcss ├── chunk-node.mcss ├── clock.mcss ├── audio-slot.mcss ├── memory-usage.mcss ├── loop-position.mcss ├── scale-chooser.mcss ├── sample-chooser.mcss ├── index.js ├── toggle-chooser.mcss ├── helper.mcss ├── audio-meter.mcss ├── value-slots.mcss ├── button.mcss ├── controller-node.mcss ├── setup-node.mcss ├── param-list.mcss ├── node-collection.mcss ├── transport.mcss ├── recording-node.mcss └── toggle-button.mcss ├── lib ├── package.json ├── periodic-waves │ ├── ah.imag │ ├── ah.real │ ├── bass.imag │ ├── bass.real │ ├── ee.imag │ ├── ee.real │ ├── ooh.imag │ ├── ooh.real │ ├── ow.imag │ ├── ow.real │ ├── wide.imag │ ├── wide.real │ ├── brass.imag │ ├── brass.real │ ├── organ.imag │ ├── organ.real │ ├── piano.imag │ ├── piano.real │ ├── bass-fuzz.imag │ ├── bass-fuzz.real │ ├── bass-sub.imag │ ├── bass-sub.real │ ├── disonant.imag │ ├── disonant.real │ ├── soft-saw.imag │ ├── soft-saw.real │ ├── strings.imag │ ├── strings.real │ ├── throaty.imag │ ├── throaty.real │ ├── trombone.imag │ ├── trombone.real │ ├── wurlitzer.imag │ ├── wurlitzer.real │ ├── guitar-fuzz.imag │ ├── guitar-fuzz.real │ ├── convert.js │ ├── raw.js │ └── index.js ├── h.js ├── clamp.js ├── property.js ├── dom-event.js ├── param-negate.js ├── no-drop.js ├── read.js ├── find-item-by-path.js ├── copy-file.js ├── cancel-event.js ├── destroy-all.js ├── bind-event.js ├── set-route.js ├── hold-active-transform.js ├── get-value.js ├── file-event.js ├── params │ ├── select.js │ ├── editable.js │ ├── text.js │ ├── sample-chooser.js │ ├── toggle-chooser.js │ └── toggle-button.js ├── widgets │ ├── collection.js │ ├── header.js │ ├── params.js │ ├── midi-output.js │ └── spawner.js ├── quantize-duration.js ├── zoom-event.js ├── active-indexes.js ├── random-color.js ├── is-triggerable.js ├── set-mapped-value.js ├── once-true.js ├── menu.js ├── array-stack.js ├── midi-to-param.js ├── get-param-value.js ├── render-node.js ├── stream-progress.js ├── quantize-to-square.js ├── key-collection.js ├── get-port-siblings.js ├── get-closest-point.js ├── resolve-node.js ├── attribute-hook.js ├── resolve-available.js ├── watch-struct.js ├── watch-buttons.js ├── double-bind.js ├── grid-slice-peaks.js ├── interpolate.js ├── update-param-references.js ├── value-at-time-getter.js ├── to-pcm.js ├── get-sound-offset.js ├── with-resolved.js ├── destroy-source-node.js ├── context-menu.js ├── map-watch-diff-stack.js ├── json-file.js ├── index-param.js ├── flash-array.js ├── mouse-position-event.js ├── param-from-number.js ├── on-trigger.js ├── sustained.js ├── callback-worker.js ├── cleaner.js ├── get-peaks.js ├── param-square.js ├── midi-clock-offset.js ├── detect-peaks.js ├── observ-keys.js ├── flag-param.js ├── global-controller.js ├── shape-slots.js ├── scale-interpolate.js ├── resolve-file-available.js ├── param-abs.js ├── midi-note.js ├── import-associated-files.js ├── mouse-drag-event.js ├── wave-hook.js ├── timeline-scheduler.js ├── wave-svg.new.js ├── test │ └── test-grab-grid.js ├── watch-nodes-changed.js ├── apply-scale.js ├── assign-available-port.js ├── processor.js ├── param-sum.js ├── observ-rms.js ├── param-clamp.js ├── query-param.js ├── import-sample.js ├── rename-widget.js └── extend-params.js ├── logo.png ├── .gitignore ├── nodes ├── clip │ └── index.js ├── hold │ ├── index.js │ └── object.js ├── link-param │ └── index.js ├── audio-buffer │ └── index.js ├── offset │ ├── index.js │ └── object.js ├── random │ └── index.js ├── trigger-value │ └── index.js ├── link-modulator │ ├── index.js │ └── object.js ├── multiply │ ├── index.js │ └── object.js ├── quantize │ ├── index.js │ └── object.js ├── chromatic-scale │ └── index.js ├── midi-cc │ ├── index.js │ └── object.js ├── project │ └── index.js ├── recording │ ├── ableton-export │ │ ├── Project8_1.cfg │ │ └── template.js │ └── index.js ├── external-chunk │ ├── index.js │ └── view.js ├── setup │ └── index.js ├── slot │ └── index.js ├── eq │ ├── index.js │ ├── params.js │ └── view.js ├── clap │ ├── index.js │ └── view.js ├── gain │ ├── index.js │ ├── view.js │ └── object.js ├── kick │ ├── index.js │ └── view.js ├── noise │ ├── index.js │ └── view.js ├── pan │ ├── index.js │ ├── object.js │ └── view.js ├── reverb │ ├── types.js │ └── index.js ├── snare │ └── index.js ├── cymbal │ ├── index.js │ └── view.js ├── delay │ └── index.js ├── dipper │ ├── index.js │ └── view.js ├── filter │ ├── index.js │ └── object.js ├── envelope │ └── index.js ├── overdrive │ ├── index.js │ └── view.js ├── bitcrusher │ ├── index.js │ ├── view.js │ └── object.js ├── compressor │ ├── index.js │ ├── view.js │ └── object.js ├── pitchshift │ ├── index.js │ └── view.js ├── ring-modulator │ ├── index.js │ ├── object.js │ └── view.js ├── ableton-link │ ├── index.js │ └── view.js ├── loop-grid-qwerty │ ├── index.js │ └── README.md ├── global-launch-control │ └── index.js ├── granular │ └── index.js ├── global-launch-control-xl │ └── index.js ├── spatial-pan │ ├── index.js │ └── object.js ├── loop-grid-push │ └── index.js ├── mixer-launch-control-xl │ ├── index.js │ └── set-lights.js ├── ping-pong-delay │ └── index.js ├── freeverb │ ├── index.js │ ├── object.js │ └── view.js ├── loop-grid-launchpad-mk2 │ └── index.js ├── loop-grid-launchpad │ ├── index.js │ └── state-lights.js ├── midi-sync-output │ ├── index.js │ └── view.js ├── oscillator-pulse │ └── index.js ├── convolution-reverb │ └── index.js ├── modulator-chunk │ ├── index.js │ └── object.js ├── oscillator │ ├── index.js │ └── shape-choices.js ├── sample │ └── index.js ├── midi-out-chunk │ └── index.js ├── mono-chromatic-chunk │ └── index.js ├── slicer-chunk │ └── index.js ├── midi-out │ ├── index.js │ └── view.js ├── lfo │ └── index.js ├── meddler-chunk │ ├── index.js │ └── external.js ├── index.js ├── external-audio-input │ ├── index.js │ └── view.js ├── triggers-chunk │ ├── index.js │ └── external.js ├── synth-chunk │ └── index.js ├── loop-grid │ ├── compute-targets.js │ └── compute-flags.js └── chromatic-chunk │ └── index.js ├── demo-project ├── DWS - OST │ ├── bd01.opus │ ├── bd08.opus │ ├── cp01.opus │ ├── cr01.opus │ ├── hh01.opus │ ├── hh02.opus │ ├── oh01.opus │ ├── sd12.opus │ ├── synthit.opus │ ├── cymbal (1).opus │ ├── kick (11).opus │ ├── snare (19).opus │ ├── synthit2.opus │ ├── tom (7) 1.opus │ ├── hi hat (15).opus │ ├── hi hat (22).opus │ ├── hi hat (34).opus │ ├── DOLLARS and TODAY and EDUCATION-Jc_RKi4rmzs.m4a │ ├── cspan-output.json │ └── synth.json ├── DWS - Synthesia │ ├── Build.opus │ └── Pitched.opus ├── DWS - Beep Boop │ ├── 1490926241152.opus │ └── Perc-output.json ├── DWS - Quantum Loop │ ├── QL Rev_1-448.ogg │ ├── QL Rev_0-1000.ogg │ ├── QL Rev_1-1000.ogg │ ├── QL Stab_5-1000.ogg │ ├── QL Rev_450-1000.ogg │ ├── Kick Gold 6_0-1000.ogg │ ├── zara intro 1_14-28.ogg │ ├── 1390801779250-24_2-906.ogg │ ├── 140530145363057_0-1000.ogg │ ├── 140530149429658_0-1000.ogg │ ├── 140530150976559_0-1000.ogg │ ├── zara intro 1_158-164.ogg │ ├── zara intro 1_170-176.ogg │ ├── zara intro 1_408-423.ogg │ ├── zara intro 1_556-565.ogg │ ├── zara intro 1_683-701.ogg │ ├── zara intro 1_786-796.ogg │ ├── zara intro 1_884-936.ogg │ ├── 1390799216470-0_469-1000.ogg │ ├── 1390799216470-0_470-773.ogg │ ├── 1390799218452-1_418-1000.ogg │ ├── 1390799218452-1_418-556.ogg │ ├── 1390799329479-2_245-560.ogg │ ├── 1390799329479-2_245-575.ogg │ ├── 1390799329479-2_316-618.ogg │ ├── 1390801779250-24_2-1000.ogg │ ├── 140530143398856_11-1000.ogg │ ├── bllooper.json │ ├── Q Tinkle.json │ ├── freedom.json │ └── Q Warp.json ├── Launchpad Example │ ├── Kick Gold 6_0-1000.ogg │ ├── Clap Porter 1_0-1000.ogg │ ├── HiHat_07_V01_2-1000.ogg │ ├── Snare Gold 8_0-1000.ogg │ ├── OpenHH AR70sOpen V127 2_0-1000.ogg │ ├── pad.json │ ├── bass.json │ └── synth.json ├── Qwerty Keys Example │ ├── Kick Gold 6_0-1000.ogg │ ├── Clap Porter 1_0-1000.ogg │ ├── HiHat_07_V01_2-1000.ogg │ ├── Snare Gold 8_0-1000.ogg │ ├── OpenHH AR70sOpen V127 2_0-1000.ogg │ ├── bass.json │ ├── pad.json │ └── synth.json └── project.json ├── views └── window.html └── scripts ├── link-lib.js ├── start.js └── export-recording.js /styles/external-node.mcss: -------------------------------------------------------------------------------- 1 | ExternalNode { 2 | $node 3 | } 4 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib", 3 | "version": "0.0.0" 4 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | **/backup 4 | demo-project/~recordings 5 | -------------------------------------------------------------------------------- /lib/periodic-waves/ah.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ah.imag -------------------------------------------------------------------------------- /lib/periodic-waves/ah.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ah.real -------------------------------------------------------------------------------- /lib/periodic-waves/bass.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/bass.imag -------------------------------------------------------------------------------- /lib/periodic-waves/bass.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/bass.real -------------------------------------------------------------------------------- /lib/periodic-waves/ee.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ee.imag -------------------------------------------------------------------------------- /lib/periodic-waves/ee.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ee.real -------------------------------------------------------------------------------- /lib/periodic-waves/ooh.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ooh.imag -------------------------------------------------------------------------------- /lib/periodic-waves/ooh.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ooh.real -------------------------------------------------------------------------------- /lib/periodic-waves/ow.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ow.imag -------------------------------------------------------------------------------- /lib/periodic-waves/ow.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/ow.real -------------------------------------------------------------------------------- /lib/periodic-waves/wide.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/wide.imag -------------------------------------------------------------------------------- /lib/periodic-waves/wide.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/wide.real -------------------------------------------------------------------------------- /nodes/clip/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'timeline/clip', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/hold/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/hold', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/link-param/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'linkParam', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /lib/periodic-waves/brass.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/brass.imag -------------------------------------------------------------------------------- /lib/periodic-waves/brass.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/brass.real -------------------------------------------------------------------------------- /lib/periodic-waves/organ.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/organ.imag -------------------------------------------------------------------------------- /lib/periodic-waves/organ.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/organ.real -------------------------------------------------------------------------------- /lib/periodic-waves/piano.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/piano.imag -------------------------------------------------------------------------------- /lib/periodic-waves/piano.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/piano.real -------------------------------------------------------------------------------- /nodes/audio-buffer/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'AudioBuffer', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/offset/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/offset', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/random/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/random', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/trigger-value/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'slot/value', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /demo-project/DWS - OST/bd01.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/bd01.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/bd08.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/bd08.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/cp01.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/cp01.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/cr01.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/cr01.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/hh01.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/hh01.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/hh02.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/hh02.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/oh01.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/oh01.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/sd12.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/sd12.opus -------------------------------------------------------------------------------- /lib/periodic-waves/bass-fuzz.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/bass-fuzz.imag -------------------------------------------------------------------------------- /lib/periodic-waves/bass-fuzz.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/bass-fuzz.real -------------------------------------------------------------------------------- /lib/periodic-waves/bass-sub.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/bass-sub.imag -------------------------------------------------------------------------------- /lib/periodic-waves/bass-sub.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/bass-sub.real -------------------------------------------------------------------------------- /lib/periodic-waves/disonant.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/disonant.imag -------------------------------------------------------------------------------- /lib/periodic-waves/disonant.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/disonant.real -------------------------------------------------------------------------------- /lib/periodic-waves/soft-saw.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/soft-saw.imag -------------------------------------------------------------------------------- /lib/periodic-waves/soft-saw.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/soft-saw.real -------------------------------------------------------------------------------- /lib/periodic-waves/strings.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/strings.imag -------------------------------------------------------------------------------- /lib/periodic-waves/strings.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/strings.real -------------------------------------------------------------------------------- /lib/periodic-waves/throaty.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/throaty.imag -------------------------------------------------------------------------------- /lib/periodic-waves/throaty.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/throaty.real -------------------------------------------------------------------------------- /lib/periodic-waves/trombone.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/trombone.imag -------------------------------------------------------------------------------- /lib/periodic-waves/trombone.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/trombone.real -------------------------------------------------------------------------------- /lib/periodic-waves/wurlitzer.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/wurlitzer.imag -------------------------------------------------------------------------------- /lib/periodic-waves/wurlitzer.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/wurlitzer.real -------------------------------------------------------------------------------- /nodes/link-modulator/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/param', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/multiply/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/multiply', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /nodes/quantize/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/quantize', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /demo-project/DWS - OST/synthit.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/synthit.opus -------------------------------------------------------------------------------- /lib/periodic-waves/guitar-fuzz.imag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/guitar-fuzz.imag -------------------------------------------------------------------------------- /lib/periodic-waves/guitar-fuzz.real: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/lib/periodic-waves/guitar-fuzz.real -------------------------------------------------------------------------------- /nodes/chromatic-scale/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'modulator/scale', 3 | object: require('./object') 4 | } 5 | -------------------------------------------------------------------------------- /demo-project/DWS - OST/cymbal (1).opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/cymbal (1).opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/kick (11).opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/kick (11).opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/snare (19).opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/snare (19).opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/synthit2.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/synthit2.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/tom (7) 1.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/tom (7) 1.opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/hi hat (15).opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/hi hat (15).opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/hi hat (22).opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/hi hat (22).opus -------------------------------------------------------------------------------- /demo-project/DWS - OST/hi hat (34).opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/hi hat (34).opus -------------------------------------------------------------------------------- /demo-project/DWS - Synthesia/Build.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Synthesia/Build.opus -------------------------------------------------------------------------------- /nodes/midi-cc/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'MIDI CC', 3 | node: 'slot/midi-cc', 4 | object: require('./object') 5 | } 6 | -------------------------------------------------------------------------------- /nodes/project/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'project', 3 | render: require('./view'), 4 | object: require('./object') 5 | } -------------------------------------------------------------------------------- /demo-project/DWS - Synthesia/Pitched.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Synthesia/Pitched.opus -------------------------------------------------------------------------------- /styles/codemirror.css: -------------------------------------------------------------------------------- 1 | .ace-ambiance .ace_scroller { 2 | background-color: transparent !important; 3 | box-shadow: none !important; 4 | } -------------------------------------------------------------------------------- /nodes/recording/ableton-export/Project8_1.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/nodes/recording/ableton-export/Project8_1.cfg -------------------------------------------------------------------------------- /styles/center-tab.mcss: -------------------------------------------------------------------------------- 1 | CenterTab { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | } -------------------------------------------------------------------------------- /demo-project/DWS - Beep Boop/1490926241152.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Beep Boop/1490926241152.opus -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/QL Rev_1-448.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/QL Rev_1-448.ogg -------------------------------------------------------------------------------- /lib/h.js: -------------------------------------------------------------------------------- 1 | module.exports = require('micro-css/h')(require('mutant/html-element')) 2 | module.exports.destroy = require('mutant/html-element').destroy 3 | -------------------------------------------------------------------------------- /views/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/QL Rev_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/QL Rev_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/QL Rev_1-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/QL Rev_1-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/QL Stab_5-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/QL Stab_5-1000.ogg -------------------------------------------------------------------------------- /nodes/external-chunk/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | node: 'externalChunk', 3 | render: require('./view'), 4 | object: require('./object') 5 | } 6 | -------------------------------------------------------------------------------- /nodes/setup/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "Setup", 3 | node: 'setup', 4 | render: require('./view'), 5 | object: require('./object') 6 | } 7 | -------------------------------------------------------------------------------- /nodes/slot/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Slot', 3 | node: 'slot', 4 | object: require('./object'), 5 | render: require('./view') 6 | } 7 | -------------------------------------------------------------------------------- /styles/source-node.mcss: -------------------------------------------------------------------------------- 1 | SourceNode { 2 | $node 3 | 4 | border-color: #4B5F5F 5 | 6 | header { 7 | background: #4B5F5F 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/QL Rev_450-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/QL Rev_450-1000.ogg -------------------------------------------------------------------------------- /demo-project/Launchpad Example/Kick Gold 6_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Launchpad Example/Kick Gold 6_0-1000.ogg -------------------------------------------------------------------------------- /styles/modulator-node.mcss: -------------------------------------------------------------------------------- 1 | ModulatorNode { 2 | $node 3 | 4 | border-color: #5a5538 5 | 6 | header { 7 | background: #5a5538 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /styles/processor-node.mcss: -------------------------------------------------------------------------------- 1 | ProcessorNode { 2 | $node 3 | 4 | border-color: #644B77 5 | 6 | header { 7 | background: #644B77 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/Kick Gold 6_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/Kick Gold 6_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_14-28.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_14-28.ogg -------------------------------------------------------------------------------- /demo-project/Launchpad Example/Clap Porter 1_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Launchpad Example/Clap Porter 1_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/Launchpad Example/HiHat_07_V01_2-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Launchpad Example/HiHat_07_V01_2-1000.ogg -------------------------------------------------------------------------------- /demo-project/Launchpad Example/Snare Gold 8_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Launchpad Example/Snare Gold 8_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/Kick Gold 6_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Qwerty Keys Example/Kick Gold 6_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390801779250-24_2-906.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390801779250-24_2-906.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/140530145363057_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/140530145363057_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/140530149429658_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/140530149429658_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/140530150976559_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/140530150976559_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_158-164.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_158-164.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_170-176.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_170-176.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_408-423.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_408-423.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_556-565.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_556-565.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_683-701.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_683-701.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_786-796.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_786-796.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/zara intro 1_884-936.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/zara intro 1_884-936.ogg -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/Clap Porter 1_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Qwerty Keys Example/Clap Porter 1_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/HiHat_07_V01_2-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Qwerty Keys Example/HiHat_07_V01_2-1000.ogg -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/Snare Gold 8_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Qwerty Keys Example/Snare Gold 8_0-1000.ogg -------------------------------------------------------------------------------- /lib/clamp.js: -------------------------------------------------------------------------------- 1 | module.exports = function clamp (value, min, max) { 2 | if (typeof value !== 'number') return min 3 | return Math.min(max, Math.max(min, value)) 4 | } 5 | -------------------------------------------------------------------------------- /lib/property.js: -------------------------------------------------------------------------------- 1 | var Value = require('mutant/value') 2 | 3 | module.exports = function Property (defaultValue) { 4 | return Value(defaultValue, { defaultValue }) 5 | } 6 | -------------------------------------------------------------------------------- /nodes/recording/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Recording', 3 | node: 'recording', 4 | render: require('./view'), 5 | object: require('./object') 6 | } 7 | -------------------------------------------------------------------------------- /styles/main-params.mcss: -------------------------------------------------------------------------------- 1 | MainParams { 2 | display: flex 3 | 4 | div + button { 5 | margin-left: 5px 6 | } 7 | 8 | div { 9 | flex: 1 10 | } 11 | } -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799216470-0_469-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799216470-0_469-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799216470-0_470-773.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799216470-0_470-773.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799218452-1_418-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799218452-1_418-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799218452-1_418-556.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799218452-1_418-556.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799329479-2_245-560.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799329479-2_245-560.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799329479-2_245-575.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799329479-2_245-575.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390799329479-2_316-618.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390799329479-2_316-618.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/1390801779250-24_2-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/1390801779250-24_2-1000.ogg -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/140530143398856_11-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - Quantum Loop/140530143398856_11-1000.ogg -------------------------------------------------------------------------------- /lib/dom-event.js: -------------------------------------------------------------------------------- 1 | module.exports = function(fn, data, opts){ 2 | var handler = { 3 | handleEvent: fn, 4 | data: data, 5 | opts: opts 6 | } 7 | return handler; 8 | } -------------------------------------------------------------------------------- /lib/param-negate.js: -------------------------------------------------------------------------------- 1 | var Multiply = require('lib/param-multiply') 2 | module.exports = ParamNegate 3 | 4 | function ParamNegate (input) { 5 | return Multiply([input, -1]) 6 | } 7 | -------------------------------------------------------------------------------- /demo-project/Launchpad Example/OpenHH AR70sOpen V127 2_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Launchpad Example/OpenHH AR70sOpen V127 2_0-1000.ogg -------------------------------------------------------------------------------- /demo-project/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "zoom": 1.1, 3 | "tempo": 85, 4 | "swing": 0.6006717158297845, 5 | "rawMode": false, 6 | "globalControllers": [], 7 | "node": "project" 8 | } -------------------------------------------------------------------------------- /lib/no-drop.js: -------------------------------------------------------------------------------- 1 | module.exports = function(el){ 2 | el.addEventListener('dragover', function(e){ 3 | e.preventDefault() 4 | e.dataTransfer.dropEffect = 'none' 5 | }, true) 6 | } -------------------------------------------------------------------------------- /lib/read.js: -------------------------------------------------------------------------------- 1 | module.exports = function(param){ 2 | if (typeof param == 'function'){ 3 | return param() 4 | } else if (param && param.read){ 5 | return param.read() 6 | } 7 | } -------------------------------------------------------------------------------- /nodes/eq/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'EQ', 3 | node: 'processor/eq', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/OpenHH AR70sOpen V127 2_0-1000.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/Qwerty Keys Example/OpenHH AR70sOpen V127 2_0-1000.ogg -------------------------------------------------------------------------------- /nodes/clap/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Clap', 3 | node: 'source/clap', 4 | group: 'drumSources', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/gain/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Gain', 3 | node: 'processor/gain', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/kick/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Kick', 3 | node: 'source/kick', 4 | group: 'drumSources', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/noise/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Noise', 3 | node: 'source/noise', 4 | group: 'sources', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/pan/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Pan', 3 | node: 'processor/pan', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/reverb/types.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | ['Fast', 'processor/freeverb'], 3 | ['Generated Impulse', 'processor/reverb'], 4 | ['Convolution', 'processor/convolution-reverb'] 5 | ] 6 | -------------------------------------------------------------------------------- /nodes/snare/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Snare', 3 | node: 'source/snare', 4 | group: 'drumSources', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /demo-project/DWS - OST/DOLLARS and TODAY and EDUCATION-Jc_RKi4rmzs.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/loop-drop-app/HEAD/demo-project/DWS - OST/DOLLARS and TODAY and EDUCATION-Jc_RKi4rmzs.m4a -------------------------------------------------------------------------------- /lib/find-item-by-path.js: -------------------------------------------------------------------------------- 1 | module.exports = findItemByPath 2 | 3 | function findItemByPath (items, path) { 4 | if (items) { 5 | return items.find(item => item.path() === path) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nodes/cymbal/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Cymbal', 3 | node: 'source/cymbal', 4 | group: 'drumSources', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/delay/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Delay', 3 | node: 'processor/delay', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/dipper/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Dipper', 3 | node: 'processor/dipper', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/filter/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Filter', 3 | node: 'processor/filter', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/reverb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Reverb', 3 | node: 'processor/reverb', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /lib/copy-file.js: -------------------------------------------------------------------------------- 1 | module.exports = function copyFile (from, to, fs, cb) { 2 | fs.createReadStream(from).on('error', cb) 3 | .pipe(fs.createWriteStream(to)).on('error', cb) 4 | .on('finish', cb) 5 | } -------------------------------------------------------------------------------- /nodes/envelope/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Envelope', 3 | group: 'triggerModulators', 4 | node: 'modulator/adsr', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/overdrive/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Overdrive', 3 | node: 'processor/overdrive', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/bitcrusher/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Bitcrusher', 3 | group: 'processors', 4 | node: 'processor/bitcrusher', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/compressor/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Compressor', 3 | node: 'processor/compressor', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/pitchshift/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Pitchshift', 3 | node: 'processor/pitchshift', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /styles/raw-editor.mcss: -------------------------------------------------------------------------------- 1 | RawEditor { 2 | flex: 1 3 | position: relative; 4 | 5 | div { 6 | background-color: transparent 7 | position: absolute 8 | top:0;right:0;bottom:0;left:0 9 | } 10 | } -------------------------------------------------------------------------------- /nodes/ring-modulator/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Ring Modulator', 3 | node: 'processor/ring-modulator', 4 | group: 'processors', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /lib/cancel-event.js: -------------------------------------------------------------------------------- 1 | module.exports = function (fn, data, opts) { 2 | var handler = { 3 | handleEvent: handle 4 | } 5 | return handler 6 | } 7 | 8 | function handle (ev) { 9 | ev.preventDefault() 10 | } 11 | -------------------------------------------------------------------------------- /lib/destroy-all.js: -------------------------------------------------------------------------------- 1 | module.exports = function (obs) { 2 | Object.keys(obs).forEach(function (key) { 3 | if (obs[key] && typeof obs[key].destroy === 'function') { 4 | obs[key].destroy() 5 | } 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /nodes/ableton-link/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Ableton Link Sync', 3 | node: 'global/ableton-link', 4 | group: 'global-controllers', 5 | object: require('./object'), 6 | render: require('./view') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/loop-grid-qwerty/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Qwerty Keys', 3 | group: 'loop-grids', 4 | node: 'controller/qwerty', 5 | render: require('../loop-grid/view'), 6 | object: require('./object') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/recording/ableton-export/template.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var ejs = require('ejs') 3 | var Path = require('path') 4 | 5 | module.exports = ejs.compile(fs.readFileSync(Path.join(__dirname, 'template.ejs'), 'utf8')) 6 | -------------------------------------------------------------------------------- /styles/global-controller-node.mcss: -------------------------------------------------------------------------------- 1 | GlobalControllerNode { 2 | $node 3 | 4 | border-color: #222 5 | border-width: 2px 6 | 7 | header { 8 | padding: 5px 3px; 9 | background-color: #333; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/bind-event.js: -------------------------------------------------------------------------------- 1 | module.exports = function (element, eventName, handler) { 2 | element.addEventListener(eventName, handler) 3 | return function unlisten () { 4 | element.removeEventListener(eventName, handler) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/set-route.js: -------------------------------------------------------------------------------- 1 | module.exports = function setRoute (node, id, value) { 2 | if (node.routes) { 3 | node.routes.put(id, value) 4 | } else if (node.node && node.node.routes) { 5 | node.node.routes.put(id, value) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nodes/global-launch-control/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Launch Control', 3 | portMatch: /^Launch Control(?! XL)/, 4 | node: 'global/launch-control', 5 | group: 'global-controllers', 6 | object: require('./object') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/granular/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Granular', 3 | node: 'source/granular', 4 | group: 'sources', 5 | spawn: false, // spawned via source/sample 6 | object: require('./object'), 7 | render: require('./view') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/global-launch-control-xl/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Launch Control XL', 3 | portMatch: /^Launch Control XL/, 4 | node: 'global/launch-control-xl', 5 | group: 'global-controllers', 6 | object: require('./object') 7 | } 8 | -------------------------------------------------------------------------------- /nodes/spatial-pan/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Spatial Pan', 3 | spawn: false, // spawned by pan 4 | node: 'processor/spatial-pan', 5 | group: 'processors', 6 | object: require('./object'), 7 | render: require('../pan/view') 8 | } 9 | -------------------------------------------------------------------------------- /styles/node-spawner.mcss: -------------------------------------------------------------------------------- 1 | NodeSpawner { 2 | background: #3D3D3D; 3 | border-top: 1px #444 solid; 4 | display: flex; 5 | padding: 5px; 6 | margin: -6px -8px; 7 | margin-top: 10px; 8 | 9 | * { 10 | margin-right: 3px 11 | } 12 | } -------------------------------------------------------------------------------- /lib/hold-active-transform.js: -------------------------------------------------------------------------------- 1 | module.exports = function holdActive (input, active) { 2 | active.forEach(function(index){ 3 | input.data[index] = { 4 | events: [[0, true]], 5 | length: 2 6 | } 7 | }) 8 | return input 9 | } 10 | -------------------------------------------------------------------------------- /nodes/loop-grid-push/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Ableton Push', 3 | group: 'loop-grids', 4 | portMatch: /^Ableton Push User/, 5 | node: 'controller/push', 6 | render: require('../loop-grid/view'), 7 | object: require('./object') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/mixer-launch-control-xl/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Launch Control XL', 3 | group: 'mixers', 4 | portMatch: /^Launch Control XL/, 5 | node: 'controller/launch-control-xl', 6 | render: require('./view'), 7 | object: require('./object') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/ping-pong-delay/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Delay', 3 | spawn: false, // spawned from delay node 4 | node: 'processor/ping-pong-delay', 5 | group: 'processors', 6 | object: require('./object'), 7 | render: require('../delay/view') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/freeverb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Freeverb', 3 | spawn: false, // can be selected using "reverb" with type option 4 | node: 'processor/freeverb', 5 | group: 'processors', 6 | object: require('./object'), 7 | render: require('./view') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/loop-grid-launchpad-mk2/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Launchpad', 3 | group: 'loop-grids', 4 | portMatch: /^Launchpad (MK2|Pro)/, 5 | node: 'controller/launchpad-mk2', 6 | render: require('../loop-grid/view'), 7 | object: require('./object') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/loop-grid-launchpad/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Launchpad Mini', 3 | group: 'loop-grids', 4 | portMatch: /^Launchpad(?! (Pro|MK2))/, 5 | node: 'controller/launchpad', 6 | render: require('../loop-grid/view'), 7 | object: require('./object') 8 | } 9 | -------------------------------------------------------------------------------- /nodes/midi-sync-output/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'MIDI Sync Out', 3 | portMatch: /./, 4 | outputOnly: true, 5 | node: 'global/midi-sync-output', 6 | group: 'global-controllers', 7 | object: require('./object'), 8 | render: require('./view') 9 | } 10 | -------------------------------------------------------------------------------- /scripts/link-lib.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var join = require('path').join 3 | 4 | process.cwd(join(__dirname, '..')) 5 | fs.symlink(join('..','lib'), join('node_modules', 'lib'), 'dir', function(err) { 6 | if (!err) { 7 | console.log('node_modules/lib -> lib') 8 | } 9 | }) -------------------------------------------------------------------------------- /lib/get-value.js: -------------------------------------------------------------------------------- 1 | module.exports = getValue 2 | 3 | function getValue(object, defaultValue){ 4 | if (object instanceof Object && !Array.isArray(object)){ 5 | return getValue(object.value, defaultValue) 6 | } else { 7 | return object != null ? object : defaultValue 8 | } 9 | } -------------------------------------------------------------------------------- /nodes/oscillator-pulse/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Oscillator (pulse)', 3 | node: 'source/oscillator-pulse', 4 | group: 'sources', 5 | spawn: false, // spawned by source/oscillator by setting type 6 | object: require('./object'), 7 | render: require('./view') 8 | } 9 | -------------------------------------------------------------------------------- /lib/periodic-waves/convert.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | writeArray('name.real', input.real) 4 | writeArray('name.imag', input.imag) 5 | 6 | function writeArray (file, array) { 7 | fs.writeFileSync( 8 | file, new Buffer(new Uint8Array(new Float32Array(array).buffer)) 9 | ) 10 | } -------------------------------------------------------------------------------- /nodes/convolution-reverb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Convolution Reverb', 3 | spawn: false, // can be selected using "reverb" with type option 4 | node: 'processor/convolution-reverb', 5 | group: 'processors', 6 | object: require('./object'), 7 | render: require('./view') 8 | } 9 | -------------------------------------------------------------------------------- /lib/file-event.js: -------------------------------------------------------------------------------- 1 | module.exports = FileEventHandler 2 | 3 | function FileEventHandler(fn, data) { 4 | return { 5 | fn: fn, 6 | data: data, 7 | handleEvent: handleEvent 8 | } 9 | } 10 | 11 | function handleEvent(ev) { 12 | var value = ev.currentTarget.files[0] 13 | this.fn(value) 14 | } -------------------------------------------------------------------------------- /nodes/modulator-chunk/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Modulator', 3 | group: 'modifierChunks', 4 | node: 'modulatorChunk', 5 | description: 'Modulate parameters on other chunks.', 6 | spawn: { 7 | flags: ['freezeSuppress'] 8 | }, 9 | object: require('./object'), 10 | render: require('./view') 11 | } 12 | -------------------------------------------------------------------------------- /nodes/oscillator/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Oscillator', 3 | node: 'source/oscillator', 4 | group: 'sources', 5 | spawn: { 6 | amp: { 7 | node: 'modulator/adsr', 8 | value: 0.6, 9 | release: 0.01 10 | } 11 | }, 12 | object: require('./object'), 13 | render: require('./view') 14 | } 15 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var spawn = require('child_process').spawn 4 | var electron = require('electron') 5 | var join = require('path').join 6 | 7 | var app = spawn(electron, ['main.js'], { 8 | stdio: 'inherit', 9 | cwd: join(__dirname, '..') 10 | }).on('exit', function(i, m) { 11 | process.exit() 12 | }) 13 | -------------------------------------------------------------------------------- /lib/params/select.js: -------------------------------------------------------------------------------- 1 | var Select = require('lib/widgets/select') 2 | var extend = require('xtend') 3 | 4 | module.exports = SelectParam 5 | 6 | function SelectParam (param, opts) { 7 | return Select(set, param, extend(opts, { 8 | selectedValue: param 9 | })) 10 | } 11 | 12 | function set (value) { 13 | this.data.set(value) 14 | } 15 | -------------------------------------------------------------------------------- /nodes/sample/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Sample', 3 | node: 'source/sample', 4 | group: 'sources', 5 | object: require('./object'), 6 | spawn: { 7 | amp: { 8 | node: 'modulator/adsr', 9 | value: 1, 10 | release: 0.01 11 | }, 12 | mode: 'oneshot' 13 | }, 14 | render: require('./view') 15 | } 16 | -------------------------------------------------------------------------------- /styles/chunk-node.mcss: -------------------------------------------------------------------------------- 1 | ChunkNode { 2 | padding: 5px; 3 | flex: 1 4 | display: flex 5 | flex-direction: row 6 | 7 | div.options { 8 | flex: 1 9 | padding: 8px 10 | } 11 | 12 | div.slot { 13 | flex: 3 14 | background: #222; 15 | padding: 5px; 16 | box-shadow: 0 0 5px black 17 | overflow-y: scroll 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /lib/params/editable.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var EditableHook = require('lib/editable-hook') 3 | 4 | module.exports = Editable 5 | 6 | function Editable (param, opts) { 7 | // opts: onChange, enabled 8 | return h('span', { 9 | hooks: [ 10 | EditableHook(param, opts) 11 | ] 12 | }) 13 | } 14 | 15 | Editable.edit = EditableHook.edit 16 | -------------------------------------------------------------------------------- /styles/clock.mcss: -------------------------------------------------------------------------------- 1 | Clock { 2 | border: 1px solid #333; 3 | background-color: #171817; 4 | font-size: 32px; 5 | padding: 4px 8px !important; 6 | margin: 4px; 7 | font-family: sans-serif; 8 | color: #686; 9 | border-radius: 3px; 10 | text-shadow: 0 0 1px black; 11 | box-shadow: inset 0 0 10px black; 12 | 13 | span { 14 | color: #ACA 15 | } 16 | } -------------------------------------------------------------------------------- /lib/widgets/collection.js: -------------------------------------------------------------------------------- 1 | var Orderable = require('./orderable.js') 2 | var renderNode = require('lib/render-node') 3 | var map = require('mutant/map') 4 | 5 | module.exports = renderCollection 6 | 7 | function renderCollection (collection) { 8 | return map(collection, function (node) { 9 | return Orderable(node, renderNode(node)) 10 | }, { maxTime: 16 }) 11 | } 12 | -------------------------------------------------------------------------------- /lib/widgets/header.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | 4 | module.exports = function (node, display, options) { 5 | var collection = node.context.collection 6 | return h('header', options, [ 7 | display, 8 | h('button.remove Button -warn', { 9 | 'ev-click': send(collection.remove, node) 10 | }, 'X') 11 | ]) 12 | } 13 | -------------------------------------------------------------------------------- /lib/quantize-duration.js: -------------------------------------------------------------------------------- 1 | module.exports = quantizeDuration 2 | 3 | function quantizeDuration (value) { 4 | var grid = getGrid(value) 5 | return Math.round(value / grid) * grid 6 | } 7 | 8 | function getGrid (duration) { 9 | if (duration < 0.7) { 10 | return 0.5 11 | } else if (duration < 1.7) { 12 | return 1 13 | } else { 14 | return 2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /styles/audio-slot.mcss: -------------------------------------------------------------------------------- 1 | AudioSlot { 2 | section { 3 | header { 4 | display: flex 5 | align-items: center 6 | button { 7 | margin-left: 10px 8 | } 9 | } 10 | border-radius: 3px 11 | background: #333 12 | padding: 6px 8px 13 | overflow: hidden 14 | } 15 | section + section { 16 | margin-top: 5px 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/zoom-event.js: -------------------------------------------------------------------------------- 1 | module.exports = ZoomEvent 2 | 3 | function ZoomEvent (fn, data) { 4 | var handler = { 5 | handleEvent: handle, 6 | fn: fn, 7 | data: data 8 | } 9 | return handler 10 | } 11 | 12 | function handle (ev) { 13 | if (ev.ctrlKey) { 14 | ev.preventDefault() 15 | ev.stopImmediatePropagation() 16 | this.fn(ev.deltaY, this.data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/active-indexes.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | 3 | module.exports = function activeIndexes (obs) { 4 | return computed([obs], function (grid) { 5 | var result = [] 6 | grid.data.forEach(pushIndexIfPresent, result) 7 | return result 8 | }) 9 | } 10 | 11 | function pushIndexIfPresent (value, index) { 12 | if (value) { 13 | this.push(index) 14 | } 15 | } -------------------------------------------------------------------------------- /lib/random-color.js: -------------------------------------------------------------------------------- 1 | module.exports = randomColor 2 | function randomColor(mix){ 3 | var red = Math.random()*256 4 | var green = Math.random()*256 5 | var blue = Math.random()*256 6 | if (mix != null) { 7 | red = (red + mix[0]) / 2 8 | green = (green + mix[1]) / 2 9 | blue = (blue + mix[2]) / 2 10 | } 11 | return [Math.floor(red),Math.floor(green),Math.floor(blue)] 12 | } -------------------------------------------------------------------------------- /lib/is-triggerable.js: -------------------------------------------------------------------------------- 1 | module.exports = isTriggerable 2 | 3 | function isTriggerable (param) { 4 | if (typeof param.triggerable === 'boolean') { 5 | return param.triggerable 6 | } else { 7 | var slot = param.context && param.context.slot 8 | return !!( 9 | slot && slot.triggerOn && 10 | slot().id !== 'output' // HACK: should handle this better 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/set-mapped-value.js: -------------------------------------------------------------------------------- 1 | var watch = require('mutant/watch') 2 | module.exports = function(values, obs, target){ 3 | var lastValue = undefined 4 | return watch(obs, function(data){ 5 | for (var i=values.length-1;i>=0;i--) { 6 | if (data[i]) { 7 | if (target() !== values[i]) { 8 | target.set(values[i]) 9 | } 10 | break 11 | } 12 | } 13 | }) 14 | } -------------------------------------------------------------------------------- /lib/once-true.js: -------------------------------------------------------------------------------- 1 | var watch = require('mutant/watch') 2 | module.exports = function onceTrue (value, fn) { 3 | var done = false 4 | var release = watch(value, (v) => { 5 | if (v && !done) { 6 | done = true 7 | setImmediate(doRelease) 8 | fn(v) 9 | } 10 | }, { nextTick: true }) 11 | 12 | return release 13 | 14 | function doRelease () { 15 | release() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nodes/midi-out-chunk/index.js: -------------------------------------------------------------------------------- 1 | var randomColor = require('lib/random-color') 2 | 3 | module.exports = { 4 | name: 'MIDI', 5 | node: 'chunk/midi-out', 6 | description: 'Output midi notes tuned to current scale.', 7 | group: 'simpleChunks', 8 | spawn: function () { 9 | return { 10 | color: randomColor() 11 | } 12 | }, 13 | object: require('./object'), 14 | render: require('./view') 15 | } 16 | -------------------------------------------------------------------------------- /nodes/mono-chromatic-chunk/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Chromatic (monophonic)', 3 | node: 'chunk/scale-mono', 4 | group: 'chunks', 5 | description: 'Monophonic version of chromatic chunk.', 6 | spawn: false, // spawned via chunk/scale 7 | external: true, 8 | renderExternal: require('../chromatic-chunk/external'), 9 | render: require('../chromatic-chunk/view'), 10 | object: require('./object') 11 | } 12 | -------------------------------------------------------------------------------- /nodes/ring-modulator/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Oscillator = require('../oscillator/object') 3 | 4 | module.exports = RingModulatorNode 5 | 6 | function RingModulatorNode (context) { 7 | var node = context.audio.createGain() 8 | 9 | var obs = Processor(context, node, node, { 10 | carrier: Oscillator(context) 11 | }) 12 | 13 | obs.carrier.connect(node.gain) 14 | return obs 15 | } 16 | -------------------------------------------------------------------------------- /lib/periodic-waves/raw.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var raw = {} 4 | 5 | fs.readdirSync(__dirname).forEach(function(file) { 6 | var ext = path.extname(file) 7 | if (ext === '.imag' || ext === '.real') { 8 | var base = path.basename(file, ext) 9 | var item = raw[base] = raw[base] || {} 10 | item[ext.slice(1)] = fs.readFileSync(path.join(__dirname, file)) 11 | } 12 | }) 13 | 14 | module.exports = raw 15 | -------------------------------------------------------------------------------- /nodes/gain/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | 5 | module.exports = function renderGain (node){ 6 | return h('ProcessorNode -gain', [ 7 | Header(node, h('span', 'Gain')), 8 | h('ParamList', [ 9 | ModRange(node.gain, { 10 | defaultValue: 1, 11 | format: 'dB', 12 | flex: true 13 | }) 14 | ]) 15 | ]) 16 | } -------------------------------------------------------------------------------- /styles/memory-usage.mcss: -------------------------------------------------------------------------------- 1 | MemoryUsage { 2 | padding: 3px 6px; 3 | font-size: 10px; 4 | border: 1px solid #6e7b6e; 5 | background: #000; 6 | color: #ACA; 7 | margin-right: 3px 8 | cursor: pointer 9 | 10 | -critical { 11 | border: 1px solid #ff6b6b; 12 | background: #711313; 13 | color: #ffd6d6; 14 | } 15 | 16 | -warn { 17 | border: 1px solid #b5ad53; 18 | background: #655907; 19 | color: #fffb07; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /nodes/slicer-chunk/index.js: -------------------------------------------------------------------------------- 1 | var randomColor = require('lib/random-color') 2 | 3 | module.exports = { 4 | node: 'chunk/slicer', 5 | name: 'Slicer', 6 | description: 'Slice an audio file into pieces and distribute across controller.', 7 | group: 'simpleChunks', 8 | object: require('./object'), 9 | spawn: function () { 10 | return { 11 | color: randomColor(), 12 | chokeAll: true 13 | } 14 | }, 15 | render: require('./view') 16 | } 17 | -------------------------------------------------------------------------------- /lib/menu.js: -------------------------------------------------------------------------------- 1 | var electron = require('electron') 2 | var defaultMenu = require('electron-default-menu') 3 | 4 | var menu = defaultMenu(electron.app, electron.shell) 5 | menu.splice(1, 0, { 6 | label: 'Project', 7 | submenu: [ 8 | { 9 | label: 'New Project...', 10 | click: (item, focusedWindow) => { 11 | electron.dialog.showMessageBox({ message: 'Do something', buttons: ['OK'] }) 12 | } 13 | } 14 | ] 15 | }) 16 | module.exports = menu 17 | -------------------------------------------------------------------------------- /styles/loop-position.mcss: -------------------------------------------------------------------------------- 1 | LoopPosition { 2 | transform: translate3d(0,0,0) 3 | display: flex 4 | height: 8px; 5 | margin: 0 8px; 6 | div { 7 | flex: 1 8 | background: #323232 9 | 10 | -active { 11 | background: #63C763 12 | } 13 | 14 | :nth-child(4n) { 15 | margin-right: 4px 16 | } 17 | 18 | :last-child { 19 | margin-right: 0 20 | } 21 | } 22 | 23 | div + div { 24 | margin-left: 1px 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /lib/array-stack.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | 3 | module.exports = ArrayStack 4 | 5 | function ArrayStack (items) { 6 | return computed(items, function(_) { 7 | var result = [] 8 | for (var i = 0; i < arguments.length; i++) { 9 | var arr = arguments[i] 10 | for (var k = 0; k < arr.length; k++) { 11 | if (arr[k] != null) { 12 | result[k] = arr[k] 13 | } 14 | } 15 | } 16 | return result 17 | }) 18 | } -------------------------------------------------------------------------------- /nodes/midi-out/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'MIDI Note', 3 | node: 'source/midi-out', 4 | group: 'sources', 5 | spawn: function (context) { 6 | // HACK: enable midi output on chunk when midi note source added 7 | if (context.externalChunk && context.externalChunk.midiOutputEnabled) { 8 | context.externalChunk.midiOutputEnabled.set(true) 9 | } 10 | return {} 11 | }, 12 | object: require('./object'), 13 | render: require('./view') 14 | } 15 | -------------------------------------------------------------------------------- /styles/scale-chooser.mcss: -------------------------------------------------------------------------------- 1 | ScaleChooser { 2 | 3 | display: flex 4 | min-width: 150px 5 | flex: 1 6 | 7 | div.button { 8 | height: 20px 9 | flex: 1 10 | margin: 2px 11 | background: #3C493C 12 | border: 1px solid #2A2A2A 13 | border-radius: 3px 14 | cursor: pointer 15 | 16 | :hover { 17 | border-color: #666 18 | } 19 | 20 | -selected { 21 | background: #53AB53 22 | border-color: #336C20 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/periodic-waves/index.js: -------------------------------------------------------------------------------- 1 | var raw = require('./raw') 2 | 3 | module.exports = function (audioContext) { 4 | var result = {} 5 | for (var k in raw) { 6 | result[k] = audioContext.createPeriodicWave( 7 | bufferAsFloat32(raw[k].real), 8 | bufferAsFloat32(raw[k].imag) 9 | ) 10 | } 11 | return result 12 | } 13 | 14 | module.exports.raw = raw 15 | 16 | function bufferAsFloat32 (buffer) { 17 | return new Float32Array(new Uint8Array(buffer).buffer) 18 | } 19 | -------------------------------------------------------------------------------- /lib/midi-to-param.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var ObservStruct = require('mutant/struct') 3 | var Observ = require('mutant/value') 4 | 5 | module.exports = MidiToParam 6 | 7 | function MidiToParam (context, id, value) { 8 | var obs = ObservStruct({ 9 | id: Observ(id) 10 | }) 11 | 12 | obs._type = 'RemoteParam' 13 | obs.currentValue = computed([value], fromMidi) 14 | return obs 15 | } 16 | 17 | function fromMidi (input) { 18 | return input / 128 19 | } 20 | -------------------------------------------------------------------------------- /nodes/lfo/index.js: -------------------------------------------------------------------------------- 1 | var isTriggerable = require('lib/is-triggerable') 2 | 3 | module.exports = { 4 | name: 'LFO', 5 | group: 'modulators', 6 | node: 'modulator/lfo', 7 | object: require('./object'), 8 | render: require('./view'), 9 | spawn: function (context) { 10 | var result = { 11 | amp: 0.5, 12 | mode: 'add', 13 | value: 0.5 14 | } 15 | if (!isTriggerable({context})) { 16 | result.trigger = false 17 | } 18 | return result 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/get-param-value.js: -------------------------------------------------------------------------------- 1 | module.exports = function getParamValue (param, at) { 2 | if (param.currentValue) { 3 | var value = param.currentValue() 4 | if (typeof value === 'number') { 5 | return value 6 | } else if (value && value.getValueAtTime && param.context && param.context.audio) { 7 | return value.getValueAtTime(at || param.context.audio.currentTime) 8 | } else { 9 | console.warn('Unable to automate param.', param) 10 | return 0 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /nodes/meddler-chunk/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'Meddler', 3 | node: 'chunk/meddler', 4 | group: 'modifierChunks', 5 | description: 'Add effects to other chunks.', 6 | spawn: { 7 | slots: [{id: 'output', node: 'slot'}], 8 | inputs: ['input'] 9 | }, 10 | external: { 11 | shape: [1, 4], 12 | color: [255, 255, 0], 13 | minimised: true 14 | }, 15 | renderExternal: require('./external'), 16 | render: require('./view'), 17 | object: require('./object') 18 | } 19 | -------------------------------------------------------------------------------- /lib/params/text.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var DomEvent = require('lib/dom-event') 3 | 4 | module.exports = TextParam 5 | 6 | function TextParam (param, options) { 7 | options = options || {} 8 | 9 | return h('input', { 10 | 'type': 'text', 11 | 'size': options.size, 12 | 'placeholder': options.placeholder, 13 | 'value': param, 14 | 'ev-change': DomEvent(handle, param) 15 | }) 16 | } 17 | 18 | function handle (event) { 19 | this.data.set(event.currentTarget.value) 20 | } 21 | -------------------------------------------------------------------------------- /lib/render-node.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | 3 | module.exports = function (node) { 4 | // render whatever the node is when it is passed to the renderNode function 5 | var descriptor = node() 6 | if (node && node.context && node.context.nodeInfo && descriptor) { 7 | var lookup = node.context.nodeInfo.lookup 8 | var info = lookup[descriptor.node] 9 | if (info && info.render) { 10 | return info.render(node) 11 | } else { 12 | return h('UnknownNode') 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /styles/sample-chooser.mcss: -------------------------------------------------------------------------------- 1 | SampleChooser { 2 | width: 80px; 3 | height: 22px; 4 | margin: 1px; 5 | padding: 3px; 6 | 7 | ::-webkit-file-upload-button { 8 | width: 100%; 9 | border: 1px solid #888; 10 | background: #6F6F6F; 11 | color: #D8D8D8; 12 | cursor: pointer; 13 | font-size: 90%; 14 | height: 18px; 15 | 16 | :hover { 17 | border: 1px solid #AA5; 18 | background: #993; 19 | color: #FFF; 20 | box-shadow: 0px 0px 5px #AA3; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /lib/stream-progress.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | var Value = require('mutant/value') 3 | 4 | module.exports = StreamProgress 5 | 6 | function StreamProgress ({sampleRate = 44100, duration}) { 7 | var length = 0 8 | var blockSize = 32 * 2 / 8 9 | var progress = Value(0) 10 | 11 | var result = pull.through((data) => { 12 | length += data.length / blockSize 13 | progress.set(length / (duration * sampleRate)) 14 | }) 15 | 16 | result.value = progress 17 | 18 | return result 19 | } 20 | -------------------------------------------------------------------------------- /styles/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var compile = require('micro-css') 4 | var result = '' 5 | var additional = '' 6 | 7 | fs.readdirSync(__dirname).forEach(function(file){ 8 | if (/\.mcss$/i.test(file)) { 9 | result += fs.readFileSync(path.resolve(__dirname, file), 'utf8') + '\n' 10 | } 11 | 12 | if (/\.css$/i.test(file)) { 13 | additional += fs.readFileSync(path.resolve(__dirname, file), 'utf8') + '\n' 14 | } 15 | }) 16 | 17 | module.exports = compile(result) + additional -------------------------------------------------------------------------------- /nodes/pan/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Param = require('lib/param') 3 | var Apply = require('lib/apply-param') 4 | 5 | module.exports = PanNode 6 | 7 | function PanNode (context) { 8 | var panner = context.audio.createStereoPanner() 9 | 10 | var releases = [] 11 | var obs = Processor(context, panner, panner, { 12 | offset: Param(context, panner.pan.defaultValue) 13 | }, releases) 14 | 15 | releases.push( 16 | Apply(context.audio, panner.pan, obs.offset) 17 | ) 18 | 19 | return obs 20 | } 21 | -------------------------------------------------------------------------------- /nodes/gain/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Param = require('lib/param') 3 | var Apply = require('lib/apply-param') 4 | 5 | module.exports = GainNode 6 | 7 | function GainNode (context) { 8 | var node = context.audio.createGain() 9 | node.gain.value = 0 10 | 11 | var releases = [] 12 | var obs = Processor(context, node, node, { 13 | gain: Param(context, node.gain.defaultValue) 14 | }, releases) 15 | 16 | releases.push( 17 | Apply(context.audio, node.gain, obs.gain) 18 | ) 19 | 20 | return obs 21 | } 22 | -------------------------------------------------------------------------------- /lib/quantize-to-square.js: -------------------------------------------------------------------------------- 1 | module.exports = function quantizeToSquare (value) { 2 | if (value > 16) { 3 | return Math.floor(value / 16) * 16 4 | } else if (value > 8) { 5 | return Math.floor(value / 8) * 8 6 | } else if (value > 4) { 7 | return Math.floor(value / 4) * 4 8 | } else if (value > 2) { 9 | return Math.floor(value / 2) * 2 10 | } else if (value > 1) { 11 | return Math.floor(value / 1) * 1 12 | } else if (value > 0.5) { 13 | return Math.floor(value / 0.5) * 0.5 14 | } else { 15 | return value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nodes/index.js: -------------------------------------------------------------------------------- 1 | var bulk = require('bulk-require') 2 | var nodes = bulk(__dirname, [ '*/index.js' ]) 3 | 4 | var self = module.exports = [] 5 | self.lookup = {} 6 | self.objectLookup = {} 7 | self.groupLookup = {} 8 | 9 | Object.keys(nodes).forEach(function (key) { 10 | var item = nodes[key].index 11 | self.push(item) 12 | self.lookup[item.node] = item 13 | self.objectLookup[item.node] = item.object 14 | if (item.group) { 15 | self.groupLookup[item.group] = self.groupLookup[item.group] || [] 16 | self.groupLookup[item.group].push(item) 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /lib/key-collection.js: -------------------------------------------------------------------------------- 1 | var MutantMappedArray = require('mutant/mapped-array') 2 | var Value = require('mutant/value') 3 | var doubleBind = require('lib/double-bind') 4 | 5 | module.exports = KeyCollection 6 | 7 | function KeyCollection (parentContext) { 8 | var context = Object.create(parentContext) 9 | 10 | var obs = MutantMappedArray([], item => { 11 | var key = Value() 12 | key.context = context 13 | doubleBind(item, key) 14 | return key 15 | }) 16 | 17 | context.collection = obs 18 | obs.context = parentContext 19 | 20 | return obs 21 | } 22 | -------------------------------------------------------------------------------- /nodes/external-audio-input/index.js: -------------------------------------------------------------------------------- 1 | var resolve = require('mutant/resolve') 2 | 3 | module.exports = { 4 | name: 'External Audio Input', 5 | node: 'global/external-audio-input', 6 | group: 'global-controllers', 7 | object: require('./object'), 8 | render: require('./view') 9 | } 10 | 11 | module.exports.spawners = function (context) { 12 | var inputs = resolve(context.audioDevices.input) 13 | return inputs.map(device => { 14 | return { 15 | name: module.exports.name, 16 | node: module.exports.node, 17 | port: device.label 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /lib/get-port-siblings.js: -------------------------------------------------------------------------------- 1 | module.exports = getPortSiblings 2 | 3 | function getPortSiblings (obs, controllers) { 4 | var index = null 5 | var matches = [] 6 | controllers.forEach(function (c, i) { 7 | if (c.port && c.port() === obs.port()) { 8 | if (c === obs) { 9 | index = i 10 | } 11 | matches.push(i) 12 | } 13 | }) 14 | return [ 15 | controllers.get(matches[mod(index - 1, matches.length)]), 16 | controllers.get(matches[mod(index + 1, matches.length)]) 17 | ] 18 | } 19 | 20 | 21 | function mod (n, m) { 22 | return ((n % m) + m) % m 23 | } 24 | -------------------------------------------------------------------------------- /lib/get-closest-point.js: -------------------------------------------------------------------------------- 1 | module.exports = function getClosestPoint(markers, time) { 2 | if (markers && markers.length) { 3 | var prev = 0 4 | for (var i = 0; i < markers.length; i++) { 5 | var marker = markers[i]//-0.02 6 | if (time === marker) { 7 | return time 8 | } else if (marker > time) { 9 | var diff = marker - time 10 | var prevDiff = time - prev 11 | if (diff > prevDiff) { 12 | return prev 13 | } else { 14 | return marker 15 | } 16 | } 17 | prev = marker 18 | } 19 | } 20 | return time 21 | } -------------------------------------------------------------------------------- /nodes/triggers-chunk/index.js: -------------------------------------------------------------------------------- 1 | var randomColor = require('lib/random-color') 2 | 3 | module.exports = { 4 | name: 'Triggers', 5 | node: 'chunk', 6 | group: 'chunks', 7 | description: 'A collection of triggerable audio slots.', 8 | spawn: { 9 | slots: [{id: 'output', node: 'slot'}], 10 | outputs: ['output'] 11 | }, 12 | external: function (context) { 13 | return { 14 | color: randomColor([255, 255, 255]), 15 | shape: [2, 4], 16 | minimised: true 17 | } 18 | }, 19 | renderExternal: require('./external'), 20 | render: require('./view'), 21 | object: require('./object') 22 | } 23 | -------------------------------------------------------------------------------- /lib/resolve-node.js: -------------------------------------------------------------------------------- 1 | module.exports = resolveNode 2 | 3 | function resolveNode (nodes, nodeName) { 4 | if (!nodeName) { 5 | return null 6 | } 7 | 8 | // quick lookup 9 | if (nodes[nodeName]) { 10 | return nodes[nodeName] 11 | } 12 | 13 | // walkies 14 | var node = nodes || {} 15 | while (nodeName && node) { 16 | var index = nodeName.indexOf('/') 17 | if (index < 0) { 18 | node = node[nodeName] 19 | nodeName = null 20 | } else { 21 | var key = nodeName.slice(0, index) 22 | nodeName = nodeName.slice(index + 1) 23 | node = node[key] 24 | } 25 | } 26 | return node 27 | } 28 | -------------------------------------------------------------------------------- /lib/attribute-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = AttributeHook; 4 | 5 | function AttributeHook(value) { 6 | if (!(this instanceof AttributeHook)) { 7 | return new AttributeHook(value); 8 | } 9 | this.value = value; 10 | } 11 | 12 | AttributeHook.prototype.hook = function (node, prop, prev) { 13 | 14 | node.setAttribute(prop, this.value); 15 | }; 16 | 17 | AttributeHook.prototype.unhook = function (node, prop, next) { 18 | if (next && next._type === 'AttributeHook') { 19 | return; 20 | } 21 | node.removeAttribute(prop) 22 | }; 23 | 24 | AttributeHook.prototype._type = 'AttributeHook'; 25 | -------------------------------------------------------------------------------- /lib/resolve-available.js: -------------------------------------------------------------------------------- 1 | module.exports = resolveAvailable 2 | 3 | function resolveAvailable (lookup, key, lastKey) { 4 | var base = key 5 | var incr = 1 6 | 7 | var numberMatch = /(.+) ([0-9]+)$/.exec(key) 8 | if (numberMatch) { 9 | base = numberMatch[1] 10 | incr = parseInt(numberMatch[2], 10) 11 | } 12 | 13 | var existing = (Array.isArray(lookup) ? lookup : Object.keys(lookup)).map(x => x.toLowerCase()) 14 | 15 | while (existing.includes(key.toLowerCase()) && key.toLowerCase() !== (lastKey ? lastKey.toLowerCase() : null)) { 16 | incr += 1 17 | key = base + ' ' + incr 18 | } 19 | 20 | return key 21 | } 22 | -------------------------------------------------------------------------------- /lib/watch-struct.js: -------------------------------------------------------------------------------- 1 | module.exports = watchStruct 2 | 3 | function watchStruct(struct, handlers){ 4 | var removeListeners = Object.keys(handlers).map(watch, {struct: struct, handlers: handlers}) 5 | return function unwatch(){ 6 | removeListeners.forEach(invoke) 7 | removeListeners.length = 0 8 | } 9 | } 10 | 11 | function watch(key){ 12 | var obs = this.struct[key] 13 | var handler = this.handlers[key] 14 | if (typeof obs === 'function' && typeof handler === 'function'){ 15 | return obs(handler.bind(obs)) 16 | } 17 | } 18 | 19 | function invoke(fn){ 20 | if (typeof fn === 'function'){ 21 | return fn() 22 | } 23 | } -------------------------------------------------------------------------------- /nodes/loop-grid-qwerty/README.md: -------------------------------------------------------------------------------- 1 | loop-qwerty 2 | === 3 | 4 | Qwerty keyboard bindings for [loop-grid](https://github.com/mmckegg/loop-grid). 5 | 6 | ![](http://loopjs.com/loop-drop-qwerty.png) 7 | 8 | [Watch "Using Loop Drop with a Qwerty Keyboard" on YouTube](http://youtu.be/tOpbRsDwYH4) 9 | 10 | ## Control Keys 11 | 12 | - **store**: space 13 | - **hold beat**: shift 14 | - **repeat**: 1-9 (none, 1, 2/3, 1/2, 1/3, 1/4, 1/6, 1/8, 3/4) 15 | - **suppress mode**: 0 16 | - **clear/flatten**: backspace 17 | - **undo**: - 18 | - **redo**: = 19 | - **halve length**: [ 20 | - **double length**: ] 21 | - **next input**: tab / enter 22 | - **swap input**: alt (hold) -------------------------------------------------------------------------------- /styles/toggle-chooser.mcss: -------------------------------------------------------------------------------- 1 | ToggleChooser { 2 | 3 | display: flex 4 | height: 18px 5 | 6 | span.title { 7 | margin: 3px 3px 0 0 8 | color: #CCC 9 | font-size: 90% 10 | font-weight: bold 11 | } 12 | 13 | div.choice { 14 | 15 | border: 1px solid #2A2A2A 16 | cursor: pointer 17 | padding: 1px 4px 18 | background: #3C493C 19 | border: 1px solid #2A2A2A 20 | cursor: pointer 21 | color: #9A9 22 | 23 | :hover { 24 | border-color: #3f7d55 25 | } 26 | 27 | -active { 28 | background: #3eb13e 29 | border-color: #84f55e !important 30 | color: white 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/watch-buttons.js: -------------------------------------------------------------------------------- 1 | module.exports = watchButtons 2 | 3 | function watchButtons(buttons, handlers){ 4 | var removeListeners = Object.keys(handlers).map(watch, {buttons: buttons, handlers: handlers}) 5 | return function unwatch(){ 6 | removeListeners.forEach(invoke) 7 | removeListeners.length = 0 8 | } 9 | } 10 | 11 | function watch(key){ 12 | var button = this.buttons[key] 13 | var handler = this.handlers[key] 14 | if (typeof button === 'function' && typeof handler === 'function'){ 15 | return button(handler.bind(button)) 16 | } 17 | } 18 | 19 | function invoke(fn){ 20 | if (typeof fn === 'function'){ 21 | return fn() 22 | } 23 | } -------------------------------------------------------------------------------- /nodes/bitcrusher/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var Range = require('lib/params/range') 4 | 5 | module.exports = function renderBitcrusher (node) { 6 | return h('ProcessorNode -bitcrusher', [ 7 | Header(node, h('span', 'Bitcrusher')), 8 | h('ParamList', [ 9 | Range(node.bitDepth, { 10 | title: 'bit depth', 11 | defaultValue: 8, 12 | format: 'bit', 13 | flex: true 14 | }), 15 | Range(node.frequency, { 16 | title: 'freq', 17 | defaultValue: 1, 18 | format: 'sampleRatio', 19 | flex: true 20 | }) 21 | ]) 22 | ]) 23 | } -------------------------------------------------------------------------------- /styles/helper.mcss: -------------------------------------------------------------------------------- 1 | Helper { 2 | padding: 5px 3 | color: #777 4 | text-align: center 5 | font-size: 120% 6 | 7 | -webkit-user-select: text 8 | cursor: text 9 | 10 | -warning { 11 | header { 12 | color: #E09A2C 13 | } 14 | color: #CCC 15 | } 16 | 17 | header { 18 | font-size: 150% 19 | } 20 | 21 | a { 22 | color: #5A91D8 23 | 24 | 25 | img { 26 | transition: -webkit-filter, opacity, 1s ease 27 | -webkit-filter: saturate(5%) 28 | opacity: 0.3 29 | } 30 | 31 | :hover { 32 | img { 33 | -webkit-filter: saturate(100%) 34 | opacity: 1 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /nodes/offset/object.js: -------------------------------------------------------------------------------- 1 | var Param = require('lib/param') 2 | var ObservStruct = require('mutant/struct') 3 | var Sum = require('lib/param-sum') 4 | 5 | module.exports = OffsetModulator 6 | 7 | function OffsetModulator (context) { 8 | var obs = ObservStruct({ 9 | offset: Param(context, 0), 10 | value: Param(context, 0) 11 | }) 12 | 13 | obs.context = context 14 | 15 | obs.currentValue = Sum([ 16 | obs.value.currentValue, obs.offset.currentValue 17 | ]) 18 | 19 | obs.triggerOn = function (at) { 20 | Param.triggerOn(obs, at) 21 | } 22 | 23 | obs.triggerOff = function (at) { 24 | Param.triggerOff(obs, at) 25 | } 26 | 27 | return obs 28 | } 29 | -------------------------------------------------------------------------------- /lib/double-bind.js: -------------------------------------------------------------------------------- 1 | module.exports = function doubleBind (a, b) { 2 | var updatingA = false 3 | var updatingB = false 4 | 5 | // initial sync 6 | b.set(a()) 7 | a.set(b()) 8 | 9 | var releaseA = a(function (value) { 10 | if (!updatingA && !updatingB) { 11 | updatingA = true 12 | b.set(value) 13 | updatingA = false 14 | } 15 | }) 16 | 17 | var releaseB = b(function (value) { 18 | if (!updatingA && !updatingB) { 19 | updatingB = true 20 | a.set(value) 21 | updatingB = false 22 | } 23 | }) 24 | return function release () { 25 | releaseA() 26 | releaseB() 27 | releaseA = releaseB = null 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/grid-slice-peaks.js: -------------------------------------------------------------------------------- 1 | var getPeaks = require('lib/get-peaks') 2 | 3 | module.exports = function (data, count, offset, cb) { 4 | offset = offset || [0, 1] 5 | 6 | var range = (offset[1] - offset[0]) * data.length 7 | var windowSize = Math.ceil(range / count) 8 | var start = Math.floor(offset[0] * data.length) 9 | var end = Math.floor(offset[1] * data.length) 10 | var searchStart = Math.floor(start + (windowSize / 2)) 11 | 12 | getPeaks({data, windowSize, start: searchStart, end}, (err, peaks) => { 13 | if (err) return cb(err) 14 | cb(null, [[start, 1]].concat(peaks).map(function (item) { 15 | return item[0] / data.length 16 | })) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /lib/interpolate.js: -------------------------------------------------------------------------------- 1 | module.exports = interpolate 2 | 3 | function interpolate(event, time){ 4 | var to = event.at + (event.duration||0) 5 | if (time < event.at){ 6 | return event.fromValue 7 | } else if (event.duration && time <= to){ 8 | var range = event.value - event.fromValue 9 | var pos = (time - event.at) / event.duration 10 | if (event.mode === 'exp'){ 11 | return event.fromValue + (range * (Math.pow(pos, 2))) 12 | } else if (event.mode === 'log'){ 13 | return event.fromValue + (range * (Math.pow(pos, 1/4))) 14 | } else { 15 | return event.fromValue + (range * pos) 16 | } 17 | } else { 18 | return event.value 19 | } 20 | } -------------------------------------------------------------------------------- /lib/update-param-references.js: -------------------------------------------------------------------------------- 1 | module.exports = updateParamReferences 2 | 3 | function updateParamReferences (node, oldId, newId) { 4 | var changed = false 5 | var result = JSON.stringify(node(), function (key, value) { 6 | if (value && value.node === 'linkParam' && value.param === oldId) { 7 | value = obtain(value) 8 | changed = true 9 | if (newId) { 10 | value.param = newId 11 | } else { 12 | return value.minValue 13 | } 14 | } 15 | return value 16 | }) 17 | 18 | if (changed) { 19 | node.set(JSON.parse(result)) 20 | } 21 | } 22 | 23 | function obtain (obj) { 24 | return JSON.parse(JSON.stringify(obj)) 25 | } 26 | -------------------------------------------------------------------------------- /lib/value-at-time-getter.js: -------------------------------------------------------------------------------- 1 | var resolve = require('mutant/resolve') 2 | var ParamSource = require('lib/param-source') 3 | 4 | module.exports = ValueAtTimeGetter 5 | 6 | function ValueAtTimeGetter (nodes, numberResult, reducer) { 7 | return function getValueAtTime (time) { 8 | var values = resolve(nodes).map(x => x.getValueAtTime && x.getValueAtTime(time) || 0) 9 | var numberValue = resolve(numberResult) 10 | if (ParamSource.isParam(numberValue)) { 11 | numberValue = numberValue.getValueAtTime(time) 12 | } 13 | if (typeof numberValue === 'number') { 14 | values = values.concat(numberValue) 15 | } 16 | return reducer(values) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nodes/bitcrusher/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Property = require('lib/property') 3 | var Bitcrusher = require('bitcrusher') 4 | var watch = require('mutant/watch') 5 | 6 | module.exports = BitcrusherNode 7 | 8 | function BitcrusherNode (context) { 9 | var node = Bitcrusher(context.audio, { bufferSize: 256 }) 10 | 11 | var obs = Processor(context, node, node, { 12 | bitDepth: Property(8), 13 | frequency: Property(1) 14 | }) 15 | 16 | watch(obs.bitDepth, function (value) { 17 | node.bitDepth = value 18 | }) 19 | 20 | watch(obs.frequency, function (value) { 21 | node.frequency = value 22 | }) 23 | 24 | return obs 25 | } 26 | -------------------------------------------------------------------------------- /lib/to-pcm.js: -------------------------------------------------------------------------------- 1 | var CallbackWorker = require('lib/callback-worker') 2 | 3 | module.exports = function (data, cb) { 4 | toPcm(data, (err, result) => { 5 | if (err) return cb(err) 6 | cb(null, Buffer.from(result.buffer)) 7 | }) 8 | } 9 | 10 | var toPcm = CallbackWorker(function (channelData, cb) { 11 | var channelCount = channelData.length 12 | var sampleCount = channelData[0].length 13 | 14 | var result = new Float32Array(channelCount * sampleCount) 15 | 16 | for (var i = 0; i < sampleCount; i++) { 17 | for (var c = 0; c < channelCount; c++) { 18 | result[i * channelCount + c] = channelData[c][i] 19 | } 20 | } 21 | 22 | cb(null, result, [result.buffer]) 23 | }) 24 | -------------------------------------------------------------------------------- /lib/get-sound-offset.js: -------------------------------------------------------------------------------- 1 | module.exports = function getSoundOffset(buffer){ 2 | 3 | if (!buffer) return 4 | 5 | var threshold = 0.01 6 | 7 | var data = buffer.getChannelData(0) 8 | var step = 32 9 | var width = buffer.length / step 10 | 11 | for(var i=0;i max){ 20 | max = datum 21 | } 22 | } 23 | 24 | if (Math.max(Math.abs(min), Math.abs(max)) > threshold){ 25 | var value = (i*step) / buffer.length 26 | return [value, 1] 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /nodes/multiply/object.js: -------------------------------------------------------------------------------- 1 | var Param = require('lib/param') 2 | var ObservStruct = require('mutant/struct') 3 | var Multiply = require('lib/param-multiply') 4 | 5 | module.exports = MultiplyModulator 6 | 7 | function MultiplyModulator (context) { 8 | var obs = ObservStruct({ 9 | multiplier: Param(context, 0), 10 | value: Param(context, 0) 11 | }) 12 | 13 | obs.context = context 14 | 15 | obs.currentValue = Multiply([ 16 | obs.value.currentValue, obs.multiplier.currentValue 17 | ]) 18 | 19 | obs.triggerOn = function (at) { 20 | Param.triggerOn(obs, at) 21 | } 22 | 23 | obs.triggerOff = function (at) { 24 | Param.triggerOff(obs, at) 25 | } 26 | 27 | return obs 28 | } 29 | -------------------------------------------------------------------------------- /lib/with-resolved.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var extend = require('xtend') 3 | 4 | module.exports = withResolved 5 | 6 | function withResolved (obj, keys) { 7 | var result = computed(keys.map(function (k) { return obj[k] }).concat(obj), function (args) { 8 | var resolvedValues = Array.from(arguments).slice(0, -1) 9 | var value = extend(arguments[arguments.length - 1]) 10 | keys.forEach(function (key, i) { 11 | value[key] = resolvedValues[i] 12 | }) 13 | return value 14 | }) 15 | 16 | for (var k in obj) { 17 | if (k !== 'set' && k !== 'destroy') { 18 | result[k] = obj[k] 19 | } 20 | } 21 | 22 | result.node = obj 23 | return result 24 | } 25 | -------------------------------------------------------------------------------- /lib/destroy-source-node.js: -------------------------------------------------------------------------------- 1 | var destroyerCache = new WeakMap() 2 | 3 | module.exports = destroySourceNode 4 | 5 | function destroySourceNode (node) { 6 | node.disconnect() 7 | 8 | if (node.stop) { 9 | var destroyer = destroyerCache.get(node.context) 10 | 11 | if (!destroyer) { 12 | destroyer = node.context.createGain() 13 | destroyer.gain.value = 0 14 | destroyer.connect(node.context.destination) 15 | destroyerCache.set(node.context, destroyer) 16 | } 17 | 18 | // HACK: collect disconnected nodes to ensure they stop correctly 19 | // see https://bugs.chromium.org/p/chromium/issues/detail?id=717528 20 | node.connect(destroyer) 21 | node.stop() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nodes/loop-grid-launchpad/state-lights.js: -------------------------------------------------------------------------------- 1 | var self = module.exports = function(r, g, flag){ 2 | if (!r || r < 0) r = 0 3 | if (r > 3) r = 3 4 | if (!g || g < 0) g = 0 5 | if (g > 3) g = 3 6 | if (flag == 'flash') { 7 | flag = 8 8 | } else if (flag == 'buffer') { 9 | flag = 0 10 | } else { 11 | flag = 12 12 | } 13 | 14 | return ((16 * g) + r) + flag 15 | } 16 | 17 | self.off = 0 18 | self.greenLow = self(0,1) 19 | self.greenMed = self(0,2) 20 | self.green = self(0,3) 21 | self.greenFlash = self(0,3, 'flash') 22 | self.redLow = self(1,0) 23 | self.redMed = self(2,0) 24 | self.red = self(3,0) 25 | self.amberLow = self(1,1) 26 | self.amber = self(3,3) 27 | self.yellow = self(1,3) -------------------------------------------------------------------------------- /styles/audio-meter.mcss: -------------------------------------------------------------------------------- 1 | AudioMeter { 2 | -webkit-transform: translate3d(0,0,0) 3 | margin: 3px; 4 | 5 | div { 6 | display: flex; 7 | margin: 1px; 8 | height: 6px; 9 | border-radius: 3px; 10 | overflow: hidden; 11 | border: 1px solid #222 12 | 13 | div { 14 | flex: 1 15 | border-radius: 1px 16 | background: #040 17 | 18 | -amber { 19 | background: #440 20 | -active { 21 | background: #FF0 22 | } 23 | } 24 | 25 | -red { 26 | background: #400 27 | -active { 28 | background: #F00 29 | } 30 | } 31 | 32 | -active { 33 | background: #0F0 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /lib/params/sample-chooser.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var FileEvent = require('lib/file-event') 3 | var QueryParam = require('lib/query-param') 4 | 5 | var importSample = require('lib/import-sample') 6 | 7 | module.exports = SampleChooser 8 | 9 | function SampleChooser(node, opts){ 10 | return h('input SampleChooser', { 11 | type: 'file', 12 | accept: 'audio/*,video/*', 13 | 'ev-change': FileEvent(handleChange, node) 14 | }) 15 | } 16 | 17 | function handleChange(file){ 18 | var node = this.data 19 | var context = this.data.context 20 | 21 | importSample(context, file.path, function(err, descriptor){ 22 | for (var k in descriptor){ 23 | QueryParam(node, k).set(descriptor[k]) 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /nodes/pan/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | var ToggleButton = require('lib/params/toggle-button') 5 | var QueryParam = require('lib/query-param') 6 | 7 | module.exports = function renderPanNode (node) { 8 | return h('ProcessorNode -pan', [ 9 | Header(node, h('span', 'Pan')), 10 | h('ParamList', [ 11 | ToggleButton(QueryParam(node, 'node'), { 12 | title: 'Spatial', 13 | onValue: 'processor/spatial-pan', 14 | offValue: 'processor/pan' 15 | }), 16 | ModRange(node.offset, { 17 | defaultValue: 0, 18 | format: 'pan', 19 | flex: true 20 | }) 21 | ]) 22 | ]) 23 | } 24 | -------------------------------------------------------------------------------- /lib/context-menu.js: -------------------------------------------------------------------------------- 1 | var electron = require('electron') 2 | var Menu = electron.remote.Menu 3 | var MenuItem = electron.remote.MenuItem 4 | var BrowserWindow = electron.remote.BrowserWindow 5 | 6 | var contextEvent = null 7 | var menu = new Menu() 8 | menu.append(new MenuItem({ 9 | label: 'Inspect Element', 10 | click: function() { 11 | var x = Math.round(contextEvent.clientX * window.rootContext.zoom()) 12 | var y = Math.round(contextEvent.clientY * window.rootContext.zoom()) 13 | BrowserWindow.getFocusedWindow().inspectElement(x, y) 14 | } 15 | })) 16 | 17 | window.addEventListener('contextmenu', function (e) { 18 | e.preventDefault(); 19 | contextEvent = e 20 | menu.popup(electron.remote.getCurrentWindow()) 21 | }, false) 22 | -------------------------------------------------------------------------------- /nodes/dipper/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | var Select = require('lib/params/select') 5 | 6 | var modeChoices = [ 7 | ['Modulate', 'modulate'], 8 | ['Source', 'source'] 9 | ] 10 | 11 | module.exports = function renderDipper (node) { 12 | return h('ProcessorNode -dipper', [ 13 | Header(node, h('span', 'Dipper')), 14 | h('ParamList', [ 15 | Select(node.mode, { 16 | defaultValue: 'modulate', 17 | options: modeChoices 18 | }), 19 | 20 | ModRange(node.ratio, { 21 | title: 'ratio', 22 | defaultValue: 1, 23 | format: 'ratio', 24 | flex: true 25 | }) 26 | ]) 27 | ]) 28 | } 29 | -------------------------------------------------------------------------------- /nodes/synth-chunk/index.js: -------------------------------------------------------------------------------- 1 | var randomColor = require('lib/random-color') 2 | 3 | module.exports = { 4 | name: 'Synth', 5 | node: 'chunk/synth', 6 | description: 'Basic subtractive synthesier using global scale specified.', 7 | group: 'simpleChunks', 8 | spawn: function () { 9 | return { 10 | color: randomColor(), 11 | osc1: { 12 | shape: 'sawtooth', 13 | amp: 0.4 14 | }, 15 | amp: { 16 | node: 'modulator/adsr', 17 | value: 1, 18 | attack: 0.1, 19 | release: 0.5 20 | }, 21 | filter: { 22 | type: "lowpass", 23 | frequency: 5400, 24 | Q: 1 25 | } 26 | } 27 | }, 28 | object: require('./object'), 29 | render: require('./view') 30 | } 31 | -------------------------------------------------------------------------------- /lib/map-watch-diff-stack.js: -------------------------------------------------------------------------------- 1 | var watch = require('mutant/watch') 2 | module.exports = function(values, obs, handler){ 3 | var stack = [] 4 | var lastValue = undefined 5 | return watch(obs, function(data){ 6 | data._diff&&Object.keys(data._diff).forEach(function(key){ 7 | var value = values[key] 8 | if (data._diff[key]){ 9 | stack.push(value) 10 | } else { 11 | remove(stack, value) 12 | } 13 | }) 14 | 15 | var top = stack[stack.length - 1] 16 | if (top !== lastValue) { 17 | if (top) { 18 | handler(top) 19 | } 20 | lastValue = top 21 | } 22 | }) 23 | } 24 | 25 | function remove(array, item){ 26 | var index = array.indexOf(item) 27 | if (~index){ 28 | array.splice(index, 1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /styles/value-slots.mcss: -------------------------------------------------------------------------------- 1 | ValueSlots { 2 | padding: 3px 3 | 4 | div.slot { 5 | 6 | -trigger { 7 | display: flex; 8 | padding: 3px; 9 | background: #555; 10 | border-radius: 3px; 11 | margin: 2px; 12 | 13 | span { 14 | display: block 15 | } 16 | } 17 | 18 | -spawn { 19 | padding: 3px; 20 | background: #888; 21 | border-radius: 3px; 22 | border: 1px solid #2A2A2A; 23 | cursor: pointer; 24 | width: 100%; 25 | position: relative; 26 | text-align: center; 27 | opacity: 0.5 28 | color: white 29 | 30 | :hover { 31 | opacity: 1 32 | color: #AFA 33 | background-color: #383 34 | border-color: #AFA 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nodes/noise/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | 4 | var Range = require('lib/params/range') 5 | var ModRange = require('lib/params/mod-range') 6 | var Select = require('lib/params/select') 7 | 8 | var types = [ 9 | ['White', 'white'], 10 | ['Pink', 'pink'] 11 | ] 12 | 13 | module.exports = function renderNoise (node) { 14 | return h('SourceNode -noise', [ 15 | Header(node, h('span', [ 16 | h('strong', 'Noise:'), ' ', 17 | h('span', node.type) 18 | ])), 19 | h('ParamList', [ 20 | Select(node.type, { 21 | options: types 22 | }), 23 | ModRange(node.amp, { 24 | title: 'amp', 25 | defaultValue: 1, 26 | format: 'dB', 27 | flex: true 28 | }) 29 | ]) 30 | ]) 31 | } 32 | -------------------------------------------------------------------------------- /styles/button.mcss: -------------------------------------------------------------------------------- 1 | Button { 2 | cursor: pointer; 3 | border: none; 4 | border-radius: 5px; 5 | font-size: 12px; 6 | font-size: 10px; 7 | padding: 0 4px; 8 | 9 | background: #3A3A3A; 10 | color: #C3C3C3; 11 | 12 | :hover { 13 | background: #202020; 14 | color: #FFF; 15 | } 16 | 17 | :focus { 18 | outline-style: none; 19 | color: white; 20 | box-shadow: white 0 0 4px; 21 | } 22 | 23 | -main { 24 | padding: 5px 25 | height: auto 26 | } 27 | 28 | -spawn { 29 | :hover { 30 | background: #467545 31 | } 32 | } 33 | 34 | -warn { 35 | :hover { 36 | background: #A00 37 | } 38 | } 39 | 40 | -edit { 41 | border: 1px solid #666; 42 | padding: 0 6px; 43 | background: #3A3A3A; 44 | color: #DDD; 45 | } 46 | } -------------------------------------------------------------------------------- /lib/widgets/params.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var map = require('mutant/map') 3 | var ModRange = require('lib/params/mod-range') 4 | var computed = require('mutant/computed') 5 | 6 | module.exports = renderParams 7 | 8 | function renderParams (node) { 9 | var paramNames = computed([node.params], (params) => params) 10 | return map(paramNames, function (paramName) { 11 | var param = node.paramValues.get(paramName) 12 | if (!param) { 13 | // ensure that it exists for backwards compat with old setups 14 | param = node.paramValues.put(paramName, 0) 15 | } 16 | return h('ParamList', [ 17 | ModRange(param, { 18 | title: paramName, 19 | format: 'ratio1', 20 | flex: true, 21 | allowSpawnModulator: true 22 | }) 23 | ]) 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /lib/json-file.js: -------------------------------------------------------------------------------- 1 | var watch = require('mutant/watch') 2 | 3 | var PARSE_ERROR = {} 4 | 5 | module.exports = JsonFile 6 | 7 | function JsonFile (file, listener) { 8 | var lastSaved = null 9 | 10 | watch(file, function (data) { 11 | if (lastSaved !== data) { 12 | var parsed = tryParse(data) 13 | if (parsed !== PARSE_ERROR) { 14 | lastSaved = data 15 | data = parsed 16 | listener(data) 17 | } 18 | } 19 | }) 20 | 21 | return function save (value) { 22 | var data = JSON.stringify(value, null, 2) 23 | if (lastSaved !== data) { 24 | lastSaved = data 25 | file.set(data) 26 | } 27 | } 28 | } 29 | 30 | function tryParse (data) { 31 | try { 32 | return JSON.parse(data) 33 | } catch (ex) { 34 | return PARSE_ERROR 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/link-modulator/object.js: -------------------------------------------------------------------------------- 1 | var Observ = require('mutant/value') 2 | var ObservStruct = require('mutant/struct') 3 | var computed = require('mutant/computed') 4 | 5 | var Param = require('lib/param') 6 | var Sum = require('lib/param-sum') 7 | 8 | module.exports = ParamModulator 9 | 10 | function ParamModulator (context) { 11 | var obs = ObservStruct({ 12 | param: Observ(), 13 | value: Param(context, 0) 14 | }) 15 | 16 | obs._type = 'ParamModulator' 17 | 18 | obs.context = context 19 | 20 | var offsetValue = computed([obs.param, context.paramLookup], function (param) { 21 | return context.paramLookup.get(param) 22 | }) 23 | 24 | obs.currentValue = Sum([obs.value, offsetValue]) 25 | 26 | obs.destroy = function () { 27 | Param.destroy(obs) 28 | } 29 | 30 | return obs 31 | } 32 | -------------------------------------------------------------------------------- /demo-project/DWS - OST/cspan-output.json: -------------------------------------------------------------------------------- 1 | {"slots":[{"id":"output","node":"slot","volume":0.4837,"noteOffset":0,"processors":[{"node":"processor/reverb","time":0.752,"decay":2,"reverse":false,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/overdrive","preBand":1.3341739535648693,"color":180,"postCut":{"node":"linkParam","minValue":5700,"maxValue":20,"param":"post cut","mode":"exp"},"gain":4.8752,"amp":1},{"node":"processor/ping-pong-delay","time":0.214,"sync":false,"feedback":0.6,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/filter","frequency":{"node":"linkParam","minValue":27,"maxValue":20000,"param":"freq","mode":"exp"},"Q":1,"gain":0,"type":"highpass"}]}],"inputs":["input"],"outputs":["output"],"params":["post cut","freq","wet"],"selectedSlotId":"output","node":"chunk/meddler","scale":"$global"} -------------------------------------------------------------------------------- /lib/widgets/midi-output.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Select = require('lib/params/select') 3 | var channelOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(number => [`Channel ${number}`, number]) 4 | var Range = require('lib/params/range') 5 | 6 | module.exports = function (node) { 7 | return h('ParamList', [ 8 | Select(node.outputMidiPort, { 9 | options: node.context.midiPorts.output, 10 | flex: true, 11 | missingPrefix: ' (disconnected)', 12 | includeBlank: 'No Midi Device' 13 | }), 14 | Select(node.outputMidiChannel, { 15 | options: channelOptions, 16 | flex: true 17 | }), 18 | Range(node.outputMidiTriggerOffset, { 19 | title: 'offset', 20 | defaultValue: 0, 21 | format: 'syncMs', 22 | flex: 'small' 23 | }) 24 | ]) 25 | } 26 | -------------------------------------------------------------------------------- /styles/controller-node.mcss: -------------------------------------------------------------------------------- 1 | ControllerNode { 2 | position: relative 3 | margin-top: 6px 4 | background: #464545; 5 | border: 1px #444 solid; 6 | color: #EEE; 7 | border-radius: 4px; 8 | width: 260px; 9 | overflow: hidden 10 | 11 | header { 12 | display: flex 13 | padding: 5px 5px 14 | 15 | span { 16 | flex: 1 17 | } 18 | 19 | button.remove { 20 | opacity: 0.2 21 | margin: -1px 0 22 | } 23 | 24 | :hover { 25 | button.remove { 26 | opacity: 1 27 | } 28 | } 29 | } 30 | 31 | -input { 32 | border-color: #6D9466 33 | header { 34 | background-color: #4B5645 35 | } 36 | } 37 | 38 | section { 39 | 40 | display: flex 41 | flex-direction: column 42 | 43 | div.controls { 44 | flex: 1 45 | } 46 | 47 | } 48 | 49 | 50 | } -------------------------------------------------------------------------------- /nodes/cymbal/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | 5 | module.exports = function renderDrumSynth (node) { 6 | return h('SourceNode -drumSynth', [ 7 | Header(node, h('span', [ 8 | h('strong', 'Drum Synth:'), ' Cymbal' 9 | ])), 10 | h('ParamList', [ 11 | ModRange(node.amp, { 12 | title: 'amp', 13 | defaultValue: 1, 14 | format: 'dB', 15 | flex: true 16 | }), 17 | ModRange(node.decay, { 18 | title: 'decay', 19 | defaultValue: 0.3, 20 | format: 'ms', 21 | flex: true 22 | }), 23 | ModRange(node.tune, { 24 | title: 'tune', 25 | defaultValue: 0, 26 | format: 'cents+', 27 | flex: true 28 | }) 29 | ]) 30 | ]) 31 | } 32 | -------------------------------------------------------------------------------- /nodes/pitchshift/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var Range = require('lib/params/range') 4 | var ModRange = require('lib/params/mod-range') 5 | 6 | module.exports = function renderPitchshift (node) { 7 | return h('ProcessorNode -pitchshift', [ 8 | Header(node, h('span', 'Pitchshift')), 9 | h('ParamList', [ 10 | Range(node.transpose, { 11 | title: 'transpose', 12 | defaultValue: 12, 13 | format: 'semitoneUp', 14 | flex: true 15 | }), 16 | ModRange(node.wet, { 17 | title: 'wet', 18 | defaultValue: 1, 19 | format: 'dB', 20 | flex: true 21 | }), 22 | ModRange(node.dry, { 23 | title: 'dry', 24 | defaultValue: 0, 25 | format: 'dB', 26 | flex: true 27 | }) 28 | ]) 29 | ]) 30 | } -------------------------------------------------------------------------------- /lib/index-param.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | 3 | module.exports = IndexParam 4 | 5 | function IndexParam (param, index, formatter) { 6 | var result = computed([param], function (value) { 7 | var val = Array.isArray(value) ? value : [] 8 | return val[index] 9 | }) 10 | 11 | result.set = function (value) { 12 | var current = read(param) 13 | var val = Array.isArray(current) ? current : [] 14 | var res = val.slice() 15 | if (typeof formatter === 'function') { 16 | value = formatter(value) 17 | } 18 | res[index] = value 19 | param.set(res) 20 | } 21 | 22 | result.context = param.context 23 | return result 24 | } 25 | 26 | function read (target) { 27 | if (typeof target === 'function') { 28 | return target() 29 | } else if (target && target.read) { 30 | return target.read() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /nodes/midi-out/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | 5 | module.exports = function renderMidiOut (node) { 6 | return h('SourceNode -midiOut', [ 7 | Header(node, h('span', [ 8 | h('strong', 'MIDI Note Output') 9 | ])), 10 | h('ParamList', [ 11 | ModRange(node.note, { 12 | title: 'pitch', 13 | format: 'midi', 14 | defaultValue: 69, 15 | flex: true 16 | }), 17 | ModRange(node.velocity, { 18 | title: 'velocity', 19 | defaultValue: 100, 20 | format: 'midi', 21 | flex: true 22 | }), 23 | ModRange(node.aftertouch, { 24 | title: 'aftertouch', 25 | defaultValue: 0, 26 | format: 'midi', 27 | flex: true 28 | }) 29 | ]) 30 | ]) 31 | } 32 | -------------------------------------------------------------------------------- /lib/flash-array.js: -------------------------------------------------------------------------------- 1 | var Observ = require('mutant/value') 2 | 3 | module.exports = FlashArray 4 | 5 | function FlashArray () { 6 | var obs = Observ([]) 7 | var active = [] 8 | var refreshing = false 9 | 10 | obs.flash = function(i, value, duration) { 11 | 12 | var obj = [i, value] 13 | active.push(obj) 14 | refresh() 15 | 16 | setTimeout(function() { 17 | active.splice(active.indexOf(obj), 1) 18 | refresh() 19 | }, duration) 20 | 21 | } 22 | 23 | return obs 24 | 25 | function refresh () { 26 | if (!refreshing) { 27 | refreshing = true 28 | window.requestAnimationFrame(refreshNow) 29 | } 30 | } 31 | 32 | function refreshNow() { 33 | obs.set(active.reduce(flatten, [])) 34 | refreshing = false 35 | } 36 | } 37 | 38 | function flatten(result, obj) { 39 | result[obj[0]] = obj[1] 40 | return result 41 | } -------------------------------------------------------------------------------- /lib/mouse-position-event.js: -------------------------------------------------------------------------------- 1 | module.exports = function (fn, data, opts) { 2 | var handler = { 3 | fn: fn, 4 | data: data || {}, 5 | opts: opts || {}, 6 | handleEvent: handle 7 | } 8 | return handler 9 | } 10 | 11 | function handle (ev) { 12 | ev.stopPropagation() 13 | var box = ev.currentTarget.getBoundingClientRect() 14 | this.fn({ 15 | x: ev.clientX, 16 | y: ev.clientY, 17 | offsetWidth: ev.currentTarget.offsetWidth, 18 | offsetHeight: ev.currentTarget.offsetHeight, 19 | offsetX: ev.clientX - box.left, 20 | offsetY: ev.clientY - box.top, 21 | dataTransfer: ev.dataTransfer, 22 | currentTarget: ev.currentTarget, 23 | 24 | ctrlKey: ev.ctrlKey, 25 | shiftKey: ev.shiftKey, 26 | altKey: ev.altKey, 27 | metaKey: ev.metaKey, 28 | 29 | event: ev, 30 | target: ev.target, 31 | data: this.data 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /nodes/oscillator/shape-choices.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | ['Basic Shapes', [ 3 | ['Sine', 'sine'], 4 | ['Square', 'square'], 5 | ['Pulse', 'pulse'], // spawns source/oscillator-pulse 6 | ['Sawtooth', 'sawtooth'], 7 | ['Triangle', 'triangle'] 8 | ]], 9 | ['Wave Tables', [ 10 | ['Bass', 'bass'], 11 | ['Bass Fuzz', 'bass-fuzz'], 12 | ['Bass Sub', 'bass-sub'], 13 | ['Brass', 'brass'], 14 | ['Disonant', 'disonant'], 15 | ['Guitar Fuzz', 'guitar-fuzz'], 16 | ['Organ', 'organ'], 17 | ['Piano', 'piano'], 18 | ['Soft Saw', 'soft-saw'], 19 | ['Strings', 'strings'], 20 | ['Throaty', 'throaty'], 21 | ['Trombone', 'trombone'], 22 | ['Wide', 'wide'], 23 | ['Wurlitzer', 'wurlitzer'] 24 | ]], 25 | ['Phonemes', [ 26 | ['Ah', 'ah'], 27 | ['Ee', 'ee'], 28 | ['Ow', 'ow'], 29 | ['Ooh', 'ooh'] 30 | ]] 31 | ] 32 | -------------------------------------------------------------------------------- /nodes/external-chunk/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var computed = require('mutant/computed') 4 | 5 | module.exports = function renderExternal (node) { 6 | var lookup = node.context.nodeInfo.lookup 7 | return computed(node.nodeName, function (nodeName) { 8 | var info = lookup[nodeName] 9 | if (info && info.renderExternal) { 10 | return info.renderExternal(node) 11 | } else { 12 | var collection = node.context.collection 13 | return h('div ExternalNode', { 14 | style: { 15 | border: '2px solid transparent' 16 | } 17 | }, [ 18 | h('header', [ 19 | h('span', [node.id, ' (external)']), 20 | h('button.remove Button -warn', { 21 | 'ev-click': send(collection.remove, node) 22 | }, 'X') 23 | ]) 24 | ]) 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /lib/param-from-number.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var Apply = require('lib/apply-param') 3 | var destroySourceNode = require('lib/destroy-source-node') 4 | 5 | module.exports = function NumberParam (audioContext, value) { 6 | var releases = [] 7 | var result = audioContext.createGain() 8 | var target = result.gain 9 | 10 | return computed([value], function (value) { 11 | return result 12 | }, { 13 | comparer: (a, b) => a === b, 14 | onListen: function () { 15 | var voltage = audioContext.createConstantSource() 16 | voltage.start() 17 | voltage.connect(result) 18 | releases.push( 19 | Apply(audioContext, target, value), 20 | () => destroySourceNode(voltage) 21 | ) 22 | }, 23 | onUnlisten: function () { 24 | while (releases.length) { 25 | releases.pop()() 26 | } 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/on-trigger.js: -------------------------------------------------------------------------------- 1 | var Event = require('geval') 2 | var watch = require('mutant/watch') 3 | 4 | module.exports = function (items) { 5 | var releases = [] 6 | var unwatch = null 7 | 8 | var result = Event(function (broadcast) { 9 | unwatch = watch(items, rebind) 10 | function rebind () { 11 | release() 12 | items.forEach(function (item, i) { 13 | if (item && item.node && item.node.onTrigger) { 14 | releases.push(item.node.onTrigger(function (data) { 15 | if (data.event === 'start') { 16 | broadcast(i) 17 | } 18 | })) 19 | } 20 | }) 21 | } 22 | }) 23 | 24 | result.destroy = function () { 25 | release() 26 | unwatch() 27 | } 28 | 29 | return result 30 | // scoped 31 | 32 | function release () { 33 | while (releases.length) { 34 | releases.pop()() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/sustained.js: -------------------------------------------------------------------------------- 1 | var watch = require('mutant/watch') 2 | var computed = require('mutant/computed') 3 | var Value = require('mutant/value') 4 | 5 | module.exports = Sustained 6 | 7 | // only broadcast value changes once a truthy value has stayed constant for more than timeThreshold 8 | 9 | function Sustained (obs, timeThreshold, checkUpdateImmediately) { 10 | var outputValue = Value(obs()) 11 | 12 | return computed(outputValue, v => v, { 13 | onListen: () => watch(obs, onChange) 14 | }) 15 | 16 | function onChange (value) { 17 | if (checkUpdateImmediately && checkUpdateImmediately(value)) { // update immediately for falsy values 18 | clearTimeout() 19 | update() 20 | } else if (value !== outputValue()) { 21 | clearTimeout() 22 | setTimeout(update, timeThreshold) 23 | } 24 | } 25 | 26 | function update () { 27 | outputValue.set(obs()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nodes/loop-grid/compute-targets.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var ArrayGrid = require('array-grid') 3 | var DictToCollection = require('mutant/dict-to-collection') 4 | var Lookup = require('mutant/lookup') 5 | 6 | module.exports = function computeTargets (chunkLookup, positions, shape) { 7 | var chunkGrids = Lookup(DictToCollection(chunkLookup), function (pair) { 8 | return [pair.key, pair.value.resolvedGrid] 9 | }) 10 | 11 | return computed([chunkGrids, positions, shape], function (chunkGrids, positions, shape) { 12 | var grid = ArrayGrid([], shape) 13 | if (chunkGrids && positions) { 14 | Object.keys(positions).forEach(function (id) { 15 | var origin = positions[id] 16 | if (chunkGrids[id] && Array.isArray(origin)) { 17 | grid.place(origin[0], origin[1], chunkGrids[id]) 18 | } 19 | }) 20 | } 21 | return grid.data 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /nodes/ring-modulator/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | 5 | var Select = require('lib/params/select') 6 | var shapeChoices = require('../oscillator/shape-choices') 7 | 8 | module.exports = function renderOscillator (node) { 9 | return h('ProcessorNode -ringModulator', [ 10 | Header(node, h('span', [ 11 | 'Ring Modulator' 12 | ])), 13 | h('ParamList', [ 14 | Select(node.carrier.shape, { 15 | options: shapeChoices 16 | }), 17 | ModRange(node.carrier.amp, { 18 | title: 'amp', 19 | defaultValue: 1, 20 | format: 'dB', 21 | flex: true 22 | }), 23 | ModRange(node.carrier.frequency, { 24 | title: 'frequency', 25 | format: 'arfo', 26 | flex: true, 27 | defaultValue: 440 28 | }) 29 | ]) 30 | ]) 31 | } 32 | -------------------------------------------------------------------------------- /scripts/export-recording.js: -------------------------------------------------------------------------------- 1 | var Path = require('path') 2 | var fs = require('fs') 3 | var spawn = require('child_process').spawn 4 | 5 | var input = process.argv[2] 6 | var output = process.argv[3] 7 | 8 | if (!input || !output) { 9 | throw new Error('Must specify input.json and output file') 10 | } 11 | 12 | var dir = Path.dirname(input) 13 | var base = Path.basename(input, '.json') 14 | 15 | fs.readdir(dir, function (err, files) { 16 | if (err) throw err 17 | files = files.filter(f => matches(base, f)) 18 | 19 | fs.writeFileSync(output + '.txt', files.map(x => `file '${Path.join(dir, x)}'`).join('\n')) 20 | var args = ['-f', 'concat', '-safe', '0', '-i', output + '.txt', '-af', 'volume=0.8', output] 21 | spawn('ffmpeg', args, {stdio: 'inherit'}) 22 | }) 23 | 24 | function matches (base, fileName) { 25 | var match = fileName.match(/^(.+)-([0-9]+)\.wav$/) 26 | return (match && match[1] === base) 27 | } 28 | -------------------------------------------------------------------------------- /lib/callback-worker.js: -------------------------------------------------------------------------------- 1 | module.exports = CallbackWorker 2 | 3 | function CallbackWorker (fn) { 4 | var callbacks = {} 5 | var count = 0 6 | 7 | var worker = new global.Worker(global.URL.createObjectURL( 8 | new global.Blob([`(${WorkerProcess.toString()})(this, ${fn.toString()})`]) 9 | )) 10 | 11 | worker.onmessage = function (e) { 12 | if (callbacks[e.data.id]) { 13 | callbacks[e.data.id](e.data.error, e.data.result) 14 | delete callbacks[e.data.id] 15 | } 16 | } 17 | 18 | return function (arg, cb) { 19 | callbacks[count] = cb 20 | worker.postMessage({arg, id: count}) 21 | count += 1 22 | } 23 | } 24 | 25 | function WorkerProcess (worker, fn) { 26 | worker.onmessage = function (e) { 27 | var id = e.data.id 28 | fn(e.data.arg, cb) 29 | function cb (err, result, transferables) { 30 | worker.postMessage({err, result, id}, transferables) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/cleaner.js: -------------------------------------------------------------------------------- 1 | module.exports = Cleaner 2 | 3 | function Cleaner (audioContext) { 4 | // rubbish collection day 5 | var pendingCleanUp = [] 6 | 7 | var timer = setInterval(function () { 8 | for (var i = pendingCleanUp.length - 1; i >= 0; i--) { 9 | var event = pendingCleanUp[i] 10 | // HACK: if the offset is less than 0.3, cancelled chokes may still be muted 11 | if (event.to && event.to < (audioContext.currentTime - 0.3)) { 12 | // if (event.to && event.to < audioContext.currentTime) { 13 | event.destroy() 14 | pendingCleanUp.splice(i, 1) 15 | } 16 | } 17 | }, 500) 18 | 19 | pendingCleanUp.clear = function () { 20 | while (pendingCleanUp.length) { 21 | pendingCleanUp.pop().destroy() 22 | } 23 | } 24 | 25 | pendingCleanUp.destroy = function () { 26 | clearInterval(timer) 27 | pendingCleanUp.clear() 28 | } 29 | 30 | return pendingCleanUp 31 | } 32 | -------------------------------------------------------------------------------- /lib/get-peaks.js: -------------------------------------------------------------------------------- 1 | var CallbackWorker = require('lib/callback-worker') 2 | module.exports = CallbackWorker((opts, cb) => { 3 | var result = [] 4 | var highestValue = 0 5 | var highestValuePos = 0 6 | var lastCross = 0 7 | var data = opts.data 8 | var windowSize = opts.windowSize 9 | var start = Math.max(0, opts.start) 10 | var step = opts.step || opts.windowSize 11 | var end = Math.min(opts.end, data.length) 12 | 13 | for (var i = start; i < end; i += step) { 14 | for (var pos = i; pos < windowSize + i; pos++) { 15 | var value = data[pos] 16 | 17 | if (value === 0 || (pos - lastCross > 128 && value < 0.01)) { 18 | lastCross = pos 19 | } 20 | 21 | if (value > highestValue) { 22 | highestValue = value 23 | highestValuePos = pos 24 | } 25 | } 26 | result.push([highestValuePos, highestValue]) 27 | highestValue = 0 28 | } 29 | 30 | cb(null, result) 31 | }) 32 | -------------------------------------------------------------------------------- /lib/param-square.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var ParamTransform = require('lib/param-transform') 3 | var ParamSource = require('lib/param-source') 4 | 5 | module.exports = ParamSquare 6 | 7 | function ParamSquare (input) { 8 | return computed([input.currentValue || input], lambda, { 9 | comparer: ParamTransform.deepEqual 10 | }) 11 | } 12 | 13 | function lambda (value) { 14 | if (value instanceof global.AudioNode) { 15 | return paramApplySquare(value) 16 | } else if (ParamSource.isParam(value)) { 17 | // lazy params 18 | return ParamSource.reduce([value], (values) => square(values[0])) 19 | } else { 20 | return square(value) 21 | } 22 | } 23 | 24 | function square (value) { 25 | return value * value 26 | } 27 | 28 | function paramApplySquare (param) { 29 | var output = param.context.createGain() 30 | param.connect(output) 31 | param.connect(output.gain) 32 | return output 33 | } 34 | -------------------------------------------------------------------------------- /nodes/eq/params.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var ModRange = require('lib/params/mod-range') 3 | 4 | module.exports = function eqParams(node) { 5 | return h('ParamList', [ 6 | ModRange(node.low, { 7 | title: 'low', 8 | defaultValue: 1, 9 | format: 'dBn', 10 | flex: 'small' 11 | }), 12 | ModRange(node.mid, { 13 | title: 'mid', 14 | defaultValue: 1, 15 | format: 'dBn', 16 | flex: 'small' 17 | }), 18 | ModRange(node.high, { 19 | title: 'high', 20 | defaultValue: 1, 21 | format: 'dBn', 22 | flex: 'small' 23 | }), 24 | 25 | ModRange(node.lowcut, { 26 | title: 'lowcut', 27 | format: 'arfo', 28 | flex: 'small', 29 | defaultValue: 0 30 | }), 31 | 32 | ModRange(node.highcut, { 33 | title: 'highcut', 34 | format: 'arfo', 35 | flex: 'small', 36 | defaultValue: 20000 37 | }) 38 | 39 | ]) 40 | } -------------------------------------------------------------------------------- /lib/widgets/spawner.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var spawnNode = require('lib/spawn-node') 4 | 5 | module.exports = Spawner 6 | 7 | function Spawner (collection, options) { 8 | var buttons = [] 9 | 10 | for (var i = 0; i < options.nodes.length; i++) { 11 | var descriptor = options.nodes[i] 12 | if (descriptor && descriptor.spawn !== false) { 13 | buttons.push(h('button Button -main -spawn', { 14 | 'title': descriptor.description || '', 15 | 'ev-click': send(spawn, { 16 | descriptor: descriptor, 17 | collection: collection, 18 | onSpawn: options.onSpawn 19 | }) 20 | }, '+ ' + descriptor.name)) 21 | } 22 | } 23 | 24 | return h('NodeSpawner', buttons) 25 | } 26 | 27 | function spawn (opts) { 28 | spawnNode(opts.collection, opts.descriptor.node, function (err, node) { 29 | opts.onSpawn && opts.onSpawn(node) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /nodes/quantize/object.js: -------------------------------------------------------------------------------- 1 | var Quantize = require('lib/param-quantize') 2 | var Param = require('lib/param') 3 | var Property = require('lib/property') 4 | var ObservStruct = require('mutant/struct') 5 | var computed = require('mutant/computed') 6 | var Sum = require('lib/param-sum') 7 | 8 | module.exports = QuantizeModulator 9 | 10 | function QuantizeModulator (context) { 11 | var obs = ObservStruct({ 12 | grid: Property(1), 13 | offset: Param(context, 0), 14 | value: Param(context, 0) 15 | }) 16 | 17 | obs.context = context 18 | 19 | var quantized = computed(obs.grid, grid => { 20 | return Quantize(obs.value.currentValue, grid) 21 | }) 22 | 23 | obs.currentValue = Sum([ 24 | quantized, obs.offset.currentValue 25 | ]) 26 | 27 | obs.triggerOn = function (at) { 28 | Param.triggerOn(obs, at) 29 | } 30 | 31 | obs.triggerOff = function (at) { 32 | Param.triggerOff(obs, at) 33 | } 34 | 35 | return obs 36 | } 37 | -------------------------------------------------------------------------------- /nodes/chromatic-chunk/index.js: -------------------------------------------------------------------------------- 1 | var randomColor = require('lib/random-color') 2 | 3 | module.exports = { 4 | name: 'Chromatic', 5 | node: 'chunk/scale', 6 | group: 'chunks', 7 | description: 'Describe a single audio slot that is chromatically scaled over specified shape.', 8 | spawn: { 9 | templateSlot: { 10 | id: { $param: 'id' }, 11 | noteOffset: { 12 | node: 'modulator/scale', 13 | value: { $param: 'value' }, 14 | offset: { $param: 'offset' }, 15 | scale: { $param: 'scale' } 16 | }, 17 | node: 'slot', 18 | output: 'output' 19 | }, 20 | outputs: ['output'], 21 | slots: [{id: 'output', node: 'slot'}], 22 | selectedSlotId: '$template' 23 | }, 24 | external: function (context) { 25 | return { 26 | color: randomColor() 27 | } 28 | }, 29 | renderExternal: require('./external'), 30 | render: require('./view'), 31 | object: require('./object') 32 | } 33 | -------------------------------------------------------------------------------- /lib/midi-clock-offset.js: -------------------------------------------------------------------------------- 1 | var Value = require('mutant/value') 2 | 3 | module.exports = function MidiClockOffset (audioContext) { 4 | var obs = Value(window.performance.now() - audioContext.currentTime * 1000) 5 | var clockDriftChecker = audioContext.createScriptProcessor(1024 * 8, 0, 1) 6 | var lastDifference = 0 7 | 8 | clockDriftChecker.onaudioprocess = function (e) { 9 | var currentOffset = Math.round(window.performance.now() - audioContext.currentTime * 1000) 10 | var difference = currentOffset - obs() 11 | 12 | // remove jitter 13 | if (lastDifference !== -difference && currentOffset !== obs()) { 14 | lastDifference = currentOffset - obs() 15 | obs.set(Math.round(currentOffset)) 16 | } 17 | } 18 | 19 | clockDriftChecker.connect(audioContext.destination) 20 | 21 | obs.destroy = function () { 22 | clockDriftChecker.disconnect() 23 | clockDriftChecker.onaudioprocess = null 24 | } 25 | 26 | return obs 27 | } 28 | -------------------------------------------------------------------------------- /nodes/filter/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Property = require('lib/property') 3 | var Param = require('lib/param') 4 | var Apply = require('lib/apply-param') 5 | var Clamp = require('lib/param-clamp') 6 | 7 | module.exports = FilterNode 8 | 9 | function FilterNode (context) { 10 | var node = context.audio.createBiquadFilter() 11 | 12 | var releases = [] 13 | var obs = Processor(context, node, node, { 14 | frequency: Param(context, node.frequency.defaultValue), 15 | Q: Param(context, node.Q.defaultValue), 16 | gain: Param(context, node.gain.defaultValue), 17 | type: Property(node.type) 18 | }, releases) 19 | 20 | obs.type(function (value) { 21 | node.type = value 22 | }) 23 | 24 | releases.push( 25 | Apply(context.audio, node.frequency, Clamp(obs.frequency, 20, 20000)), 26 | Apply(context.audio, node.Q, obs.Q), 27 | Apply(context.audio, node.gain, obs.gain) 28 | ) 29 | 30 | return obs 31 | } 32 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/bllooper.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","node":"slot","noteOffset":0,"volume":1,"processors":[{"node":"processor/pitchshift","transpose":12,"wet":0.1229,"dry":0.8941},{"node":"processor/reverb","time":0.129,"decay":2,"reverse":false,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/delay","time":0.25,"sync":true,"feedback":0.6,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/overdrive","preBand":0.5,"color":135,"postCut":3000,"gain":1,"amp":1}]}],"outputs":["output"],"templateSlot":{"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"node":"slot","output":"output","volume":1,"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.6,"release":4.1,"attack":7.6},"noteOffset":0,"octave":-1,"detune":0,"shape":"sine"}]},"selectedSlotId":"output","scale":"$global","inputs":[],"params":[]} -------------------------------------------------------------------------------- /lib/detect-peaks.js: -------------------------------------------------------------------------------- 1 | var getPeaks = require('lib/get-peaks') 2 | 3 | module.exports = function (data, count, offset, cb) { 4 | offset = offset || [0, 1] 5 | 6 | var range = (offset[1] - offset[0]) * data.length 7 | var step = Math.ceil(range / count / 2) 8 | var windowSize = Math.ceil(range / count) 9 | var start = Math.floor(offset[0] * data.length) 10 | var end = Math.floor(offset[1] * data.length) 11 | 12 | getPeaks({data, windowSize, step, start, end}, (err, peaks) => { 13 | if (err) return cb(err) 14 | peaks = [[start, 1]].concat(peaks) 15 | cb(peaks.filter(function (peak, i) { 16 | var prev = peaks[i - 1] 17 | return peak[0] < end && (!prev || (peak[0] - prev[0]) > step / 4) 18 | }).sort(function (a, b) { 19 | return a[1] - b[1] 20 | }).slice(-count).sort(function (a, b) { 21 | return a[0] - b[0] 22 | }).map(function (item) { 23 | return Math.min(offset[1], item[0] / data.length) 24 | })) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /lib/observ-keys.js: -------------------------------------------------------------------------------- 1 | var ObservStruct = require('mutant/struct') 2 | var Observ = require('mutant/value') 3 | var watch = require('mutant/watch') 4 | 5 | module.exports = ObservKeys 6 | 7 | function ObservKeys (obs, mapping) { 8 | var keys = Object.keys(mapping) 9 | var lookup = keys.reduce(function (result, key) { 10 | if (Array.isArray(mapping[key])) { 11 | mapping[key].forEach(function (k) { 12 | result[k] = key 13 | }) 14 | } else { 15 | result[mapping[key]] = key 16 | } 17 | return result 18 | }, {}) 19 | 20 | var obj = keys.reduce(function (result, key) { 21 | result[key] = Observ(null) 22 | return result 23 | }, {}) 24 | 25 | var struct = ObservStruct(obj) 26 | 27 | watch(obs, function (down) { 28 | struct.set(down.reduce(function (result, code) { 29 | var key = lookup[code] 30 | if (key != null) result[key] = true 31 | return result 32 | }, {})) 33 | }) 34 | 35 | return struct 36 | } 37 | -------------------------------------------------------------------------------- /nodes/overdrive/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | 5 | module.exports = function renderOverdrive (node) { 6 | return h('ProcessorNode -overdrive', [ 7 | Header(node, h('span', 'Overdrive')), 8 | h('ParamList', [ 9 | ModRange(node.preBand, { 10 | title: 'pre band', 11 | defaultValue: 0.5, 12 | format: 'ratio', 13 | flex: true 14 | }), 15 | ModRange(node.color, { 16 | title: 'color', 17 | defaultValue: 800, 18 | format: 'arfo', 19 | flex: true 20 | }), 21 | ModRange(node.gain, { 22 | title: 'gain', 23 | defaultValue: 1, 24 | format: 'dB', 25 | flex: true 26 | }), 27 | ModRange(node.postCut, { 28 | title: 'post cut', 29 | defaultValue: 3000, 30 | format: 'arfo', 31 | flex: true 32 | }) 33 | ]) 34 | ]) 35 | } 36 | -------------------------------------------------------------------------------- /lib/flag-param.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | 3 | module.exports = FlagParam 4 | 5 | function FlagParam (param, flag) { 6 | var result = computed([param], function (value) { 7 | return Array.isArray(value) ? !!~value.indexOf(flag) : false 8 | }) 9 | 10 | result.set = function (value) { 11 | var current = read(param) 12 | var val = Array.isArray(current) ? current.slice() : [] 13 | 14 | var index = val.indexOf(flag) 15 | var currentState = !!~index 16 | var newState = !!value 17 | 18 | if (newState !== currentState) { 19 | if (newState) { 20 | val.push(flag) 21 | } else { 22 | val.splice(index, 1) 23 | } 24 | } 25 | 26 | param.set(val) 27 | } 28 | 29 | result.context = param.context 30 | return result 31 | } 32 | 33 | function read (target) { 34 | if (typeof target === 'function') { 35 | return target() 36 | } else if (target && target.read) { 37 | return target.read() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/global-controller.js: -------------------------------------------------------------------------------- 1 | var MidiStream = require('midi-stream') 2 | var extend = require('xtend') 3 | 4 | var portMatch = [ 5 | [/$Launch Control/, 'global-controller/launch-control'] 6 | ] 7 | 8 | var portChoices = Observ([]) 9 | MidiStream.watchPortNames(function (ports) { 10 | obs.portChoices.set(ports.filter(matchPort)) 11 | }) 12 | 13 | 14 | function GlobalController (context) { 15 | var obs = Node(context) 16 | obs.portChoices = portChoices 17 | 18 | var lastPortName = null 19 | obs(function (data) { 20 | if (data && data.port !== lastPortName) { 21 | lastPortName = data.port 22 | var nodeName = matchPort(data.port) 23 | if (nodeName) { 24 | obs.set(extend(data, { 25 | node: nodeName 26 | })) 27 | } 28 | } 29 | }) 30 | 31 | return obs 32 | } 33 | 34 | function matchPort (name) { 35 | for (var i=0;i { 9 | var newLength = (Array.isArray(shape) && shape[0] * shape[1]) || 0 10 | var currentLength = result.length 11 | if (currentLength !== newLength) { 12 | for (var i = currentLength; i < newLength; i++) { 13 | result[i] = i 14 | } 15 | result.length = newLength 16 | return result 17 | } else { 18 | return computed.NO_CHANGE 19 | } 20 | }) 21 | } 22 | 23 | function deepMatch (obs) { 24 | var lastValue = null 25 | return computed(obs, function (value) { 26 | value = value == null ? null : value 27 | if (!deepEqual(lastValue, value)) { 28 | lastValue = JSON.parse(JSON.stringify(value)) 29 | return value 30 | } else { 31 | return computed.NO_CHANGE 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /nodes/compressor/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | 5 | module.exports = function renderCompressor (node){ 6 | return h('ProcessorNode -compressor', [ 7 | Header(node, h('span', 'Compressor')), 8 | h('ParamList', [ 9 | ModRange(node.threshold, { 10 | title: 'threshold', 11 | format: 'dBn', 12 | flex: true 13 | }), 14 | ModRange(node.knee, { 15 | title: 'knee', 16 | format: 'dBn', 17 | flex: true 18 | }), 19 | ModRange(node.ratio, { 20 | title: 'ratio', 21 | format: 'compressionRatio', 22 | flex: true 23 | }), 24 | ModRange(node.attack, { 25 | title: 'attack', 26 | format: 'ms', 27 | flex: true 28 | }), 29 | ModRange(node.release, { 30 | title: 'release', 31 | format: 'ms', 32 | flex: true 33 | }) 34 | ]) 35 | ]) 36 | } 37 | -------------------------------------------------------------------------------- /nodes/hold/object.js: -------------------------------------------------------------------------------- 1 | var ParamSource = require('lib/param-source') 2 | var Param = require('lib/param') 3 | var ObservStruct = require('mutant/struct') 4 | var resolve = require('mutant/resolve') 5 | 6 | module.exports = HoldModulator 7 | 8 | function HoldModulator (context) { 9 | var obs = ObservStruct({ 10 | attack: Param(context, 0), 11 | value: Param(context, 0) 12 | }) 13 | 14 | obs.context = context 15 | obs.currentValue = ParamSource(context, 0) 16 | 17 | obs.triggerOn = function (at) { 18 | var value = obs.value.getValueAtTime(at) 19 | var attackTime = obs.attack.getValueAtTime(at) 20 | if (attackTime) { 21 | obs.currentValue.cancelScheduledValues(at) 22 | obs.currentValue.setTargetAtTime(value, at, attackTime / 8) 23 | } else { 24 | obs.currentValue.setValueAtTime(value, at) 25 | } 26 | Param.triggerOn(obs, at) 27 | } 28 | 29 | obs.triggerOff = function (at) { 30 | Param.triggerOff(obs, at) 31 | } 32 | 33 | return obs 34 | } 35 | -------------------------------------------------------------------------------- /demo-project/DWS - Beep Boop/Perc-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "slots": [ 3 | { 4 | "id": "output", 5 | "output": null, 6 | "volume": 1, 7 | "sources": [], 8 | "processors": [ 9 | { 10 | "threshold": -45.67430775096497, 11 | "knee": 30, 12 | "ratio": 12, 13 | "attack": 0.003000000026077032, 14 | "release": 0.25, 15 | "node": "processor/compressor" 16 | }, 17 | { 18 | "time": 0.461, 19 | "decay": 2.88, 20 | "reverse": true, 21 | "cutoff": 20000, 22 | "filterType": "lowpass", 23 | "wet": 1, 24 | "dry": 1, 25 | "node": "processor/reverb" 26 | } 27 | ], 28 | "noteOffset": 0, 29 | "node": "slot" 30 | } 31 | ], 32 | "inputs": [ 33 | "input" 34 | ], 35 | "outputs": [ 36 | "output" 37 | ], 38 | "params": [], 39 | "selectedSlotId": "output", 40 | "node": "chunk/meddler", 41 | "scale": "$global" 42 | } -------------------------------------------------------------------------------- /lib/scale-interpolate.js: -------------------------------------------------------------------------------- 1 | module.exports = scaleInterpolate 2 | 3 | function scaleInterpolate (currentValue, toValue, state) { 4 | var difference = absDifference(currentValue, toValue) 5 | if (difference != null && difference > 3 && (!state.lastSync || Date.now() - state.lastSync > 50)) { 6 | if (state.interpolatingFrom == null) { 7 | state.interpolatingFrom = toValue 8 | } 9 | if (absDifference(state.interpolatingFrom, currentValue) < difference) { 10 | if (currentValue <= 0) { 11 | toValue = 1 12 | } else { 13 | toValue = toValue / state.interpolatingFrom * currentValue 14 | } 15 | } else { 16 | state.interpolatingFrom = toValue 17 | toValue = currentValue 18 | } 19 | state.lastSync = null 20 | } else { 21 | state.interpolatingFrom = null 22 | state.lastSync = Date.now() 23 | } 24 | 25 | return toValue 26 | } 27 | 28 | function absDifference (a, b) { 29 | if (a != null && b != null) { 30 | return Math.abs(a - b) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/Q Tinkle.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/filter","frequency":11000,"Q":1.55654342083404,"gain":10.802892825059846,"type":"peaking"}],"node":"slot","noteOffset":0,"volume":1}],"outputs":["output"],"selectedSlotId":"output","scale":{"notes":[0,2,4,7,9],"offset":3},"templateSlot":{"sources":[{"node":"source/oscillator","amp":0.5905,"shape":"sawtooth","octave":-1,"noteOffset":{"node":"modulator/adsr","release":0.059,"value":0},"detune":0}],"processors":[{"node":"processor/filter","frequency":{"node":"modulator/adsr","release":0,"value":3000,"decay":0.918,"sustain":0.15461363678111523},"type":"highpass","Q":1.1822679254505744,"gain":0},{"node":"processor/delay","sync":true,"time":0.25,"feedback":0.6535,"cutoff":20000,"filterType":"lowpass","wet":0.9828,"dry":1}],"volume":0.3992,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[],"params":[]} -------------------------------------------------------------------------------- /lib/resolve-file-available.js: -------------------------------------------------------------------------------- 1 | var getExt = require('path').extname 2 | var getBaseName = require('path').basename 3 | var getDirName = require('path').dirname 4 | var join = require('path').join 5 | 6 | module.exports = resolveAvailable 7 | 8 | function resolveAvailable (path, fs, cb) { 9 | // check if file exists, 10 | // if so increment number and try again 11 | // otherwise return path 12 | 13 | var ext = getExt(path) 14 | var base = getBaseName(path, ext) 15 | var dir = getDirName(path) 16 | var numberMatch = /(^.+) ([0-9]+)$/.exec(base) 17 | 18 | fs.exists(path, function(exists){ 19 | if (exists){ 20 | if (numberMatch){ 21 | var number = parseInt(numberMatch[2]) + 1 22 | var fileName = numberMatch[1] + ' ' + number + ext 23 | resolveAvailable(join(dir, fileName), fs, cb) 24 | } else { 25 | var fileName = base + ' 1' + ext 26 | resolveAvailable(join(dir, fileName), fs, cb) 27 | } 28 | } else { 29 | cb(null, path) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /nodes/meddler-chunk/external.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var renderRouting = require('lib/widgets/routing') 3 | var renderChunk = require('lib/widgets/chunk') 4 | var renderParams = require('lib/widgets/params') 5 | var ToggleButton = require('lib/params/toggle-button') 6 | var FlagParam = require('lib/flag-param') 7 | 8 | module.exports = function (external) { 9 | return renderChunk(external, { 10 | external: true, 11 | extraHeader: h('span.type', ['meddler']), 12 | main: [ 13 | h('section', [ 14 | renderParams(external), 15 | h('ParamList', [ 16 | h('div -block', [ 17 | h('div.extTitle', 'Use Global'), 18 | h('ParamList -compact', [ 19 | ToggleButton(FlagParam(external.flags, 'noRepeat'), { 20 | title: 'Repeat', 21 | onValue: false, 22 | offValue: true 23 | }) 24 | ]) 25 | ]), 26 | renderRouting(external) 27 | ]) 28 | ]) 29 | ] 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/param-abs.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var ParamTransform = require('lib/param-transform') 3 | var ParamSource = require('lib/param-source') 4 | var absCurve = new Float32Array([1, 0, 1]) 5 | 6 | module.exports = ParamSquare 7 | 8 | function ParamSquare (input) { 9 | return computed([input.currentValue || input], lambda, { 10 | comparer: ParamTransform.deepEqual 11 | }) 12 | } 13 | 14 | function lambda (value) { 15 | if (value instanceof global.AudioNode) { 16 | return paramApplyAbs(value) 17 | } else if (ParamSource.isParam(value)) { 18 | // lazy params 19 | return ParamSource.reduce([value], (values) => abs(values[0])) 20 | } else { 21 | return abs(value) 22 | } 23 | } 24 | 25 | function abs (value) { 26 | return Math.abs(value) 27 | } 28 | 29 | function paramApplyAbs (param) { 30 | // TODO: this should probably handle values greater than 1, right now clips 31 | var output = param.context.createWaveShaper() 32 | output.curve = absCurve 33 | param.connect(output) 34 | return output 35 | } 36 | -------------------------------------------------------------------------------- /lib/midi-note.js: -------------------------------------------------------------------------------- 1 | var clamp = require('lib/clamp') 2 | 3 | module.exports = MidiNote 4 | 5 | function MidiNote (context, opts) { 6 | if (!(this instanceof MidiNote)) return new MidiNote(context, opts) 7 | this.offset = context.midiClockOffset() + (opts.offset || 0) 8 | this.output = opts.output 9 | this.velocity = clamp(Math.round(opts.velocity), 1, 127) 10 | this.channel = clamp(Math.round(opts.channel) || 1, 1, 16) 11 | this.note = clamp(Math.round(opts.note), 0, 127) 12 | this.state = 0 13 | } 14 | 15 | MidiNote.prototype.start = function (at) { 16 | if (this.state === 0) { 17 | this.output.write([144 + this.channel - 1, this.note, this.velocity], getMidiTime(this.offset, at)) 18 | this.state = 1 19 | } 20 | } 21 | 22 | MidiNote.prototype.stop = function (at) { 23 | if (this.state === 1) { 24 | this.output.write([128 + this.channel - 1, this.note, 0], getMidiTime(this.offset, at)) 25 | this.state = 2 26 | } 27 | } 28 | 29 | function getMidiTime (offset, at) { 30 | return at ? (at * 1000) + offset : window.performance.now() 31 | } 32 | -------------------------------------------------------------------------------- /nodes/compressor/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Param = require('lib/param') 3 | var Apply = require('lib/apply-param') 4 | 5 | module.exports = CompressorNode 6 | 7 | function CompressorNode (context) { 8 | var node = context.audio.createDynamicsCompressor() 9 | node.ratio.value = 20 10 | node.threshold.value = -1 11 | 12 | var releases = [] 13 | var obs = Processor(context, node, node, { 14 | threshold: Param(context, node.threshold.defaultValue), 15 | knee: Param(context, node.knee.defaultValue), 16 | ratio: Param(context, node.ratio.defaultValue), 17 | attack: Param(context, node.attack.defaultValue), 18 | release: Param(context, node.release.defaultValue) 19 | }, releases) 20 | 21 | releases.push( 22 | Apply(context.audio, node.threshold, obs.threshold), 23 | Apply(context.audio, node.knee, obs.knee), 24 | Apply(context.audio, node.ratio, obs.ratio), 25 | Apply(context.audio, node.attack, obs.attack), 26 | Apply(context.audio, node.release, obs.release) 27 | ) 28 | 29 | return obs 30 | } 31 | -------------------------------------------------------------------------------- /nodes/modulator-chunk/object.js: -------------------------------------------------------------------------------- 1 | var Property = require('lib/property') 2 | var Slots = require('lib/slots') 3 | var lookup = require('mutant/lookup') 4 | var ParamSum = require('lib/param-sum') 5 | var BaseChunk = require('lib/base-chunk') 6 | var destroyAll = require('lib/destroy-all') 7 | var MutantMap = require('mutant/map') 8 | 9 | module.exports = ModulatorChunk 10 | 11 | function ModulatorChunk (parentContext) { 12 | var context = Object.create(parentContext) 13 | var slots = Slots(context) 14 | context.slotLookup = lookup(slots, 'id') 15 | 16 | var obs = BaseChunk(context, { 17 | slots: slots, 18 | color: Property([0, 0, 0]) 19 | }) 20 | 21 | obs._type = 'ModulatorChunk' 22 | obs.context = context 23 | context.chunk = obs 24 | 25 | // TODO: ParamSum should just be able to accept obs.slots directly 26 | 27 | var values = MutantMap(obs.slots, function (slot) { 28 | return slot.currentValue 29 | }) 30 | 31 | obs.currentValue = ParamSum(values) 32 | 33 | obs.destroy = function () { 34 | destroyAll(obs) 35 | } 36 | 37 | return obs 38 | } 39 | -------------------------------------------------------------------------------- /lib/import-associated-files.js: -------------------------------------------------------------------------------- 1 | var resolvePath = require('path').resolve 2 | var fs = require('fs') 3 | var each = require('async-each') 4 | 5 | module.exports = function (descriptor, originalDirectory, targetDirectory, cb) { 6 | each(getFiles(descriptor), (file, next) => { 7 | var from = resolvePath(originalDirectory, file) 8 | var to = resolvePath(targetDirectory, file) 9 | fs.exists(from, (exists) => { 10 | if (exists) { 11 | fs.exists(to, (exists) => { 12 | if (!exists) { 13 | copyFile(from, to, next) 14 | } else { 15 | next() 16 | } 17 | }) 18 | } else { 19 | next() 20 | } 21 | }) 22 | }, cb) 23 | } 24 | 25 | function copyFile (from, to, cb) { 26 | fs.createReadStream(from).pipe(fs.createWriteStream(to)).on('finish', cb) 27 | } 28 | 29 | function getFiles (descriptor) { 30 | var result = [] 31 | JSON.stringify(descriptor, function (key, value) { 32 | if (value && (value.node === 'AudioBuffer')) { 33 | result.push(value.src) 34 | } 35 | return value 36 | }) 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /lib/mouse-drag-event.js: -------------------------------------------------------------------------------- 1 | module.exports = function (fn, data, opts) { 2 | var handler = { 3 | fn: fn, 4 | data: data || {}, 5 | opts: opts || {}, 6 | handleEvent: handle 7 | } 8 | return handler 9 | } 10 | 11 | function handle (ev) { 12 | if (ev.type === 'mousedown') { 13 | ev.preventDefault() 14 | document.documentElement.addEventListener('mousemove', this) 15 | document.documentElement.addEventListener('mouseup', this) 16 | this.target = ev.target 17 | this.startX = ev.clientX 18 | this.startY = ev.clientY 19 | } else if (ev.type === 'mouseup') { 20 | document.documentElement.removeEventListener('mousemove', this) 21 | document.documentElement.removeEventListener('mouseup', this) 22 | } 23 | 24 | this.fn({ 25 | x: ev.clientX, 26 | y: ev.clientY, 27 | offsetX: ev.clientX - this.startX, 28 | offsetY: ev.clientY - this.startY, 29 | altKey: ev.altKey, 30 | metaKey: ev.metaKey, 31 | ctrlKey: ev.ctrlKey, 32 | shiftKey: ev.shiftKey, 33 | type: ev.type, 34 | target: this.target, 35 | currentTarget: ev.currentTarget 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /lib/wave-hook.js: -------------------------------------------------------------------------------- 1 | var WaveSvg = require('lib/wave-svg') 2 | var Path = require('path') 3 | var resolve = require('mutant/resolve') 4 | 5 | module.exports = WaveHook 6 | 7 | var cache = {} 8 | 9 | function WaveHook (context, src) { 10 | return function (element) { 11 | var path = Path.resolve(resolve(context.cwd), src) 12 | if (!cache[path]) { 13 | cache[path] = WaveSvg(path, context) 14 | } 15 | 16 | var svg = cache[path] 17 | element.innerHTML = '' 18 | 19 | waitForLoad(svg, function () { 20 | element.innerHTML = svg() 21 | var innerElement = element.querySelector('svg') 22 | if (innerElement) { 23 | svg.onAppendChild(function (fragment) { 24 | innerElement.insertAdjacentHTML('afterbegin', fragment) 25 | }) 26 | } 27 | }) 28 | 29 | return svg.destroy 30 | } 31 | } 32 | 33 | function waitForLoad (svg, cb) { 34 | if (svg()) { 35 | process.nextTick(cb) 36 | } else { 37 | var release = svg(function (val) { 38 | if (val) { 39 | process.nextTick(release) 40 | cb() 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/timeline-scheduler.js: -------------------------------------------------------------------------------- 1 | module.exports = AudioTimelineScheduler 2 | 3 | function AudioTimelineScheduler (audioContext) { 4 | var listeners = [] 5 | var timer = null 6 | var lastTime = audioContext.currentTime 7 | var lastSchedule = null 8 | 9 | var obs = function (listener) { 10 | if (!listeners.length) { 11 | lastTime = audioContext.currentTime 12 | listeners.push(listener) 13 | schedule() 14 | timer = setInterval(schedule, 50) 15 | } else { 16 | listener(lastSchedule) 17 | listeners.push(listener) 18 | } 19 | return function remove () { 20 | var index = listeners.indexOf(listener) 21 | if (~index) listeners.splice(index, 1) 22 | if (!listeners.length) { 23 | clearInterval(timer) 24 | } 25 | } 26 | } 27 | 28 | return obs 29 | 30 | // scoped 31 | 32 | function schedule () { 33 | var to = audioContext.currentTime + 0.1 34 | var data = [lastTime, to - lastTime] 35 | lastTime = to 36 | for (var i = 0;i < listeners.length;i++) { 37 | listeners[i](data) 38 | } 39 | lastSchedule = data 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/wave-svg.new.js: -------------------------------------------------------------------------------- 1 | module.exports = WaveSvg 2 | 3 | function WaveSvg (path, options) { 4 | return computed(path, function (path) { 5 | var svg = Value() 6 | var svgPath = path + '.svg' 7 | options.fs.readFile(svgPath, 'utf8', function (err, value) { 8 | if (err) { 9 | decodeRange = RangeDecoder(path, options, function (err, res) { 10 | if (err) return onDone && onDone(err) 11 | meta = res 12 | remaining = meta.duration 13 | nextChunk() 14 | }) 15 | 16 | // cue points 17 | var timePath = path + '.time' 18 | options.fs.readFile(timePath, function (err, buffer) { 19 | if (!err) { 20 | var data = new Float32Array(new Uint8Array(buffer).buffer) 21 | markerPath = svg('path', { 22 | d: getMarkerPath(data, scale, height), 23 | fill: 'rgba(255,255,255,0.1)' 24 | }) 25 | refresh() 26 | } 27 | }) 28 | } else { 29 | obs.set(value) 30 | onDone && onDone(null, value) 31 | } 32 | }) 33 | return svg 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /nodes/clap/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | 4 | var Range = require('lib/params/range') 5 | var ModRange = require('lib/params/mod-range') 6 | var Select = require('lib/params/select') 7 | 8 | module.exports = function renderDrumSynth (node){ 9 | var data = node() 10 | 11 | return h('SourceNode -drumSynth', [ 12 | Header(node, h('span', [ 13 | h('strong', 'Drum Synth:'), ' Clap' 14 | ])), 15 | h('ParamList', [ 16 | ModRange(node.amp, { 17 | title: 'amp', 18 | defaultValue: 1, 19 | format: 'dB', 20 | flex: true 21 | }), 22 | ModRange(node.decay, { 23 | title: 'decay', 24 | defaultValue: 0.5, 25 | format: 'ms', 26 | flex: true 27 | }), 28 | ModRange(node.tone, { 29 | title: 'tone', 30 | defaultValue: 0.5, 31 | format: 'ratio', 32 | flex: true 33 | }), 34 | ModRange(node.density, { 35 | title: 'density', 36 | defaultValue: 0.2, 37 | format: 'ratio', 38 | flex: true 39 | }) 40 | ]) 41 | ]) 42 | } 43 | -------------------------------------------------------------------------------- /lib/test/test-grab-grid.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var GrabGrid = require('../grab-grid') 3 | var ObservGrid = require('observ-grid') 4 | var Observ = require('mutant/value') 5 | 6 | var grid = ObservGrid([0,0,0,0], [2,2]) 7 | 8 | var grab = GrabGrid(grid) 9 | var changes = [] 10 | 11 | var release1 = grab(function (value) { 12 | changes.push([1, value.data]) 13 | }) 14 | 15 | grid.set(0, 0, 1) 16 | grid.set(0, 1, 10) 17 | 18 | var release2 = grab(function (value) { 19 | changes.push([2, value.data]) 20 | }) 21 | 22 | var release3 = grab(function (value) { 23 | changes.push([3, value.data]) 24 | }, { exclude: [0, 1]}) 25 | 26 | grid.set(0, 1, 1) 27 | grid.set(0, 0, 0) 28 | grid.set(1, 1, 1) 29 | 30 | var release4 = grab(function (value) { 31 | changes.push([4, value.data]) 32 | }, { exclude: Observ([2])}) 33 | 34 | grid.set(1, 0, 1) 35 | 36 | assert.deepEqual(changes, [ 37 | [ 1, [] ], 38 | [ 1, [ 1 ] ], 39 | [ 1, [ 1, 10 ] ], 40 | [ 2, [] ], 41 | [ 3, [] ], 42 | [ 1, [ 1, null ] ], 43 | [ 2, [ , 1 ] ], 44 | [ 1, [ null, null ] ], 45 | [ 3, [ , , , 1 ] ], 46 | [ 4, [] ], 47 | [ 3, [ , , 1, 1 ] ] 48 | ]) 49 | -------------------------------------------------------------------------------- /lib/watch-nodes-changed.js: -------------------------------------------------------------------------------- 1 | var forEach = require('mutant/for-each') 2 | var forEachPair = require('mutant/for-each-pair') 3 | 4 | module.exports = watchNodesChanged 5 | 6 | function watchNodesChanged (collectionOrLookup, fn) { 7 | var nodes = new global.Set() 8 | return collectionOrLookup(function (value) { 9 | var currentItems = new global.Set() 10 | var changed = false 11 | 12 | if (Array.isArray(value)) { 13 | forEach(collectionOrLookup, function (item) { 14 | currentItems.add(item) 15 | if (!nodes.has(item)) { 16 | nodes.add(item) 17 | changed = true 18 | } 19 | }) 20 | } else { 21 | forEachPair(collectionOrLookup, function (key, item) { 22 | currentItems.add(item) 23 | if (!nodes.has(item)) { 24 | nodes.add(item) 25 | changed = true 26 | } 27 | }) 28 | } 29 | 30 | Array.from(nodes.values()).forEach(function (node) { 31 | if (!currentItems.has(node)) { 32 | nodes.delete(node) 33 | changed = true 34 | } 35 | }) 36 | 37 | if (changed) { 38 | fn() 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /styles/setup-node.mcss: -------------------------------------------------------------------------------- 1 | SetupNode { 2 | padding: 4px 3 | display: flex 4 | flex-direction: column 5 | flex: 1 6 | overflow: hidden 7 | 8 | div.main { 9 | 10 | display: flex 11 | flex: 1 12 | min-height: 0 13 | 14 | div.controllers { 15 | flex: 1 16 | } 17 | 18 | div.chunks { 19 | width: 350px 20 | } 21 | 22 | div { 23 | overflow-y: auto 24 | overflow-x: hidden 25 | } 26 | 27 | } 28 | div.options { 29 | 30 | display: flex; 31 | margin: -4px; 32 | background: #222; 33 | box-shadow: 0 0 5px #000; 34 | margin-top: 1px; 35 | z-index: 100; 36 | flex-shrink: 0 37 | 38 | section { 39 | padding: 14px; 40 | display: flex 41 | h1 { 42 | flex: none 43 | margin-right: 5px 44 | } 45 | div.param { 46 | flex: 1 47 | display: flex 48 | margin: 3px; 49 | } 50 | div.chooser { 51 | flex: 1 52 | display: flex 53 | } 54 | } 55 | 56 | section.scale { 57 | flex: 1 58 | } 59 | 60 | section.volume { 61 | width: 350px 62 | } 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /lib/apply-scale.js: -------------------------------------------------------------------------------- 1 | var defaultScale = { 2 | offset: 0, 3 | notes: [0, 2, 4, 5, 7, 9, 11] 4 | } 5 | 6 | module.exports = applyScale 7 | 8 | function applyScale (base, scale) { 9 | var offset = scale && scale.offset || defaultScale.offset 10 | var notes = scale && scale.notes || defaultScale.notes 11 | base = Math.round(base * 100000000) / 100000000 // weed out floating point errors 12 | 13 | var multiplier = Math.floor(base / notes.length) 14 | var scalePosition = mod(base, notes.length) 15 | var absScalePosition = Math.floor(scalePosition) 16 | var fraction = scalePosition - absScalePosition 17 | 18 | var note = notes[absScalePosition] + offset 19 | 20 | if (fraction) { 21 | var interval = getInterval(absScalePosition, notes) 22 | return note + (interval * fraction) + (multiplier * 12) 23 | } else { 24 | return note + (multiplier * 12) 25 | } 26 | } 27 | 28 | function getInterval (current, notes) { 29 | if (current >= notes.length - 1) { 30 | return 12 + notes[0] - notes[current] 31 | } else { 32 | return notes[current + 1] - notes[current] 33 | } 34 | } 35 | 36 | function mod (n, m) { 37 | return ((n % m) + m) % m 38 | } 39 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/freedom.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","node":"slot","noteOffset":0,"volume":1,"processors":[{"node":"processor/delay","time":{"node":"modulator/lfo","trigger":true,"value":0.058,"amp":13.47},"sync":false,"feedback":0.7891,"cutoff":1900,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/overdrive","preBand":0.5,"color":1800,"postCut":10000,"gain":1.9182,"amp":1},{"node":"processor/reverb","time":1.4,"decay":2,"reverse":true,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/pitchshift","transpose":12,"wet":0.2865,"dry":0.8719}]}],"outputs":["output"],"templateSlot":{"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"node":"slot","output":"output","volume":1,"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.6,"release":0.01},"noteOffset":0,"octave":0,"detune":0,"shape":"sawtooth"}],"processors":[{"node":"processor/filter","frequency":{"node":"modulator/adsr","release":0.1,"value":3800,"attack":0.357},"Q":1,"gain":0,"type":"lowpass"}]},"selectedSlotId":"output","scale":"$global","inputs":[],"params":[]} -------------------------------------------------------------------------------- /lib/params/toggle-chooser.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var computed = require('mutant/computed') 4 | 5 | module.exports = ToggleChooser 6 | 7 | function ToggleChooser (param, opts) { 8 | return h('ToggleChooser', [ 9 | h('span.title', opts.title), 10 | computed([opts.options], function (options) { 11 | return options.map(function (option) { 12 | if (Array.isArray(option)) { 13 | return h('div.choice', { 14 | classList: computed([param, option[1]], activeWhenEqual), 15 | events: { 16 | click: send(param.set, option[1]) 17 | } 18 | }, option[0]) 19 | } else if (typeof option === 'string' || typeof option === 'number') { 20 | return h('div.choice', { 21 | classList: computed([param, option], activeWhenEqual), 22 | events: { 23 | click: send(param.set, option) 24 | } 25 | }, String(option)) 26 | } 27 | }) 28 | }) 29 | ]) 30 | } 31 | 32 | function activeWhenEqual (a, b) { 33 | if (a === b || (a == null && b == null)) { 34 | return '-active' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/mixer-launch-control-xl/set-lights.js: -------------------------------------------------------------------------------- 1 | module.exports = setLights 2 | 3 | var message = [240, 0, 32, 41, 2, 17, 120, 8] 4 | var off = 0 5 | 6 | function setLights (state, stream) { 7 | var currentState = {} 8 | stream(function (port) { 9 | if (port) { 10 | var toUpdate = [] 11 | Object.keys(currentState).forEach(function (key) { 12 | var id = parseInt(key, 10) 13 | var value = currentState[key] || off 14 | toUpdate.push(id, value) 15 | }) 16 | if (toUpdate.length) { 17 | stream.write(message.concat(toUpdate.slice(0, 64), 247)) 18 | if (toUpdate.length > 64) { 19 | stream.write(message.concat(toUpdate.slice(64), 247)) 20 | } 21 | } 22 | } 23 | }) 24 | return state(function (values) { 25 | var toUpdate = [] 26 | values.forEach(function (value, id) { 27 | if (!same(currentState[id], value)) { 28 | currentState[id] = value || 0 29 | toUpdate.push(id, value) 30 | } 31 | }) 32 | if (toUpdate.length) { 33 | stream.write(message.concat(toUpdate, 247)) 34 | } 35 | }) 36 | } 37 | 38 | function same (a, b) { 39 | return a === b 40 | } 41 | -------------------------------------------------------------------------------- /nodes/freeverb/object.js: -------------------------------------------------------------------------------- 1 | var watch = require('mutant/watch') 2 | 3 | var Freeverb = require('freeverb') 4 | var Processor = require('lib/processor') 5 | var Property = require('lib/property') 6 | 7 | var Param = require('lib/param') 8 | var Apply = require('lib/apply-param') 9 | var Multiply = require('lib/param-multiply') 10 | 11 | module.exports = FreeverbNode 12 | 13 | function FreeverbNode (context) { 14 | var reverb = Freeverb(context.audio) 15 | var output = context.audio.createGain() 16 | reverb.connect(output) 17 | 18 | var releases = [] 19 | var obs = Processor(context, reverb, output, { 20 | roomSize: Param(context, 0.8), 21 | dampening: Param(context, 3000), 22 | wet: Param(context, 1), 23 | dry: Param(context, 1) 24 | }, releases) 25 | 26 | releases.push( 27 | Apply(context.audio, reverb.wet, Multiply([obs.wet, 1 / 4])), 28 | Apply(context.audio, reverb.dry, obs.dry) 29 | ) 30 | 31 | reverb.combFilters.forEach((combFilter) => { 32 | releases.push( 33 | Apply(context.audio, combFilter.resonance, obs.roomSize), 34 | Apply(context.audio, combFilter.dampening, obs.dampening) 35 | ) 36 | }) 37 | 38 | return obs 39 | } 40 | -------------------------------------------------------------------------------- /lib/assign-available-port.js: -------------------------------------------------------------------------------- 1 | module.exports = assignAvailablePort 2 | 3 | function assignAvailablePort (node) { 4 | var nodeInfo = node.context.nodeInfo.lookup[node().node] 5 | if (nodeInfo && node.port && nodeInfo.portMatch) { 6 | if (!node.port() || !nodeInfo.portMatch.exec(node.port())) { 7 | var availablePorts = node.context.midiPorts().filter(function (name) { 8 | return nodeInfo.portMatch.exec(name) 9 | }) 10 | 11 | var usedPorts = [] 12 | node.context.collection.forEach(function (controller) { 13 | if (controller) { 14 | var name = controller.port && controller.port() 15 | if (availablePorts.includes(name)) { 16 | usedPorts.push(name) 17 | } 18 | } 19 | }) 20 | 21 | var portName = getRarest(availablePorts.concat(usedPorts)) 22 | node.port.set(portName) 23 | } 24 | } 25 | } 26 | 27 | function getRarest (array) { 28 | var ranked = array.reduce(function (result, item) { 29 | result[item] = (result[item] || 0) + 1 30 | return result 31 | }, {}) 32 | 33 | return Object.keys(ranked).sort(function (a, b) { 34 | return ranked[a] - ranked[b] 35 | })[0] 36 | } 37 | -------------------------------------------------------------------------------- /nodes/midi-cc/object.js: -------------------------------------------------------------------------------- 1 | var Struct = require('mutant/struct') 2 | var computed = require('mutant/computed') 3 | var applyMidiParam = require('lib/apply-midi-param') 4 | var Property = require('lib/property') 5 | var Param = require('lib/param') 6 | var destroyAll = require('lib/destroy-all') 7 | var clamp = require('lib/clamp') 8 | 9 | module.exports = MidiCCNode 10 | 11 | function MidiCCNode (context) { 12 | var port = context.outputMidiPort 13 | var channel = context.outputMidiChannel 14 | 15 | var obs = Struct({ 16 | code: Property(1), 17 | value: Param(context, 0) 18 | }) 19 | 20 | Param.triggerOn(obs, context.audio.currentTime) 21 | 22 | var message = computed([channel, obs.code], (channel, code) => { 23 | var channelOffset = clamp(channel, 1, 16) - 1 24 | if (code === 'PC') { 25 | return [192 + channelOffset] 26 | } else { 27 | return [176 + channelOffset, code] 28 | } 29 | }) 30 | 31 | var releaseMidiParam = applyMidiParam(context, { port, message }, obs.value) 32 | 33 | obs.resend = releaseMidiParam.resend 34 | 35 | obs.destroy = function () { 36 | destroyAll(obs) 37 | releaseMidiParam() 38 | } 39 | 40 | obs.context = context 41 | 42 | return obs 43 | } 44 | -------------------------------------------------------------------------------- /lib/processor.js: -------------------------------------------------------------------------------- 1 | var ObservStruct = require('mutant/struct') 2 | var Param = require('lib/param') 3 | 4 | module.exports = ProcessorNode 5 | 6 | function ProcessorNode (context, input, output, params, releases) { 7 | var obs = ObservStruct(params, {merge: true}) 8 | 9 | obs.input = input 10 | obs.output = output 11 | obs.connect = output.connect.bind(output) 12 | obs.disconnect = output.disconnect.bind(output) 13 | obs.getReleaseDuration = Param.getReleaseDuration.bind(this, obs) 14 | obs.getAttackDuration = Param.getAttackDuration.bind(this, obs) 15 | 16 | obs.context = context 17 | 18 | obs.triggerOn = function (at) { 19 | at = at || context.audio.currentTime 20 | Param.triggerOn(obs, at) 21 | } 22 | 23 | obs.triggerOff = function (at) { 24 | at = at || context.audio.currentTime 25 | var stopAt = obs.getReleaseDuration(at) + at 26 | Param.triggerOff(obs, stopAt) 27 | } 28 | 29 | obs.destroy = function () { 30 | while (releases && releases.length) { 31 | releases.pop()() 32 | } 33 | Object.keys(obs).forEach(function (key) { 34 | if (obs[key] && typeof obs[key].destroy === 'function') { 35 | obs[key].destroy() 36 | } 37 | }) 38 | } 39 | 40 | return obs 41 | } 42 | -------------------------------------------------------------------------------- /nodes/eq/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var ModRange = require('lib/params/mod-range') 4 | var Select = require('lib/params/select') 5 | 6 | module.exports = function renderFilter(node) { 7 | return h('ProcessorNode -filter', [ 8 | Header(node, h('span', 'EQ')), 9 | h('section', [ 10 | h('ParamList', [ 11 | ModRange(node.low, { 12 | title: 'low', 13 | defaultValue: 0, 14 | format: 'dBn', 15 | flex: 'small' 16 | }), 17 | ModRange(node.mid, { 18 | title: 'mid', 19 | defaultValue: 0, 20 | format: 'dBn', 21 | flex: 'small' 22 | }), 23 | ModRange(node.high, { 24 | title: 'high', 25 | defaultValue: 0, 26 | format: 'dBn', 27 | flex: 'small' 28 | }), 29 | ModRange(node.highcut, { 30 | title: 'highcut', 31 | format: 'arfo', 32 | flex: 'small', 33 | defaultValue: 20000 34 | }), 35 | ModRange(node.lowcut, { 36 | title: 'lowcut', 37 | format: 'arfo', 38 | flex: 'small', 39 | defaultValue: 0 40 | }) 41 | ]) 42 | ]) 43 | ]) 44 | } 45 | -------------------------------------------------------------------------------- /nodes/ableton-link/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var computed = require('mutant/computed') 4 | var when = require('mutant/when') 5 | var Range = require('lib/params/range') 6 | 7 | module.exports = renderMidiSync 8 | 9 | function renderMidiSync (controller) { 10 | var collection = controller.context.collection 11 | var name = computed(controller, x => x.name) 12 | return h('GlobalControllerNode', [ 13 | h('header', [ 14 | h('span.name', [ 15 | h('strong', [name, ': ']), ' ', 16 | plural(controller.peerCount, 'peer', 'peers') 17 | ]), 18 | h('button.remove Button -warn', { 19 | 'ev-click': send(collection.remove, controller) 20 | }, 'X') 21 | ]), 22 | h('section', [ 23 | h('ParamList', [ 24 | Range(controller.syncOffset, { 25 | title: 'sync offset', 26 | defaultValue: 0, 27 | format: 'syncMs', 28 | flex: true 29 | }) 30 | ]) 31 | ]) 32 | ]) 33 | } 34 | 35 | function plural (...args) { 36 | return computed(args, (value, singular, plural) => { 37 | if (value === 1) { 38 | return `${value} ${singular}` 39 | } else { 40 | return `${value} ${plural}` 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /lib/param-sum.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var Connector = require('lib/connector') 3 | var ParamTransform = require('lib/param-transform') 4 | var ParamFromNumber = require('lib/param-from-number') 5 | var ValueAtTimeGetter = require('lib/value-at-time-getter') 6 | 7 | module.exports = ParamSum 8 | 9 | function ParamSum (inputs) { 10 | return ParamTransform(inputs, sumParams, sumValues) 11 | } 12 | 13 | function sumParams (audioContext, params, number) { 14 | var connections = new Connector() 15 | var getValueAtTime = ValueAtTimeGetter(params, number, sumValues) 16 | 17 | return computed([params, ParamFromNumber(audioContext, number)], function (params, numberParam) { 18 | connections.clear() 19 | 20 | var result = audioContext.createGain() 21 | 22 | for (var i = 0; i < params.length; i++) { 23 | connections.add(params[i], result) 24 | } 25 | 26 | if (numberParam) { 27 | connections.add(numberParam, result) 28 | } 29 | 30 | result.getValueAtTime = getValueAtTime 31 | return result 32 | }, { 33 | onListen: () => connections.connect(), 34 | onUnlisten: () => connections.disconnect() 35 | }) 36 | } 37 | 38 | function sumValues (values) { 39 | return values.reduce((a, b) => a + b, 0) 40 | } 41 | -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/bass.json: -------------------------------------------------------------------------------- 1 | {"scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.2271,"release":0.382,"attack":0},"frequency":440,"shape":"sawtooth","octave":-3,"noteOffset":0,"detune":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.4223,"attack":0,"release":0.378},"frequency":440,"octave":-3,"noteOffset":0,"detune":0,"shape":"sine"}],"processors":[{"node":"processor/filter","frequency":{"node":"modulator/adsr","value":5700,"attack":0,"release":0,"sustain":0.018729838057848537,"decay":0.341},"Q":0.2828344239468248,"gain":0,"type":"lowpass"}],"chokeGroup":"bass","volume":0.5788,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"slots":[{"id":"output","output":null,"volume":1,"sources":[],"processors":[{"mode":"modulate","ratio":0.43649348257397047,"node":"processor/dipper"},{"frequency":6100,"Q":2.1336110493933447,"gain":-5.183449928990768,"type":"peaking","node":"processor/filter"},{"mode":"modulate","ratio":0.43649348257397047,"node":"processor/dipper"}],"noteOffset":0,"node":"slot"}],"inputs":[],"outputs":["output"],"params":[],"selectedSlotId":"output","node":"chunk/scale"} -------------------------------------------------------------------------------- /nodes/kick/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var computed = require('mutant/computed') 4 | var ModRange = require('lib/params/mod-range') 5 | var Select = require('lib/params/select') 6 | 7 | var types = [ 8 | ['808', '808'], 9 | ['909', '909'] 10 | ] 11 | 12 | module.exports = function renderDrumSynth (node) { 13 | return h('SourceNode -drumSynth', [ 14 | Header(node, h('span', [ 15 | h('strong', 'Drum Synth:'), ' Kick ', 16 | h('span', computed(node, d => d.node)) 17 | ])), 18 | h('ParamList', [ 19 | Select(node.type, { 20 | options: types 21 | }), 22 | ModRange(node.amp, { 23 | title: 'amp', 24 | defaultValue: 1, 25 | format: 'dB', 26 | flex: true 27 | }), 28 | ModRange(node.tone, { 29 | title: 'tone', 30 | defaultValue: 0.5, 31 | format: 'ratio', 32 | flex: true 33 | }), 34 | ModRange(node.decay, { 35 | title: 'decay', 36 | defaultValue: 0.5, 37 | format: 'ms', 38 | flex: true 39 | }), 40 | ModRange(node.tune, { 41 | title: 'tune', 42 | defaultValue: 0, 43 | format: 'cents+', 44 | flex: true 45 | }) 46 | ]) 47 | ]) 48 | } 49 | -------------------------------------------------------------------------------- /lib/observ-rms.js: -------------------------------------------------------------------------------- 1 | var Value = require('mutant/value') 2 | var gainToDecibels = require('decibels/from-gain') 3 | 4 | module.exports = ObservRms 5 | 6 | function ObservRms (audioNode) { 7 | var value = [-Infinity, -Infinity] 8 | var obs = Value(value) 9 | var broadcast = obs.set 10 | obs.set = null 11 | 12 | var audioContext = audioNode.context 13 | 14 | var meter = audioContext.createScriptProcessor(512 * 2, 2, 2) 15 | audioNode.connect(meter) 16 | 17 | var lastL = 0 18 | var lastR = 0 19 | var smoothing = 0.8 20 | 21 | meter.onaudioprocess = function (e) { 22 | var inputL = e.inputBuffer.getChannelData(0) 23 | var inputR = e.inputBuffer.getChannelData(1) 24 | var rmsL = Math.max(rms(inputL), lastL * smoothing) 25 | var rmsR = Math.max(rms(inputR), lastR * smoothing) 26 | if (rmsL !== lastL || rmsR !== lastR) { 27 | value[0] = gainToDecibels(rmsL) 28 | value[1] = gainToDecibels(rmsR) 29 | broadcast(value) 30 | lastL = rmsL 31 | lastR = rmsR 32 | } 33 | } 34 | 35 | meter.connect(audioContext.destination) 36 | 37 | return obs 38 | } 39 | 40 | function rms (input) { 41 | var sum = 0 42 | for (var i = 0; i < input.length; i++) { 43 | sum += input[i] * input[i] 44 | } 45 | return Math.sqrt(sum / input.length) 46 | } 47 | -------------------------------------------------------------------------------- /nodes/freeverb/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var Header = require('lib/widgets/header') 3 | var Range = require('lib/params/range') 4 | var ModRange = require('lib/params/mod-range') 5 | var Select = require('lib/params/select') 6 | var QueryParam = require('lib/query-param') 7 | var nodeChoices = require('../reverb/types') 8 | 9 | module.exports = function renderFreeverb (node) { 10 | return h('ProcessorNode -reverb', [ 11 | Header(node, h('span', 'Reverb')), 12 | h('ParamList', [ 13 | Select(QueryParam(node, 'node'), { 14 | defaultValue: 'processor/freeverb', 15 | options: nodeChoices 16 | }), 17 | ModRange(node.roomSize, { 18 | title: 'room size', 19 | defaultValue: 0.8, 20 | format: 'ratio1Log', 21 | flex: true 22 | }), 23 | ModRange(node.dampening, { 24 | title: 'dampening', 25 | defaultValue: 3000, 26 | format: 'arfo', 27 | flex: true 28 | }), 29 | ModRange(node.wet, { 30 | title: 'wet', 31 | defaultValue: 1, 32 | format: 'dB', 33 | flex: true 34 | }), 35 | ModRange(node.dry, { 36 | title: 'dry', 37 | defaultValue: 1, 38 | format: 'dB', 39 | flex: true 40 | }) 41 | ]) 42 | ]) 43 | } 44 | -------------------------------------------------------------------------------- /styles/param-list.mcss: -------------------------------------------------------------------------------- 1 | ParamList { 2 | margin: 5px 3 | display: flex 4 | flex-wrap: wrap 5 | 6 | -compact { 7 | margin: 0 8 | 9 | button { 10 | margin: 0 11 | } 12 | 13 | button + button { 14 | margin-left: 4px 15 | } 16 | 17 | } 18 | 19 | table { 20 | (td.title){ 21 | width: 90% 22 | } 23 | } 24 | 25 | select { 26 | cursor: pointer 27 | margin: 4px 28 | -flex { 29 | flex: 1 30 | } 31 | } 32 | 33 | input { 34 | [type='text'] { 35 | flex: 1 36 | flex-shrink: 0 37 | margin: 4px 38 | } 39 | } 40 | 41 | button { 42 | margin: 4px 43 | height: 18px 44 | } 45 | 46 | div { 47 | div.title { 48 | font-size: 80% 49 | font-weight: bold 50 | position: absolute 51 | padding: 4px 0 0 2px 52 | z-index: 1 53 | opacity: 0.6 54 | pointer-events: none 55 | } 56 | 57 | div.extTitle { 58 | font-size: 80% 59 | color: #AAA 60 | } 61 | 62 | margin: 4px; 63 | display: flex 64 | 65 | -block { 66 | display: block 67 | select { 68 | margin: 0 69 | } 70 | } 71 | 72 | -flex { 73 | flex: 1 300px 74 | } 75 | 76 | -flexSmall { 77 | flex: 1 100px 78 | } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /nodes/loop-grid/compute-flags.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var ArrayGrid = require('array-grid') 3 | 4 | module.exports = function computeFlags (obsChunkLookup, obsPositions, obsShape) { 5 | return computed([obsChunkLookup, obsPositions, obsShape], function (chunkLookup, positions, shape) { 6 | var grid = ArrayGrid([], shape) 7 | if (chunkLookup && positions) { 8 | Object.keys(positions).forEach(function (id) { 9 | var chunk = obsChunkLookup.get(id) 10 | var origin = positions[id] 11 | if (chunk && chunk.flags && chunk.flags() && chunk.grid) { 12 | var flags = chunk.flags() 13 | var chunkGrid = chunk.grid() 14 | 15 | var result = [] 16 | if (Array.isArray(flags)) { 17 | if (flags.length) { 18 | chunkGrid.data.forEach(function (id, i) { 19 | result[i] = flags 20 | }) 21 | } 22 | } else { 23 | chunkGrid.data.forEach(function (id, i) { 24 | if (flags[id]) { 25 | result[i] = flags[id] 26 | } 27 | }) 28 | } 29 | 30 | grid.place(origin[0], origin[1], new ArrayGrid(result, chunkGrid.shape)) 31 | } 32 | }) 33 | } 34 | return grid 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /styles/node-collection.mcss: -------------------------------------------------------------------------------- 1 | NodeCollection { 2 | padding: 4px 3 | 4 | h1 { 5 | button.condense { 6 | float: right 7 | margin-top: 3px 8 | cursor: pointer 9 | opacity: 0.4 10 | height: 18px 11 | width: 18px 12 | border-radius: 3px 13 | margin-right: 3px 14 | border: none 15 | background: transparent 16 | background-repeat: no-repeat 17 | background-position: center 18 | background-image: svg(condense) 19 | 20 | :hover { 21 | opacity: 0.8 22 | background-color: #888 23 | } 24 | 25 | :focus { 26 | outline: none 27 | opacity: 0.8 28 | } 29 | 30 | :active { 31 | opacity: 1 32 | } 33 | 34 | @svg condense { 35 | width: 8px 36 | height: 10px 37 | content: "" 38 | path { 39 | fill: #FFF 40 | } 41 | } 42 | } 43 | } 44 | 45 | -wrap { 46 | display: flex 47 | flex-wrap: wrap 48 | div { 49 | margin: 4px 50 | } 51 | } 52 | 53 | -across { 54 | overflow: hidden 55 | display: block 56 | div { 57 | display: block 58 | float: left 59 | margin: 4px 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /lib/param-clamp.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var ParamSource = require('lib/param-source') 3 | var clamp = require('lib/clamp') 4 | 5 | module.exports = ParamClamp 6 | 7 | function ParamClamp (param, min, max) { 8 | return computed([param.currentValue || param, min, max], getClampedParam) 9 | } 10 | 11 | function getClampedParam (param, min, max) { 12 | if (param instanceof global.AudioNode) { 13 | var range = max - min 14 | var shaper = param.context.createWaveShaper() 15 | shaper.curve = new Float32Array([min, max]) 16 | scale(param, 2 / range, -min - (range / 2)).connect(shaper) 17 | return shaper 18 | } else if (ParamSource.isParam(param)) { 19 | return ParamSource.reduce([param], (values) => clamp(values[0], min, max)) 20 | } else if (typeof param === 'number') { 21 | return clamp(param, min, max) 22 | } 23 | } 24 | 25 | function scale (param, multiplier, offset) { 26 | var sum = param.context.createGain() 27 | var offsetParam = getValue(offset, param) 28 | offsetParam.connect(sum) 29 | param.connect(sum) 30 | sum.gain.value = multiplier 31 | return sum 32 | } 33 | 34 | function getValue (value, param) { 35 | var result = param.context.createWaveShaper() 36 | result.curve = new Float32Array([value, value]) 37 | param.connect(result) 38 | return result 39 | } 40 | -------------------------------------------------------------------------------- /nodes/triggers-chunk/external.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var when = require('mutant/when') 3 | 4 | var renderRouting = require('lib/widgets/routing') 5 | var renderChunk = require('lib/widgets/chunk') 6 | var renderParams = require('lib/widgets/params') 7 | var renderMidiOutputOptions = require('lib/widgets/midi-output') 8 | var ToggleButton = require('lib/params/toggle-button') 9 | var FlagParam = require('lib/flag-param') 10 | 11 | module.exports = function (external) { 12 | var node = external.node 13 | return renderChunk(external, { 14 | volume: true, 15 | external: true, 16 | main: [ 17 | h('section', [ 18 | renderParams(external), 19 | h('ParamList', [ 20 | h('div -block', [ 21 | h('div.extTitle', 'Use Global'), 22 | h('ParamList -compact', [ 23 | ToggleButton(FlagParam(external.flags, 'noRepeat'), { 24 | title: 'Repeat', 25 | onValue: false, 26 | offValue: true 27 | }) 28 | ]) 29 | ]), 30 | renderRouting(external) 31 | ]) 32 | ]), 33 | when(node.midiOutputEnabled, [ 34 | h('h1', 'Midi Output'), 35 | h('section', [ 36 | renderMidiOutputOptions(external) 37 | ]) 38 | ]) 39 | ] 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /nodes/external-audio-input/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var when = require('mutant/when') 4 | var computed = require('mutant/computed') 5 | var ToggleButton = require('lib/params/toggle-button') 6 | 7 | module.exports = renderExternalAudioInput 8 | 9 | function renderExternalAudioInput (controller) { 10 | var collection = controller.context.collection 11 | var name = computed(controller, x => x.name) 12 | var port = controller.port 13 | return h('GlobalControllerNode', { 14 | classList: [ 15 | when(controller.minimised, '-minimised') 16 | ] 17 | }, [ 18 | h('header', [ 19 | h('button.twirl', { 20 | 'ev-click': send(toggleParam, controller.minimised) 21 | }), 22 | h('span.name', [ 23 | h('strong', name), when(port, [': ', port]) 24 | ]), 25 | h('button.remove Button -warn', { 26 | 'ev-click': send(collection.remove, controller) 27 | }, 'X') 28 | ]), 29 | h('section', [ 30 | h('ParamList', [ 31 | ToggleButton(controller.includeInRecording, { 32 | title: 'Include in recording' 33 | }), 34 | ToggleButton(controller.monitor, { 35 | title: 'Monitor' 36 | }) 37 | ]) 38 | ]) 39 | ]) 40 | } 41 | 42 | function toggleParam (param) { 43 | param.set(!param()) 44 | } 45 | -------------------------------------------------------------------------------- /nodes/midi-sync-output/view.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var when = require('mutant/when') 4 | var computed = require('mutant/computed') 5 | 6 | module.exports = renderMidiSync 7 | 8 | function renderMidiSync (controller) { 9 | var collection = controller.context.collection 10 | var name = computed(controller, x => x.name) 11 | var port = controller.port 12 | return h('GlobalControllerNode', { 13 | classList: [ 14 | when(controller.minimised, '-minimised') 15 | ] 16 | }, [ 17 | h('header', [ 18 | h('button.twirl', { 19 | 'ev-click': send(toggleParam, controller.minimised) 20 | }), 21 | h('span.name', [ 22 | h('strong', name), when(port, [': ', port]) 23 | ]), 24 | h('button.remove Button -warn', { 25 | 'ev-click': send(collection.remove, controller) 26 | }, 'X') 27 | ]), 28 | h('section', [ 29 | h('ParamList', [ 30 | h('ModParam', [ 31 | h('button.action', {'ev-click': send(controller.startSync)}, 'Send Start'), ' ', 32 | h('button.action', { 33 | 'ev-click': send(controller.stop), 34 | style: {'margin-left': '5px'} 35 | }, 'Send Stop') 36 | ]) 37 | ]) 38 | ]) 39 | ]) 40 | } 41 | 42 | function toggleParam (param) { 43 | param.set(!param()) 44 | } 45 | -------------------------------------------------------------------------------- /nodes/spatial-pan/object.js: -------------------------------------------------------------------------------- 1 | var Processor = require('lib/processor') 2 | var Param = require('lib/param') 3 | var Apply = require('lib/apply-param') 4 | var Multiply = require('lib/param-multiply') 5 | var ParamClamp = require('lib/param-clamp') 6 | 7 | module.exports = SpatialPanNode 8 | 9 | function SpatialPanNode (context) { 10 | var delayL = context.audio.createDelay(0.04) 11 | var delayR = context.audio.createDelay(0.04) 12 | var splitter = context.audio.createChannelSplitter(2) 13 | var merger = context.audio.createChannelMerger(2) 14 | var panner = context.audio.createStereoPanner() 15 | 16 | splitter.channelCount = 2 17 | splitter.channelCountMode = 'explicit' 18 | 19 | splitter.connect(delayL, 0) 20 | splitter.connect(delayR, 1) 21 | delayL.connect(merger, 0, 0) 22 | delayR.connect(merger, 0, 1) 23 | merger.connect(panner) 24 | 25 | var releases = [] 26 | 27 | var obs = Processor(context, splitter, panner, { 28 | offset: Param(context, 0.1) 29 | }, releases) 30 | 31 | releases.push( 32 | Apply(context.audio, delayL.delayTime, Multiply([ 33 | ParamClamp(obs.offset, 0, 1), 0.003 34 | ])), 35 | Apply(context.audio, delayR.delayTime, Multiply([ 36 | ParamClamp(obs.offset, -1, 0), -0.003 37 | ])), 38 | Apply(context.audio, panner.pan, Multiply([ 39 | obs.offset, 0.5 40 | ])) 41 | ) 42 | return obs 43 | } 44 | -------------------------------------------------------------------------------- /demo-project/DWS - Quantum Loop/Q Warp.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/dipper","ratio":1.0182533675212468,"mode":"modulate"},{"node":"processor/filter","frequency":2500,"Q":1,"gain":0,"type":"lowpass"}],"node":"slot","noteOffset":0,"volume":1.0426}],"outputs":["output"],"selectedSlotId":"$template","scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.5796,"attack":0.01974395807139611,"release":0.016},"octave":-4,"noteOffset":0,"detune":0,"shape":"sine"},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.5851,"attack":0.2996237677372736,"release":0.014485743365267778,"startValue":0},"detune":{"node":"modulator/adsr","value":2.0888980159988684,"attack":0.3372630042653024},"octave":0,"noteOffset":0}],"processors":[{"node":"processor/overdrive","cut":9231.437537779038,"gain":8,"color":61,"band":1.2825678005957046,"preBand":0.6636364748326865,"postCut":3000,"amp":1},{"node":"processor/filter","frequency":{"node":"modulator/lfo","value":1968.1946381662178,"amp":1713.1688465179009,"rate":6.6658595641646485,"mode":"add","trigger":true}},{"node":"processor/delay","time":0.1026393404866564}],"volume":1.0115,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[]} -------------------------------------------------------------------------------- /demo-project/Launchpad Example/pad.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/dipper"}],"node":"slot"}],"outputs":["output"],"selectedSlotId":"$template","scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0,"attack":2.256754490111679,"release":0.957235496743463},"frequency":440,"octave":-2,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.6,"attack":1.7766753830242663,"release":1.4995717713989172},"shape":"triangle","detune":{"node":"modulator/lfo","value":0,"amp":18.014529823658684,"rate":11.214029444513175,"mode":"add","trigger":true},"octave":-1,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.2915,"attack":0.7702878926489645,"release":1.3246224917130511},"frequency":440,"shape":"square","octave":0,"noteOffset":0},{"node":"source/oscillator","amp":0.0272,"frequency":440,"shape":"sawtooth","octave":1,"noteOffset":5}],"processors":[{"node":"processor/overdrive","color":19755.784240480218,"postCut":{"node":"modulator/lfo","value":4204.3912087086155,"rate":0.4707476299044454,"amp":2541.3271807080737,"mode":"add","trigger":true}},{"node":"processor/delay"}],"volume":0.2713,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[],"params":[]} -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/pad.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/dipper"}],"node":"slot"}],"outputs":["output"],"selectedSlotId":"$template","scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0,"attack":2.256754490111679,"release":0.957235496743463},"frequency":440,"octave":-2,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.6,"attack":1.7766753830242663,"release":1.4995717713989172},"shape":"triangle","detune":{"node":"modulator/lfo","value":0,"amp":18.014529823658684,"rate":11.214029444513175,"mode":"add","trigger":true},"octave":-1,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.2915,"attack":0.7702878926489645,"release":1.3246224917130511},"frequency":440,"shape":"square","octave":0,"noteOffset":0},{"node":"source/oscillator","amp":0.0272,"frequency":440,"shape":"sawtooth","octave":1,"noteOffset":5}],"processors":[{"node":"processor/overdrive","color":19755.784240480218,"postCut":{"node":"modulator/lfo","value":4204.3912087086155,"rate":0.4707476299044454,"amp":2541.3271807080737,"mode":"add","trigger":true}},{"node":"processor/delay"}],"volume":0.2713,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[],"params":[]} -------------------------------------------------------------------------------- /lib/query-param.js: -------------------------------------------------------------------------------- 1 | var jsonQuery = require('json-query') 2 | var computed = require('mutant/computed') 3 | var resolve = require('mutant/resolve') 4 | 5 | module.exports = QueryParam 6 | 7 | function QueryParam (param, query, defaultValue) { 8 | var result = computed([param], function (value) { 9 | var res = jsonQuery(query, {data: value}) 10 | 11 | // fallback to inner value 12 | res = res.value === undefined && param.node 13 | ? jsonQuery(query, {data: read(param.node)}) 14 | : res 15 | 16 | return res.value === undefined ? defaultValue : res.value 17 | }) 18 | 19 | result.set = function (value) { 20 | var newObject = obtain(resolve(param)) 21 | 22 | var res = jsonQuery(query, {data: newObject, force: defaultValue}) 23 | var obj = jsonQuery.lastParent(res) 24 | 25 | if (obj) { 26 | if (value === undefined) { 27 | delete obj[res.key] 28 | } else { 29 | obj[res.key] = value 30 | } 31 | param.set(newObject) 32 | return true 33 | } else { 34 | return false 35 | } 36 | } 37 | 38 | result.context = param.context 39 | 40 | return result 41 | } 42 | 43 | function obtain (obj) { 44 | return JSON.parse(JSON.stringify(obj)) 45 | } 46 | 47 | function read (target) { 48 | if (typeof target === 'function') { 49 | return target() 50 | } else if (target && target.read) { 51 | return target.read() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/import-sample.js: -------------------------------------------------------------------------------- 1 | var getBase = require('path').basename 2 | var getSoundOffset = require('lib/get-sound-offset') 3 | var resolveFileAvailable = require('lib/resolve-file-available') 4 | var copyFile = require('lib/copy-file') 5 | 6 | module.exports = importSample 7 | 8 | function importSample (context, filePath, cb) { 9 | var fileObject = context.fileObject 10 | 11 | var fileName = getBase(filePath) 12 | var from = filePath 13 | var to = fileObject.resolvePath('./' + fileName) 14 | 15 | if (from === to) { 16 | // already imported 17 | console.log('already imported') 18 | getInfo(context, fileObject, to, cb) 19 | } else { 20 | resolveFileAvailable(to, context.fs, function (err, to) { 21 | if (err) return cb && cb(err) 22 | copyFile(from, to, context.fs, function () { 23 | getInfo(context, fileObject, to, cb) 24 | }) 25 | }) 26 | } 27 | } 28 | 29 | function getInfo (context, fileObject, path, cb) { 30 | var relativeSrc = fileObject.relative(path) 31 | var obs = context.nodes.AudioBuffer(context) 32 | 33 | obs.currentValue(function (buffer) { 34 | if (buffer) { 35 | cb && cb(null, { 36 | buffer: { node: 'AudioBuffer', src: relativeSrc }, 37 | offset: getSoundOffset(buffer) || [0, 1] 38 | }) 39 | 40 | // destroy after callback to avoid double decode 41 | obs.destroy() 42 | } 43 | }) 44 | obs.set({ src: relativeSrc }) 45 | } 46 | -------------------------------------------------------------------------------- /lib/rename-widget.js: -------------------------------------------------------------------------------- 1 | var getBaseName = require('path').basename 2 | var getExtName = require('path').extname 3 | var h = require('lib/h') 4 | 5 | 6 | module.exports = renameElement 7 | 8 | function renameElement (fileName, saveRename, cancelRename, data) { 9 | var element = h('span') 10 | var extname = getExtName(fileName) 11 | 12 | element.textContent = getBaseName(fileName, extname) 13 | element.contentEditable = true 14 | 15 | element.onkeydown = function (e) { 16 | if (e.keyCode === 13) { 17 | save() 18 | return false 19 | } else if (e.keyCode === 27) { 20 | cancelRename() 21 | return false 22 | } 23 | } 24 | element.onblur = function handleRenameBlur (e) { 25 | setImmediate(function () { 26 | if (element.parentNode) { 27 | // only save if still active! 28 | save() 29 | } 30 | }) 31 | } 32 | 33 | setImmediate(function () { 34 | selectInside(element) 35 | }) 36 | 37 | return element 38 | 39 | // scoped 40 | 41 | function save () { 42 | var value = element.textContent.trim() + extname 43 | if (value !== fileName) { 44 | saveRename(value) 45 | } else { 46 | cancelRename() 47 | } 48 | } 49 | } 50 | 51 | function selectInside (element) { 52 | var range = document.createRange() 53 | range.selectNodeContents(element) 54 | var sel = window.getSelection() 55 | sel.removeAllRanges() 56 | sel.addRange(range) 57 | } 58 | -------------------------------------------------------------------------------- /demo-project/Launchpad Example/bass.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/dipper","ratio":1.1194323036880616},{"node":"processor/reverb","time":0.068,"decay":2,"reverse":false,"cutoff":20000,"filterType":"lowpass","wet":4.599,"dry":1},{"node":"processor/overdrive","gain":5.743,"preBand":0.10639269092972742,"color":220,"postCut":2600,"amp":1},{"node":"processor/filter","frequency":190,"Q":1,"gain":-17.048777781243015,"type":"peaking"},{"node":"processor/filter","frequency":6900,"Q":0.38228166905568806,"gain":-18.276826037753082,"type":"peaking"}],"node":"slot","noteOffset":0,"volume":1}],"outputs":["output"],"selectedSlotId":"$template","scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.3625,"release":0.3044558742176154,"attack":0},"frequency":440,"shape":"sawtooth","octave":-3,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.1504,"attack":0,"release":0.10700607022835093},"frequency":440,"octave":-3,"noteOffset":0}],"processors":[{"node":"processor/filter","frequency":{"node":"modulator/adsr","value":1177.1673952359295,"attack":0,"release":0,"sustain":0.4120783242203967,"decay":0.2291123240186734},"Q":0.26548506516605186}],"chokeGroup":"bass","volume":0.5788,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[],"params":[]} -------------------------------------------------------------------------------- /lib/params/toggle-button.js: -------------------------------------------------------------------------------- 1 | var h = require('lib/h') 2 | var send = require('mutant/send') 3 | var computed = require('mutant/computed') 4 | var Set = require('mutant/set') 5 | var resolve = require('mutant/resolve') 6 | 7 | module.exports = function (param, options) { 8 | var value = computed([param, options.defaultValue || false], function (value, defaultValue) { 9 | return value == null ? defaultValue : value 10 | }) 11 | 12 | var onValue = 'onValue' in options ? options.onValue : true 13 | var offValue = 'offValue' in options ? options.offValue : false 14 | 15 | var caption = computed([value, options.title, options.offTitle], function (value, title, offTitle) { 16 | return value === onValue ? title : (offTitle || title) 17 | }) 18 | 19 | var classList = Set(options.classList) 20 | 21 | if (!options.custom) { 22 | classList.add('ToggleButton') 23 | } 24 | 25 | classList.add(computed([value], function (value) { 26 | return value === onValue ? '-active' : null 27 | })) 28 | 29 | return h('button', { 30 | classList: classList, 31 | title: options.description || '', 32 | 'ev-click': send(toggleValue, { 33 | param: param, 34 | onValue: onValue, 35 | offValue: offValue, 36 | value: value 37 | }) 38 | }, caption) 39 | } 40 | 41 | function toggleValue (ev) { 42 | if (resolve(ev.value) !== ev.onValue) { 43 | ev.param.set(resolve(ev.onValue)) 44 | } else { 45 | ev.param.set(resolve(ev.offValue)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /styles/transport.mcss: -------------------------------------------------------------------------------- 1 | Transport { 2 | display: flex 3 | margin: -2px 0 4 | 5 | button { 6 | display: block; 7 | height: 34px 8 | width: 34px 9 | border-radius: 2px; 10 | border: #6B806B 1px solid; 11 | background-color: #2F2F2F; 12 | background-position: center; 13 | background-repeat: no-repeat; 14 | cursor: pointer; 15 | margin: 0 1px 16 | box-shadow: 1px 1px 3px #111 17 | 18 | -beginning { 19 | background-image: svg(beginning) 20 | } 21 | 22 | -play { 23 | background-image: svg(play) 24 | -active { 25 | background-image: svg(play -active) 26 | background-color: #264525; 27 | border-color: #58AE56; 28 | box-shadow: 0 0 10px #26611C 29 | } 30 | } 31 | 32 | :focus { 33 | outline-style: none; 34 | box-shadow: rgb(109, 232, 101) 0 0 2px; 35 | } 36 | } 37 | 38 | button.splice { 39 | margin-left: 10px 40 | color: #AAA 41 | font-size: 15px 42 | } 43 | 44 | @svg play { 45 | height: 16px 46 | width: 16px 47 | content: "" 48 | 49 | path { 50 | fill: #AAA 51 | } 52 | 53 | -active { 54 | path { 55 | fill: #AFA 56 | } 57 | } 58 | } 59 | 60 | @svg beginning { 61 | height: 16px 62 | width: 16px 63 | content: "" 64 | 65 | path { 66 | fill: #AAA 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /demo-project/Launchpad Example/synth.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/filter","frequency":7600,"Q":1,"gain":2.1870010300322242,"type":"peaking"},{"node":"processor/reverb","time":0.379,"decay":2,"reverse":false,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/overdrive","preBand":0.5,"color":3400,"postCut":22000,"gain":1.2688,"amp":1}],"node":"slot","noteOffset":0,"volume":1}],"outputs":["output"],"selectedSlotId":"$template","scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.737,"attack":0.022438594109078478,"release":0.021323813414298707},"shape":"sawtooth","detune":{"node":"modulator/lfo","value":0,"amp":13.678323601175872,"rate":7.662933566816159,"mode":"add","trigger":true},"octave":-1,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.3468,"attack":0.39887686883483425,"release":0.2153259646038883},"frequency":440,"shape":"square","octave":0,"noteOffset":0}],"processors":[{"node":"processor/filter","frequency":{"node":"modulator/adsr","value":17464.588838816086,"attack":0.049,"startValue":300.53407259519037,"decay":0.191,"sustain":0.05523259137853783},"type":"lowpass","Q":1,"gain":0},{"node":"processor/filter","frequency":528.7087523748194,"type":"highpass"}],"volume":0.3783,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[],"params":[]} -------------------------------------------------------------------------------- /demo-project/DWS - OST/synth.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","templateSlot":{"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"node":"slot","output":"output","volume":1,"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.3665,"release":2.9,"decay":{"node":"linkParam","minValue":0.1,"maxValue":2,"param":"decay","mode":"linear"},"sustain":0.02},"frequency":440,"noteOffset":0,"octave":0,"detune":0,"shape":"square"}]},"outputs":["output"],"slots":[{"id":"output","node":"slot","volume":0.6647,"noteOffset":0,"processors":[{"node":"processor/reverb"},{"node":"processor/ring-modulator","carrier":{"amp":0.8894,"frequency":440,"noteOffset":0,"octave":0,"detune":0,"shape":"sawtooth"}},{"node":"processor/filter","frequency":{"node":"linkParam","minValue":17000,"maxValue":140,"param":"cutoff","mode":"exp"},"Q":1,"gain":0,"type":"lowpass"},{"node":"processor/filter","frequency":475,"Q":1,"gain":0,"type":"highpass"},{"node":"processor/ping-pong-delay","time":0.3333333333333333,"sync":true,"feedback":{"node":"linkParam","minValue":0.139,"maxValue":0.8617,"param":"wet","mode":"linear"},"cutoff":20000,"filterType":"lowpass","wet":{"node":"linkParam","minValue":0,"maxValue":0.8602,"param":"wet","mode":"linear"},"dry":1},{"node":"processor/dipper","mode":"modulate","ratio":{"node":"linkParam","minValue":0,"maxValue":1.2087565425730242,"param":"wet","mode":"linear"}}]}],"selectedSlotId":"output","scale":"$global","inputs":[],"params":["cutoff","decay","wet"]} -------------------------------------------------------------------------------- /demo-project/Qwerty Keys Example/synth.json: -------------------------------------------------------------------------------- 1 | {"node":"chunk/scale","slots":[{"id":"output","processors":[{"node":"processor/filter","frequency":7600,"Q":1,"gain":2.1870010300322242,"type":"peaking"},{"node":"processor/reverb","time":0.379,"decay":2,"reverse":false,"cutoff":20000,"filterType":"lowpass","wet":1,"dry":1},{"node":"processor/overdrive","preBand":0.5,"color":3400,"postCut":22000,"gain":1.2688,"amp":1}],"node":"slot","noteOffset":0,"volume":1}],"outputs":["output"],"selectedSlotId":"$template","scale":"$global","templateSlot":{"sources":[{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.737,"attack":0.022438594109078478,"release":0.021323813414298707},"shape":"sawtooth","detune":{"node":"modulator/lfo","value":0,"amp":13.678323601175872,"rate":7.662933566816159,"mode":"add","trigger":true},"octave":-1,"noteOffset":0},{"node":"source/oscillator","amp":{"node":"modulator/adsr","value":0.3468,"attack":0.39887686883483425,"release":0.2153259646038883},"frequency":440,"shape":"square","octave":0,"noteOffset":0,"detune":0}],"processors":[{"node":"processor/filter","frequency":{"node":"modulator/adsr","value":17464.588838816086,"attack":0.049,"startValue":300.53407259519037,"decay":0.191,"sustain":0.05523259137853783},"type":"lowpass","Q":1,"gain":0},{"node":"processor/filter","frequency":528.7087523748194,"type":"highpass"}],"volume":0.3783,"id":{"$param":"id"},"noteOffset":{"node":"modulator/scale","value":{"$param":"value"},"offset":{"$param":"offset"},"scale":{"$param":"scale"}},"output":"output","node":"slot"},"inputs":[],"params":[]} -------------------------------------------------------------------------------- /styles/recording-node.mcss: -------------------------------------------------------------------------------- 1 | RecordingNode { 2 | display: flex 3 | flex-direction: column 4 | flex: 1 5 | overflow: hidden 6 | 7 | div.main { 8 | flex: 1 9 | display: flex 10 | 11 | :focus { 12 | outline: none 13 | } 14 | } 15 | 16 | div.options { 17 | 18 | display: flex; 19 | background: #222; 20 | box-shadow: 0 0 5px #000; 21 | margin-top: 1px; 22 | z-index: 100; 23 | flex-shrink: 0 24 | justify-content: space-between 25 | position: relative 26 | 27 | progress { 28 | position: absolute; 29 | left: 0; 30 | right: 0; 31 | top: -8px; 32 | width: 100%; 33 | } 34 | 35 | section { 36 | padding: 14px; 37 | display: flex 38 | h1 { 39 | flex: none 40 | margin-right: 5px 41 | } 42 | progress { 43 | margin-left: 10px 44 | height: 30px 45 | width: 200px 46 | } 47 | div.param { 48 | flex: 1 49 | display: flex 50 | margin: 3px; 51 | } 52 | div.chooser { 53 | flex: 1 54 | display: flex 55 | } 56 | 57 | button.export { 58 | background-color: #444; 59 | border: 1px solid #777; 60 | color: #FFF; 61 | box-shadow: 1px 1px 3px #111; 62 | cursor: pointer; 63 | 64 | :hover { 65 | background-color: #222; 66 | } 67 | 68 | :focus { 69 | border-color: white 70 | outline: none 71 | } 72 | } 73 | } 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /lib/extend-params.js: -------------------------------------------------------------------------------- 1 | var computed = require('mutant/computed') 2 | var MutantArray = require('mutant/array') 3 | var resolveAvailable = require('lib/resolve-available') 4 | 5 | module.exports = function extendParams (obs) { 6 | var paramOverrideStack = MutantArray([]) 7 | obs.overrideParams = function (params) { 8 | paramOverrideStack.push(params) 9 | return function release () { 10 | paramOverrideStack.delete(params) 11 | } 12 | } 13 | 14 | var raw = {} 15 | 16 | var paramLookup = computed([obs.params, obs.paramValues, paramOverrideStack], function (params, values, overrides) { 17 | var result = {} 18 | var rawResult = {} 19 | for (var i = 0; i < params.length; i++) { 20 | var key = params[i] 21 | var override = paramOverrideStack.get(paramOverrideStack.getLength() - 1) 22 | if (override && override[i] != null) { 23 | result[key] = typeof override[i] === 'function' ? override[i]() : override[i] || 0 24 | rawResult[key] = override[i] 25 | } else { 26 | result[key] = values && values[key] || 0 27 | rawResult[key] = obs.paramValues.get(key) 28 | } 29 | } 30 | raw = rawResult 31 | return result 32 | }) 33 | 34 | paramLookup.get = function (key) { 35 | return raw[key] 36 | } 37 | 38 | paramLookup.keys = function (key) { 39 | return Object.keys(raw) 40 | } 41 | 42 | obs.context.paramLookup = paramLookup 43 | 44 | obs.resolveAvailableParam = function (id, lastId) { 45 | return resolveAvailable(obs.params(), id, lastId) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /styles/toggle-button.mcss: -------------------------------------------------------------------------------- 1 | ToggleButton { 2 | text-decoration: none; 3 | font-size: 90%; 4 | padding: 2px 5px; 5 | border-radius: 4px; 6 | display: inline-block; 7 | border: #949a94 1px solid; 8 | background: #6F6F6F; 9 | color: #D0D0D0; 10 | cursor: pointer; 11 | 12 | -main { 13 | border-radius: 0 14 | padding: 5px 7px 15 | font-size: 110% 16 | display: block 17 | 18 | :after { 19 | border-radius: 4px; 20 | width: 8px; 21 | height: 8px; 22 | } 23 | } 24 | 25 | -active { 26 | background: #325736; 27 | :after { 28 | background: #82D182; 29 | } 30 | } 31 | 32 | -record { 33 | border: #888888 1px solid 34 | 35 | :after { 36 | background: #AEAEAE; 37 | } 38 | 39 | :focus { 40 | box-shadow: rgb(232, 109, 101) 0 0 2px; 41 | } 42 | 43 | :hover { 44 | border-color: #BD8489; 45 | color: #FFEEEE; 46 | } 47 | 48 | -active { 49 | background: #573236; 50 | 51 | :after { 52 | background: #D18282; 53 | } 54 | } 55 | } 56 | 57 | :focus { 58 | outline-style: none; 59 | color: white; 60 | box-shadow: rgb(109, 232, 101) 0 0 2px; 61 | } 62 | 63 | :hover { 64 | border-color: #84BD89; 65 | color: #D6FFC9; 66 | } 67 | 68 | :after { 69 | display: inline-block; 70 | border-radius: 3px; 71 | width: 6px; 72 | height: 6px; 73 | background: #AAA4A4; 74 | content: ''; 75 | margin-left: 3px; 76 | vertical-align: middle; 77 | margin-bottom: 1px; 78 | } 79 | } 80 | --------------------------------------------------------------------------------