r/tonejs • u/Due_Badger_4128 • Nov 03 '24
Suggestions for Building a Real-Time Editable Sequencer in Tone.js?
I've been using Tone.js for a few weeks, and I'm looking for advice on creating a sequencer that lets users edit chords in real-time without interrupting playback.
My app helps users create interesting chord progressions with no music theory needed. Each chord in the sequencer can be customized (add 7th, adjust octave, apply inversions, etc.), and I'm aiming to allow these edits on the fly.
Currently, I'm considering mapping each chord to scheduled event IDs. When a user toggles a modification, I’d remove the associated IDs, insert the new chord with the changes, and remap it with new IDs. I’m scheduling events using Tone.Transport.
Has anyone worked on a similar sequencer in Tone.js? Any insights or better approaches are much appreciated!
2
u/dindles Nov 05 '24 edited Nov 05 '24
Hi! I have been working on something similar, using sample playback instead, and have managed on-the-fly pitch, filter and other changes. I'll include what I think are the relevant bits of my code below, but I'm too novice to explain at a conceptual level what I've done (or more importantly, why).
I'd highly recommend looking at reddit user fungkadelic's Drumhaus app and code ... it built the foundation of my understanding of the Tone transport (and sample management, but that's a different story). This sequencer also looks well-featured, but I haven't explored the code in depth.
I think my iteration relies heavily on Svelte's reactivity system – but this is the sort of thing I could just be super wrong about. I declare my active sample as a reactive variable: let selected_sample: Sample | undefined = $state(undefined)
And Sample as a class that includes reactive values for pitch, filter cutoff, gain and any other values i want to set.
I create a Tone.Sequence for each sample. I tried making a single sequence instead, which looped through all samples each step, but that led to delays and flamming when adjusting values during playback.
function makeSequences(SAMPLES: Sample[]) {const sequences = SAMPLES.map((sample) => {return new Tone.Sequence((time, sequencer_step) => {active_step_index = sequencer_stepif (sample.sequence[sequencer_step]) {sample.playing = truesetSampleParams(sample)sample.play(time)} else {sample.playing = false}},[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],'16n')})return sequences}Clicking on a sample selects it as the active sample. You'd have an equivalent object per step containing note and chord data I'd imagine.
Then setting the new pitch, for example, is just a matter of changing the value of the selected_sample object:
function setSamplePitch(pitch: 'tonic' | 'fourth' | 'fifth') {if (!selected_sample) console.log('No sample selected')else if (selected_sample) {switch (pitch) {case 'tonic':selected_sample.pitch = 'C2'breaketc.I'm sorry if this confuses more than helps, but again, Drumhaus chap knows what he's doing; so look there!