Multi-Track Playback
BPM-locked, sample-aligned playback via the Web Audio API
Overview
Every track in a song plays back together, sample-aligned, via the Web Audio API. This is the core listening experience: however many people uploaded a part, everyone hears the full mix together in sync in the browser — no server-side mixing or transcoding involved.
How it works
MultiTrackPlayer.tsxowns a singleAudioContextfor the song. Each track is fetched from itsdownloadUrl(a raw GitHub URL or a public Blob URL — both fetchable with no auth) and decoded into anAudioBufferviadecodeAudioData.- Play schedules every track's
AudioBufferSourceNodetostart()at the same futureaudioContext.currentTime + 0.05— the fixed lead time gives every source the same scheduling instant, which is what keeps tracks sample-accurately in sync rather than drifting apart from independent.play()calls. - Volume and mute are per-track
GainNodes that persist across plays/pauses, so adjusting a fader or toggling mute doesn't require re-decoding or restarting playback. - Pause records the elapsed offset (
playbackOffsetRef) so Play resumes from the same point rather than restarting from 0; Stop resets that offset back to 0. - Recording a new take reuses this same playback path — see In-Browser Mic Recording.
Key files
| File | Role |
|---|---|
src/components/MultiTrackPlayer.tsx | Transport (play/pause/stop), track list, AudioContext/GainNode management |
src/lib/audio.ts | drawWaveform() — draws PCM waveform to canvas from a decoded AudioBuffer |
src/components/TrackRow.tsx | Per-track UI: waveform, volume, mute, delete |
Limitations / notes
- No per-track offset/timeline positioning — every track always plays from position 0. There's no way to nudge a track earlier/later if someone's take was recorded slightly off.
- Sync quality depends on all tracks having been recorded at the same BPM with a click track — the app has no way to detect or correct drift between mismatched recordings.