Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Drum 'n' JS

Drum 'n' JS

Good design practices and clean code ensure your code is scalable and performant. But what to do when they can't keep up?

In this talk, we’re cranking things up a notch by building a fully-featured drum machine in Angular with the WebAudio API. We’ll bend the rules, embrace unintuitive data structures, and sprinkle in some design anti-patterns for good measure—all in the name of peak performance. And don’t worry; I have the mind-blowing stats to prove it.

Prepare to have your coding beliefs shaken (and stirred).

Miroslav Jonaš

December 07, 2024
Tweet

More Decks by Miroslav Jonaš

Other Decks in Technology

Transcript

  1. PLAY "t100 p8 e8 e8 f8 g8 g8 f8 e8

    d8 c8 c8 d8 e8 e4. d16 d4" @meeroslav
  2. Web Audio 101 const AudioContext = window.AudioContext || window.webkitAudioContext; const

    audioContext = new AudioContext(); // load and decode a sound file const sample = await fetch('https:// example.com/mysound.wav'); const arrayBuffer = await sample.arrayBuffer(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // create a buffer source const source = audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(audioContext.destination); source.start(); @meeroslav
  3. Effects const master = audioContext.createGain(); master.gain.value = 0.8; source.connect(master); const

    lowPassFilter = audioContext.createBiquadFilter(); lowPassFilter.type = 'lowpass'; lowPassFilter.frequency.value = 5000; master.connect(lowPassFilter); lowPassFilter.connect(audioContext.destination); Buffer Source Gain Output Low Pass Filter @meeroslav
  4. Build your own synth const master = audioContext.createGain(); master.gain.value =

    0.8; master.connect(audioContext.destination); Gain Output const oscillator = audioContext.createOscillator(); oscillator.type = 'sine'; oscillator.frequency.value = 440; oscillator.start(); oscillator.connect(master); Oscillator @meeroslav
  5. A bit of math @meeroslav •120 BPM •2 beats per

    second •8 tics per second •1 tic every 125 ms
  6. Know your loops const nodes = []; for (let i

    = 0; i < nodes.length; i++ ) { const v = nodes[i]; v.children.forEach((c) => { for (key in c) { collection[key] = { ... myInit }; } }); } 1 2 3 4 @meeroslav
  7. const audioTracks = [ /* audio tracks */ ]; unmuteAllTracks(audioTracks);

    // <--- implement this // immutable version function unmuteAllTracks(audioTracks) { return audioTracks.map(track => ({ ... track, sequence: [... track.sequence], muted: false, })); } @meeroslav
  8. const audioTracks = [ /* audio tracks */ ]; unmuteAllTracks(audioTracks);

    // <--- implement this // mutable version function unmuteAllTracks(audioTracks) { audioTracks.forEach(track => { track.muted = false; }); } 27* times faster (for 10 tracks) @meeroslav
  9. ? const track: DrumTrack = { id: 'abcdef', volume: 0.8,

    reverb: 0.2, solo: false, mute: false, gain: context.createGain(), type: 'DRUM', sample: 'kick', sequence: [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, ] } @meeroslav
  10. 50%* faster (for 16 step sequence) @meeroslav const track: DrumTrack

    = { id: 'abcdef', volume: 0.8, reverb: 0.2, solo: false, mute: false, gain: context.createGain(), type: 'DRUM', sample: 'kick', sequence: [] }
  11. const track: SamplerTrack = { id: 'abcdef', volume: 0.8, reverb:

    0.2, solo: false, mute: false, gain: context.createGain(), type: 'SAMPLER', sample: 'kick', envelope: { attack: 0.1, decay: 0.2, sustain: 0.5, release: 0.3 }, sequence: [ { note: 6 }, { note: 4 }, { note: 8 }, { note: undefined }, { note: 2 }, { note: '-' }, { note: '-' }, { note: undefined }, { note: 6 }, { note: 4 }, { note: 8 }, { note: undefined }, { note: 2 }, { note: '-' }, { note: '-' }, { note: undefined }, ] } @meeroslav export type SamplerTrack = { id: string; volume: number; reverb: number; solo: boolean; mute: boolean; gain: GainNode; type: 'SAMPLER'; sample: 'string'; envelope: { attack: number; decay: number; sustain: number; release: number; }; sequence: SynthNote[]; }; type SynthNote = { note: number | undefined | '-'; frequency?: number; }; // track.sequence[0].note // track.envelope.sustain
  12. const track = [ 'abcdef', 0.8, 0.2, false, false, context.createGain(),

    1, 'kick', [ 0.1, 0.2, 0.5, 0.3 ], [ 6, 4, 8, undefined, 2, '-', '-', undefined, 6, 4, 8, undefined, 2, '-', '-', undefined ] ]; @meeroslav const track: SamplerTrack = { id: 'abcdef', volume: 0.8, reverb: 0.2, solo: false, mute: false, gain: context.createGain(), type: 'SAMPLER', sample: 'kick', envelope: { attack: 0.1, decay: 0.2, sustain: 0.5, release: 0.3 }, sequence: [ { note: 6 }, { note: 4 }, { note: 8 }, { note: undefined }, { note: 2 }, { note: '-' }, { note: '-' }, { note: undefined }, { note: 6 }, { note: 4 }, { note: 8 }, { note: undefined }, { note: 2 }, { note: '-' }, { note: '-' }, { note: undefined }, ] }
  13. const TRACK_ID = 0; const TRACK_VOLUME = 1; const TRACK_REVERB

    = 2; const TRACK_SOLO = 3; const TRACK_MUTE = 4; const TRACK_GAIN = 5; const TRACK_TYPE = 6; const TRACK_SAMPLE = 7; const TRACK_ENVELOPE = 8; const TRACK_SEQUENCE = 9; const ATTACK = 0; const DECAY = 1; const SUSTAIN = 2; const RELEASE = 3; // track.sequence[5] track[TRACK_SEQUENCE][5] // track.envelope.sustain track[TRACK_ENVELOPE][SUSTAIN] 18% faster @meeroslav const track = [ 'abcdef', 0.8, 0.2, false, false, context.createGain(), 1, 'kick', [ 0.1, 0.2, 0.5, 0.3 ], [ 6, 4, 8, undefined, 2, '-', '-', undefined, 6, 4, 8, undefined, 2, '-', '-', undefined ] ];
  14. const ROMPLER_SAMPLES = [ { sample: '303_1_g', offset: BASE_NOTE -

    5, name: '303 Open' }, { sample: '303_2_g', offset: BASE_NOTE - 5, name: '303 Closed' }, { sample: 'bass_a', offset: 0, name: 'Bass' }, { sample: 'bass_chonk_c', offset: BASE_NOTE - 12, name: 'Bass Chonk' }, { sample: 'bass_growl_c', offset: BASE_NOTE - 12, name: 'Bass Growl' }, { sample: 'bass_long_f', offset: BASE_NOTE - 7, name: 'Bass Long' }, { sample: 'bass_reese', offset: BASE_NOTE, name: 'Reese Bass' }, { sample: 'pluck_bass_c', offset: BASE_NOTE - 12, name: 'Pluck Bass' }, { sample: 'chase_synth_c', offset: BASE_NOTE, name: 'Chase Synth' }, { sample: 'choir_c', offset: BASE_NOTE - 12, name: 'Choir' }, { sample: 'efx_game', offset: BASE_NOTE, name: 'Game Efx' }, { sample: 'funk_pad_c', offset: BASE_NOTE, name: 'Funk Pad' }, { sample: 'grider_fsharp', offset: BASE_NOTE - 6, name: 'Grooverider Grawl' }, { sample: 'lead_inner', offset: BASE_NOTE, name: 'Inner Lead' }, { sample: 'organ_d', offset: BASE_NOTE, name: 'Organ' }, { sample: 'piano_c', offset: BASE_NOTE, name: 'Piano' }, { sample: 'tashepad_c', offset: BASE_NOTE, name: 'Tasche Pad' }, ]; ROMPLER_SAMPLES.find(s => s.sample === 'choir_c').offset; @meeroslav
  15. 42* times faster (for 17 samples) const ROMPLER_SAMPLES = {

    '303_1_g': { offset: BASE_NOTE - 5, name: '303 Open' }, '303_2_g': { offset: BASE_NOTE - 5, name: '303 Closed' }, 'bass_a': { offset: 0, name: 'Bass' }, 'bass_chonk_c': { offset: BASE_NOTE - 12, name: 'Bass Chonk' }, 'bass_growl_c': { offset: BASE_NOTE - 12, name: 'Bass Growl' }, 'bass_long_f': { offset: BASE_NOTE - 7, name: 'Bass Long' }, 'bass_reese': { offset: BASE_NOTE, name: 'Reese Bass' }, 'pluck_bass_c': { offset: BASE_NOTE - 12, name: 'Pluck Bass' }, 'chase_synth_c': { offset: BASE_NOTE, name: 'Chase Synth' }, 'choir_c': { offset: BASE_NOTE - 12, name: 'Choir' }, 'efx_game': { offset: BASE_NOTE, name: 'Game Efx' }, 'funk_pad_c': { offset: BASE_NOTE, name: 'Funk Pad' }, 'grider_fsharp': { offset: BASE_NOTE - 6, name: 'Grooverider Grawl' }, 'lead_inner': { offset: BASE_NOTE, name: 'Inner Lead' }, 'organ_d': { offset: BASE_NOTE, name: 'Organ' }, 'piano_c': { offset: BASE_NOTE, name: 'Piano' }, 'tashepad_c': { offset: BASE_NOTE, name: 'Tasche Pad' }, }; ROMPLER_SAMPLES['choir_c'].offset; @meeroslav
  16. 3.5* times slower const ROMPLER_SAMPLES = new Map([ ['303_1_g', {

    offset: BASE_NOTE - 5, name: '303 Open' }], ['303_2_g', { offset: BASE_NOTE - 5, name: '303 Closed' }], ['bass_a', { offset: 0, name: 'Bass' }], ['bass_chonk_c', { offset: BASE_NOTE - 12, name: 'Bass Chonk' }], ['bass_growl_c', { offset: BASE_NOTE - 12, name: 'Bass Growl' }], ['bass_long_f', { offset: BASE_NOTE - 7, name: 'Bass Long' }], ['bass_reese', { offset: BASE_NOTE, name: 'Reese Bass' }], ['pluck_bass_c', { offset: BASE_NOTE - 12, name: 'Pluck Bass' }], ['chase_synth_c', { offset: BASE_NOTE, name: 'Chase Synth' }], ['choir_c', { offset: BASE_NOTE - 12, name: 'Choir' }], ['efx_game', { offset: BASE_NOTE, name: 'Game Efx' }], ['funk_pad_c', { offset: BASE_NOTE, name: 'Funk Pad' }], ['grider_fsharp', { offset: BASE_NOTE - 6, name: 'Grooverider Grawl' }], ['lead_inner', { offset: BASE_NOTE, name: 'Inner Lead' }], ['organ_d', { offset: BASE_NOTE, name: 'Organ' }], ['piano_c', { offset: BASE_NOTE, name: 'Piano' }], ['tashepad_c', { offset: BASE_NOTE, name: 'Tasche Pad' }], ]); ROMPLER_SAMPLES.get('choir_c').offset; @meeroslav
  17. 7* times faster tracks .filter(t => t.type === 'SAMPLER') .map(t

    => t.sequence) .reduce((acc, cur) => [ ... acc, ... cur ], []); const seq = []; tracks.forEach(t => { if (t.type === 'SAMPLER') { t.sequence.forEach(s => { seq.push(s) }) } }) @meeroslav
  18. know your loops don't be afraid to mutate choose structure

    types wisely skip HOF when performance matters check against your use case @meeroslav