Vibecode Soundtrap
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Soundtrap, record and arrange a simple multitrack song in a browser and export stems. The hard boundary is realtime collaboration, loops, instruments, education workflow, and hosted rendering, plus licensed content, rights, and distribution network.
You are building a lean indie version of Soundtrap. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Soundtrap · indie build A browser multitrack recorder with a click, latency-compensated recording that lands on the beat, regions you can move and trim without destroying the take, a mixer with levels and pan, and export of the mix and every stem at sample-aligned starts. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running in the browser | audio capture and playback are both browser APIs and there is no installation to distribute | | Audio | Web Audio API with an AudioWorklet for anything per-sample | the worklet runs on the audio thread, which is where recording and metering must happen | | Recording | MediaRecorder for capture, decoded to buffers for editing | capture and edit are different jobs and want different representations | | Storage | IndexedDB for audio buffers and project state | takes are large and must survive a reload, which is the failure people never forgive | | Export | Offline rendering, not real-time playback capture | rendering offline is faster than real time and produces identical output every run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An audio input, ideally an interface with direct monitoring** · a basic interface is around 100 US dollars; measurement works without one - Why: Latency compensation can be measured with any input, but playing along to a click through browser monitoring is genuinely difficult past about twenty milliseconds, and direct monitoring removes that. - Get it: Any USB audio interface with a direct monitor control, or headphones plus a built-in microphone for measurement only. - Verify: The device appears as an input and a round trip measurement completes - [ ] **A way to measure the round trip** · free with a cable you own - Why: Phase 2 needs the click to reach the input. A physical loop is the most accurate method and a microphone in front of a speaker is adequate. - Get it: A cable from output to input if your interface allows it, or place a microphone near a speaker in a quiet room. - Verify: The calibration routine produces a stable figure across three runs - [ ] **Something percussive to play on the beat** · free - Why: Alignment is verified by looking at transients against the grid, and a sustained sound has no transient to look at. - Get it: Clapping works. So does tapping a desk. - Verify: A four-hit recording shows four clear transients - [ ] **A current browser with AudioWorklet support** · free - Why: Metering and any per-sample work must run on the audio thread, and the worklet is how that happens. - Get it: A current Chromium, Firefox or Safari. Test in at least two, since audio behaviour differs. - Verify: An AudioWorklet loads and posts messages ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. - realtime collaboration, loops, instruments, education workflow, and hosted rendering - licensed music catalog - royalty and distribution relationships - global streaming infrastructure - artist network and recommendations If one of those is essential to you, that is the reason to keep paying for Soundtrap, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Soundtrap The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser multitrack recorder that records in time, arranges regions and exports stems to replace Soundtrap. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The whole difficulty in a recorder is that what you record arrives late. Between your voice and the file there is input buffering, processing and output buffering, and the recorded audio lands after the beat you played it on by that round trip. Every recorder that sounds out of time has skipped compensating for it. So Phase 2 measures the round trip on your actual hardware and shifts recordings back by it, and it does that before any arranging, mixing or effects exist. Build the timing first; everything else is easier and none of it matters if the timing is wrong. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running in the browser · audio capture and playback are both browser APIs and there is no installation to distribute - Audio: Web Audio API with an AudioWorklet for anything per-sample · the worklet runs on the audio thread, which is where recording and metering must happen - Recording: MediaRecorder for capture, decoded to buffers for editing · capture and edit are different jobs and want different representations - Storage: IndexedDB for audio buffers and project state · takes are large and must survive a reload, which is the failure people never forgive - Export: Offline rendering, not real-time playback capture · rendering offline is faster than real time and produces identical output every run ### Data model (create this before Phase 1) - projects(id, name, tempo_bpm, time_signature, sample_rate, created_at) - tracks(id, project_id, name, ordinal, gain_db, pan, muted, soloed, input_device_id) - takes(id, track_id, buffer_key, recorded_at, raw_start_ms, compensated_start_ms, duration_ms, latency_applied_ms) - regions(id, track_id, take_id, timeline_start_ms, source_offset_ms, duration_ms, gain_db, fade_in_ms, fade_out_ms) - calibrations(id, device_id, measured_round_trip_ms, measured_at, method) - A region references a take with an offset. Trimming moves the region's window, never the recorded audio, so every edit stays reversible. ### Phase 1 · Playback on a clock Build: Set up an audio context and a metronome. Schedule clicks ahead using the audio clock, not a JavaScript timer. Run for ten minutes and confirm the last click is where the tempo says it should be. Trap: Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. Done when: A metronome at a set tempo plays without audible jitter. After ten minutes the final click's time matches the calculated time within a few milliseconds. Switching to another browser tab and back does not shift the click. Changing tempo takes effect at the next bar rather than mid-beat. Do not build yet: No recording, no tracks, no export. ### Phase 2 · Recording that lands on the beat Build: Measure the round trip latency of the actual input and output devices, store it as a calibration, and shift every recording back by it. Provide a measurement routine rather than a number you typed. Then record against the click and check alignment by looking at the waveform. Trap: Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. Trap: Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. Done when: A measured calibration exists for the current device and is stored. A recording of four percussive hits on the beat shows transients aligned to the grid within a few milliseconds, verified by looking at the waveform. Changing the output device invalidates the calibration and prompts a re-measurement. The raw and compensated start times are both stored. Do not build yet: No arranging, mixing or export. ### Phase 3 · Tracks, takes and non-destructive regions Build: Multiple tracks, each recording to its own take. Regions referencing a take with an offset and a duration, so trimming changes the window rather than the audio. Takes persist to IndexedDB immediately so a reload loses nothing. Trap: Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. Done when: Trimming a region and then extending it again restores the original audio. A reload immediately after recording preserves the take. Two tracks recorded separately play back in sync. Deleting a region does not delete the take it referenced if another region uses it. Do not build yet: No mixing or export yet. ### Phase 4 · Monitoring and the mixer Build: Per-track gain, pan, mute and solo, plus metering. Input monitoring with an honest warning that browser monitoring adds the same round trip you are compensating for, and an option to disable it in favour of hardware monitoring. Trap: Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. Done when: Gain, pan, mute and solo all affect playback correctly, and solo on one track silences the others. Meters show level without blocking the audio thread. Input monitoring displays the measured delay in milliseconds next to the control. Disabling monitoring silences the loop immediately. Do not build yet: No export yet. ### Phase 5 · Export, mix and stems Build: Render offline rather than capturing playback. Export the mix and each track as a stem, all starting at the same timeline zero so they reassemble in any other tool. Verify by summing the stems and comparing against the mix. Trap: Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. Done when: Summing the exported stems produces the mix within a negligible difference, verified numerically rather than by ear. Every stem has identical length and starts at timeline zero. Offline rendering completes faster than the project's duration. Exporting twice produces identical files. Do not build yet: Project sharing and cleanup come last. ### Phase 6 · Projects, storage and honest limits Build: Save and load projects with their audio. Export a project as a folder of stems plus a manifest. Warn as storage approaches the browser's quota. Then write down what this does not do, starting with real-time collaboration and instruments. Trap: Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. Done when: A project saves and reloads with all audio intact. Approaching the storage quota produces a warning before recording is armed rather than a failure during it. A project exports as stems plus a manifest that another tool can use. The documentation states what is not included. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time collaboration, which is what the hosted product is for and which is substantially harder than the recording itself. - Instruments, loops and the licensed sound library, which is content with licensing attached rather than code. - The education workflow and classroom features that are much of why schools buy it. - Effects processing beyond gain and pan. Reverb, compression and equalisation are each their own project, and a recorder that does timing correctly is more useful than one with poor effects. ### README must contain - That recording latency is measured per device and compensated, and how to re-measure - That scheduling uses the audio clock rather than JavaScript timers, and why - That regions are windows onto takes, so trimming is never destructive - That stems export from timeline zero with leading silence, so they reassemble elsewhere - That browser monitoring adds the round trip delay, with the measured number ===== AGENTS.md ===== # Agent instructions · Soundtrap indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, running in the browser, Web Audio API with an AudioWorklet for anything per-sample, MediaRecorder for capture, decoded to buffers for editing, IndexedDB for audio buffers and project state, Offline rendering, not real-time playback capture. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ===== BUILD_PLAN.md ===== # Build plan · Soundtrap A browser multitrack recorder with a click, latency-compensated recording that lands on the beat, regions you can move and trim without destroying the take, a mixer with levels and pan, and export of the mix and every stem at sample-aligned starts. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Playback on a clock A click plays in perfect time and does not drift over ten minutes. ### Steps 1. Create the audio context and a lookahead scheduler Schedule events a short window ahead against the audio clock, driven by a timer that only refills the queue. Files: `src/clock.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Build the metronome on it Scheduled clicks, not triggered ones. This is the pattern everything timed will use. Files: `src/metronome.ts` 3. Measure drift over ten minutes Compare the final click's actual time against the calculated time. This is the test that catches a timer-based scheduler. Files: `src/clock.test.ts` ### Done when - [ ] The metronome plays without audible jitter - [ ] Ten minutes of playback shows no measurable drift - [ ] Backgrounding the tab does not shift timing - [ ] A tempo change takes effect at a bar boundary ### Watch out - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. ## Phase 2 · Recording that lands on the beat What you play on the beat is recorded on the beat. ### Steps 1. Write the calibration routine Play a click, record it through the loop, and measure the offset. Store the result per device with a timestamp. Files: `src/calibrate.ts` 2. Apply compensation on every take Shift the recorded start back by the measured round trip. Store both times so the correction is inspectable. Files: `src/record.ts` 3. Verify against a percussive test Four hits on the beat, then look at the transients against the grid. Do not skip the visual check. ### Done when - [ ] A device calibration is measured and stored - [ ] Four on-beat hits align to the grid within a few milliseconds - [ ] Changing devices invalidates the calibration - [ ] Raw and compensated start times are both recorded ### Watch out - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. ## Phase 3 · Tracks, takes and non-destructive regions You can record several tracks, move and trim them, and never lose a take. ### Steps 1. Persist takes immediately To IndexedDB as recording finishes. A lost take is the failure people abandon a tool over. Files: `src/store.ts` 2. Model regions as windows Take reference, source offset, duration. Trimming edits the window only. Files: `src/regions.ts` 3. Keep takes alive while referenced Reference counting, so deleting one region does not destroy another's source. Files: `src/takes.ts` ### Done when - [ ] Trim then extend restores the original audio - [ ] A reload after recording preserves the take - [ ] Two tracks play back in sync - [ ] Deleting a region keeps a take another region uses ### Watch out - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. ## Phase 4 · Monitoring and the mixer You can hear yourself while recording without the delay making it impossible to play. ### Steps 1. Build the mixer graph Gain and pan per track into a master. Solo is a mute of everything else, computed rather than stored per track. Files: `src/mixer.ts` 2. Meter on the audio thread An AudioWorklet computing levels and posting them, so metering never blocks audio. Files: `src/meter-worklet.js` 3. Be honest about monitoring delay Show the measured number next to the toggle. It is the difference between a usable session and a frustrating one. Files: `src/monitor.ts` ### Done when - [ ] Solo silences other tracks - [ ] Meters update without blocking audio - [ ] Monitoring shows the measured delay in milliseconds - [ ] Disabling monitoring silences the loop immediately ### Watch out - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. ## Phase 5 · Export, mix and stems What you export is what you heard, and the stems line up when reassembled. ### Steps 1. Render offline An OfflineAudioContext rendering the graph. Faster than real time and deterministic, unlike capturing playback. Files: `src/export.ts` 2. Export stems from timeline zero Same length, same start, leading silence included. This is what makes them portable. Files: `src/export.ts` 3. Verify by summing Sum the stems and compare against the mix numerically. A difference means a gain or pan is applied in one path and not the other. Files: `src/export.test.ts` ### Done when - [ ] Summed stems match the mix numerically - [ ] Every stem has identical length and start - [ ] Offline rendering is faster than real time - [ ] Two exports are byte-identical ### Watch out - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ## Not in this build - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. ## After v1, if you want it - Punch recording over a region, which the take and region model already supports - A simple equaliser per track, once timing and export are trustworthy - Tempo mapping so a recording made without a click can be gridded afterwards ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Starting tempo for a new project. VITE_DEFAULT_TEMPO=120 # Required. How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. VITE_SCHEDULER_LOOKAHEAD_MS=120 # Required. Warn as a project approaches this size, before the browser quota fails a recording. VITE_MAX_PROJECT_MB=800 # Required. Above this measured monitoring delay, warn that playing along will be difficult. VITE_MONITOR_WARN_MS=20 # Required. Render rate for export. Matching your recording rate avoids a resample. VITE_EXPORT_SAMPLE_RATE=48000
You are building a lean indie version of Soundtrap. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Soundtrap · indie build A browser multitrack recorder with a click, latency-compensated recording that lands on the beat, regions you can move and trim without destroying the take, a mixer with levels and pan, and export of the mix and every stem at sample-aligned starts. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running in the browser | audio capture and playback are both browser APIs and there is no installation to distribute | | Audio | Web Audio API with an AudioWorklet for anything per-sample | the worklet runs on the audio thread, which is where recording and metering must happen | | Recording | MediaRecorder for capture, decoded to buffers for editing | capture and edit are different jobs and want different representations | | Storage | IndexedDB for audio buffers and project state | takes are large and must survive a reload, which is the failure people never forgive | | Export | Offline rendering, not real-time playback capture | rendering offline is faster than real time and produces identical output every run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An audio input, ideally an interface with direct monitoring** · a basic interface is around 100 US dollars; measurement works without one - Why: Latency compensation can be measured with any input, but playing along to a click through browser monitoring is genuinely difficult past about twenty milliseconds, and direct monitoring removes that. - Get it: Any USB audio interface with a direct monitor control, or headphones plus a built-in microphone for measurement only. - Verify: The device appears as an input and a round trip measurement completes - [ ] **A way to measure the round trip** · free with a cable you own - Why: Phase 2 needs the click to reach the input. A physical loop is the most accurate method and a microphone in front of a speaker is adequate. - Get it: A cable from output to input if your interface allows it, or place a microphone near a speaker in a quiet room. - Verify: The calibration routine produces a stable figure across three runs - [ ] **Something percussive to play on the beat** · free - Why: Alignment is verified by looking at transients against the grid, and a sustained sound has no transient to look at. - Get it: Clapping works. So does tapping a desk. - Verify: A four-hit recording shows four clear transients - [ ] **A current browser with AudioWorklet support** · free - Why: Metering and any per-sample work must run on the audio thread, and the worklet is how that happens. - Get it: A current Chromium, Firefox or Safari. Test in at least two, since audio behaviour differs. - Verify: An AudioWorklet loads and posts messages ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. - realtime collaboration, loops, instruments, education workflow, and hosted rendering - licensed music catalog - royalty and distribution relationships - global streaming infrastructure - artist network and recommendations If one of those is essential to you, that is the reason to keep paying for Soundtrap, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Soundtrap The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser multitrack recorder that records in time, arranges regions and exports stems to replace Soundtrap. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The whole difficulty in a recorder is that what you record arrives late. Between your voice and the file there is input buffering, processing and output buffering, and the recorded audio lands after the beat you played it on by that round trip. Every recorder that sounds out of time has skipped compensating for it. So Phase 2 measures the round trip on your actual hardware and shifts recordings back by it, and it does that before any arranging, mixing or effects exist. Build the timing first; everything else is easier and none of it matters if the timing is wrong. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running in the browser · audio capture and playback are both browser APIs and there is no installation to distribute - Audio: Web Audio API with an AudioWorklet for anything per-sample · the worklet runs on the audio thread, which is where recording and metering must happen - Recording: MediaRecorder for capture, decoded to buffers for editing · capture and edit are different jobs and want different representations - Storage: IndexedDB for audio buffers and project state · takes are large and must survive a reload, which is the failure people never forgive - Export: Offline rendering, not real-time playback capture · rendering offline is faster than real time and produces identical output every run ### Data model (create this before Phase 1) - projects(id, name, tempo_bpm, time_signature, sample_rate, created_at) - tracks(id, project_id, name, ordinal, gain_db, pan, muted, soloed, input_device_id) - takes(id, track_id, buffer_key, recorded_at, raw_start_ms, compensated_start_ms, duration_ms, latency_applied_ms) - regions(id, track_id, take_id, timeline_start_ms, source_offset_ms, duration_ms, gain_db, fade_in_ms, fade_out_ms) - calibrations(id, device_id, measured_round_trip_ms, measured_at, method) - A region references a take with an offset. Trimming moves the region's window, never the recorded audio, so every edit stays reversible. ### Phase 1 · Playback on a clock Build: Set up an audio context and a metronome. Schedule clicks ahead using the audio clock, not a JavaScript timer. Run for ten minutes and confirm the last click is where the tempo says it should be. Trap: Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. Done when: A metronome at a set tempo plays without audible jitter. After ten minutes the final click's time matches the calculated time within a few milliseconds. Switching to another browser tab and back does not shift the click. Changing tempo takes effect at the next bar rather than mid-beat. Do not build yet: No recording, no tracks, no export. ### Phase 2 · Recording that lands on the beat Build: Measure the round trip latency of the actual input and output devices, store it as a calibration, and shift every recording back by it. Provide a measurement routine rather than a number you typed. Then record against the click and check alignment by looking at the waveform. Trap: Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. Trap: Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. Done when: A measured calibration exists for the current device and is stored. A recording of four percussive hits on the beat shows transients aligned to the grid within a few milliseconds, verified by looking at the waveform. Changing the output device invalidates the calibration and prompts a re-measurement. The raw and compensated start times are both stored. Do not build yet: No arranging, mixing or export. ### Phase 3 · Tracks, takes and non-destructive regions Build: Multiple tracks, each recording to its own take. Regions referencing a take with an offset and a duration, so trimming changes the window rather than the audio. Takes persist to IndexedDB immediately so a reload loses nothing. Trap: Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. Done when: Trimming a region and then extending it again restores the original audio. A reload immediately after recording preserves the take. Two tracks recorded separately play back in sync. Deleting a region does not delete the take it referenced if another region uses it. Do not build yet: No mixing or export yet. ### Phase 4 · Monitoring and the mixer Build: Per-track gain, pan, mute and solo, plus metering. Input monitoring with an honest warning that browser monitoring adds the same round trip you are compensating for, and an option to disable it in favour of hardware monitoring. Trap: Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. Done when: Gain, pan, mute and solo all affect playback correctly, and solo on one track silences the others. Meters show level without blocking the audio thread. Input monitoring displays the measured delay in milliseconds next to the control. Disabling monitoring silences the loop immediately. Do not build yet: No export yet. ### Phase 5 · Export, mix and stems Build: Render offline rather than capturing playback. Export the mix and each track as a stem, all starting at the same timeline zero so they reassemble in any other tool. Verify by summing the stems and comparing against the mix. Trap: Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. Done when: Summing the exported stems produces the mix within a negligible difference, verified numerically rather than by ear. Every stem has identical length and starts at timeline zero. Offline rendering completes faster than the project's duration. Exporting twice produces identical files. Do not build yet: Project sharing and cleanup come last. ### Phase 6 · Projects, storage and honest limits Build: Save and load projects with their audio. Export a project as a folder of stems plus a manifest. Warn as storage approaches the browser's quota. Then write down what this does not do, starting with real-time collaboration and instruments. Trap: Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. Done when: A project saves and reloads with all audio intact. Approaching the storage quota produces a warning before recording is armed rather than a failure during it. A project exports as stems plus a manifest that another tool can use. The documentation states what is not included. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time collaboration, which is what the hosted product is for and which is substantially harder than the recording itself. - Instruments, loops and the licensed sound library, which is content with licensing attached rather than code. - The education workflow and classroom features that are much of why schools buy it. - Effects processing beyond gain and pan. Reverb, compression and equalisation are each their own project, and a recorder that does timing correctly is more useful than one with poor effects. ### README must contain - That recording latency is measured per device and compensated, and how to re-measure - That scheduling uses the audio clock rather than JavaScript timers, and why - That regions are windows onto takes, so trimming is never destructive - That stems export from timeline zero with leading silence, so they reassemble elsewhere - That browser monitoring adds the round trip delay, with the measured number ===== AGENTS.md ===== # Agent instructions · Soundtrap indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, running in the browser, Web Audio API with an AudioWorklet for anything per-sample, MediaRecorder for capture, decoded to buffers for editing, IndexedDB for audio buffers and project state, Offline rendering, not real-time playback capture. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ===== BUILD_PLAN.md ===== # Build plan · Soundtrap A browser multitrack recorder with a click, latency-compensated recording that lands on the beat, regions you can move and trim without destroying the take, a mixer with levels and pan, and export of the mix and every stem at sample-aligned starts. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Playback on a clock A click plays in perfect time and does not drift over ten minutes. ### Steps 1. Create the audio context and a lookahead scheduler Schedule events a short window ahead against the audio clock, driven by a timer that only refills the queue. Files: `src/clock.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Build the metronome on it Scheduled clicks, not triggered ones. This is the pattern everything timed will use. Files: `src/metronome.ts` 3. Measure drift over ten minutes Compare the final click's actual time against the calculated time. This is the test that catches a timer-based scheduler. Files: `src/clock.test.ts` ### Done when - [ ] The metronome plays without audible jitter - [ ] Ten minutes of playback shows no measurable drift - [ ] Backgrounding the tab does not shift timing - [ ] A tempo change takes effect at a bar boundary ### Watch out - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. ## Phase 2 · Recording that lands on the beat What you play on the beat is recorded on the beat. ### Steps 1. Write the calibration routine Play a click, record it through the loop, and measure the offset. Store the result per device with a timestamp. Files: `src/calibrate.ts` 2. Apply compensation on every take Shift the recorded start back by the measured round trip. Store both times so the correction is inspectable. Files: `src/record.ts` 3. Verify against a percussive test Four hits on the beat, then look at the transients against the grid. Do not skip the visual check. ### Done when - [ ] A device calibration is measured and stored - [ ] Four on-beat hits align to the grid within a few milliseconds - [ ] Changing devices invalidates the calibration - [ ] Raw and compensated start times are both recorded ### Watch out - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. ## Phase 3 · Tracks, takes and non-destructive regions You can record several tracks, move and trim them, and never lose a take. ### Steps 1. Persist takes immediately To IndexedDB as recording finishes. A lost take is the failure people abandon a tool over. Files: `src/store.ts` 2. Model regions as windows Take reference, source offset, duration. Trimming edits the window only. Files: `src/regions.ts` 3. Keep takes alive while referenced Reference counting, so deleting one region does not destroy another's source. Files: `src/takes.ts` ### Done when - [ ] Trim then extend restores the original audio - [ ] A reload after recording preserves the take - [ ] Two tracks play back in sync - [ ] Deleting a region keeps a take another region uses ### Watch out - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. ## Phase 4 · Monitoring and the mixer You can hear yourself while recording without the delay making it impossible to play. ### Steps 1. Build the mixer graph Gain and pan per track into a master. Solo is a mute of everything else, computed rather than stored per track. Files: `src/mixer.ts` 2. Meter on the audio thread An AudioWorklet computing levels and posting them, so metering never blocks audio. Files: `src/meter-worklet.js` 3. Be honest about monitoring delay Show the measured number next to the toggle. It is the difference between a usable session and a frustrating one. Files: `src/monitor.ts` ### Done when - [ ] Solo silences other tracks - [ ] Meters update without blocking audio - [ ] Monitoring shows the measured delay in milliseconds - [ ] Disabling monitoring silences the loop immediately ### Watch out - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. ## Phase 5 · Export, mix and stems What you export is what you heard, and the stems line up when reassembled. ### Steps 1. Render offline An OfflineAudioContext rendering the graph. Faster than real time and deterministic, unlike capturing playback. Files: `src/export.ts` 2. Export stems from timeline zero Same length, same start, leading silence included. This is what makes them portable. Files: `src/export.ts` 3. Verify by summing Sum the stems and compare against the mix numerically. A difference means a gain or pan is applied in one path and not the other. Files: `src/export.test.ts` ### Done when - [ ] Summed stems match the mix numerically - [ ] Every stem has identical length and start - [ ] Offline rendering is faster than real time - [ ] Two exports are byte-identical ### Watch out - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ## Not in this build - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. ## After v1, if you want it - Punch recording over a region, which the take and region model already supports - A simple equaliser per track, once timing and export are trustworthy - Tempo mapping so a recording made without a click can be gridded afterwards ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Starting tempo for a new project. VITE_DEFAULT_TEMPO=120 # Required. How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. VITE_SCHEDULER_LOOKAHEAD_MS=120 # Required. Warn as a project approaches this size, before the browser quota fails a recording. VITE_MAX_PROJECT_MB=800 # Required. Above this measured monitoring delay, warn that playing along will be difficult. VITE_MONITOR_WARN_MS=20 # Required. Render rate for export. Matching your recording rate avoids a resample. VITE_EXPORT_SAMPLE_RATE=48000
You are building a production product version of Soundtrap. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Soundtrap · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Soundtrap, record and arrange a simple multitrack song in a browser and export stems. The hard boundary is realtime collaboration, loops, instruments, education workflow, and hosted rendering, plus licensed content, rights, and distribution network. ## Product outcome A recorder where takes land on the beat because the round trip was measured rather than assumed, trimming never destroys a take, and exported stems reassemble correctly in any other tool. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - legally owned local audio files - ffmpeg - local storage - optional audio-analysis models ## Explicit non-goals for v1 - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. - realtime collaboration, loops, instruments, education workflow, and hosted rendering - licensed music catalog - royalty and distribution relationships - global streaming infrastructure - artist network and recommendations ## Success criteria - Ten minutes of playback shows no measurable drift - Four on-beat hits align to the grid after compensation - Trim then extend is confirmed to restore original audio - Summed stems match the mix numerically - A quota warning fires before recording is armed ===== BRIEF.md ===== # Build brief · Soundtrap The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser multitrack recorder that records in time, arranges regions and exports stems to replace Soundtrap. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The whole difficulty in a recorder is that what you record arrives late. Between your voice and the file there is input buffering, processing and output buffering, and the recorded audio lands after the beat you played it on by that round trip. Every recorder that sounds out of time has skipped compensating for it. So Phase 2 measures the round trip on your actual hardware and shifts recordings back by it, and it does that before any arranging, mixing or effects exist. Build the timing first; everything else is easier and none of it matters if the timing is wrong. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running in the browser · audio capture and playback are both browser APIs and there is no installation to distribute - Audio: Web Audio API with an AudioWorklet for anything per-sample · the worklet runs on the audio thread, which is where recording and metering must happen - Recording: MediaRecorder for capture, decoded to buffers for editing · capture and edit are different jobs and want different representations - Storage: IndexedDB for audio buffers and project state · takes are large and must survive a reload, which is the failure people never forgive - Export: Offline rendering, not real-time playback capture · rendering offline is faster than real time and produces identical output every run ### Data model (create this before Phase 1) - projects(id, name, tempo_bpm, time_signature, sample_rate, created_at) - tracks(id, project_id, name, ordinal, gain_db, pan, muted, soloed, input_device_id) - takes(id, track_id, buffer_key, recorded_at, raw_start_ms, compensated_start_ms, duration_ms, latency_applied_ms) - regions(id, track_id, take_id, timeline_start_ms, source_offset_ms, duration_ms, gain_db, fade_in_ms, fade_out_ms) - calibrations(id, device_id, measured_round_trip_ms, measured_at, method) - A region references a take with an offset. Trimming moves the region's window, never the recorded audio, so every edit stays reversible. ### Phase 1 · Playback on a clock Build: Set up an audio context and a metronome. Schedule clicks ahead using the audio clock, not a JavaScript timer. Run for ten minutes and confirm the last click is where the tempo says it should be. Trap: Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. Done when: A metronome at a set tempo plays without audible jitter. After ten minutes the final click's time matches the calculated time within a few milliseconds. Switching to another browser tab and back does not shift the click. Changing tempo takes effect at the next bar rather than mid-beat. Do not build yet: No recording, no tracks, no export. ### Phase 2 · Recording that lands on the beat Build: Measure the round trip latency of the actual input and output devices, store it as a calibration, and shift every recording back by it. Provide a measurement routine rather than a number you typed. Then record against the click and check alignment by looking at the waveform. Trap: Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. Trap: Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. Done when: A measured calibration exists for the current device and is stored. A recording of four percussive hits on the beat shows transients aligned to the grid within a few milliseconds, verified by looking at the waveform. Changing the output device invalidates the calibration and prompts a re-measurement. The raw and compensated start times are both stored. Do not build yet: No arranging, mixing or export. ### Phase 3 · Tracks, takes and non-destructive regions Build: Multiple tracks, each recording to its own take. Regions referencing a take with an offset and a duration, so trimming changes the window rather than the audio. Takes persist to IndexedDB immediately so a reload loses nothing. Trap: Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. Done when: Trimming a region and then extending it again restores the original audio. A reload immediately after recording preserves the take. Two tracks recorded separately play back in sync. Deleting a region does not delete the take it referenced if another region uses it. Do not build yet: No mixing or export yet. ### Phase 4 · Monitoring and the mixer Build: Per-track gain, pan, mute and solo, plus metering. Input monitoring with an honest warning that browser monitoring adds the same round trip you are compensating for, and an option to disable it in favour of hardware monitoring. Trap: Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. Done when: Gain, pan, mute and solo all affect playback correctly, and solo on one track silences the others. Meters show level without blocking the audio thread. Input monitoring displays the measured delay in milliseconds next to the control. Disabling monitoring silences the loop immediately. Do not build yet: No export yet. ### Phase 5 · Export, mix and stems Build: Render offline rather than capturing playback. Export the mix and each track as a stem, all starting at the same timeline zero so they reassemble in any other tool. Verify by summing the stems and comparing against the mix. Trap: Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. Done when: Summing the exported stems produces the mix within a negligible difference, verified numerically rather than by ear. Every stem has identical length and starts at timeline zero. Offline rendering completes faster than the project's duration. Exporting twice produces identical files. Do not build yet: Project sharing and cleanup come last. ### Phase 6 · Projects, storage and honest limits Build: Save and load projects with their audio. Export a project as a folder of stems plus a manifest. Warn as storage approaches the browser's quota. Then write down what this does not do, starting with real-time collaboration and instruments. Trap: Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. Done when: A project saves and reloads with all audio intact. Approaching the storage quota produces a warning before recording is armed rather than a failure during it. A project exports as stems plus a manifest that another tool can use. The documentation states what is not included. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time collaboration, which is what the hosted product is for and which is substantially harder than the recording itself. - Instruments, loops and the licensed sound library, which is content with licensing attached rather than code. - The education workflow and classroom features that are much of why schools buy it. - Effects processing beyond gain and pan. Reverb, compression and equalisation are each their own project, and a recorder that does timing correctly is more useful than one with poor effects. ### README must contain - That recording latency is measured per device and compensated, and how to re-measure - That scheduling uses the audio clock rather than JavaScript timers, and why - That regions are windows onto takes, so trimming is never destructive - That stems export from timeline zero with leading silence, so they reassemble elsewhere - That browser monitoring adds the round trip delay, with the measured number ===== ARCHITECTURE.md ===== # Architecture · Soundtrap ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running in the browser | audio capture and playback are both browser APIs and there is no installation to distribute | | Audio | Web Audio API with an AudioWorklet for anything per-sample | the worklet runs on the audio thread, which is where recording and metering must happen | | Recording | MediaRecorder for capture, decoded to buffers for editing | capture and edit are different jobs and want different representations | | Storage | IndexedDB for audio buffers and project state | takes are large and must survive a reload, which is the failure people never forgive | | Export | Offline rendering, not real-time playback capture | rendering offline is faster than real time and produces identical output every run | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Clock | Scheduling against the audio context. | Not swappable for a timer-based approach at any point. Everything timed depends on this being right. | | Calibration | Measuring and applying round trip latency. | The measurement method can change; using a measured value rather than a constant cannot. | | Takes and regions | Recorded audio and non-destructive windows onto it. | The window model is what makes editing reversible and should survive any change. | | Mixer | Gain, pan and metering. | Effects would extend this considerably. Metering must stay on the audio thread whatever else changes. | | Export | Offline rendering of mix and stems. | Offline rather than captured playback is the determinism decision. Timeline-zero stems is the portability decision. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_DEFAULT_TEMPO` · required · Starting tempo for a new project. - `VITE_SCHEDULER_LOOKAHEAD_MS` · required · How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. - `VITE_MAX_PROJECT_MB` · required · Warn as a project approaches this size, before the browser quota fails a recording. - `VITE_MONITOR_WARN_MS` · required · Above this measured monitoring delay, warn that playing along will be difficult. - `VITE_EXPORT_SAMPLE_RATE` · required · Render rate for export. Matching your recording rate avoids a resample. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Soundtrap product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, running in the browser, Web Audio API with an AudioWorklet for anything per-sample, MediaRecorder for capture, decoded to buffers for editing, IndexedDB for audio buffers and project state, Offline rendering, not real-time playback capture. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. - Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. ===== MILESTONES.md ===== # Delivery milestones · Soundtrap Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Playback on a clock A click plays in perfect time and does not drift over ten minutes. ### Steps 1. Create the audio context and a lookahead scheduler Schedule events a short window ahead against the audio clock, driven by a timer that only refills the queue. Files: `src/clock.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Build the metronome on it Scheduled clicks, not triggered ones. This is the pattern everything timed will use. Files: `src/metronome.ts` 3. Measure drift over ten minutes Compare the final click's actual time against the calculated time. This is the test that catches a timer-based scheduler. Files: `src/clock.test.ts` ### Done when - [ ] The metronome plays without audible jitter - [ ] Ten minutes of playback shows no measurable drift - [ ] Backgrounding the tab does not shift timing - [ ] A tempo change takes effect at a bar boundary ### Watch out - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. ## M2 · Recording that lands on the beat What you play on the beat is recorded on the beat. ### Steps 1. Write the calibration routine Play a click, record it through the loop, and measure the offset. Store the result per device with a timestamp. Files: `src/calibrate.ts` 2. Apply compensation on every take Shift the recorded start back by the measured round trip. Store both times so the correction is inspectable. Files: `src/record.ts` 3. Verify against a percussive test Four hits on the beat, then look at the transients against the grid. Do not skip the visual check. ### Done when - [ ] A device calibration is measured and stored - [ ] Four on-beat hits align to the grid within a few milliseconds - [ ] Changing devices invalidates the calibration - [ ] Raw and compensated start times are both recorded ### Watch out - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. ## M3 · Tracks, takes and non-destructive regions You can record several tracks, move and trim them, and never lose a take. ### Steps 1. Persist takes immediately To IndexedDB as recording finishes. A lost take is the failure people abandon a tool over. Files: `src/store.ts` 2. Model regions as windows Take reference, source offset, duration. Trimming edits the window only. Files: `src/regions.ts` 3. Keep takes alive while referenced Reference counting, so deleting one region does not destroy another's source. Files: `src/takes.ts` ### Done when - [ ] Trim then extend restores the original audio - [ ] A reload after recording preserves the take - [ ] Two tracks play back in sync - [ ] Deleting a region keeps a take another region uses ### Watch out - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. ## M4 · Monitoring and the mixer You can hear yourself while recording without the delay making it impossible to play. ### Steps 1. Build the mixer graph Gain and pan per track into a master. Solo is a mute of everything else, computed rather than stored per track. Files: `src/mixer.ts` 2. Meter on the audio thread An AudioWorklet computing levels and posting them, so metering never blocks audio. Files: `src/meter-worklet.js` 3. Be honest about monitoring delay Show the measured number next to the toggle. It is the difference between a usable session and a frustrating one. Files: `src/monitor.ts` ### Done when - [ ] Solo silences other tracks - [ ] Meters update without blocking audio - [ ] Monitoring shows the measured delay in milliseconds - [ ] Disabling monitoring silences the loop immediately ### Watch out - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. ## M5 · Export, mix and stems What you export is what you heard, and the stems line up when reassembled. ### Steps 1. Render offline An OfflineAudioContext rendering the graph. Faster than real time and deterministic, unlike capturing playback. Files: `src/export.ts` 2. Export stems from timeline zero Same length, same start, leading silence included. This is what makes them portable. Files: `src/export.ts` 3. Verify by summing Sum the stems and compare against the mix numerically. A difference means a gain or pan is applied in one path and not the other. Files: `src/export.test.ts` ### Done when - [ ] Summed stems match the mix numerically - [ ] Every stem has identical length and start - [ ] Offline rendering is faster than real time - [ ] Two exports are byte-identical ### Watch out - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ## M6 · Projects, storage and honest limits (production only) Projects survive, storage does not fill silently, and you know what this is not. ### Steps 1. Save and reload whole projects State plus audio. Reload and play to confirm, since a project that loads but is silent is a common failure. Files: `src/project.ts` 2. Check quota before arming Estimate available storage and warn before recording rather than failing during it. Files: `src/quota.ts` 3. Export a portable project folder Stems plus a manifest with tempo, track names and region positions. Files: `src/export.ts` ### Done when - [ ] A saved project reloads and plays correctly - [ ] Approaching the quota warns before arming a recording - [ ] A project exports as stems plus a manifest - [ ] The documentation states what is not included ### Watch out - Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. ===== OPERATIONS.md ===== # Operations · Soundtrap ## Backup Projects live in browser storage, which can be evicted. Exporting a project folder of stems plus a manifest is the backup, and it is also the format another tool can open. ## Restore Import the project folder. Because stems start at timeline zero, a reassembled project lines up even in a different application, which is the property worth protecting. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Storage use against quota, and calibration age per device. A stale calibration after a device change produces recordings that are subtly out of time, which is hard to diagnose after the fact. ## Incident checklist A lost take is the incident that matters, and immediate persistence plus quota warning are the two controls. If a take is ever lost, the persistence timing is the bug rather than the storage limit. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Ten minutes of playback shows no measurable drift - [ ] Four on-beat hits align to the grid after compensation - [ ] Trim then extend is confirmed to restore original audio - [ ] Summed stems match the mix numerically - [ ] A quota warning fires before recording is armed ## Launch constraint Do not market omitted Soundtrap capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Starting tempo for a new project. VITE_DEFAULT_TEMPO=120 # Required. How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. VITE_SCHEDULER_LOOKAHEAD_MS=120 # Required. Warn as a project approaches this size, before the browser quota fails a recording. VITE_MAX_PROJECT_MB=800 # Required. Above this measured monitoring delay, warn that playing along will be difficult. VITE_MONITOR_WARN_MS=20 # Required. Render rate for export. Matching your recording rate avoids a resample. VITE_EXPORT_SAMPLE_RATE=48000
# Soundtrap · indie build A browser multitrack recorder with a click, latency-compensated recording that lands on the beat, regions you can move and trim without destroying the take, a mixer with levels and pan, and export of the mix and every stem at sample-aligned starts. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running in the browser | audio capture and playback are both browser APIs and there is no installation to distribute | | Audio | Web Audio API with an AudioWorklet for anything per-sample | the worklet runs on the audio thread, which is where recording and metering must happen | | Recording | MediaRecorder for capture, decoded to buffers for editing | capture and edit are different jobs and want different representations | | Storage | IndexedDB for audio buffers and project state | takes are large and must survive a reload, which is the failure people never forgive | | Export | Offline rendering, not real-time playback capture | rendering offline is faster than real time and produces identical output every run | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the build. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An audio input, ideally an interface with direct monitoring** · a basic interface is around 100 US dollars; measurement works without one - Why: Latency compensation can be measured with any input, but playing along to a click through browser monitoring is genuinely difficult past about twenty milliseconds, and direct monitoring removes that. - Get it: Any USB audio interface with a direct monitor control, or headphones plus a built-in microphone for measurement only. - Verify: The device appears as an input and a round trip measurement completes - [ ] **A way to measure the round trip** · free with a cable you own - Why: Phase 2 needs the click to reach the input. A physical loop is the most accurate method and a microphone in front of a speaker is adequate. - Get it: A cable from output to input if your interface allows it, or place a microphone near a speaker in a quiet room. - Verify: The calibration routine produces a stable figure across three runs - [ ] **Something percussive to play on the beat** · free - Why: Alignment is verified by looking at transients against the grid, and a sustained sound has no transient to look at. - Get it: Clapping works. So does tapping a desk. - Verify: A four-hit recording shows four clear transients - [ ] **A current browser with AudioWorklet support** · free - Why: Metering and any per-sample work must run on the audio thread, and the worklet is how that happens. - Get it: A current Chromium, Firefox or Safari. Test in at least two, since audio behaviour differs. - Verify: An AudioWorklet loads and posts messages ## Quick start ```sh npm create vite@latest . -- --template vanilla-ts ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. - realtime collaboration, loops, instruments, education workflow, and hosted rendering - licensed music catalog - royalty and distribution relationships - global streaming infrastructure - artist network and recommendations If one of those is essential to you, that is the reason to keep paying for Soundtrap, and the README should say so rather than pretend.
# Build brief · Soundtrap The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser multitrack recorder that records in time, arranges regions and exports stems to replace Soundtrap. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The whole difficulty in a recorder is that what you record arrives late. Between your voice and the file there is input buffering, processing and output buffering, and the recorded audio lands after the beat you played it on by that round trip. Every recorder that sounds out of time has skipped compensating for it. So Phase 2 measures the round trip on your actual hardware and shifts recordings back by it, and it does that before any arranging, mixing or effects exist. Build the timing first; everything else is easier and none of it matters if the timing is wrong. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running in the browser · audio capture and playback are both browser APIs and there is no installation to distribute - Audio: Web Audio API with an AudioWorklet for anything per-sample · the worklet runs on the audio thread, which is where recording and metering must happen - Recording: MediaRecorder for capture, decoded to buffers for editing · capture and edit are different jobs and want different representations - Storage: IndexedDB for audio buffers and project state · takes are large and must survive a reload, which is the failure people never forgive - Export: Offline rendering, not real-time playback capture · rendering offline is faster than real time and produces identical output every run ### Data model (create this before Phase 1) - projects(id, name, tempo_bpm, time_signature, sample_rate, created_at) - tracks(id, project_id, name, ordinal, gain_db, pan, muted, soloed, input_device_id) - takes(id, track_id, buffer_key, recorded_at, raw_start_ms, compensated_start_ms, duration_ms, latency_applied_ms) - regions(id, track_id, take_id, timeline_start_ms, source_offset_ms, duration_ms, gain_db, fade_in_ms, fade_out_ms) - calibrations(id, device_id, measured_round_trip_ms, measured_at, method) - A region references a take with an offset. Trimming moves the region's window, never the recorded audio, so every edit stays reversible. ### Phase 1 · Playback on a clock Build: Set up an audio context and a metronome. Schedule clicks ahead using the audio clock, not a JavaScript timer. Run for ten minutes and confirm the last click is where the tempo says it should be. Trap: Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. Done when: A metronome at a set tempo plays without audible jitter. After ten minutes the final click's time matches the calculated time within a few milliseconds. Switching to another browser tab and back does not shift the click. Changing tempo takes effect at the next bar rather than mid-beat. Do not build yet: No recording, no tracks, no export. ### Phase 2 · Recording that lands on the beat Build: Measure the round trip latency of the actual input and output devices, store it as a calibration, and shift every recording back by it. Provide a measurement routine rather than a number you typed. Then record against the click and check alignment by looking at the waveform. Trap: Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. Trap: Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. Done when: A measured calibration exists for the current device and is stored. A recording of four percussive hits on the beat shows transients aligned to the grid within a few milliseconds, verified by looking at the waveform. Changing the output device invalidates the calibration and prompts a re-measurement. The raw and compensated start times are both stored. Do not build yet: No arranging, mixing or export. ### Phase 3 · Tracks, takes and non-destructive regions Build: Multiple tracks, each recording to its own take. Regions referencing a take with an offset and a duration, so trimming changes the window rather than the audio. Takes persist to IndexedDB immediately so a reload loses nothing. Trap: Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. Done when: Trimming a region and then extending it again restores the original audio. A reload immediately after recording preserves the take. Two tracks recorded separately play back in sync. Deleting a region does not delete the take it referenced if another region uses it. Do not build yet: No mixing or export yet. ### Phase 4 · Monitoring and the mixer Build: Per-track gain, pan, mute and solo, plus metering. Input monitoring with an honest warning that browser monitoring adds the same round trip you are compensating for, and an option to disable it in favour of hardware monitoring. Trap: Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. Done when: Gain, pan, mute and solo all affect playback correctly, and solo on one track silences the others. Meters show level without blocking the audio thread. Input monitoring displays the measured delay in milliseconds next to the control. Disabling monitoring silences the loop immediately. Do not build yet: No export yet. ### Phase 5 · Export, mix and stems Build: Render offline rather than capturing playback. Export the mix and each track as a stem, all starting at the same timeline zero so they reassemble in any other tool. Verify by summing the stems and comparing against the mix. Trap: Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. Done when: Summing the exported stems produces the mix within a negligible difference, verified numerically rather than by ear. Every stem has identical length and starts at timeline zero. Offline rendering completes faster than the project's duration. Exporting twice produces identical files. Do not build yet: Project sharing and cleanup come last. ### Phase 6 · Projects, storage and honest limits Build: Save and load projects with their audio. Export a project as a folder of stems plus a manifest. Warn as storage approaches the browser's quota. Then write down what this does not do, starting with real-time collaboration and instruments. Trap: Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. Done when: A project saves and reloads with all audio intact. Approaching the storage quota produces a warning before recording is armed rather than a failure during it. A project exports as stems plus a manifest that another tool can use. The documentation states what is not included. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time collaboration, which is what the hosted product is for and which is substantially harder than the recording itself. - Instruments, loops and the licensed sound library, which is content with licensing attached rather than code. - The education workflow and classroom features that are much of why schools buy it. - Effects processing beyond gain and pan. Reverb, compression and equalisation are each their own project, and a recorder that does timing correctly is more useful than one with poor effects. ### README must contain - That recording latency is measured per device and compensated, and how to re-measure - That scheduling uses the audio clock rather than JavaScript timers, and why - That regions are windows onto takes, so trimming is never destructive - That stems export from timeline zero with leading silence, so they reassemble elsewhere - That browser monitoring adds the round trip delay, with the measured number
# Agent instructions · Soundtrap indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: TypeScript with Vite, running in the browser, Web Audio API with an AudioWorklet for anything per-sample, MediaRecorder for capture, decoded to buffers for editing, IndexedDB for audio buffers and project state, Offline rendering, not real-time playback capture. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere.
# Build plan · Soundtrap A browser multitrack recorder with a click, latency-compensated recording that lands on the beat, regions you can move and trim without destroying the take, a mixer with levels and pan, and export of the mix and every stem at sample-aligned starts. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Playback on a clock A click plays in perfect time and does not drift over ten minutes. ### Steps 1. Create the audio context and a lookahead scheduler Schedule events a short window ahead against the audio clock, driven by a timer that only refills the queue. Files: `src/clock.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Build the metronome on it Scheduled clicks, not triggered ones. This is the pattern everything timed will use. Files: `src/metronome.ts` 3. Measure drift over ten minutes Compare the final click's actual time against the calculated time. This is the test that catches a timer-based scheduler. Files: `src/clock.test.ts` ### Done when - [ ] The metronome plays without audible jitter - [ ] Ten minutes of playback shows no measurable drift - [ ] Backgrounding the tab does not shift timing - [ ] A tempo change takes effect at a bar boundary ### Watch out - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. ## Phase 2 · Recording that lands on the beat What you play on the beat is recorded on the beat. ### Steps 1. Write the calibration routine Play a click, record it through the loop, and measure the offset. Store the result per device with a timestamp. Files: `src/calibrate.ts` 2. Apply compensation on every take Shift the recorded start back by the measured round trip. Store both times so the correction is inspectable. Files: `src/record.ts` 3. Verify against a percussive test Four hits on the beat, then look at the transients against the grid. Do not skip the visual check. ### Done when - [ ] A device calibration is measured and stored - [ ] Four on-beat hits align to the grid within a few milliseconds - [ ] Changing devices invalidates the calibration - [ ] Raw and compensated start times are both recorded ### Watch out - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. ## Phase 3 · Tracks, takes and non-destructive regions You can record several tracks, move and trim them, and never lose a take. ### Steps 1. Persist takes immediately To IndexedDB as recording finishes. A lost take is the failure people abandon a tool over. Files: `src/store.ts` 2. Model regions as windows Take reference, source offset, duration. Trimming edits the window only. Files: `src/regions.ts` 3. Keep takes alive while referenced Reference counting, so deleting one region does not destroy another's source. Files: `src/takes.ts` ### Done when - [ ] Trim then extend restores the original audio - [ ] A reload after recording preserves the take - [ ] Two tracks play back in sync - [ ] Deleting a region keeps a take another region uses ### Watch out - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. ## Phase 4 · Monitoring and the mixer You can hear yourself while recording without the delay making it impossible to play. ### Steps 1. Build the mixer graph Gain and pan per track into a master. Solo is a mute of everything else, computed rather than stored per track. Files: `src/mixer.ts` 2. Meter on the audio thread An AudioWorklet computing levels and posting them, so metering never blocks audio. Files: `src/meter-worklet.js` 3. Be honest about monitoring delay Show the measured number next to the toggle. It is the difference between a usable session and a frustrating one. Files: `src/monitor.ts` ### Done when - [ ] Solo silences other tracks - [ ] Meters update without blocking audio - [ ] Monitoring shows the measured delay in milliseconds - [ ] Disabling monitoring silences the loop immediately ### Watch out - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. ## Phase 5 · Export, mix and stems What you export is what you heard, and the stems line up when reassembled. ### Steps 1. Render offline An OfflineAudioContext rendering the graph. Faster than real time and deterministic, unlike capturing playback. Files: `src/export.ts` 2. Export stems from timeline zero Same length, same start, leading silence included. This is what makes them portable. Files: `src/export.ts` 3. Verify by summing Sum the stems and compare against the mix numerically. A difference means a gain or pan is applied in one path and not the other. Files: `src/export.test.ts` ### Done when - [ ] Summed stems match the mix numerically - [ ] Every stem has identical length and start - [ ] Offline rendering is faster than real time - [ ] Two exports are byte-identical ### Watch out - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ## Not in this build - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. ## After v1, if you want it - Punch recording over a region, which the take and region model already supports - A simple equaliser per track, once timing and export are trustworthy - Tempo mapping so a recording made without a click can be gridded afterwards
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Starting tempo for a new project. VITE_DEFAULT_TEMPO=120 # Required. How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. VITE_SCHEDULER_LOOKAHEAD_MS=120 # Required. Warn as a project approaches this size, before the browser quota fails a recording. VITE_MAX_PROJECT_MB=800 # Required. Above this measured monitoring delay, warn that playing along will be difficult. VITE_MONITOR_WARN_MS=20 # Required. Render rate for export. Matching your recording rate avoids a resample. VITE_EXPORT_SAMPLE_RATE=48000
# Soundtrap · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Soundtrap, record and arrange a simple multitrack song in a browser and export stems. The hard boundary is realtime collaboration, loops, instruments, education workflow, and hosted rendering, plus licensed content, rights, and distribution network. ## Product outcome A recorder where takes land on the beat because the round trip was measured rather than assumed, trimming never destroys a take, and exported stems reassemble correctly in any other tool. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - legally owned local audio files - ffmpeg - local storage - optional audio-analysis models ## Explicit non-goals for v1 - Real-time collaboration. - Instruments, loops and the licensed sound library. - The education and classroom workflow. - Effects beyond gain and pan. - realtime collaboration, loops, instruments, education workflow, and hosted rendering - licensed music catalog - royalty and distribution relationships - global streaming infrastructure - artist network and recommendations ## Success criteria - Ten minutes of playback shows no measurable drift - Four on-beat hits align to the grid after compensation - Trim then extend is confirmed to restore original audio - Summed stems match the mix numerically - A quota warning fires before recording is armed
# Build brief · Soundtrap The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a browser multitrack recorder that records in time, arranges regions and exports stems to replace Soundtrap. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The whole difficulty in a recorder is that what you record arrives late. Between your voice and the file there is input buffering, processing and output buffering, and the recorded audio lands after the beat you played it on by that round trip. Every recorder that sounds out of time has skipped compensating for it. So Phase 2 measures the round trip on your actual hardware and shifts recordings back by it, and it does that before any arranging, mixing or effects exist. Build the timing first; everything else is easier and none of it matters if the timing is wrong. ### Stack (fixed, do not substitute) - Runtime: TypeScript with Vite, running in the browser · audio capture and playback are both browser APIs and there is no installation to distribute - Audio: Web Audio API with an AudioWorklet for anything per-sample · the worklet runs on the audio thread, which is where recording and metering must happen - Recording: MediaRecorder for capture, decoded to buffers for editing · capture and edit are different jobs and want different representations - Storage: IndexedDB for audio buffers and project state · takes are large and must survive a reload, which is the failure people never forgive - Export: Offline rendering, not real-time playback capture · rendering offline is faster than real time and produces identical output every run ### Data model (create this before Phase 1) - projects(id, name, tempo_bpm, time_signature, sample_rate, created_at) - tracks(id, project_id, name, ordinal, gain_db, pan, muted, soloed, input_device_id) - takes(id, track_id, buffer_key, recorded_at, raw_start_ms, compensated_start_ms, duration_ms, latency_applied_ms) - regions(id, track_id, take_id, timeline_start_ms, source_offset_ms, duration_ms, gain_db, fade_in_ms, fade_out_ms) - calibrations(id, device_id, measured_round_trip_ms, measured_at, method) - A region references a take with an offset. Trimming moves the region's window, never the recorded audio, so every edit stays reversible. ### Phase 1 · Playback on a clock Build: Set up an audio context and a metronome. Schedule clicks ahead using the audio clock, not a JavaScript timer. Run for ten minutes and confirm the last click is where the tempo says it should be. Trap: Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. Done when: A metronome at a set tempo plays without audible jitter. After ten minutes the final click's time matches the calculated time within a few milliseconds. Switching to another browser tab and back does not shift the click. Changing tempo takes effect at the next bar rather than mid-beat. Do not build yet: No recording, no tracks, no export. ### Phase 2 · Recording that lands on the beat Build: Measure the round trip latency of the actual input and output devices, store it as a calibration, and shift every recording back by it. Provide a measurement routine rather than a number you typed. Then record against the click and check alignment by looking at the waveform. Trap: Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. Trap: Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. Done when: A measured calibration exists for the current device and is stored. A recording of four percussive hits on the beat shows transients aligned to the grid within a few milliseconds, verified by looking at the waveform. Changing the output device invalidates the calibration and prompts a re-measurement. The raw and compensated start times are both stored. Do not build yet: No arranging, mixing or export. ### Phase 3 · Tracks, takes and non-destructive regions Build: Multiple tracks, each recording to its own take. Regions referencing a take with an offset and a duration, so trimming changes the window rather than the audio. Takes persist to IndexedDB immediately so a reload loses nothing. Trap: Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. Done when: Trimming a region and then extending it again restores the original audio. A reload immediately after recording preserves the take. Two tracks recorded separately play back in sync. Deleting a region does not delete the take it referenced if another region uses it. Do not build yet: No mixing or export yet. ### Phase 4 · Monitoring and the mixer Build: Per-track gain, pan, mute and solo, plus metering. Input monitoring with an honest warning that browser monitoring adds the same round trip you are compensating for, and an option to disable it in favour of hardware monitoring. Trap: Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. Done when: Gain, pan, mute and solo all affect playback correctly, and solo on one track silences the others. Meters show level without blocking the audio thread. Input monitoring displays the measured delay in milliseconds next to the control. Disabling monitoring silences the loop immediately. Do not build yet: No export yet. ### Phase 5 · Export, mix and stems Build: Render offline rather than capturing playback. Export the mix and each track as a stem, all starting at the same timeline zero so they reassemble in any other tool. Verify by summing the stems and comparing against the mix. Trap: Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. Done when: Summing the exported stems produces the mix within a negligible difference, verified numerically rather than by ear. Every stem has identical length and starts at timeline zero. Offline rendering completes faster than the project's duration. Exporting twice produces identical files. Do not build yet: Project sharing and cleanup come last. ### Phase 6 · Projects, storage and honest limits Build: Save and load projects with their audio. Export a project as a folder of stems plus a manifest. Warn as storage approaches the browser's quota. Then write down what this does not do, starting with real-time collaboration and instruments. Trap: Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment. Done when: A project saves and reloads with all audio intact. Approaching the storage quota produces a warning before recording is armed rather than a failure during it. A project exports as stems plus a manifest that another tool can use. The documentation states what is not included. Do not build yet: Nothing after this. ### Out of scope (and why) - Real-time collaboration, which is what the hosted product is for and which is substantially harder than the recording itself. - Instruments, loops and the licensed sound library, which is content with licensing attached rather than code. - The education workflow and classroom features that are much of why schools buy it. - Effects processing beyond gain and pan. Reverb, compression and equalisation are each their own project, and a recorder that does timing correctly is more useful than one with poor effects. ### README must contain - That recording latency is measured per device and compensated, and how to re-measure - That scheduling uses the audio clock rather than JavaScript timers, and why - That regions are windows onto takes, so trimming is never destructive - That stems export from timeline zero with leading silence, so they reassemble elsewhere - That browser monitoring adds the round trip delay, with the measured number
# Architecture · Soundtrap ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | TypeScript with Vite, running in the browser | audio capture and playback are both browser APIs and there is no installation to distribute | | Audio | Web Audio API with an AudioWorklet for anything per-sample | the worklet runs on the audio thread, which is where recording and metering must happen | | Recording | MediaRecorder for capture, decoded to buffers for editing | capture and edit are different jobs and want different representations | | Storage | IndexedDB for audio buffers and project state | takes are large and must survive a reload, which is the failure people never forgive | | Export | Offline rendering, not real-time playback capture | rendering offline is faster than real time and produces identical output every run | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Clock | Scheduling against the audio context. | Not swappable for a timer-based approach at any point. Everything timed depends on this being right. | | Calibration | Measuring and applying round trip latency. | The measurement method can change; using a measured value rather than a constant cannot. | | Takes and regions | Recorded audio and non-destructive windows onto it. | The window model is what makes editing reversible and should survive any change. | | Mixer | Gain, pan and metering. | Effects would extend this considerably. Metering must stay on the audio thread whatever else changes. | | Export | Offline rendering of mix and stems. | Offline rather than captured playback is the determinism decision. Timeline-zero stems is the portability decision. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `VITE_DEFAULT_TEMPO` · required · Starting tempo for a new project. - `VITE_SCHEDULER_LOOKAHEAD_MS` · required · How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. - `VITE_MAX_PROJECT_MB` · required · Warn as a project approaches this size, before the browser quota fails a recording. - `VITE_MONITOR_WARN_MS` · required · Above this measured monitoring delay, warn that playing along will be difficult. - `VITE_EXPORT_SAMPLE_RATE` · required · Render rate for export. Matching your recording rate avoids a resample. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Soundtrap product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: TypeScript with Vite, running in the browser, Web Audio API with an AudioWorklet for anything per-sample, MediaRecorder for capture, decoded to buffers for editing, IndexedDB for audio buffers and project state, Offline rendering, not real-time playback capture. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. - Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment.
# Delivery milestones · Soundtrap Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Playback on a clock A click plays in perfect time and does not drift over ten minutes. ### Steps 1. Create the audio context and a lookahead scheduler Schedule events a short window ahead against the audio clock, driven by a timer that only refills the queue. Files: `src/clock.ts` ```sh npm create vite@latest . -- --template vanilla-ts ``` 2. Build the metronome on it Scheduled clicks, not triggered ones. This is the pattern everything timed will use. Files: `src/metronome.ts` 3. Measure drift over ten minutes Compare the final click's actual time against the calculated time. This is the test that catches a timer-based scheduler. Files: `src/clock.test.ts` ### Done when - [ ] The metronome plays without audible jitter - [ ] Ten minutes of playback shows no measurable drift - [ ] Backgrounding the tab does not shift timing - [ ] A tempo change takes effect at a bar boundary ### Watch out - Scheduling audio with setTimeout or setInterval produces audible jitter immediately and measurable drift within minutes, because those timers are not real-time and are throttled in a background tab. Use a lookahead scheduler that queues events against the audio context's own clock, which is sample accurate. ## M2 · Recording that lands on the beat What you play on the beat is recorded on the beat. ### Steps 1. Write the calibration routine Play a click, record it through the loop, and measure the offset. Store the result per device with a timestamp. Files: `src/calibrate.ts` 2. Apply compensation on every take Shift the recorded start back by the measured round trip. Store both times so the correction is inspectable. Files: `src/record.ts` 3. Verify against a percussive test Four hits on the beat, then look at the transients against the grid. Do not skip the visual check. ### Done when - [ ] A device calibration is measured and stored - [ ] Four on-beat hits align to the grid within a few milliseconds - [ ] Changing devices invalidates the calibration - [ ] Raw and compensated start times are both recorded ### Watch out - Round trip latency varies by device, browser and buffer size, so a hard coded compensation value will be right on your machine and wrong on every other one. Measure it, store it per device, and re-measure when the device changes. - Verify alignment visually and audibly, not by trusting the number. Record a sharp percussive sound exactly on four beats and look at where the transients sit relative to the grid. ## M3 · Tracks, takes and non-destructive regions You can record several tracks, move and trim them, and never lose a take. ### Steps 1. Persist takes immediately To IndexedDB as recording finishes. A lost take is the failure people abandon a tool over. Files: `src/store.ts` 2. Model regions as windows Take reference, source offset, duration. Trimming edits the window only. Files: `src/regions.ts` 3. Keep takes alive while referenced Reference counting, so deleting one region does not destroy another's source. Files: `src/takes.ts` ### Done when - [ ] Trim then extend restores the original audio - [ ] A reload after recording preserves the take - [ ] Two tracks play back in sync - [ ] Deleting a region keeps a take another region uses ### Watch out - Trimming by rewriting the audio buffer destroys the material outside the trim, and the first time someone trims too far they have lost the take. A region is a window onto a take: trimming moves the window, and dragging the edge back out restores what was there. ## M4 · Monitoring and the mixer You can hear yourself while recording without the delay making it impossible to play. ### Steps 1. Build the mixer graph Gain and pan per track into a master. Solo is a mute of everything else, computed rather than stored per track. Files: `src/mixer.ts` 2. Meter on the audio thread An AudioWorklet computing levels and posting them, so metering never blocks audio. Files: `src/meter-worklet.js` 3. Be honest about monitoring delay Show the measured number next to the toggle. It is the difference between a usable session and a frustrating one. Files: `src/monitor.ts` ### Done when - [ ] Solo silences other tracks - [ ] Meters update without blocking audio - [ ] Monitoring shows the measured delay in milliseconds - [ ] Disabling monitoring silences the loop immediately ### Watch out - Monitoring your own voice through the browser adds the full round trip to what you hear, and past roughly twenty milliseconds that delay makes singing or playing genuinely difficult. Offer the option, state the measured delay next to it, and recommend hardware or direct monitoring where the interface provides it. ## M5 · Export, mix and stems What you export is what you heard, and the stems line up when reassembled. ### Steps 1. Render offline An OfflineAudioContext rendering the graph. Faster than real time and deterministic, unlike capturing playback. Files: `src/export.ts` 2. Export stems from timeline zero Same length, same start, leading silence included. This is what makes them portable. Files: `src/export.ts` 3. Verify by summing Sum the stems and compare against the mix numerically. A difference means a gain or pan is applied in one path and not the other. Files: `src/export.test.ts` ### Done when - [ ] Summed stems match the mix numerically - [ ] Every stem has identical length and start - [ ] Offline rendering is faster than real time - [ ] Two exports are byte-identical ### Watch out - Exporting stems trimmed to each track's first sound means they no longer share a start, and dropping them into another tool puts everything in the wrong place. Every stem starts at timeline zero including its leading silence, which is what makes them usable elsewhere. ## M6 · Projects, storage and honest limits (production only) Projects survive, storage does not fill silently, and you know what this is not. ### Steps 1. Save and reload whole projects State plus audio. Reload and play to confirm, since a project that loads but is silent is a common failure. Files: `src/project.ts` 2. Check quota before arming Estimate available storage and warn before recording rather than failing during it. Files: `src/quota.ts` 3. Export a portable project folder Stems plus a manifest with tempo, track names and region positions. Files: `src/export.ts` ### Done when - [ ] A saved project reloads and plays correctly - [ ] Approaching the quota warns before arming a recording - [ ] A project exports as stems plus a manifest - [ ] The documentation states what is not included ### Watch out - Browser storage has a quota and audio fills it quickly, and hitting it mid-recording loses the take you were making. Check available quota before arming a recording and warn well before the limit rather than failing at the worst moment.
# Operations · Soundtrap ## Backup Projects live in browser storage, which can be evicted. Exporting a project folder of stems plus a manifest is the backup, and it is also the format another tool can open. ## Restore Import the project folder. Because stems start at timeline zero, a reassembled project lines up even in a different application, which is the property worth protecting. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Storage use against quota, and calibration age per device. A stale calibration after a device change produces recordings that are subtly out of time, which is hard to diagnose after the fact. ## Incident checklist A lost take is the incident that matters, and immediate persistence plus quota warning are the two controls. If a take is ever lost, the persistence timing is the bug rather than the storage limit. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Ten minutes of playback shows no measurable drift - [ ] Four on-beat hits align to the grid after compensation - [ ] Trim then extend is confirmed to restore original audio - [ ] Summed stems match the mix numerically - [ ] A quota warning fires before recording is armed ## Launch constraint Do not market omitted Soundtrap capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Starting tempo for a new project. VITE_DEFAULT_TEMPO=120 # Required. How far ahead events are queued against the audio clock. Longer is safer and less responsive to tempo changes. VITE_SCHEDULER_LOOKAHEAD_MS=120 # Required. Warn as a project approaches this size, before the browser quota fails a recording. VITE_MAX_PROJECT_MB=800 # Required. Above this measured monitoring delay, warn that playing along will be difficult. VITE_MONITOR_WARN_MS=20 # Required. Render rate for export. Matching your recording rate avoids a resample. VITE_EXPORT_SAMPLE_RATE=48000
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Soundtrap because the product is the catalog, rights chain, and distribution network; a player or processing tool is only the visible edge. The recurring cost buys catalog licensing, royalties, identity, content delivery, moderation, recommendations, payouts, codecs, and rights disputes, not just the visible interface.
xrealtime collaboration, loops, instruments, education workflow, and hosted rendering
xlicensed music catalog
xroyalty and distribution relationships
xglobal streaming infrastructure
xartist network and recommendations
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Soundtrap pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| basic | $0/user | $0/user | 1 account; projects can be created and saved; numeric project, export and storage caps are not publicly stated. |
| sound starter | custom | — | 1 account; numeric project, export and storage caps are not publicly stated. |
| music production | custom | — | 1 account; numeric project, export and storage caps are not publicly stated. |
| vocals & songwriting | custom | — | 1 account; numeric project, export and storage caps are not publicly stated. |
| production & vocals | custom | — | 1 account; numeric project, export and storage caps are not publicly stated. |
| music maker supreme promotional offer | $14.99/user | — | 1 account; exact numeric project, export and storage caps are not stated in the promotional banner. |
free tier1 account; projects can be created and saved; numeric project, export and storage caps are not publicly stated.
billingBasic is permanent free. Paid plans are offered through monthly/annual checkout, but exact current public U.S. rates were not reliably exposed for the named core plans; a separate Supreme promotion states $0.99 for 3 months then $14.99/month.
hidden costsPaid trials require a payment method; canceling during a trial can end paid access immediately; app-store billing and regional prices can differ.
verified 2026-08-12 · source ↗
Vibecode Soundtrap
Kinda. The core of Soundtrap is buildable in a weekend with the prompt on this page, but there are real gaps: realtime collaboration, loops, instruments, education workflow, and hosted rendering, licensed music catalog. Read the honest list above before committing.
How much does Soundtrap cost?
Soundtrap costs about $14.99/month (Music Maker Supreme promotional offer, checked 2026-08-12), which is $179.88 per year.
What do I lose by replacing Soundtrap?
Honestly: realtime collaboration, loops, instruments, education workflow, and hosted rendering; licensed music catalog; royalty and distribution relationships; global streaming infrastructure; artist network and recommendations. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Soundtrap?
Yes: Audiotool (A no-cap browser DAW; stems require the ancient ritual of solo, bounce and repeat.) BandLab (A genuinely free 16-track browser studio; fifteen minutes is the ceiling, which should settle the prog-rock ambitions.) The prompt is for when you want it exactly your way.