Stems That Sum to the Mix — and the Null Test That Proves It

· 4 min read · Clint Johnson

A render pipeline that only outputs a stereo master is a studio with no multitrack tape. The moment you want a human touch on the mix — yours in a DAW, a collaborator’s, a mastering engineer’s — you need stems: one WAV per instrument which, played together at unity gain, reproduce the mix exactly. That last word is where naive exports fail, and there’s a century-old engineering test that catches it: flip the polarity of the mix, play it against the summed stems, and listen for silence. Digitally: subtract and measure. Here’s that null test on a track from my pipeline — fourteen stems against the shipped master:

Null test: mix, sum of stems, and their difference at -87 dBFS

The difference signal is −87 dBFS RMS — about 68 dB below the music, the noise floor of 16-bit quantization itself. The stems don’t approximately recombine; they are the mix. Getting there means moving three things that usually live on the master bus into every stem.

Rule 1: the master chain must distribute over addition

Whatever happens after the sum has to be applied to each stem instead — which is only possible for linear operations. Gains, fades, and mixing (addition) distribute; that’s arithmetic:

buses = {"bass": bass, "drums": drums, "gtr": gtr * 0.85, "pad": pad * 0.5}

# Master fade + normalization, computed ONCE from the summed mix...
mix = sum(buses.values())
env = fade_envelope(mix.shape[1])          # fade-in / fade-out ramps
norm = 0.9 / max(np.abs(mix * env).max(), 1e-9)

# ...then applied identically to every stem. Linear ops distribute:
# sum(stem * env * norm) == (sum(stem)) * env * norm == the master.
for name, bus in buses.items():
    write_wav(f"stems/{name}.wav", bus * env * norm)
write_wav("master.wav", mix * env * norm)

The subtle trap is computing norm per-stem — normalizing each stem to its own peak destroys the balance and nothing nulls. One envelope, one normalization constant, derived from the mix, applied everywhere.

A master-bus compressor or limiter does not distribute — it’s nonlinear, and its gain reduction at any instant depends on the sum, not the parts. If your pipeline has one, you choose: export stems pre-limiter and let whoever mixes re-limit (the professional convention — nobody wants your limiter baked in anyway), or accept stems that null against the pre-limiter mix instead of the shipped master. Say which one you did in the stem folder’s README.

Rule 2: shared effect returns become their own stems

My pipeline glues the band with one convolution reverb fed by every bus at different send levels. That room return belongs to everyone — so it exports as its own stem, exactly like a reverb-return fader on a console:

room_send = (0.7 * drums + 0.55 * gtr + 0.45 * pad + 0.25 * bass)
room = Convolution(ROOM_IR, mix=1.0)(room_send, SR) * room_gain

buses["room_return"] = room          # 14th stem on my postrock track:
                                     # "13 Room Reverb Return.wav"

Folding the room back into each instrument stem proportionally is the tempting shortcut, and it’s wrong twice: the recombination no longer nulls (each stem now carries reverb of the other instruments), and the mixer downstream loses the single most useful fader — how much room the whole band sits in.

Rule 3: assert it, don’t assume it

The null test is four lines. My export path runs it on every render and refuses to ship stems that drift:

resid = master - sum_of_stems
resid_db = 20 * np.log10(max(np.sqrt((resid ** 2).mean()), 1e-12))
assert resid_db < -80, f"stems do not null: residual {resid_db:.1f} dBFS"

Every regression this has caught was a change someone (me) made to the master path and forgot to mirror into the stem path. It’s the audio equivalent of a round-trip serialization test, and it earns its keep the same way.

One honest footnote: export stems as float32 WAVs and they null to −inf; the −87 dBFS floor above is just int16 rounding, present in the master too. 16-bit stems are fine for a human mixing session; use float if a machine consumes them downstream.

The payoff: a one-command DAW session

The reason to want provably-correct stems is what they unlock. On macOS:

open -a GarageBand stems/*.wav

GarageBand builds a session with one track per stem, faders at unity, sounding identical to the shipped master — because the stems null. From there it’s a human mixing session: ride the lead guitar, automate the room return, mute the pads in the bridge. The same folder drags into Logic, Reaper, Ableton, or a collaborator’s inbox.

This is the pipeline’s escape hatch, and it matters philosophically as much as practically. Code-first production isn’t a bet that fader instincts are worthless — it’s a bet that placement, performance, and processing are better expressed as reproducible code, while taste remains interactive. Stems that sum to the mix are the boundary between those worlds, and the null test is what keeps the boundary honest. The pipeline hands the DAW a perfect starting point; the DAW hands back the five percent only ears can do.