You drop a song into a stem splitter to pull out the vocal. A progress bar starts crawling. Then you notice it's the wrong song, press Cancel, and drop in the right one. A few seconds later, four stems appear. They're from the song you cancelled.
Every musician who has used AI audio tools has met a version of this: the app that freezes while it thinks, the 80 MB model it downloads again on every visit, the Cancel button that hides the spinner while the work quietly carries on. None of these are model problems. The model did its job. They're all in the part of the app around it, and that part is most of the work.
Step through what has to happen for one run, then switch to the other cases and watch where things can go wrong.
In each case, which step stops the wrong result from reaching the musician?
One run, four ways
A song is chosen
The musician picks song A. Nothing heavy happens yet: no download, no model.
- Song
- A
- Run
- none yet
- Audio sent anywhere
- no
In the "Cancel during separation" case, why isn't sending the worker a "please stop" message enough? Make a guess.
Why a musician feels every one of these
A stem splitter, a pitch tracker or a neural instrument isn't a backend job to the person using it. They're in the middle of making something. Every gap in the lifecycle lands on them as a musical interruption:
- A frozen page means the track they were looping stutters, the transport stops answering, and they lose their place.
- A repeat download means waiting a minute for an 80 MB file they already fetched yesterday, on the train, on a metered connection.
- A fake Cancel means the laptop fan keeps roaring, battery drains, and sometimes the cancelled result appears anyway, over the top of what they're working on.
- Stems from the wrong song means they might mix, edit or export something they never meant to keep.
The good news is that each one has a plain, well-known fix. Here's the whole run on one timeline, with what Cancel has to do at each moment:
What Cancel means at each stage
Check before you download
A musician shouldn't wait through a 79 MB download only to learn their device can't run the model. Check first: does the browser support the runtime the model needs, and is there a realistic amount of memory? "WebGPU is available" is one check, not proof the whole model fits, so treat the first load as part of the check and fail clearly if it runs out of room.
And let the musician choose when the download starts. Opening a page, or looking at a result they already have, shouldn't trigger it. A button that says how big the download is ("Download separator, 79 MB") respects their connection and their patience.
Download once, check it every time
Store the model in the browser's cache after the first download so the next visit starts in seconds. But a cached file can be truncated by a crash or damaged by a full disk, and a broken model doesn't always fail loudly: it can produce subtly wrong audio. So before using a file, compare its SHA-256 hash with the one you expect. If it doesn't match, throw it away and fetch it again.
Two failures look alike and aren't. The cache is full is a storage problem: you can still run the model from memory this once and tell the musician it'll download again next time. The model failed is a different problem with a different message. Lumping them together sends people hunting for a bug that isn't there.
Run it where the music can't feel it
A model running on the page's main thread blocks everything else on that thread: buttons, meters, scrolling, sometimes the transport. Put it in a Web Worker, a separate thread the browser gives you for exactly this, and send it the audio and a run tag. The page stays responsive while the model grinds.
The audio callback is stricter still. It has a few milliseconds to deliver the next block of sound, so it must never wait for a worker, a download or a model. Prepare the result elsewhere, then hand the finished audio to playback.
Same idea, different place: keeping heavy work off the audio thread and keeping it off the page's thread are the same rule. Each protects the part of the app the musician can feel: one their ears, the other their hands. Realtime and numerical safety covers the audio side.
Progress that tells the truth
A progress bar is a promise about time. Name the stage ("Downloading model", "Separating, window 3 of 8") and count something real: bytes received, windows processed. When you can't know the total, show an animated "working" state with the stage name instead of a bar that crawls to 99% and sits there. Musicians plan around progress bars. Make yours one they can trust.
Make Cancel real
A worker handles messages one at a time. While the model is in the middle of a long calculation, your "please stop" message waits in line until it finishes. That's why the cancelled run in the stepper terminates the worker: the browser ends it outright, frees its memory, and nothing can come out of it afterwards. You start a fresh worker for the next run, and the cached model makes that fast.
Cancel is really three jobs:
- Stop the work: abort the download, or terminate the worker.
- Refuse late results: tag every run, and install a result only if its tag is still the current one. This also handles the swapped song, where nobody pressed Cancel at all.
- Get ready again: release what the dead run held and bring back the Start button.
Here is a sketch of those jobs with standard browser features. It isn't a library; it's the shape to look for in whatever you build on.
let currentRun = 0
let worker: Worker | undefined
let download: AbortController | undefined
async function separate(song: ArrayBuffer) {
const run = ++currentRun // a new tag for every run
download = new AbortController()
const model = await loadVerifiedModel(download.signal) // cache, else fetch + SHA-256 check
if (run !== currentRun) return // cancelled or replaced while downloading
worker = new Worker(new URL("./separator.worker.ts", import.meta.url), { type: "module" })
worker.onmessage = (event) => {
if (event.data.run !== currentRun) return // a late result: drop it
if (event.data.type === "progress") showProgress(event.data)
if (event.data.type === "stems") installStems(event.data.stems)
}
worker.postMessage({ run, model, song })
}
function cancel() {
currentRun++ // every older result is now stale
download?.abort() // stops the network, not just the bar
worker?.terminate() // stops the calculation mid-way
worker = undefined
showReady()
}
Say where the audio goes
Musicians care whether their unreleased song leaves their computer, so tell them, right on the button. A model running in the browser keeps the audio on their device. A server method sends the audio away to be processed, and a paid provider sends it to someone else's service. Cancelling differs too: a local worker can be stopped instantly, a server job may only stop between windows, and a provider's job may keep running after you stop asking about it. The stem splitter behind the stepper above offers all three, and labels each by where it runs.
Make it yours
Take a model you already have running in a demo, and put it through the stepper's four cases in your own app:
- Add a device check and a download button that states the size.
- Cache the verified file, then corrupt it on purpose (edit a byte) and confirm your app notices and re-fetches.
- Press Cancel during the download and during the run. Watch your network panel and task manager, not just the spinner: does the work actually stop?
- Swap the input mid-run and make sure the old result never appears.
When the result does come through, save a run record beside it: the input, the model and its version, the device and the date. The day a musician asks "why does this vocal sound different from last week's?", that record is your answer.
Quick check
- A musician swaps songs while the model is still running. A moment later the worker returns four perfect stems. Should the app show them? No. They're perfect stems of the previous song. The model did nothing wrong; the run tag no longer matches, so the result is stale. This is the case people forget, because nobody pressed Cancel.
- You press Cancel and the progress bar disappears instantly. Has the work stopped? Not necessarily. Hiding the bar is the easy part. Check whether the download was aborted or the worker terminated, and whether a late result is still refused. Your laptop fan is a surprisingly good test.
- Saving the model to the cache fails because storage is full. Should the separation fail too? No. The model is in memory and verified, so the run can go ahead. Tell the musician it'll need downloading again next visit. Treating a storage problem as a model failure would block them for no reason.
Next steps
- Explore the map: What AI can do in music. Compare what different models take in and return before choosing one for the app: labels, notes, audio or control.
- Go deeper: Testing sound. Your app now runs the model reliably. But how do you check that what comes out is any good, when there's no correct answer to compare against?
- Use it: Stem separation. The musical task behind the stepper, and what the estimated stems can and can't give you.

















