I Built a Mastering Engine, Then Built a Second One to Grade It. It Failed My Masters.
·17 min read·music
Share

I Built a Mastering Engine, Then Built a Second One to Grade It. It Failed My Masters.

DAJAI Stewart

A complete signal chain, a PI controller, 36 tracks measured - and the true-peak defect my own QC engine found in every single one. All numbers reproducible from the shipped data.

36 tracks. 72 logged engine runs. 12.21 hours of compute. 26 came out PLATINUM, 8 GOLD, 2 failed outright, and 13 of them scored a perfect 27 of 27 bands.

Every master landed within 0.18 dB of the -14 LUFS streaming target.

And 33 of 36 of them measure above 0 dBTP, which means they are wrong for streaming in a way I did not catch until I wrote a second program whose only job was to distrust the first one.

That last sentence is the article. Everything below it is reproducible from files that ship with this post.

What the engine actually is

It is a Python program that hosts iZotope VST3 plugins through pedalboard and drives them from measurements instead of presets. It runs on a Mac, offloads the plugin work to a Windows box over the LAN, and never calls a language model anywhere in the signal path. Reference selection is a least-distance contour match against 30 pre-measured commercial masters - data, not inference.

The shape of it:

  • Vocal chain, 9 moves. 80 Hz highpass, RX 11 Voice De-noise at 0.30 reduction, RX 11 De-ess, RX 11 Breath Control, Nectar 4 Gate (3:1, 0.5 ms attack, 200 ms release), a 12-band Neutron 5 EQ, Nectar 4 Compressor in Vintage mode (3:1, 5 ms / 120 ms, 75% wet), Nectar 4 tape saturation at 0.18 amount / 50% wet, and Ozone 12 Clarity at 0.35.
  • Beat chain, 3 moves. A 31-band DSP gap EQ derived from the collision analysis, Neutron 5 Transient Shaper, NI Transient Master.
  • Master polish, 5 Ozone modules. Low End Focus, Vintage Tape, 4-band Dynamics, 4-band Exciter, Imager.
  • A gain stage after every single plugin. gain_stage(target=0.90, ceiling=0.95) is not advice in a README, it is a line of code that runs after each module in run_chain(). If you skip it you bake distortion into the next stage and you never find out where it came from.

None of that is unusual. Two things below it are.

The trick that makes plugin automation possible at all

iZotope VST3 parameters silently ignore setattr. You write p.parameters["comp_ratio"] = 3.0, you get no error, and the plugin stays exactly where it was. This is the single reason most headless mastering attempts quietly produce nothing.

The workaround in set_param_smart() is a 50-iteration binary search on the parameter's normalized raw_value in [0,1], reading param.string_value back on every step until the plugin itself reports the requested value:

def find_freq_raw(param, hz):
    lo, hi = 0.0, 1.0
    for _ in range(50):
        mid = (lo + hi) / 2; param.raw_value = mid
        try: val = float(param.string_value)
        except: val = 0
        if val < hz: lo = mid
        else: hi = mid
    param.raw_value = (lo + hi) / 2; return (lo + hi) / 2

set_param_smart() routes by the parameter name's suffix: _enable and _bypass become a raw 1.0 or 0.0, a string value goes to a linear scan for the matching label, _frequency_hz and _q and everything numeric go to their own binary search. You are not setting the parameter. You are negotiating with it and checking what it says back.

Control theory, pointed at a spectrum

The tonal matching is a PI controller. Constants are locked in the source:

KP, KI    = 0.55, 0.12
MAX_ITER  = 30
TOL       = 2.0        # dB, the "within 2 dB" band
SCOREABLE = 27         # ISO bands 20 Hz .. 8 kHz

Each iteration measures the render on 31 ISO third-octave centres, computes the per-band error against the reference contour, and folds that error into a 31-band EQ correction. Integral term clipped to +/-12 dB (anti-windup), per-step move capped at +/-3 dB, accumulated correction capped at +/-18 dB. There is no D term, deliberately: the plant here is a noisy STFT measurement, and a derivative on noise is just an amplifier for noise.

The most important design decision in the whole engine is one line, and it is also the source of the worst behaviour I will show you in a minute:

_off  = float(np.mean((ref_spectrum[:SCOREABLE] - out_spec[:SCOREABLE])[_lm]))
error = (ref_spectrum - _off) - out_spec

The reference is level-matched to the output before the error is computed. Without it the controller spends its whole budget chasing a uniform loudness offset - my target sits at -14 LUFS, the commercial references sit far louder - instead of matching tone. With it, the controller matches shape, and loudness is set separately. That is correct. It also has a consequence nobody wrote down.

The trace: what the controller actually did

Here is a real run, simulation, straight out of recover_protectbug.log. Iteration, bands within 2 dB, RMS band error, and the in-loop loudness:

iter  0   16/27   RMS 4.78   LUFS -14.4
iter  3   23/27   RMS 2.45   LUFS -15.4
iter  7   25/27   RMS 1.54   LUFS -19.8
iter 12   26/27   RMS 1.39   LUFS -24.1   <- best
iter 14   24/27   RMS 1.38   LUFS -25.9
iter 19   23/27   RMS 1.54   LUFS -29.4
iter 28   23/27   RMS 1.85   LUFS -31.4

It hit its best score at iteration 12, then spent the remaining seventeen iterations of its 30-iteration budget getting worse while quietly attenuating the entire record by 17 dB.

That is not a fluke. Across the twelve runs in that log:

  • 10 of 12 runs finished below their own best score.
  • 10 of 12 collapsed the in-loop loudness by more than 6 dB. The worst, ellastical, dropped 18.6 dB.
  • Median run length was 29 iterations against a MAX_ITER of 30 - almost nothing converged and stopped.

The mechanism is exactly the level-matching line above. Because the reference is re-offset to the output on every iteration, a uniform level collapse is invisible to the error signal. The controller has no term that objects to making everything quieter, so integral action is free to walk the whole spectrum down, and it does. The loudness recovery step downstream is capped - the maximizer input-gain sweep only tries range(2, 13) - so once the signal has been attenuated far enough, the sweep pins at its ceiling and stops compensating. Every one of those LUFS numbers above was measured at IG=12, the top of the sweep, for the entire run.

I do not have a shipped fix for this. The honest description is: the loop is stable in shape and unbounded in level, and the only reason it does not ship garbage is the thing in the next section.

Never-regress is doing all the work

Three layers, all in code:

  1. In-loop best-snapshot. The controller keeps the best iteration it has seen - primary key bands-within-2 dB, tiebreak RMS band error - and returns that, not the last one.
  2. Batch-level archive. Any previous master is copied to a timestamped PRE-...wav before a new one is written.
  3. Operator-level refusal. A job already marked done will not re-run without --force.

Given the traces above, layer 1 is not a nicety. In 10 of 12 runs it is the only reason a good render exists at all.

Measured across the whole batch, comparing the pre-recovery QC baseline to the shipped one:

before recoveryshipped
PLATINUM2226
GOLD128
NEEDS WORK22
perfect 27/27813

19 of the 36 tracks changed between the two measurements.

The exception that indicts my own metric

my_heat_goes_boom went from 27/27 bands to 26/27 - a worse headline number - while its RMS band error improved from 0.89 dB to 0.67 dB. The render got tonally closer to the reference and the score went down, because the tiebreak orders band count first and RMS second. just_dippin did the mirror version: it held 27/27 while its RMS drifted from 0.67 to 0.68.

I am not going to pretend that is a rounding artifact. It is a scoring bug in the direction that flatters nobody. It is why the operating doctrine's ninth rule is written the way it is:

DAJAI's ears are the final judge - not the score, not the metrics.

Two graders, one batch, seven disagreements

The engine reports its own score. qc_engine.py reloads the finished WAVs from disk and re-derives everything independently, adding BS.1770 integrated loudness and 4x-oversampled true peak that the engine never measured at all.

They disagree on 7 of 36 tracks, and the reason is structural rather than numerical: the PI loop scores the loud -12 LUFS mixtape render, but the file that ships is the -14 LUFS master. Different file, different measurement, same reported number. On the twelve recovery tracks, the engine's own log matches the independent grader's mixtape score 10 times out of 12 and its master score only 8 times out of 12 - which is how you confirm which render it was actually looking at.

If your engine grades a file you do not ship, your grade is a proxy. Mine was, for months, and I did not know.

The defect

Every peak control in this chain operates on sample peaks. Here is the function that sets the final ceiling:

def normalize_lufs(y, sr, target, tp_db=-1.0):
    cur = measure_lufs(y, sr)
    y = y * (10 ** ((target - cur) / 20.0))
    pk = float(np.max(np.abs(y)) + 1e-20)   # <-- SAMPLE peak
    ceil = 10 ** (tp_db / 20.0)             # <-- named tp_db
    if pk > ceil:
        y = y * (ceil / pk)
    return y.astype(np.float32)

The parameter is called tp_db. It is applied to np.max(np.abs(y)), which is the largest sample value, not the largest value the waveform reaches between samples. Same story one level up: peak_db() is 20*log10(max(abs(y))), the batch gate is TP_MAX = -0.9 compared against that sample-domain number, the shipped stage_meta.json declares the gate as "true peak <= -0.9 dBTP", and the rendered spectrogram PNG prints the same sample-peak figure with a dBTP label burned into the title.

So the batch's own gate passed 36 of 36. The measured reality:

  • Sample peak: exactly -1.00 dBFS on all 36 masters - which is the tell. That is the signature of a scale-to-ceiling operation, not a limiter.
  • True peak, 4x oversampled: mean +1.14 dBTP, max +2.75, min -0.98.
  • 33 of 36 exceed 0 dBTP. All 36 of 36 exceed -1 dBTP.

The overshoot is real, not a measurement artifact. I re-measured three masters at 1x, 2x, 4x and 8x oversampling myself:

TrackSample peak2x4x8x
so_emotional-1.0000+2.7533+2.7543+2.7546
rack_city-1.0000+1.5429+1.5440+1.5442
maybach_truck-1.0000-0.9955-0.9762-0.9532

The two overshooting tracks agree across oversampling factors to within 0.002 dB. Only the one track actually sitting at the ceiling drifts at all (0.04 dB across 2x to 8x), which is the expected behaviour when the true peak is close to the sample-rate-limited estimate.

Spotify's published loudness documentation normalizes to -14 dB LUFS per ITU 1770 and states masters should stay below -1 dB true peak, because inter-sample overshoot turns into audible distortion in the lossy transcode. A master at +2.75 dBTP is not "slightly hot." It is a master that will clip inside somebody else's encoder, on hardware I do not control, after I have stopped looking.

The fix is small and it is not shipped. The Ozone Maximizer's max_output_level is already set to -1.0 dB, so the maximizer is doing its job - but the engine then applies an 18.5 kHz lowpass and re-normalizes after it, outside the maximizer's true-peak protection. The correct shape is: measure with oversampling in peak_db(), apply the ceiling with an oversampled limiter rather than a scalar multiply, and make TP_MAX compare against a number that is actually a true peak. I am publishing the batch before the fix rather than after, because a defect you disclose is worth more than a defect you quietly patch and then describe as a feature.

The one-word bug that cost twelve tracks

-        full = protect(vocals + inst, 0.95)
+        full = R.protect(vocals + inst, 0.95)

A missing module prefix. Twelve tracks died mid-batch.

The recovery was itself engineered rather than improvised: a script took the in-flight run's PID, polled kill -0 every 60 seconds so the two runs would never contend for the single Windows box, then re-fired exactly those twelve slugs and exited clean. Outcome on the twelve: 8 PLATINUM, 4 GOLD, and six of the batch's 13 perfect scores.

That recovery run is also where every trace in this article came from. An independent grader is what turns a crash into data.

All 36 tracks, failures first

Sorted by RMS band error descending, so the two failures are at the top rather than buried. True peaks above 0 dBTP are highlighted - that is 33 of 36 rows.

Fable 5 mastering batch, 36 tracks, sorted by RMS band error descending
TrackGradeBands ±2 dBLUFSTrue peakSample peakRMS err
mount_kushNEEDS WORK18/27-14.03+0.76-1.002.65
sex_appealNEEDS WORK20/27-14.10+0.86-1.002.28
simulationGOLD25/27-14.05+0.81-1.002.01
bouncing_on_my_dickGOLD23/27-14.05+1.41-1.001.37
big_vitoGOLD25/27-14.06+1.30-1.001.29
h0rnyGOLD24/27-14.06+1.59-1.001.28
back_it_upGOLD24/27-14.04-0.38-1.001.12
ellasticalGOLD25/27-14.02+1.12-1.001.08
yes_thats_itPLATINUM26/27-14.17+1.11-1.001.01
buck_emPLATINUM26/27-14.12+1.78-1.000.93
excuse_to_shakePLATINUM26/27-14.03+1.85-1.000.91
maybach_truckPLATINUM26/27-14.00-0.98-1.000.86
richer_than_youPLATINUM26/27-14.08+1.14-1.000.85
call_from_the_bank_pt2PLATINUM26/27-14.02+1.64-1.000.84
bounce_bouncePLATINUM26/27-14.18+0.47-1.000.82
rob_van_damnPLATINUM26/27-14.18+1.57-1.000.81
flashing_lightsGOLD25/27-14.11+0.98-1.000.80
white_girl_rich_dadPLATINUM27/27-14.08-0.25-1.000.78
packing_a_punchPLATINUM27/27-14.02+1.05-1.000.75
still_a_g_thangPLATINUM27/27-14.02+1.40-1.000.73
perfect_timingPLATINUM27/27-14.02+1.47-1.000.72
just_dippinPLATINUM27/27-14.16+1.74-1.000.68
my_heat_goes_boomPLATINUM26/27-14.11+1.81-1.000.67
yacht_off_the_coastPLATINUM27/27-14.15+1.00-1.000.66
iron_my_jeansPLATINUM26/27-14.05+1.49-1.000.64
yacht_to_yachtGOLD25/27-14.05+1.53-1.000.64
never_gone_knowPLATINUM26/27-14.04+0.64-1.000.62
black_n_mildPLATINUM27/27-14.13+0.09-1.000.60
huracanPLATINUM27/27-14.12+0.84-1.000.52
so_emotionalPLATINUM27/27-14.08+2.75-1.000.52
a_miraclePLATINUM27/27-14.05+1.76-1.000.51
shiesty_onPLATINUM26/27-14.06+1.10-1.000.49
spend_datPLATINUM27/27-14.17+1.98-1.000.48
high_fivesPLATINUM27/27-14.12+1.09-1.000.46
pass_that_shitPLATINUM27/27-14.11+1.13-1.000.45
rack_cityPLATINUM26/27-14.12+1.54-1.000.44

Download the raw numbers: fable5-mastering-batch-2026-07-30.csv

The two failures are honest failures. mount_kush landed 18/27 with an RMS band error of 2.65 dB and sex_appeal 20/27 at 2.28 dB. Both improved substantially over their pre-recovery baselines (11/27 and 14/27) and both are still not good enough. They are in the table because leaving them out would make every other number in it meaningless.

What this is and is not

Six things it does that the subscription services do not:

  1. The metric is published, per track, with the failures included.
  2. The chain is readable - every plugin, every parameter, in source.
  3. It is closed-loop against a reference contour, not a feed-forward preset.
  4. The reference is a built composite, not a single borrowed master.
  5. Never-regress means a re-run cannot hand you something worse than what you had.
  6. Nothing leaves the building.

And the counter-column, which is the part that makes the list above trustworthy:

This required a Windows box full of paid iZotope licences, an SSH pipeline between two machines, roughly ten minutes of DSP per pass, 12.21 hours of compute for one 36-track batch, and it shipped 2 outright failures plus 33 masters that are out of spec on true peak. A commercial AI mastering service will hand you a finished master in about three minutes for the price of a sandwich, and it will not overshoot 0 dBTP.

The claim is not that this is better. The claim is that it is measurable, ownable, and mine - and that when it is wrong, I can tell you exactly which line is wrong, which is not a property you get from a service that returns a WAV and no numbers.

Reproduce every number here

cd ~/daj-ai-hub/fable5-triple/vst_rigor_batch

# grade distribution
jq -r '.tracks|to_entries|[.[].value.master.grade]|group_by(.)
       |map("\(.[0])=\(length)")|join(" ")' qc_final_shipped.json

# perfect scores
jq -r '[.tracks[].master.w2|select(.==27)]|length' qc_final_shipped.json

# masters above 0 dBTP
jq -r '[.tracks[].master.tp|select(.>0)]|length' qc_final_shipped.json

# re-grade the WAVs from disk yourself
python3 qc_engine.py my_own_run.json

Related reading on this site: the Fable 5 masters, the mastering service and the chain it runs, the full catalog, the most-played cuts, and what I actually own on the publishing side.

FAQ

What is the difference between sample peak and true peak

Sample peak is the largest value stored in the file. True peak is the largest value the waveform actually reaches once it is reconstructed into a continuous signal, including between the stored samples. A digital-to-analog converter or a lossy encoder sees the reconstructed signal, so a file that reads -1.0 dBFS on sample peak can genuinely reach +2.75 dBTP. Measuring it requires oversampling the audio before taking the maximum - 4x is the common minimum.

Why did the batch pass its own quality gate with 33 defective masters

Because the gate measured the wrong quantity. The constant was named TP_MAX, the shipped metadata described it as a true-peak gate, and the function it compared against returned 20*log10(max(abs(y))) - a sample peak. Every master was scaled to exactly -1.00 dBFS sample peak, so the gate passed 36 of 36 while the real true peak failed 33 of 36. A gate that measures the wrong thing is worse than no gate, because it produces confidence.

What does a PLATINUM grade actually mean here

It means at least 93 percent of the 27 scoreable ISO third-octave bands - so 26 or 27 of them - landed within 2 dB of a level-matched composite reference contour. GOLD is 80 percent, 22 bands. It is a measure of tonal shape against one chosen reference, nothing more. It says nothing about arrangement, performance, or whether the record is any good, and as this batch shows it can move in the opposite direction from the RMS band error on the same file.

Can I use this engine

Not as a product. It is a personal pipeline with hard dependencies on specific paid iZotope VST3 plugins, a second physical machine, and a reference library I built. What is portable is the method: drive plugins by binary-searching normalized parameter values and reading the value back, close the loop with a PI controller against a measured reference instead of a preset, keep the best iteration rather than the last, and grade the file you actually ship with a program that did not produce it.

Stream DAJAI everywhere

Music catalog, smart-links, and the twin — one artist, owned stack.

Related