Created
August 24, 2018 03:55
-
-
Save psema4/5249a24f3f6875e32081b38bf55538bc to your computer and use it in GitHub Desktop.
A simple webaudio sequencer
This file contains hidden or 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
class SimpleSequencer { | |
/* SimpleSequencer essentially combines the following tutorials and wraps them in es6 | |
* | |
* https://github.com/kylestetz/Web-Audio-Basics/tree/gh-pages/4-Continuous-Sequencing | |
* https://www.html5rocks.com/en/tutorials/audio/scheduling/ | |
* http://clockworkchilli.com/blog/3_html5_drum_machine_with_web_audio | |
* | |
* | |
* Usage: | |
* let sequencer = new SimpleSequencer({ | |
* tracks: [ ... see constructor ... ], | |
* instruments: [ ... see constructor ... ], | |
* ui: { | |
* play: document.getElementById('btnPlay'), | |
* stop: document.getElementById('btnStop'), | |
* volume: document.getElementById('inpVolume'), | |
* } | |
* }) | |
* | |
* | |
* Filesize: | |
* unminified: ~8.5kb | |
* minified with uglifyjs-es: ~4.0kb | |
* minified + zipped: ~1.4kb | |
* | |
*/ | |
constructor(opts = {}) { | |
this.context = new AudioContext() | |
window.context = this.context | |
this.bpm = 120 | |
this.noteLength = this.bpm / 60 * (1/8) // an eighth note at the given bpm | |
this.lookahead = 0.04 // 40ms expressed in seconds | |
this.intervalTime = 25 // 25ms expressed in milliseconds | |
this.nextNoteTime = null | |
this.currentNote = 0 | |
this.intervalId = null | |
this.masterVolume = this.context.createGain() | |
this.masterVolume.connect(this.context.destination) | |
this.masterVolume.gain.value = 0.5 | |
this.noiseBuffer = context.createBuffer(1, 44100, 44100) | |
this.noiseBufferOutput = this.noiseBuffer.getChannelData(0) | |
for (var i = 0; i < 44100; i++) | |
this.noiseBufferOutput[i] = Math.random() * 2 - 1 | |
// Instrument tuple fields: type, attack, decay | |
// where type is one of: 0 = sine, 1 = square, 2 = triangle, 3 = sawtooth ] | |
this.instruments = opts.instruments || [ | |
[ 0, 0, 0 ], // ignore control track | |
[ 0, 0, 0 ], // ignore kick track | |
[ 0, 0, 0 ], // ignore snare track | |
[ 0, 0, 0 ], // ignore hihat track | |
[ 0, 1/64, 1/32 ], | |
[ 1, 1/64, 1/8 ], | |
] | |
this.tracks = opts.tracks || [ | |
[ 0, 0, 0, 0, 0, 0, 0, 0, ], // Control | |
[ 1, 0, 0, 0, 1, 0, 0, 0, ], // Kick | |
[ 0, 0, 1, 0, 0, 0, 1, 0, ], // Snare | |
[ 0, 1, 0, 1, 0, 1, 0, 1, ], // Hihat | |
[ 66, 69, 68, 67, 66, 65, 66, 67, ], // Instrument 1 | |
[ 55, 0, 0, 0, 62, 0, 55, 0, ], // Instrument 2 | |
] | |
this.ui = {} | |
// input type=button | |
if (opts.ui && opts.ui.play) { | |
this.ui.play = opts.ui.play | |
this.ui.play.addEventListener('click', this.startSequencer.bind(this)) | |
} | |
// input type=button | |
if (opts.ui && opts.ui.stop) { | |
this.ui.stop = opts.ui.stop | |
this.ui.stop.addEventListener('click', this.stopSequencer.bind(this)) | |
} | |
// input type=range | |
if (opts.ui && opts.ui.volume) { | |
this.ui.volume = opts.ui.volume | |
this.masterVolume.gain.value = opts.ui.volume.value / 100 | |
this.ui.volume.addEventListener('change', (evt) => { | |
this.masterVolume.gain.value = evt.srcElement.value / 100 | |
}) | |
} | |
} | |
setNote(track = 0, value = 0) { | |
this.tracks[ track ] = value | |
} | |
startSequencer() { | |
this.ui.play && this.ui.play.setAttribute('disabled', 'disabled') | |
this.ui.stop && this.ui.stop.removeAttribute('disabled') | |
this.nextNoteTime = this.context.currentTime | |
this.scheduleSequence() | |
this.intervalId = setInterval(this.scheduleSequence.bind(this), this.intervalTime) | |
} | |
stopSequencer() { | |
this.ui.play && this.ui.play.removeAttribute('disabled') | |
this.ui.stop && this.ui.stop.setAttribute('disabled', 'disabled') | |
this.intervalId = clearInterval(this.intervalId) | |
} | |
scheduleSequence() { | |
while(this.nextNoteTime < this.context.currentTime + this.lookahead) { | |
this.tracks.forEach((track, trackId) => { | |
if (trackId === 0) | |
return | |
let currentNoteValue = track[this.currentNote] | |
if (trackId <= 3 && currentNoteValue > 0) { | |
this.schedulePercussion( trackId, this.nextNoteTime, this.currentNote ) | |
} else { | |
this.scheduleNote( currentNoteValue, this.nextNoteTime, this.currentNote, this.instruments[trackId] ) | |
} | |
}) | |
this.nextNoteTime += this.noteLength | |
this.currentNote = ++this.currentNote % this.tracks[0].length // Control track | |
} | |
} | |
scheduleNote(note, time, current, instrument) { | |
if (! (Array.isArray(instrument) && instrument.length == 3)) | |
return | |
let oscillator = this.context.createOscillator() | |
let gain = this.context.createGain() | |
let attack = instrument[1] | |
let decay = instrument[2] | |
let oscTypes = [ 'sine', 'square', 'triangle', 'sawtooth' ] | |
oscillator.frequency.value = this.mtof(note) | |
oscillator.type = oscTypes[instrument[0]] | |
oscillator.connect(gain) | |
gain.connect(this.masterVolume) | |
gain.gain.setValueAtTime(0.01, time) | |
gain.gain.linearRampToValueAtTime(this.masterVolume.gain.value, time + attack) | |
gain.gain.linearRampToValueAtTime(0.01, time + this.noteLength - decay) | |
oscillator.start(time) | |
oscillator.stop(time + this.noteLength) | |
} | |
schedulePercussion(trackId, time, current) { | |
switch(trackId) { | |
case 3: | |
this.hihat(time) | |
break | |
case 2: | |
this.snare(time) | |
break | |
case 1: | |
this.kick(time) | |
break | |
case 0: | |
default: | |
} | |
} | |
kick(time = 0) { | |
let osc = this.context.createOscillator() | |
let gain = this.context.createGain() | |
osc.frequency.setValueAtTime(150, time) | |
osc.frequency.exponentialRampToValueAtTime(0.01, time + this.noteLength) | |
gain.gain.setValueAtTime(3, time) | |
gain.gain.exponentialRampToValueAtTime(0.01, time + this.noteLength) | |
osc.connect(gain) | |
gain.connect(this.masterVolume) | |
osc.start(time) | |
osc.stop(time + this.noteLength) | |
} | |
hihat(time = 0) { | |
let noise = this.context.createBufferSource() | |
noise.buffer = this.noiseBuffer | |
let noiseFilter = this.context.createBiquadFilter() | |
noiseFilter.type = 'highpass' | |
noiseFilter.frequency.value = 10000 | |
noise.connect(noiseFilter) | |
let noiseEnvelope = this.context.createGain() | |
noiseFilter.connect(noiseEnvelope) | |
noiseEnvelope.connect(this.masterVolume) | |
noiseEnvelope.gain.setValueAtTime(0.7, time) | |
noiseEnvelope.gain.exponentialRampToValueAtTime(0.5, time + 0.5) | |
noiseEnvelope.gain.exponentialRampToValueAtTime(0.01, time + 0.1) | |
noise.start(time) | |
noise.stop(time + 0.1) | |
} | |
snare(time = 0) { | |
let noise = this.context.createBufferSource() | |
noise.buffer = this.noiseBuffer | |
let noiseFilter = this.context.createBiquadFilter() | |
noiseFilter.type = 'highpass' | |
noiseFilter.frequency.value = 1000 | |
noise.connect(noiseFilter) | |
let noiseEnvelope = this.context.createGain() | |
noiseFilter.connect(noiseEnvelope) | |
noiseEnvelope.connect(this.masterVolume) | |
let osc = this.context.createOscillator() | |
osc.type = 'triangle' | |
let oscEnvelope = this.context.createGain() | |
osc.connect(oscEnvelope) | |
oscEnvelope.connect(this.masterVolume) | |
noiseEnvelope.gain.setValueAtTime(1, time) | |
noiseEnvelope.gain.exponentialRampToValueAtTime(0.01, time + 0.2) | |
noise.start(time) | |
osc.frequency.setValueAtTime(100, time) | |
oscEnvelope.gain.setValueAtTime(0.7, time) | |
oscEnvelope.gain.exponentialRampToValueAtTime(0.01, time + 0.1) | |
osc.start(time) | |
osc.stop(time + 0.2) | |
noise.stop(time + 0.2) | |
} | |
mtof(note) { | |
return ( Math.pow(2, ( note-69 ) / 12) ) * 440.0 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment