CoProducer Measure a mix
Six glowing waveform strips in cyan, magenta, gold, violet, teal and amber laid in parallel on black marble, all starting at one thin gold line, one dim strip slightly out of place, a black drive with a cyan light beside them

Delivery · 20 October 2026 · 8 min

Sending stems to a mixer? Export them so they line up and add back up

NoDAW Labs · Lacquer Vol. 05

Stems a mixer can use all start at the same point, run the same length, share the session’s sample rate, and add back up to your rough mix when every fader sits at zero. Export every track from the same start to the same end, keep send effects on their own stems, switch off the mix-bus chain, and null the stems against your bounce before you send the link.

You send the link, feel good about it, and the first reply isn’t about the mix. The vocal comes in two bars early. The 808 sounds a little sharp. Where are the adlibs? And the rough you sent has a glue the mixer can’t find on any of the stems. Day one of a paid mix gets spent on email.

Every one of those is an export setting, and you can catch all of them in about thirty seconds before the files leave your machine.

What “lined up” actually means

The last check is a null test. Flip the polarity of your rough bounce and play it against all the stems at once. If what’s left is silence, the stems are the mix, and the mixer starts exactly where you left off. If what’s left is loud, something on the rough never made it into the stems.

We broke a session on purpose

To put numbers on it we built a small synthetic session: eight bars at 140 BPM, 48 kHz, five stems (drums, 808, vocal, keys, adlibs). The vocal doesn’t start until bar 3 and the adlibs until bar 5. We exported it cleanly once. Then we exported it the way a rushed export goes: the vocal as region-only, the 808 at 44.1 kHz with its fader pushed 12 dB, the adlib track still muted, and a soft-clip glue on the mix bus that only the rough went through.

What went wrongWhat the mixer getsThe fix
Vocal exported from its first noteA 10.256 s file instead of 13.714 s. Dropped at zero, it plays 3.459 s early: two bars plus the 30 ms pickupExport every stem from the same start bar, even when the track is silent there
808 bounced at 44.1 kHz in a 48 kHz sessionEither the DAW converts it on import, or it plays about 8.8% fast and 1.47 semitones sharp (48,000 ÷ 44,100)Export everything at the session rate. Let the mixer decide on any conversion
808 fader pushed 12 dB, 24-bit fixedThe peak went from −11.4 dBFS to over full scale, so 18,488 samples were flattened at 0 dBFS. That clipping is printed, and the mixer can’t undo itLeave headroom on the stem, or export 32-bit float, which keeps overs intact
Adlib track mutedA file of digital silence with the right nameUnmute and unsolo everything, and check no file is empty
Glue on the mix bus, rough onlyThe stems summed back up miss the rough by a residual only 9.9 dB under the mix. That’s a different recordBypass the bus chain for the stems and the rough, or send a second rough with it on and tell them what the chain is

The clean export, by contrast, nulled to −120.4 dB under the mix, which is just rounding, with zero samples of lag. That’s what a stem folder should look like.

Check the folder before you send it

This reads a folder of stems and, optionally, your rough bounce. It prints each stem’s rate, format, length, peak and where its first audio starts, then lists anything a mixer would email you about. With a rough it lines every stem up at zero, adds them together and runs the null test:

#!/usr/bin/env python3
"""Check a folder of exported stems before they go to a mixer.
usage: python3 stem_check.py STEMS_DIR [MIX.wav]      (pip install numpy soundfile)"""
import sys, pathlib
import numpy as np, soundfile as sf

def db(v): return 20 * np.log10(max(float(v), 1e-12))
folder = pathlib.Path(sys.argv[1]); mix_path = sys.argv[2] if len(sys.argv) > 2 else None
files = sorted(p for p in folder.iterdir() if p.suffix.lower() in (".wav", ".aif", ".aiff", ".flac"))
problems, stems = [], {}
print(f"{'stem':<11}{'rate':>7}{'format':>8}{'length s':>10}{'peak dBFS':>11}{'first audio':>13}")
for p in files:
    i = sf.info(p); x, sr = sf.read(p, dtype="float64", always_2d=True); stems[p.name] = (x, sr)
    peak = np.abs(x).max(); loud = np.flatnonzero(np.abs(x).max(axis=1) > 1e-4)   # above -80 dBFS
    first = f"{loud[0] / sr:.2f} s" if loud.size else "silent"
    print(f"{p.stem:<11}{sr:>7}{i.subtype:>8}{i.frames / sr:>10.3f}{(f"{db(peak):.1f}" if peak else "-inf"):>11}{first:>13}")
    if not loud.size: problems.append(f"{p.name}: silent (muted when you exported?)")
    clipped = int((np.abs(x) >= 0.99999).sum())
    if clipped and i.subtype != "FLOAT": problems.append(f"{p.name}: {clipped} samples at full scale, clipped on export")
rates = {sr for _, sr in stems.values()}; lengths = {len(x) for x, _ in stems.values()}
if len(rates) > 1: problems.append(f"mixed sample rates {sorted(rates)}: re-export everything at the session rate")
if len(lengths) > 1:
    longest = max(len(x) / sr for x, sr in stems.values())
    for name, (x, sr) in stems.items():
        if longest - len(x) / sr > 0.001:
            problems.append(f"{name}: {longest - len(x) / sr:.3f} s shorter than the longest stem")
if mix_path and len(rates) == 1:
    mix, _ = sf.read(mix_path, dtype="float64", always_2d=True)
    total = np.zeros_like(mix)
    for x, sr in stems.values():                     # line every stem up at zero, the way a mixer imports them
        n = min(len(x), len(total)); total[:n, :x.shape[1]] += x[:n]
    a, b = mix.mean(axis=1), total.mean(axis=1)      # lag between the mix and the stems added back up
    n = 1 << int(np.ceil(np.log2(len(a) * 2)))
    xc = np.fft.irfft(np.fft.rfft(a, n) * np.conj(np.fft.rfft(b, n)), n)
    lag = int(np.argmax(xc)); lag = lag - n if lag > n // 2 else lag
    resid = db(np.sqrt(np.mean((mix - total) ** 2)) / np.sqrt(np.mean(mix ** 2)))
    print(f"\nnull vs {pathlib.Path(mix_path).name}: residual {resid:.1f} dB under the mix, best lag {lag} samples")
    if resid > -40: problems.append("stems don't add back up to the mix (bus processing, missing FX return, moved stem?)")
print("\n" + ("\n".join("- " + s for s in problems) if problems else "ok: same rate, same length, nulls against the mix"))

On the clean export:

$ python3 stem_check.py clean mix_clean.wav
stem          rate  format  length s  peak dBFS  first audio
01_drums     48000  PCM_24    13.714       -1.6       0.00 s
02_808       48000  PCM_24    13.714      -11.4       0.00 s
03_vocal     48000  PCM_24    13.714      -16.5       3.46 s
04_keys      48000  PCM_24    13.714      -18.7       0.00 s
05_adlibs    48000  PCM_24    13.714      -20.5       8.15 s

null vs mix_clean.wav: residual -120.4 dB under the mix, best lag 0 samples

ok: same rate, same length, nulls against the mix

On the rushed one. The null test is skipped because the sample rates don’t match, and that alone is a reason to re-export:

$ python3 stem_check.py messy mix_glue.wav
stem          rate  format  length s  peak dBFS  first audio
01_drums     48000  PCM_24    13.714       -1.6       0.00 s
02_808       44100  PCM_24    13.714        0.0       0.00 s
03_vocal     48000  PCM_24    10.256      -16.5       0.00 s
04_keys      48000  PCM_24    13.714      -18.7       0.00 s
05_adlibs    48000  PCM_24    13.714       -inf       silent

- 02_808.wav: 18488 samples at full scale, clipped on export
- 05_adlibs.wav: silent (muted when you exported?)
- mixed sample rates [44100, 48000]: re-export everything at the session rate
- 03_vocal.wav: 3.459 s shorter than the longest stem

And on correct stems sent with a rough that went through the bus glue. Everything looks fine file by file. Only the null test catches it:

$ python3 stem_check.py busonly mix_glue.wav
stem          rate  format  length s  peak dBFS  first audio
01_drums     48000  PCM_24    13.714       -1.6       0.00 s
02_808       48000  PCM_24    13.714      -11.4       0.00 s
03_vocal     48000  PCM_24    13.714      -16.5       3.46 s
04_keys      48000  PCM_24    13.714      -18.7       0.00 s
05_adlibs    48000  PCM_24    13.714      -20.5       8.15 s

null vs mix_glue.wav: residual -9.9 dB under the mix, best lag 0 samples

- stems don't add back up to the mix (bus processing, missing FX return, moved stem?)

Note the vocal’s “first audio” of 3.46 s on the clean export. That’s correct. A stem should carry its silence. A first-audio time of 0.00 on a part you know comes in late is the tell that it was trimmed.

Export settings that save a day

Mono or stereo files is mostly preference. Mono sources can go as mono files; stereo is never wrong. If you don’t know what your mixer likes, ask once and save the answer.

Lost the session and only have the two-track? StemSplit can pull vocals, drums and bass out of the bounce on your own machine, so a mixer can at least hear what they’re working with. Just tell them which stems came from a separator. Separated stems won’t null against the bounce, and they’re a rescue, not a delivery: stems diagnose mixes, they don’t replace them.

Genre notes

How do I export stems for a mixing engineer?
Export every track or group from the same start point to the same end, past the last tail, as WAV at the session sample rate in 24-bit or 32-bit float. Bypass the mix-bus chain, give send effects their own stems, then sum the stems against your rough bounce. If they null, the mixer can rebuild your rough exactly.
Should stems have effects on them?
Print insert effects that are part of the sound, like 808 distortion or vocal tuning, on the stem. Put send effects such as reverb and delay on their own stems so the mixer can rebalance them. Leave the mix-bus chain off, and send a second rough with it on if it’s part of the vibe.
What sample rate and bit depth should stems be?
The session’s own sample rate and 24-bit or 32-bit float WAV. Don’t mix rates in one folder: in our test a 44.1 kHz 808 in a 48 kHz session would play about 8.8% fast and 1.47 semitones sharp if the DAW didn’t convert it. Use float if any stem peaks over 0 dBFS.
How do I check that my stems line up?
Drop them all at zero with your rough bounce, flip the bounce’s polarity and play. Silence means they line up and add back up. In our synthetic session a clean export nulled to −120 dB, while correct stems against a rough with mix-bus glue left a residual only 9.9 dB under the mix.

A free one-pager

The checklist we run before every release.

Loudness, true peak, mono, tails, export settings and the metadata basics, on one page. Leave your email and it opens straight away, or read it here first. No email needed for that.