Slide 1

Slide 1 text

Miroslav Jonaš / @meeroslav / NGBE 2024 Drum 'n' JS

Slide 2

Slide 2 text

Miroslav Jonaš DPE at @meeroslav

Slide 3

Slide 3 text

•Solving puzzles •Sharing knowledge •Art @meeroslav

Slide 4

Slide 4 text

@meeroslav YES!

Slide 5

Slide 5 text

First... there was sound @meeroslav

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

PLAY "t100 p8 e8 e8 f8 g8 g8 f8 e8 d8 c8 c8 d8 e8 e4. d16 d4" @meeroslav

Slide 8

Slide 8 text

Fastracker II @meeroslav

Slide 9

Slide 9 text

Hammerhead @meeroslav

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

@meeroslav

Slide 14

Slide 14 text

A bit of math @meeroslav •120 BPM •2 beats per second •8 tics per second •1 tic every 125 ms

Slide 15

Slide 15 text

@meeroslav setInterval rendering context audio context framework render loop data processing

Slide 16

Slide 16 text

Schedule play vs. play now @meeroslav

Slide 17

Slide 17 text

The performance @meeroslav

Slide 18

Slide 18 text

TTI FCP TBT LCP LSD NBA @meeroslav

Slide 19

Slide 19 text

Hydration @meeroslav

Slide 20

Slide 20 text

Partial hydration @meeroslav

Slide 21

Slide 21 text

Battle tested @meeroslav

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

MUTATIONS ARE BAD

Slide 24

Slide 24 text

NOT ALWAYS, THOUGH

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

PERFORMANCE DETECTION Repetitio est mater studio…

Slide 28

Slide 28 text

? 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

Slide 29

Slide 29 text

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: [] }

Slide 30

Slide 30 text

“No code is faster than any code”  Me @meeroslav

Slide 31

Slide 31 text

PERFORMANCE DETECTION ARRAYS vs OBJECTS @meeroslav

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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 }, ] }

Slide 34

Slide 34 text

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 ] ];

Slide 35

Slide 35 text

PERFORMANCE DETECTION Don't Lookup tables

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

MAPS VS HASHMAPS MAPS VS HASHMAPS

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Higher order functions @meeroslav

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

@meeroslav Zone.js? OnPush?

Slide 43

Slide 43 text

@meeroslav Mutate & persist

Slide 44

Slide 44 text

PERFORMANCE DETECTION

Slide 45

Slide 45 text

JSBENCH.ME @meeroslav

Slide 46

Slide 46 text

console.time 4 the win @meeroslav

Slide 47

Slide 47 text

The Butterfly Effect: How we gave the Linter a 100x boost @meeroslav

Slide 48

Slide 48 text

@meeroslav Full demo

Slide 49

Slide 49 text

know your loops don't be afraid to mutate choose structure types wisely skip HOF when performance matters check against your use case @meeroslav

Slide 50

Slide 50 text

Follow your passion(s) @meeroslav

Slide 51

Slide 51 text

console.time @meeroslav