Skip to content

Instantly share code, notes, and snippets.

@staltz
Created March 4, 2016 21:22
Show Gist options
  • Save staltz/e269f847ae42b3c5cd3c to your computer and use it in GitHub Desktop.
Save staltz/e269f847ae42b3c5cd3c to your computer and use it in GitHub Desktop.
Cycle.js demo with MIDI and Web Audio
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);
{
"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