Chapter 12 — Mix, Master, and the Proof in the Silence

The last chapter of Part II turns renders into records. It covers the final stage of the pipeline — balancing the band, the fade-in and fade-out, setting the overall loudness — and then the machinery this studio treats as a shipping requirement: exporting stems (one audio file per band member) that provably recombine into the finished master, verified by an automatic test on every render. It ends at the border crossing where script-land hands work to the rest of the audio world: a folder of WAVs that opens as a faithful session in any music software on earth.

The mix: five numbers

bass  *= 1.0
drums *= 0.95
gtr   *= 0.85
pad   *= 0.5
dry = bass + drums + gtr + pad
master = dry + room_wet            # the shared room, from Chapter 11

That’s the mix. Five multipliers. It looks impossibly few next to a DAW’s hundred-fader console — until you notice how much mixing already happened upstream, on purpose. Balance within each instrument happened in Chapter 2 (note velocities, per-channel panning). Tone happened in the rigs (Chapter 10 — the compressor is why the bass level stays put; the amp’s input knob is the guitar’s presence). Space happened on the sends (Chapter 11). By the time the buses meet, mixing has been reduced to its irreducible question — how loud is each member? — and five numbers answer it.

This layering is also your debugging map: when the balance feels wrong here, the cause is usually upstream — a rig too dark, a send too wet — and the architecture tells you where to look, instead of offering four hundred equally-plausible knobs.

Convention: the most important member gets 1.0 (here the bass — it’s that kind of record) so every other number reads as a relationship. Drums nearly equal; guitars a step behind; pad at half, as texture. The mix is legible as intent.

Fades and final loudness

n = master.shape[1]                      # total number of samples
env = np.ones(n, dtype=np.float32)      # start with "multiply by 1"
fade_in, fade_out = int(0.05 * SR), int(5.0 * SR)
env[:fade_in]   = np.linspace(0, 1, fade_in)    # 50 ms ramp up
env[-fade_out:] = np.linspace(1, 0, fade_out)   # 5 s ramp down
master *= env

peak = max(np.abs(master).max(), 1e-9)   # the loudest single sample
master = master / peak * 0.9             # scale so the peak sits at 0.9

(np.linspace(a, b, n) makes n evenly spaced values from a to b — our fade ramps.) The 50 ms fade-in guards against a click at the start; the long fade-out is a musical decision that happens to live in the mastering code. The final scaling — peak normalization — sets the loudest sample to 0.9 rather than the maximum 1.0, leaving a little headroom that MP3/AAC encoding will thank you for.

For an album, matching peaks isn’t enough: two songs with identical peaks can feel very different in loudness. The album scripts therefore match average energy (the RMS measure from last chapter), with the peak as a ceiling:

scale = min(0.055 / max(rms(mix), 1e-9),           # loudness target
            0.9   / max(np.abs(mix).max(), 1e-9))  # peak ceiling
mix *= scale

Whichever constraint is tighter wins. A song that would clip at the loudness target simply sits a hair quieter — the right direction to lose.

Stems: files that owe you silence

A stereo master is a studio with no multitrack tape. The moment your record needs other hands — yours in GarageBand, a collaborator’s, a mastering engineer’s — you need stems: one WAV per band member which, played together with all faders at zero adjustment, reproduce the master exactly.

That “exactly” is a testable claim, with a classic test: play the stems against the master with one of them inverted — or digitally, subtract — and listen to what’s left. If the stems are honest, the answer is silence. It’s called the null test, and making it pass requires three rules.

Rule 1: whatever happened to the master must happen to every stem. The fades and the normalization above were applied to the summed master — so apply the identical envelope and the identical scale factor to each stem:

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

mix = sum(buses.values())
env = master_envelope(mix.shape[1])
norm = 0.9 / max(np.abs(mix * env).max(), 1e-9)    # ONE number, from the mix

for name, bus in buses.items():
    write_wav(f"stems/{name}.wav", bus * env * norm)
write_wav("master.wav", mix * env * norm)

Why does this work? Because multiplication distributes over addition — grade-school algebra: (a + b) × n equals a×n + b×n. Each stem carries its share of the master processing, and the sum reassembles perfectly. The classic mistake is normalizing each stem to its own loudest peak — which destroys the balance and the null in one stroke. One envelope, one scale factor, computed from the mix, applied to everything.

(The fine print: this only works for linear operations — gains, fades, addition. A master-bus limiter or compressor is not linear — its behavior at each instant depends on the sum of everything — so it cannot be distributed onto stems. This pipeline deliberately has no master limiter; if yours does, follow the industry convention and export stems from before it, with a note saying so.)

Rule 2: the shared room is its own stem. Notice room_return in the dictionary. The room belongs to everyone, so it ships as its own file — like the reverb channel on a console. The tempting shortcut — folding a share of the room into each instrument’s stem — fails twice: each stem then carries reverb of the other instruments (unmixable), and the person remixing loses the best fader they had: the one that moves the whole band nearer or further.

Rule 3: make the machine check.

resid = master - sum_of_written_stems
resid_db = 20 * np.log10(max(rms(resid), 1e-12))   # convert to decibels
assert resid_db < -80, f"stems do not null: {resid_db:.1f} dB"

This runs on every export. The only bug it can catch is the only bug this system ever has — a change made to the master path and forgotten in the stem path — and it catches it at render time instead of in someone’s session next month.

For calibration: on a real fourteen-stem track from this pipeline, the leftover measures −87 dB relative to full scale — that’s the unavoidable rounding noise of 16-bit audio files themselves, about 68 dB below the music, beneath any possibility of hearing. The stems don’t approximately recombine. They are the mix.

The payoff: a one-command handoff

Why all this rigor? For what it makes possible:

open -a GarageBand stems/*.wav

One command (macOS — or drag the folder into any DAW anywhere), and GarageBand builds a session: one track per stem, and it plays back identical to the shipped master, because the stems null. From there it’s a human mixing session with everything a script can’t do — riding the lead by feel through the bridge, pulling the room down for one verse, a collaborator’s taste layered on yours.

This is Part II’s closing argument, so let me say it plainly. Code-first production is not a claim that fader instincts are obsolete. It’s a claim about which decisions belong to which medium: placement, performance, rigs, and space are better expressed as reproducible, diffable, loop-sweepable code — while final taste is better expressed by ears on faders in real time. Honest stems are the treaty line between those two worlds, and the null test is what keeps the treaty honest. The pipeline hands over a perfect session; what comes back is the five percent only a human can add — and because the handoff is lossless, that five percent cost nothing to acquire.

Practical notes


Part II is done: the band plays (8), like humans (9), through their rigs (10), in one room (11), onto tape anyone can remix (12). Part III makes records with it — an album engine, an honest chapter about AI, and shipping. But before turning the page: change one number in a movements list, render, and listen. That loop is the whole point, and it’s yours now.