# Suno Master Overhaul — Three-Phase Pipeline Design

**Date:** 2026-06-11
**Goal:** Professional-level masters from Suno AI source material. Fix the three root failures of the current chain: (1) master-bus-only processing cannot improve the mix, (2) output still "sounds like AI" — band-limited highs (~16 kHz codec roll-off), smeared transients, fake stereo, (3) broken/stub DSP fundamentals (placeholder dereverb/deesser, hard-clip "limiter", static "dynamic" EQ, no oversampling, no lookahead, no true-peak).

**Approach:** Hybrid (Approach 2). demucs for stem separation, pedalboard (JUCE C++) for bread-and-butter dynamics/EQ, custom DSP only where no library does it right.

**Hardware budget:** Intel Ultra 7 255H, 16 threads, 61 GB RAM, CPU-only torch. Quality over time; minutes per track acceptable. No CUDA — neural bandwidth extension (AudioSR) is out; DSP spectral band replication is in.

---

## 1. Architecture Overview

```
input.wav
   │
   ▼
┌──────────────────────────────────────────────────────┐
│ PHASE A — REPAIR & REMIX  (fixes the mix)            │
│  demucs htdemucs_ft → drums / bass / vocals / other  │
│  per-stem repair chains → artifact-gated remix       │
│  HF bandwidth extension (SBR-style) on remix bus     │
└──────────────────────────────────────────────────────┘
   │  remixed pre-master (float64, native SR)
   ▼
┌──────────────────────────────────────────────────────┐
│ PHASE B — MASTER BUS  (rebuilt fundamentals)         │
│  reference match-EQ (linear-phase FIR, capped)       │
│  4-band Linkwitz-Riley multiband compressor          │
│  oversampled glue saturation                         │
│  frequency-dependent stereo width vs reference       │
│  lookahead true-peak limiter (4x OS) + TPDF dither   │
└──────────────────────────────────────────────────────┘
   │  master.wav (24-bit, -1.0 dBTP, genre LUFS)
   ▼
┌──────────────────────────────────────────────────────┐
│ PHASE C — VERIFICATION GATE                          │
│  LUFS-matched metric comparison vs genre reference   │
│  pass/fail per metric + A/B snippet export           │
└──────────────────────────────────────────────────────┘
```

New package: `tools/automaster_app/pipeline/` (orchestrator + phases). Existing `modules/` retained where sound (imaging mono-bass, spectral_smoother) and replaced where broken. Existing `analyzer.py` metrics reused by Phase C; existing `decision.py` genre profiles feed per-stem and master targets.

### New dependencies
| Dep | Purpose | Notes |
|-----|---------|-------|
| `demucs` (htdemucs_ft) | stem separation | torch CPU, segment clamped to model max (7.8s for htdemucs_ft), 16 threads; ~3-6 min/track |
| `pedalboard` | compressors, shelf/peak EQ, gates inside stem chains | C++ (JUCE), fast |
| existing: numpy, scipy, soundfile, pyloudnorm, librosa | core DSP + metrics | unchanged |

No matchering dependency — match-EQ implemented in-house (~100 lines, full control over cap/smoothing).

---

## 2. Phase A — Repair & Remix

### 2.1 `stem_separator.py`
- Wraps demucs python API (not CLI). Model `htdemucs_ft`, 4 stems.
- **Disk cache:** `./tmp/stems/<sha1(file)>/{drums,bass,vocals,other}.wav` — separation runs once per input ever.
- **Artifact gate:** after separation, compute null residual `r = input − Σstems` (time-aligned). If RMS(r) > −15 dBFS relative to input, flag `separation_quality = degraded` (calibrated against measured htdemucs_ft residuals of −21 to −22 dBFS on real program material; the original −40 dBFS draft value was unreachable and forced permanent light mode); pipeline then applies *light mode* (reduced per-stem processing depth, see 2.6).
- Errors (OOM, model load failure) → raise `StemSeparationUnavailable`; orchestrator falls back to Phase B directly on the original mix. Never silently produce garbage.

### 2.2 Per-stem chains (`stem_chains.py`)
All chains parameterized by genre profile + analyzer metrics of the *stem* (analyzer runs per-stem, cheap). pedalboard processors unless noted custom.

**Vocals**
1. De-esser — real one: split-band (5–9 kHz, LR4 crossover) + pedalboard Compressor on the band (ratio 4:1, fast attack), band re-summed. Threshold from stem sibilance_ratio metric.
2. De-reverb — spectral gating: STFT, per-bin noise/tail floor estimated from decaying-tail frames (bottom decile magnitude per bin), soft spectral subtraction capped at −12 dB reduction. Custom (~80 lines). Conservative by default (Suno vocal reverb is baked in; goal = tighten, not remove).
3. Presence: +1.5–3 dB bell at 3–5 kHz (pedalboard PeakFilter), amount from stem spectral slope.
4. Level ride: pedalboard Compressor, 2:1, slow (opto-style), 2–4 dB GR max.

**Drums**
1. Transient shaper — custom rebuild: dual envelope (1 ms / 80 ms) with **lookahead 5 ms** and Hann-smoothed gain curve (no per-sample hard switching → no zipper noise). Attack boost up to +4 dB from punch_factor deficit.
2. Parallel compression: pedalboard Compressor (8:1, fast) mixed 25–40 %.
3. HF exciter for hat/snare sharpness: custom oversampled (4x polyphase) tanh on >6 kHz band, pre+post AA filters. This is the #1 "not sharp" fix at stem level.

**Bass**
1. Mono below 100 Hz (reuse `imaging.make_mono_bass`).
2. Harmonic saturation — custom oversampled tanh, drive from genre profile. Generates 2nd/3rd harmonics so bass reads on laptop speakers.
3. **Phase-locked sub reinforcement** (replaces blind 60 Hz `sub_synth`): pYIN pitch track of bass stem (librosa), synthesize sub one octave below *tracked* fundamental, phase-continuous oscillator, amplitude follows stem envelope, LP at 80 Hz, only active when stem sub-energy deficit detected. Custom.

**Other (synths/guitars)**
1. Resonance smoothing — reuse `spectral_smoother` (keep; it is sound).
2. Harshness control: dynamic band 2–5 kHz — LR4 split + pedalboard Compressor on band (the *true* dynamic EQ primitive; same pattern as de-esser).
3. Stereo decorrelation for fake-stereo detection: if stem L/R correlation > 0.95, apply complementary micro-EQ — alternating ±1.5 dB bells on L/R at interleaved ⅓-octave centers between 1–10 kHz (L boosts where R cuts and vice versa; sums flat in mono). Haas-free, mono-compatible. Custom (~40 lines).

### 2.3 `hf_extension.py` — bandwidth extension (custom, the core "sounds like AI" fix)
SBR-style spectral band replication on the remix bus:
1. STFT (4096, 75 % overlap). Detect roll-off: frequency where smoothed magnitude falls 30 dB below 1–4 kHz mean (Suno typical: 14–16 kHz).
2. If roll-off < 0.85 × Nyquist: copy the octave below roll-off up above it (bin shift), apply −3 dB/octave decaying envelope matched to the spectral slope measured below roll-off, blend 45 % shaped white noise (hiss realism; raised from 30 % alongside a tonal-peak cap — source-band magnitude peaks are limited to 6 dB above the local median before bin-shift, preventing replicated tonal images from registering as whistles), crossfade region ±1 kHz. The decay envelope is enforced monotonically (≥ −3 dB/oct from roll-off) across the entire extended region including the noise component, and extended band magnitude is hard-capped at the slope-predicted level (prevents top-octave overshoot observed at 16–20 kHz).
3. Transient frames (spectral flux top decile) get +3 dB extension level — restores cymbal/consonant sharpness where it matters.
4. Cap: extension never exceeds level the slope predicts; never engages if input already extends past 0.85 × Nyquist (real masters in, no-op out).
ISTFT back. Deterministic, CPU-cheap (<5 s/track).

### 2.4 Remix bus (`remixer.py`)
- Gain trims per stem from genre profile balance targets (e.g., vocal −1 dB vs instrumental for EDM, +1 dB for pop) compared against measured stem LUFS.
- Sum → single pre-master bus. Headroom managed: peak normalize to −6 dBFS before Phase B.

### 2.4a Whistle suppression in chains
Vocal/other chains (de-esser stage) notch narrow resonant peaks (`_notch_whistle_peaks`) so stem processing never raises the master's whistle count above the input's — separation can surface latent resonances in individual stems that the mix masked.

### 2.5 Light mode
When `separation_quality = degraded`: skip de-reverb and decorrelation (artifact amplifiers), halve exciter/saturation amounts, keep transient shaping + EQ + remix trims. HF extension unaffected (runs on remix bus).

### 2.6 Bypass mode
`StemSeparationUnavailable` or user `--no-stems`: Phase B runs on original mix. Pipeline never hard-fails on Phase A.

---

## 3. Phase B — Master Bus

### 3.1 `reference_engine.py` — reference profiles
- User drops commercial reference tracks into `refs/<genre>/*.wav` (gitignored).
- `build_reference_profiles()` precomputes per-genre JSON: smoothed ⅓-octave average spectrum (LUFS-normalized), integrated LUFS, LRA, crest/PLR, per-band stereo width (3 bands), true-peak headroom. Stored `tools/automaster_app/references/<genre>.json`.
- **Shipped defaults:** if no refs provided, fall back to built-in genre target curves derived from current genre library's `eq_emphasis` + standard tilt curves. Reference files always win when present.

### 3.2 `match_eq.py` (custom)
- Target = reference ⅓-octave curve; source = pre-master measured the same way.
- Correction = difference, smoothed (octave-domain Gaussian), **capped ±4 dB**, zeroed below 30 Hz. Above the source's *pre-extension* roll-off frequency, boost is disallowed (cut only) — match-EQ must not amplify the synthesized HF region nor try to "fix" missing highs itself (that is hf_extension's job).
- Applied as linear-phase FIR (4097 taps, windowed-sinc from the correction curve), `scipy.signal.fftconvolve`, latency-compensated.
- Replaces the 7-band heuristic `dynamic_eq` decision block on master bus.

### 3.3 `mb_compressor.py` (custom crossovers + pedalboard per band)
- 4 bands: LR4 complementary crossovers at 120 Hz / 700 Hz / 6 kHz (flat-sum verified by unit test, < ±0.1 dB ripple).
- Per-band pedalboard Compressor; thresholds set so each band gets 1–3 dB GR at program level (auto-calibrated from band RMS); soft-knee, genre-profile ratios. Makeup per band from reference band balance.
- Replaces gain-only `multiband` and the static `dynamic_eq` band logic.

### 3.4 `saturation.py` (custom, shared oversampling core)
- One `oversampled(fn, factor=4)` wrapper: polyphase up (resample_poly) → nonlinearity → polyphase down, pre-emphasis-free, used by all nonlinear stages (master glue, drum exciter, bass saturation).
- Master glue: arctan blend 5–10 %, drive from genre profile. Replaces `vintage`/`harmonics` master-bus duties (modules stay for optional color but route through the oversampling core).

### 3.5 `width.py`
- Frequency-dependent width: 3 bands; compare per-band S/M energy vs reference profile; correct ±50 % max (raised from ±20 %: real-corpus width deviations up to 0.41 vs the ±25 % gate tolerance made the lower cap structurally unable to converge). Mono-bass below genre cutoff retained.
- Replaces Haas widening on master bus (Haas survives only as the stem-level decorrelator alternative — and that one is comb-allpass, not delay).

### 3.6 Residual correction stage
Because mb-compression, saturation, and stem processing re-tilt the spectrum after the initial match-EQ/width passes, residual passes run immediately before the TP limiter: (1) re-measure the bus third-octave spectrum and apply a second match-EQ correction toward the profile (same ±4 dB cap, zero below 30 Hz; above the pre-extension roll-off cuts are allowed — to tame extension excess — but boosts remain disallowed); (2) an iterative width-matching pass (±50 % cap per iteration, up to 3, early-stop when all band deviations < 0.02). Both run BEFORE the limiter: linked limiting applies equal per-sample gain to both channels, so it cannot change S/(M+S) band ratios except by time-reweighting. On hard-limited material that drift reaches ~0.06 (gain reduction lands on mono-heavy kick moments, reweighting time toward S-heavy segments), so width+limit runs as a closed loop (up to 3 rounds): limit, measure post-limiter band-width drift vs the profile target, and if any band exceeds 0.03 (under the 0.05 gate floor), bias the width target the opposite way, redo width on the saved pre-limit signal and re-limit. Measured on psytrance: round 0 high-band drift 0.064 → round 1 −0.012, converged. Post-limiter width was tried and rejected — it raises true peaks, and the required TP re-trim destroys the LUFS/PLR targets. Single-pass corrections measured pre-dynamics were landing 4–9 dB off in individual bands on real material.

### 3.6 `tp_limiter.py` (custom — the centerpiece)
1. Gain stage to target LUFS (measure → gain), genre target from reference profile (e.g., −9 LUFS EDM, −11 pop). **Never the current −8 default for everything.**
2. 4x oversample (polyphase) → true-peak detection per ITU-R BS.1770-4.
3. Lookahead 5 ms: gain curve = running minimum of required gain over lookahead window (sliding-window minimum, O(n)), then Hann-smoothed (attack), program-dependent release (50–200 ms, scaled by crest).
4. Apply gain in oversampled domain, downsample, final safety clamp.
5. Ceiling **−1.0 dBTP**. Re-measure LUFS; one corrective iteration if off by > 0.5 LU.
6. Output: 24-bit TPDF dither (explicit, not soundfile default); optional 16-bit with noise shaping.
- Replaces `apply_limiter` (global gain + np.clip) and `ms_limiter` master duties.

### 3.7 Retired / rerouted modules
| Old module | Fate |
|------------|------|
| `restoration.py` dereverb/deesser stubs | **deleted**; real implementations live in vocal stem chain |
| `dynamics.apply_limiter` | replaced by `tp_limiter` |
| `dynamics.apply_multiband` | replaced by `mb_compressor` |
| `eq.apply_dynamic_eq` | master-bus use replaced by `match_eq` + `mb_compressor`; band primitive (LR4 split + compressor) reused for de-esser/harshness |
| `sub_synth` | replaced by phase-locked version in bass stem chain |
| `exciter`, `harmonics`, `vintage` | kept, but all nonlinearities routed through `saturation.oversampled()` |
| `imaging` mono-bass, `spectral_smoother` | kept as-is |
| `transient.py` | replaced by lookahead smoothed version (drums stem + optional bus) |

---

## 4. Phase C — Verification Gate (`verify.py` + `evaluate_library.py` rework)

Per track, render LUFS-matched (−14 LUFS playback normalization) master vs genre reference, then score:

| Metric | Pass criterion |
|--------|----------------|
| Spectral distance | mean abs ⅓-octave deviation vs reference ≤ 2.5 dB; no single band > 5 dB |
| True peak | ≤ −1.0 dBTP (4x OS measurement) |
| Integrated LUFS | within ±1 LU of genre target |
| PLR / crest | within reference ±2 dB |
| Per-band stereo width | within reference envelope ±25 % |
| HF sharpness | energy 10–16 kHz within 3 dB of reference (proves extension worked) |
| Sibilance ratio | ≤ reference × 1.2 |
| Artifact score | whistle_count not worse than input (pass criterion); separation residual flag recorded (degraded → warning, not fail — light mode is the designed mitigation) |

- Output: per-track pass/fail table + JSON; `evaluate_library.py` batches the whole library, summary CSV.
- A/B export: 20 s loudness-matched snippets (`master_AB.wav` = ref/master interleaved) for ear validation.
- **Gate is the project's definition of done:** a wave is not complete until the test corpus passes.

---

## 5. Orchestration & CLI

- `pipeline/orchestrator.py`: runs A→B→C, per-phase timing, stem cache, light/bypass modes, writes intermediate artifacts to `./tmp/pipeline/<hash>/` for debugging.
- `main.py` gains: `--no-stems`, `--refs <dir>`, `--genre <g>` (existing), `--light`. Existing UI/progress (rich) preserved — orchestrator emits same progress callbacks worker_pro expects.
- Honest labeling: reports say "Rule-based mastering + neural stem separation", not "AI Generated Master".
- **Scope note (orchestrator decision, Wave 5):** the GUI engine (`src/engine/` — `AudioEngine`, `live_processor`) is explicitly OUT of this project's scope and stays on its own legacy chain. This overhaul covers the CLI path (`main.py` / `worker_pro.py` / `autoprocess.py`) only; GUI migration to `pipeline.orchestrator` is a future project.

## 6. Error handling
- Every phase: catch, log, degrade gracefully (A→bypass, B match-EQ→skip if no profile, C always runs).
- DSP invariants asserted in dev: crossover flat-sum, FIR latency compensation exact, oversampling round-trip null < −80 dB, limiter never exceeds ceiling (property test with synthetic worst-case impulse trains).

## 7. Testing
- Unit: each custom DSP block with synthetic signals (sine sweeps, impulses, pink noise): flat-sum, no-op when disabled, ceiling compliance, HF extension no-op on full-bandwidth input.
- Integration: 3-5 track corpus through full pipeline; Phase C gate must pass; runtime budget < 10 min/track.
- Regression: metric snapshots per corpus track; fail on > 1 dB drift.
- Per user global rule (git = runtime only): tests live under `./tmp/tests/` during development, not committed. Verification evidence required before any wave is "done".

## 8. Out of scope (YAGNI)
- GPU/OpenVINO acceleration of demucs (works on CPU; optimize later if needed).
- Neural bandwidth extension (AudioSR) — CPU-infeasible.
- Real-time/preview path for the new pipeline (offline batch first; GUI preview keeps old path until new chain proven).
- Per-stem manual UI controls.

## Architecture Decisions

- **Collapsed:** match-EQ "curve builder" and "FIR designer" — one module (`match_eq.py`); a separate designer had exactly one caller (single-adapter test).
- **Collapsed:** standalone `dither.py` — folded into `tp_limiter.py` output stage; deletion test failed (complexity wouldn't scatter, it's 15 lines with one caller).
- **Kept deep:** `tp_limiter`, `hf_extension`, `stem_separator` (cache+gate+fallback), `reference_engine` — all hide non-trivial internals behind small interfaces; internals replaceable (e.g., demucs → mdx model swap) without touching callers.
- **Kept medium:** `stem_chains` (per-stem recipes; interface = stem in/out, params from profile), `mb_compressor`, `match_eq`, `verify`.
- **Rejected candidate:** merging Phase A and Phase B into one processor class — rejected because bypass mode (no stems) requires Phase B to be independently invokable, and the phases have different failure/fallback semantics.
- **Rejected candidate:** depending on `matchering` package for match-EQ — its EQ and limiter are coupled; we need the EQ alone, and it's ~100 lines in-house.
