Created
March 4, 2016 21:22
-
-
Save staltz/e269f847ae42b3c5cd3c to your computer and use it in GitHub Desktop.
Cycle.js demo with MIDI and Web Audio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Observable, Disposable} from 'rx'; | |
import {run} from '@cycle/core' | |
const jsondiffpatch = require('jsondiffpatch').create({ | |
objectHash: function(obj) { | |
return obj.name; | |
} | |
}); | |
function generateCurve(steps){ | |
var curve = new Float32Array(steps) | |
var deg = Math.PI / 180 | |
for (var i=0;i<steps;i++) { | |
var x = i * 2 / steps - 1 | |
curve[i] = (3 + 10) * x * 20 * deg / (Math.PI + 10 * Math.abs(x)) | |
} | |
return curve | |
} | |
function WebAudioDriver(instructions$) { | |
let audioContext = new AudioContext(); | |
let shaper = audioContext.createWaveShaper(); | |
// shaper.curve = new Float32Array([-1, 1]); | |
shaper.curve = generateCurve(22050) // half of 44100 (sample rate) | |
let amp = audioContext.createGain(); | |
const MAX_GAIN = 2; | |
const MAX_RELEASE = 2; | |
let releaseMUTABLE = 0.01; | |
amp.gain.value = MAX_GAIN; | |
amp.connect(shaper); | |
shaper.connect(audioContext.destination); | |
let oscillators = []; | |
const instruments$ = instructions$ | |
.filter(x => x.type === 'instrument') | |
.map(x => x.payload); | |
const gain$ = instructions$ | |
.filter(x => x.type === 'gain') | |
.map(x => x.payload); | |
const release$ = instructions$ | |
.filter(x => x.type === 'release') | |
.map(x => x.payload); | |
const distortion$ = instructions$ | |
.filter(x => x.type === 'distortion') | |
.map(x => x.payload); | |
gain$.subscribe(gain => { | |
amp.gain.value = gain * MAX_GAIN; | |
}); | |
release$.subscribe(release => { | |
releaseMUTABLE = Math.max(release * MAX_RELEASE, 0.01); | |
}); | |
distortion$.subscribe(distortion => { | |
if (distortion) { | |
shaper.curve = generateCurve(22050) // half of 44100 (sample rate) | |
} else { | |
shaper.curve = null; | |
} | |
}); | |
instruments$ | |
.startWith([]) | |
.pairwise() | |
.subscribe(([oldInstruments, newInstruments]) => { | |
const delta = jsondiffpatch.diff(oldInstruments, newInstruments); | |
if (!delta) { | |
return; | |
} | |
// console.log(JSON.stringify(delta, null, ' ')); | |
if (delta._t === 'a') { | |
for (let key in delta) { | |
if (key !== '_t' && delta.hasOwnProperty(key)) { | |
if (key.substr(0, 1) === '_') { | |
key = key.substr(1); | |
if (typeof parseInt(key) === 'number') { | |
let oscillator = oscillators.splice(parseInt(key), 1)[0]; | |
oscillator.envelope.gain.setTargetAtTime(0, audioContext.currentTime, releaseMUTABLE) | |
oscillator.stop(audioContext.currentTime + 5); | |
} | |
} else if (typeof parseInt(key) === 'number') { | |
var envelope = audioContext.createGain(); | |
envelope.connect(amp); | |
envelope.gain.value = 0; | |
envelope.gain.setTargetAtTime(1, audioContext.currentTime, 0.01); | |
let oscillator = audioContext.createOscillator(); | |
oscillator.envelope = envelope; | |
oscillator.connect(envelope); | |
oscillator.type = delta[key][0].type; | |
oscillator.detune.value = delta[key][0].note; | |
oscillator.start(); | |
oscillators.push(oscillator); | |
} | |
} | |
} | |
} | |
}); | |
} | |
function MIDIDriver() { | |
return Observable.fromPromise(navigator.requestMIDIAccess()) | |
.map(midi => midi.inputs.values().next().value) | |
.flatMap(object => | |
Observable.create(observer => { | |
if (object.observers === undefined) { | |
object.observers = []; | |
object.onmidimessage = (event) => { | |
object.observers.forEach(observer => { | |
observer.onNext(event); | |
}); | |
} | |
} | |
object.observers.push(observer); | |
return Disposable.create(() => { | |
object.observers = object.observers.filter(x => x !== observer); | |
}); | |
}) | |
); | |
} | |
// ============================================================================ | |
function main(sources) { | |
const message$ = sources.MIDI.map(x => { | |
return { | |
status: x.data[0] & 0xf0, | |
data: [ | |
x.data[1], | |
x.data[2] | |
] | |
}; | |
}); | |
const knob1$ = message$.filter(x => x.data[0] === 1); | |
const knob2$ = message$.filter(x => x.data[0] === 2); | |
const knob3$ = message$.filter(x => x.data[0] === 3); | |
const pad1$ = message$.filter(x => x.data[0] === 36); | |
const pad2$ = message$.filter(x => x.data[0] === 37); | |
const pad3$ = message$.filter(x => x.data[0] === 38); | |
const pad4$ = message$.filter(x => x.data[0] === 39); | |
const pad5$ = message$.filter(x => x.data[0] === 40); | |
const pad6$ = message$.filter(x => x.data[0] === 41); | |
const pad7$ = message$.filter(x => x.data[0] === 42); | |
const pad8$ = message$.filter(x => x.data[0] === 43); | |
const pad1Down$ = pad1$.filter(x => x.status === 144); | |
const pad1Up$ = pad1$.filter(x => x.status === 128); | |
const pad2Down$ = pad2$.filter(x => x.status === 144); | |
const pad2Up$ = pad2$.filter(x => x.status === 128); | |
const pad3Down$ = pad3$.filter(x => x.status === 144); | |
const pad3Up$ = pad3$.filter(x => x.status === 128); | |
const pad4Down$ = pad4$.filter(x => x.status === 144); | |
const pad4Up$ = pad4$.filter(x => x.status === 128); | |
const pad5Down$ = pad5$.filter(x => x.status === 144); | |
const pad5Up$ = pad5$.filter(x => x.status === 128); | |
const pad6Down$ = pad6$.filter(x => x.status === 144); | |
const pad6Up$ = pad6$.filter(x => x.status === 128); | |
const pad7Down$ = pad7$.filter(x => x.status === 144); | |
const pad7Up$ = pad7$.filter(x => x.status === 128); | |
const pad8Down$ = pad8$.filter(x => x.status === 144); | |
const pad8Up$ = pad8$.filter(x => x.status === 128); | |
const sound1$ = Observable.merge( | |
pad1Down$.map({type: 'sine', note: -900}), | |
pad1Up$.map(null), | |
).startWith(null); | |
const sound2$ = Observable.merge( | |
pad2Down$.map({type: 'sine', note: -700}), | |
pad2Up$.map(null), | |
).startWith(null); | |
const sound3$ = Observable.merge( | |
pad3Down$.map({type: 'sine', note: -600}), | |
pad3Up$.map(null), | |
).startWith(null); | |
const sound4$ = Observable.merge( | |
pad4Down$.map({type: 'sine', note: -400}), | |
pad4Up$.map(null), | |
).startWith(null); | |
const sound8$ = Observable.merge( | |
pad8Down$.map({type: 'sine', note: -200}), | |
pad8Up$.map(null), | |
).startWith(null); | |
const sound7$ = Observable.merge( | |
pad7Down$.map({type: 'sine', note: -100}), | |
pad7Up$.map(null), | |
).startWith(null); | |
const sound6$ = Observable.merge( | |
pad6Down$.map({type: 'sine', note: 100}), | |
pad6Up$.map(null), | |
).startWith(null); | |
const sound5$ = Observable.merge( | |
pad5Down$.map({type: 'sine', note: 300}), | |
pad5Up$.map(null), | |
).startWith(null); | |
const sound$ = Observable.combineLatest( | |
sound1$, sound2$, sound3$, sound4$, sound5$, sound6$, sound7$, sound8$, | |
(...args) => args.filter(x => !!x) | |
); | |
const gain$ = knob1$.map(x => x.data[1] / 127); | |
const release$ = knob2$.map(x => x.data[1] / 127); | |
const distortion$ = knob3$.map(x => (x.data[1] / 127) > 0.5).distinctUntilChanged(); | |
const log = (x) => console.log(JSON.stringify(x)); | |
const instructions$ = Observable.merge( | |
sound$.map(x => ({type: 'instrument', payload: x})).do(log), | |
gain$.map(x => ({type: 'gain', payload: x})).do(log), | |
release$.map(x => ({type: 'release', payload: x})).do(log), | |
distortion$.map(x => ({type: 'distortion', payload: x})).do(log), | |
) | |
return { | |
Audio: instructions$ | |
}; | |
} | |
let drivers = { | |
MIDI: MIDIDriver, | |
Audio: WebAudioDriver, | |
}; | |
run(main, drivers); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "example", | |
"version": "0.0.0", | |
"private": true, | |
"author": "Andre Staltz", | |
"license": "MIT", | |
"dependencies": { | |
"@cycle/core": "6.0.2", | |
"rx": "^4.0.7", | |
"jsondiffpatch": "^0.1.38" | |
}, | |
"devDependencies": { | |
"babel": "5.6.x", | |
"babelify": "6.1.x", | |
"browserify": "11.0.1", | |
"mkdirp": "0.5.x", | |
"watchify": "^3.7.0" | |
}, | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1", | |
"prebrowserify": "mkdirp dist", | |
"browserify": "browserify src/main.js -t babelify --outfile dist/main.js", | |
"start": "npm install && npm run browserify && echo 'OPEN index.html IN YOUR BROWSER'", | |
"watch": "watchify src/main.js -t babelify --poll --outfile dist/main.js" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment