Skip to content

Instantly share code, notes, and snippets.

@psema4
Created August 24, 2018 03:55
Show Gist options
  • Save psema4/5249a24f3f6875e32081b38bf55538bc to your computer and use it in GitHub Desktop.
Save psema4/5249a24f3f6875e32081b38bf55538bc to your computer and use it in GitHub Desktop.
A simple webaudio sequencer
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