Neural Drum Machine Density Remix A developer built a neural drum machine using Magenta's MusicRNN and MusicVAE models, combined with Tone.js for audio synthesis. The system generates drum patterns with humanization and reverb effects, mapping MIDI notes to nine drum classes with velocity layers. | const DRUM CLASSES = | | 'Kick', | | 'Snare', | | 'Hi-hat closed', | | 'Hi-hat open', | | 'Tom low', | | 'Tom mid', | | 'Tom high', | | 'Clap', | | 'Rim' | | ; | | const TIME HUMANIZATION = 0.01; | | | | let sampleBaseUrl = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699'; | | | | let reverb = new Tone.Convolver | | ${sampleBaseUrl}/small-drum-room.wav | | .toMaster ; | | reverb.wet.value = 0.35; | | | | let snarePanner = new Tone.Panner .connect reverb ; | | new Tone.LFO 0.13, -0.25, 0.25 .connect snarePanner.pan .start ; | | | | let drumKit = | | new Tone.Players { | | high: ${sampleBaseUrl}/808-kick-vh.mp3 , | | med: ${sampleBaseUrl}/808-kick-vm.mp3 , | | low: ${sampleBaseUrl}/808-kick-vl.mp3 | | } .toMaster , | | new Tone.Players { | | high: ${sampleBaseUrl}/flares-snare-vh.mp3 , | | med: ${sampleBaseUrl}/flares-snare-vm.mp3 , | | low: ${sampleBaseUrl}/flares-snare-vl.mp3 | | } .connect snarePanner , | | new Tone.Players { | | high: ${sampleBaseUrl}/808-hihat-vh.mp3 , | | med: ${sampleBaseUrl}/808-hihat-vm.mp3 , | | low: ${sampleBaseUrl}/808-hihat-vl.mp3 | | } .connect new Tone.Panner -0.5 .connect reverb , | | new Tone.Players { | | high: ${sampleBaseUrl}/808-hihat-open-vh.mp3 , | | med: ${sampleBaseUrl}/808-hihat-open-vm.mp3 , | | low: ${sampleBaseUrl}/808-hihat-open-vl.mp3 | | } .connect new Tone.Panner -0.5 .connect reverb , | | new Tone.Players { | | high: ${sampleBaseUrl}/slamdam-tom-low-vh.mp3 , | | med: ${sampleBaseUrl}/slamdam-tom-low-vm.mp3 , | | low: ${sampleBaseUrl}/slamdam-tom-low-vl.mp3 | | } .connect new Tone.Panner -0.4 .connect reverb , | | new Tone.Players { | | high: ${sampleBaseUrl}/slamdam-tom-mid-vh.mp3 , | | med: ${sampleBaseUrl}/slamdam-tom-mid-vm.mp3 , | | low: ${sampleBaseUrl}/slamdam-tom-mid-vl.mp3 | | } .connect reverb , | | new Tone.Players { | | high: ${sampleBaseUrl}/slamdam-tom-high-vh.mp3 , | | med: ${sampleBaseUrl}/slamdam-tom-high-vm.mp3 , | | low: ${sampleBaseUrl}/slamdam-tom-high-vl.mp3 | | } .connect new Tone.Panner 0.4 .connect reverb , | | new Tone.Players { | | high: ${sampleBaseUrl}/909-clap-vh.mp3 , | | med: ${sampleBaseUrl}/909-clap-vm.mp3 , | | low: ${sampleBaseUrl}/909-clap-vl.mp3 | | } .connect new Tone.Panner 0.5 .connect reverb , | | new Tone.Players { | | high: ${sampleBaseUrl}/909-rim-vh.wav , | | med: ${sampleBaseUrl}/909-rim-vm.wav , | | low: ${sampleBaseUrl}/909-rim-vl.wav | | } .connect new Tone.Panner 0.5 .connect reverb | | ; | | let midiDrums = 36, 38, 42, 46, 41, 43, 45, 49, 51 ; | | let reverseMidiMapping = new Map | | 36, 0 , | | 35, 0 , | | 38, 1 , | | 27, 1 , | | 28, 1 , | | 31, 1 , | | 32, 1 , | | 33, 1 , | | 34, 1 , | | 37, 1 , | | 39, 1 , | | 40, 1 , | | 56, 1 , | | 65, 1 , | | 66, 1 , | | 75, 1 , | | 85, 1 , | | 42, 2 , | | 44, 2 , | | 54, 2 , | | 68, 2 , | | 69, 2 , | | 70, 2 , | | 71, 2 , | | 73, 2 , | | 78, 2 , | | 80, 2 , | | 46, 3 , | | 67, 3 , | | 72, 3 , | | 74, 3 , | | 79, 3 , | | 81, 3 , | | 45, 4 , | | 29, 4 , | | 41, 4 , | | 61, 4 , | | 64, 4 , | | 84, 4 , | | 48, 5 , | | 47, 5 , | | 60, 5 , | | 63, 5 , | | 77, 5 , | | 86, 5 , | | 87, 5 , | | 50, 6 , | | 30, 6 , | | 43, 6 , | | 62, 6 , | | 76, 6 , | | 83, 6 , | | 49, 7 , | | 55, 7 , | | 57, 7 , | | 58, 7 , | | 51, 8 , | | 52, 8 , | | 53, 8 , | | 59, 8 , | | 82, 8 | | ; | | | | let temperature = 1.2; | | | | let outputs = { | | internal: { | | play: drumIdx, velocity, time = { | | drumKit drumIdx .get velocity .start time ; | | } | | } | | }; | | | | let rnn = new mm.MusicRNN | | 'https://storage.googleapis.com/download.magenta.tensorflow.org/tfjs checkpoints/music rnn/drum kit rnn' | | ; | | let vae = new mm.MusicVAE | | 'https://storage.googleapis.com/download.magenta.tensorflow.org/tfjs checkpoints/music vae/drums 2bar hikl small' | | ; | | Promise.all | | rnn.initialize , | | vae.initialize , | | new Promise res = Tone.Buffer.on 'load', res | | .then vars = { | | | | | | let state = { | | patternLength: 32, | | seedLength: 4, | | swing: 0.55, | | pattern: 0 , , 2 .concat .times 32, i = , | | tempo: 120 | | }; | | let stepEls = , | | hasBeenStarted = false, | | sequence, | | densityRange = null, | | activeOutput = 'internal'; | | | | function generatePattern seed, length { | | let seedSeq = toNoteSequence seed ; | | return rnn | | .continueSequence seedSeq, length, temperature | | .then r = seed.concat fromNoteSequence r, length ; | | } | | | | function getStepVelocity step { | | if step % 4 === 0 { | | return 'high'; | | } else if step % 2 === 0 { | | return 'med'; | | } else { | | return 'low'; | | } | | } | | | | function humanizeTime time { | | return time - TIME HUMANIZATION / 2 + Math.random TIME HUMANIZATION; | | } | | | | function playPattern { | | sequence = new Tone.Sequence | | time, { drums, stepIdx } = { | | let isSwung = stepIdx % 2 == 0; | | if isSwung { | | time += state.swing - 0.5 Tone.Time '8n' .toSeconds ; | | } | | let velocity = getStepVelocity stepIdx ; | | drums.forEach d = { | | let humanizedTime = stepIdx === 0 ? time : humanizeTime time ; | | outputs activeOutput .play d, velocity, time ; | | visualizePlay humanizedTime, stepIdx, d ; | | } ; | | }, | | state.pattern.map drums, stepIdx = { drums, stepIdx } , | | '16n' | | .start ; | | } | | | | function visualizePlay time, stepIdx, drumIdx { | | Tone.Draw.schedule = { | | if stepEls stepIdx return; | | let animTime = Tone.Time '2n' .toMilliseconds ; | | let cellEl = stepEls stepIdx .cellEls drumIdx ; | | if cellEl.classList.contains 'on' { | | let baseColor = stepIdx < state.seedLength ? ' e91e63' : ' 64b5f6'; | | cellEl.animate | | | | { | | transform: 'translateZ -100px ', | | backgroundColor: ' fad1df' | | }, | | { | | transform: 'translateZ 50px ', | | offset: 0.7 | | }, | | { transform: 'translateZ 0 ', backgroundColor: baseColor } | | , | | { duration: animTime, easing: 'cubic-bezier 0.23, 1, 0.32, 1 ' } | | ; | | } | | }, time ; | | } | | | | function renderPattern regenerating = false { | | let seqEl = document.querySelector '.sequencer .steps' ; | | while stepEls.length state.pattern.length { | | let { stepEl, gutterEl } = stepEls.pop ; | | stepEl.remove ; | | if gutterEl gutterEl.remove ; | | } | | for let stepIdx = 0; stepIdx < state.pattern.length; stepIdx++ { | | let step = state.pattern stepIdx ; | | let stepEl, gutterEl, cellEls; | | if stepEls stepIdx { | | stepEl = stepEls stepIdx .stepEl; | | gutterEl = stepEls stepIdx .gutterEl; | | cellEls = stepEls stepIdx .cellEls; | | } else { | | stepEl = document.createElement 'div' ; | | stepEl.classList.add 'step' ; | | stepEl.dataset.stepIdx = stepIdx; | | seqEl.appendChild stepEl ; | | cellEls = ; | | } | | | | stepEl.style.flex = stepIdx % 2 === 0 ? state.swing : 1 - state.swing; | | | | if gutterEl && stepIdx < state.pattern.length - 1 { | | gutterEl = document.createElement 'div' ; | | gutterEl.classList.add 'gutter' ; | | seqEl.insertBefore gutterEl, stepEl.nextSibling ; | | } else if gutterEl && stepIdx = state.pattern.length { | | gutterEl.remove ; | | gutterEl = null; | | } | | | | if gutterEl && stepIdx === state.seedLength - 1 { | | gutterEl.classList.add 'seed-marker' ; | | } else if gutterEl { | | gutterEl.classList.remove 'seed-marker' ; | | } | | | | for let cellIdx = 0; cellIdx < DRUM CLASSES.length; cellIdx++ { | | let cellEl; | | if cellEls cellIdx { | | cellEl = cellEls cellIdx ; | | } else { | | cellEl = document.createElement 'div' ; | | cellEl.classList.add 'cell' ; | | cellEl.classList.add .kebabCase DRUM CLASSES cellIdx ; | | cellEl.dataset.stepIdx = stepIdx; | | cellEl.dataset.cellIdx = cellIdx; | | stepEl.appendChild cellEl ; | | cellEls cellIdx = cellEl; | | } | | if step.indexOf cellIdx = 0 { | | cellEl.classList.add 'on' ; | | } else { | | cellEl.classList.remove 'on' ; | | } | | } | | stepEls stepIdx = { stepEl, gutterEl, cellEls }; | | | | let stagger = stepIdx 300 / state.patternLength - state.seedLength ; | | setTimeout = { | | if stepIdx < state.seedLength { | | stepEl.classList.add 'seed' ; | | } else { | | stepEl.classList.remove 'seed' ; | | if regenerating { | | stepEl.classList.add 'regenerating' ; | | } else { | | stepEl.classList.remove 'regenerating' ; | | } | | } | | }, stagger ; | | } | | | | setTimeout repositionRegenerateButton, 0 ; | | } | | | | function repositionRegenerateButton { | | let regenButton = document.querySelector '.regenerate' ; | | let sequencerEl = document.querySelector '.sequencer' ; | | let seedMarkerEl = document.querySelector '.gutter.seed-marker' ; | | let regenLeft = | | sequencerEl.offsetLeft + | | seedMarkerEl.offsetLeft + | | seedMarkerEl.offsetWidth / 2 - | | regenButton.offsetWidth / 2; | | let regenTop = | | sequencerEl.offsetTop + | | seedMarkerEl.offsetTop + | | seedMarkerEl.offsetHeight / 2 - | | regenButton.offsetHeight / 2; | | regenButton.style.left = ${regenLeft}px ; | | regenButton.style.top = ${regenTop}px ; | | regenButton.style.visibility = 'visible'; | | } | | | | function regenerate { | | let seed = .take state.pattern, state.seedLength ; | | renderPattern true ; | | return generatePattern seed, state.patternLength - seed.length .then | | result = { | | state.pattern = result; | | onPatternUpdated ; | | setDensityValue ; | | updateDensityRange ; | | } | | ; | | } | | | | | | function onPatternUpdated { | | if sequence { | | sequence.dispose ; | | sequence = null; | | } | | renderPattern ; | | } | | | | function toggleStep cellEl { | | if state.pattern && cellEl.classList.contains 'cell' { | | let stepIdx = +cellEl.dataset.stepIdx; | | let cellIdx = +cellEl.dataset.cellIdx; | | let isOn = cellEl.classList.contains 'on' ; | | if isOn { | | .pull state.pattern stepIdx , cellIdx ; | | cellEl.classList.remove 'on' ; | | } else { | | state.pattern stepIdx .push cellIdx ; | | cellEl.classList.add 'on' ; | | } | | if sequence { | | sequence.at stepIdx, { stepIdx, drums: state.pattern stepIdx } ; | | } | | setDensityValue ; | | densityRange = null; | | } | | } | | | | function setDensityValue { | | let totalCellCount = state.pattern.length 9; | | let activeCellCount = .sum state.pattern.map p = p.length ; | | let density = activeCellCount / totalCellCount; | | let roundedDensity = Math.round density / 0.05 0.05; | | document.querySelector ' density' .value = roundedDensity; | | } | | | | function updateDensityRange | | density = +document.querySelector ' density' .value | | { | | let stepsDown = density / 0.05; | | let stepsUp = 0.75 - density / 0.05; | | let stepsBeyond = 0.25 / 0.05; | | | | let emptySeq = toNoteSequence ; | | let fullSeq = toNoteSequence | | .times state.pattern.length, = .range 9 | | ; | | let currentSeq = toNoteSequence state.pattern ; | | | | densityRange = ; | | let interpsUp = stepsDown 0 ? vae.interpolate emptySeq, currentSeq , stepsDown : Promise.resolve ; | | let interpsDown = stepsUp 0 ? vae.interpolate | | currentSeq, fullSeq , | | stepsUp + stepsBeyond | | : Promise.resolve ; | | | | interpsDown.then interps = { | | for let noteSeq of interps { | | densityRange.push fromNoteSequence noteSeq, state.pattern.length ; | | } | | } .then = densityRange.push state.pattern | | .then = interpsUp | | .then interps = { | | for let noteSeq of interps { | | if stepsUp-- 0 { | | densityRange.push fromNoteSequence noteSeq, state.pattern.length ; | | } | | } | | } ; | | } | | | | function toNoteSequence pattern { | | return mm.sequences.quantizeNoteSequence | | { | | ticksPerQuarter: 220, | | totalTime: pattern.length / 2, | | timeSignatures: | | { | | time: 0, | | numerator: 4, | | denominator: 4 | | } | | , | | tempos: | | { | | time: 0, | | qpm: 120 | | } | | , | | notes: .flatMap pattern, step, index = | | step.map d = { | | pitch: midiDrums d , | | startTime: index 0.5, | | endTime: index + 1 0.5 | | } | | | | }, | | 1 | | ; | | } | | | | function fromNoteSequence { notes }, patternLength { | | let res = .times patternLength, = ; | | for let { pitch, quantizedStartStep } of notes { | | res quantizedStartStep .push reverseMidiMapping.get pitch ; | | } | | return res; | | } | | | | function setSwing newSwing { | | state.swing = newSwing; | | renderPattern ; | | } | | | | function setPatternLength newPatternLength { | | if newPatternLength < state.patternLength { | | state.pattern.length = newPatternLength; | | } else { | | for let i = state.pattern.length; i < newPatternLength; i++ { | | state.pattern.push ; | | } | | } | | let lengthRatio = newPatternLength / state.patternLength; | | state.seedLength = Math.max | | 1, | | Math.min newPatternLength - 1, Math.round state.seedLength lengthRatio | | ; | | state.patternLength = newPatternLength; | | onPatternUpdated ; | | if Tone.Transport.state === 'started' { | | playPattern ; | | } | | } | | | | function updatePlayPauseIcons { | | if Tone.Transport.state === 'started' { | | document.querySelector '.playpause .pause-icon' .style.display = null; | | document.querySelector '.playpause .play-icon' .style.display = 'none'; | | } else { | | document.querySelector '.playpause .play-icon' .style.display = null; | | document.querySelector '.playpause .pause-icon' .style.display = 'none'; | | } | | } | | | | function encodeState { | | return Object.keys state | | .reduce a, k = { | | a.push k + '=' + JSON.stringify state k ; | | return a; | | }, | | .join '&' ; | | } | | | | WebMidi.enable err = { | | if err { | | console.error 'WebMidi could not be enabled', err ; | | return; | | } | | document.querySelector '.webmidi-enabled' .style.display = 'block'; | | let outputSelector = document.querySelector '.midi-output' ; | | | | function onOutputsChange { | | while outputSelector.firstChild { | | outputSelector.firstChild.remove ; | | } | | let internalOption = document.createElement 'option' ; | | internalOption.value = 'internal'; | | internalOption.innerText = 'Internal drumkit'; | | outputSelector.appendChild internalOption ; | | for let output of WebMidi.outputs { | | let option = document.createElement 'option' ; | | option.value = output.id; | | option.innerText = output.name; | | outputSelector.appendChild option ; | | } | | onActiveOutputChange 'internal' ; | | } | | | | function onActiveOutputChange id { | | if activeOutput == 'internal' { | | outputs activeOutput = null; | | } | | activeOutput = id; | | if activeOutput == 'internal' { | | let output = WebMidi.getOutputById id ; | | outputs id = { | | play: drumIdx, velo, time = { | | let delay = time - Tone.now 1000; | | let duration = Tone.Time '16n' .toMilliseconds ; | | let velocity = { high: 1, med: 0.75, low: 0.5 }; | | output.playNote midiDrums drumIdx , 1, { | | time: delay 0 ? +${delay} : WebMidi.now, | | velocity, | | duration | | } ; | | } | | }; | | } | | for let option of Array.from outputSelector.children { | | option.selected = option.value === id; | | } | | } | | | | onOutputsChange ; | | WebMidi.addListener 'connected', onOutputsChange ; | | WebMidi.addListener 'disconnected', onOutputsChange ; | | $ outputSelector | | .on 'change', evt = onActiveOutputChange evt.target.value | | .material select ; | | } ; | | | | document.querySelector '.app' .addEventListener 'click', event = { | | if event.target.classList.contains 'cell' { | | toggleStep event.target ; | | } | | } ; | | document.querySelector '.regenerate' .addEventListener 'click', event = { | | event.preventDefault ; | | event.currentTarget.classList.remove 'pulse' ; | | document.querySelector '.playpause' .classList.remove 'pulse' ; | | regenerate .then = { | | if hasBeenStarted { | | Tone.context.resume ; | | Tone.Transport.start ; | | updatePlayPauseIcons ; | | hasBeenStarted = true; | | } | | if Tone.Transport.state === 'started' { | | setTimeout = playPattern , 0 ; | | } | | } ; | | } ; | | document.querySelector '.playpause' .addEventListener 'click', event = { | | event.preventDefault ; | | document.querySelector '.playpause' .classList.remove 'pulse' ; | | if Tone.Transport.state == 'started' { | | Tone.context.resume ; | | Tone.Transport.start ; | | playPattern ; | | updatePlayPauseIcons ; | | hasBeenStarted = true; | | } else { | | if sequence { | | sequence.dispose ; | | sequence = null; | | } | | Tone.Transport.pause ; | | updatePlayPauseIcons ; | | } | | } ; | | | | let draggingSeedMarker = false; | | document.querySelector '.app' .addEventListener 'mousedown', evt = { | | let el = evt.target; | | if | | el.classList.contains 'gutter' && | | el.classList.contains 'seed-marker' | | { | | draggingSeedMarker = true; | | evt.preventDefault ; | | } | | } ; | | document.querySelector '.app' .addEventListener 'mouseup', = { | | draggingSeedMarker = false; | | } ; | | document.querySelector '.app' .addEventListener 'mouseover', evt = { | | if draggingSeedMarker { | | let el = evt.target; | | while el { | | if el.classList.contains 'step' { | | let stepIdx = +el.dataset.stepIdx; | | if stepIdx 0 { | | state.seedLength = stepIdx; | | renderPattern ; | | } | | break; | | } | | el = el.parentElement; | | } | | } | | } ; | | document.querySelector ' density' .addEventListener 'input', evt = { | | let newDensity = +evt.target.value; | | let patternIndex = Math.round newDensity / 0.05 ; | | if .isNull densityRange { | | updateDensityRange newDensity ; | | } | | if | | densityRange && | | patternIndex = 0 && | | patternIndex < densityRange.length - 1 | | { | | state.pattern = densityRange patternIndex ; | | renderPattern ; | | if sequence { | | state.pattern.forEach drums, stepIdx = | | sequence.at stepIdx, { stepIdx, drums } | | ; | | } | | } | | } ; | | document | | .querySelector ' swing' | | .addEventListener 'input', evt = setSwing +evt.target.value ; | | document | | .querySelector ' temperature' | | .addEventListener 'input', evt = | | temperature = +evt.target.value | | ; | | document | | .querySelector ' tempo' | | .addEventListener | | 'input', | | evt = Tone.Transport.bpm.value = state.tempo = +evt.target.value | | ; | | | | window.addEventListener 'resize', repositionRegenerateButton ; | | | | renderPattern ; | | setDensityValue ; | | | | document.querySelector '.progress' .remove ; | | document.querySelector '.app' .style.display = null; | | } ; |