Upload sAI.png
#2
by MercilessArtist - opened
- .gitattributes +36 -0
- .github/workflows/sync-to-hf.yml +0 -20
- .gitignore +0 -6
- CHANGELOG.md +0 -287
- PRESETS_IMPROVEMENTS.md +0 -407
- README.md +11 -11
- analysis.py +0 -1194
- app.py +22 -1073
- build_schematic.py +0 -1064
- dsp.py +90 -378
- launch.bat +0 -21
- presets.py +20 -110
- sAI.png +3 -0
- stereo.py +10 -49
- visualization.py +11 -40
.gitattributes
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
sAI.png filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/sync-to-hf.yml
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
name: Sync to Hugging Face Spaces
|
| 2 |
-
on:
|
| 3 |
-
push:
|
| 4 |
-
branches: [main]
|
| 5 |
-
|
| 6 |
-
jobs:
|
| 7 |
-
sync:
|
| 8 |
-
runs-on: ubuntu-latest
|
| 9 |
-
steps:
|
| 10 |
-
- uses: actions/checkout@v4
|
| 11 |
-
with:
|
| 12 |
-
fetch-depth: 0
|
| 13 |
-
lfs: true
|
| 14 |
-
- name: Push to HuggingFace
|
| 15 |
-
env:
|
| 16 |
-
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 17 |
-
run: |
|
| 18 |
-
git remote add hf https://AnimalMonk:$HF_TOKEN@huggingface.co/spaces/AnimalMonk/audio-mastering-suite || true
|
| 19 |
-
git fetch hf || true
|
| 20 |
-
git push hf main:main --force
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
__pycache__/
|
| 2 |
-
*.pyc
|
| 3 |
-
*.wav
|
| 4 |
-
nul
|
| 5 |
-
ANALOG_SCHEMATIC.pdf
|
| 6 |
-
sAI.png
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CHANGELOG.md
CHANGED
|
@@ -1,292 +1,5 @@
|
|
| 1 |
# Changelog — Audio Mastering Suite
|
| 2 |
|
| 3 |
-
## v4.2.1 — 2026-03-20
|
| 4 |
-
|
| 5 |
-
### Super AI — Slider Overflow Fix
|
| 6 |
-
- **Super AI no longer maps parameters back to UI sliders** — The full parametric params (6-band EQ with arbitrary frequencies, per-band compression) exceed the slider ranges (e.g. bass freq slider max 100 Hz, but AI might use 249 Hz). Sliders now stay untouched after Super AI runs. Full settings are displayed in the DSP Settings table and Applied Settings report.
|
| 7 |
-
|
| 8 |
-
### Gemini Reliability
|
| 9 |
-
- **3-retry logic with escalating delays** — On 503 errors, waits 10s, 20s, 30s between retries before failing. Timeout increased to 120s per call.
|
| 10 |
-
- **15-second cooldown between Gemini calls** — Prevents rapid-fire API calls in Auto Master (4 calls) and Super AI (5 calls)
|
| 11 |
-
- **GeminiUnavailableError** — Clear error message shown to user: "This is an issue with Google's AI servers, not with StudioAI." Process aborts without consuming a usage credit.
|
| 12 |
-
- **Usage credits only consumed on success** — `_check_ai_key(consume=True)` only called after all passes complete successfully
|
| 13 |
-
|
| 14 |
-
### Memory Management
|
| 15 |
-
- **gc.collect() at start of each run** — Frees previous run's data before starting a new Auto Master or Super AI. No mid-pipeline garbage collection (was causing issues).
|
| 16 |
-
|
| 17 |
-
### GitHub → HuggingFace Auto-Deploy
|
| 18 |
-
- **GitHub Action workflow** — Every push to `main` auto-syncs to HuggingFace Spaces. Binary files (PDF, PNG) excluded from git and uploaded directly to HF.
|
| 19 |
-
|
| 20 |
-
---
|
| 21 |
-
|
| 22 |
-
## v4.2 — 2026-03-20
|
| 23 |
-
|
| 24 |
-
### Super AI — 6-Band Parametric EQ
|
| 25 |
-
- **Expanded from 4 to 6 parametric EQ bands** — AI now has 6 independently configurable bands (peak/low_shelf/high_shelf with freq, gain, Q). Provides room for both tonal shaping and surgical notch cuts without trade-offs
|
| 26 |
-
- **Updated all prompts, clamp functions, DSP loops, and display reports** to handle bands 5 and 6
|
| 27 |
-
|
| 28 |
-
### Super AI — Bass Protection Prompts
|
| 29 |
-
- **"HEAVY, UNCONSTRAINED BASS" directive** — AI is now explicitly instructed to prioritize additive EQ for bass weight, never over-compress <200 Hz, and default low-band compressor to bypass (ratio 1:1) on low-crest-factor material
|
| 30 |
-
- **Crest factor check between passes** — If crest factor in <200 Hz range decreases between passes, AI must back off low-band compression
|
| 31 |
-
- **Sidechain emulation note** — AI treats low-band compressor as if it has a 100 Hz HPF on its sidechain detector (slow attack / low ratio to preserve kick transients)
|
| 32 |
-
- **HPF frequency cap** — Must stay at or below 25 Hz for bass-heavy material
|
| 33 |
-
- **"LESS IS MORE" philosophy** — AI instructed to use the minimum processing needed, every parameter must have a reason, gentle moves preferred
|
| 34 |
-
|
| 35 |
-
### Super AI — Tighter Parameter Clamps
|
| 36 |
-
- **EQ gain clamped to ±6 dB** (was ±12 dB) — prevents overly aggressive EQ moves
|
| 37 |
-
- **Compression threshold clamped to -20 dB minimum** (was -40 dB) — prevents always-on compression
|
| 38 |
-
- **Compression ratio clamped to 4.0:1 maximum** (was 20.0:1) — prevents brick-wall limiting behavior
|
| 39 |
-
|
| 40 |
-
### DSP Settings Table
|
| 41 |
-
- **DSP settings now displayed side-by-side with Loudness Statistics** — New `dsp_display` Markdown component in a `gr.Row()` shows all applied parameters (HPF, EQ bands, crossovers, per-band compression, stereo width) next to the LUFS/true peak table
|
| 42 |
-
- **Works for all modes** — Manual mastering, Auto Master, and Super AI all populate the DSP table
|
| 43 |
-
|
| 44 |
-
### AI Context Clearing
|
| 45 |
-
- **Analysis history cleared on each Auto Master / Super AI run** — `analysis_history_state` is reset to `[]` on the first yield, preventing previous test runs from influencing new mastering sessions
|
| 46 |
-
|
| 47 |
-
### Unkey API Key Validation
|
| 48 |
-
- **Replaced hardcoded AI_ACCESS_KEY with Unkey API verification** — `_check_ai_key()` now validates against Unkey's v2 API with per-key rate limiting and tier-based master counts
|
| 49 |
-
- **Usage tracking** — Each AI operation decrements the key's `remaining` count; keys with 0 remaining are rejected with a "limit reached" message
|
| 50 |
-
- **Tier metadata** — Keys store tier (starter/pro/studio), email, and Stripe customer ID
|
| 51 |
-
|
| 52 |
-
### Pricing Page (new HF Space)
|
| 53 |
-
- **New `AnimalMonk/studio-ai-pricing` Space** — Pure HTML/FastAPI pricing page with Stripe Payment Links for Starter ($9/10 masters), Pro ($24/30 masters), and Studio ($49/100 masters)
|
| 54 |
-
- **Stripe webhook integration** — `checkout.session.completed` creates Unkey API keys automatically with tier-appropriate rate limits
|
| 55 |
-
- **Success page** — Post-purchase redirect displays the API key with copy button
|
| 56 |
-
- **Key lookup** — Email-based key recovery searches Unkey by email metadata
|
| 57 |
-
- **Resend email integration** — API key delivered via email on purchase (pending domain verification)
|
| 58 |
-
|
| 59 |
-
### Schematic PDF — Pages 4 & 5
|
| 60 |
-
- **Page 4: Super AI 5-Pass Workflow** — Visual diagram of the iterative refinement loop, full parametric parameter table with ranges, fixed vs AI-controlled stages
|
| 61 |
-
- **Page 5: AI Prompting Philosophy** — Mastering philosophy, tonal direction (warm + enhanced bass), true peak guidance (soft goal), compare pass guidelines, final report structure
|
| 62 |
-
- **Version bumped to v4.1** in PDF title
|
| 63 |
-
|
| 64 |
-
---
|
| 65 |
-
|
| 66 |
-
## v3.9 — 2026-03-18
|
| 67 |
-
|
| 68 |
-
### Maximizer Removed
|
| 69 |
-
- **Removed iterative gain + brick-wall limiter loop** — The maximizer was designed to push true peak into the -1.0 to -0.1 dBTP range by iteratively boosting gain, limiting peaks, and re-normalizing. In practice, this created a dynamics death spiral: each iteration crushed crest factor further, producing a flat "sausage" waveform with no dynamics — while still failing to push true peak into range on already-compressed source material
|
| 70 |
-
- **`_maximize()` function deleted** from `dsp.py`, `Limiter` import removed
|
| 71 |
-
- **Removed from both `master_audio()` and `master_audio_full()` pipelines**
|
| 72 |
-
|
| 73 |
-
### Super AI Prompt Updates
|
| 74 |
-
- **True peak is now a soft goal, not a hard constraint** — The -1.0 to -0.1 dBTP range is ideal but the AI is explicitly told NOT to over-compress or crush dynamics to achieve it
|
| 75 |
-
- **Added source material awareness** — Prompts now explain that heavily limited source material (Suno, Udio, AI-generated tracks) has low crest factor, and when normalized to streaming LUFS targets the true peak will naturally land well below -1.0 dBTP — this is correct and expected
|
| 76 |
-
- **Tonal direction added to all Super AI prompts** — Slightly warm overall tone (low-mid richness 200-500 Hz, smooth non-harsh highs) with slightly enhanced bass response
|
| 77 |
-
- **Maximizer references removed** from signal flow documentation in all prompts
|
| 78 |
-
|
| 79 |
-
### Signal Flow (both pipelines)
|
| 80 |
-
```
|
| 81 |
-
Pre-Gain Drop (-18 LUFS) → HPF → EQ → Multiband Compression (3-band) → Stereo Width → LUFS Normalization → Soft Clipper → True Peak Ceiling → Output
|
| 82 |
-
```
|
| 83 |
-
|
| 84 |
-
---
|
| 85 |
-
|
| 86 |
-
## v3.8 — 2026-03-18
|
| 87 |
-
|
| 88 |
-
### Super AI (beta)
|
| 89 |
-
- **New "Super AI (beta)" button** — 5-pass AI mastering with full parametric control. The AI controls every parameter in the signal chain: 4-band parametric EQ (peak/low_shelf/high_shelf with configurable freq, gain, Q), per-band multiband compression (independent threshold, ratio, attack, release), adjustable crossover frequencies, HPF frequency, and stereo width
|
| 90 |
-
- **`master_audio_full()`** — New DSP function accepting granular per-band parameters instead of slider values
|
| 91 |
-
- **5-pass workflow:** (1) AI analyzes raw audio → full settings → master, (2-4) AI compares and makes small incremental adjustments → re-master, (5) Final quality report only
|
| 92 |
-
- **Session-persistent history** — AI maintains full context across all 5 passes to prevent oscillation
|
| 93 |
-
- **Safety clamps** in both `_clamp_super_params()` (analysis) and `master_audio_full()` (DSP) prevent dangerous parameter values
|
| 94 |
-
|
| 95 |
-
### Soft Clipper & True Peak Ceiling
|
| 96 |
-
- **Piecewise tanh soft clipper** added as safety net — knee at 2 dB below -0.1 dBTP ceiling, linear below knee, tanh saturation above
|
| 97 |
-
- **True peak ceiling** at -0.1 dBTP — scales signal down if residual inter-sample peaks exceed ceiling after 4x oversampled measurement (ITU-R BS.1770)
|
| 98 |
-
|
| 99 |
-
---
|
| 100 |
-
|
| 101 |
-
## v3.7 — 2026-03-11
|
| 102 |
-
|
| 103 |
-
### Auto Master
|
| 104 |
-
- **New "Auto Master" button** — One-click iterative AI mastering loop: AI Recommend → Master → AI Compare with revised values → Re-master → AI Compare → Re-master → Final AI Compare report. 3 mastering passes, 4 Gemini API calls, ~2 minutes
|
| 105 |
-
- **`compare_master_structured()`** — New analysis function that returns both a markdown comparison report and structured JSON slider values, enabling the auto-master loop to apply AI-suggested adjustments automatically
|
| 106 |
-
- **`_clamp_settings()` / `_strip_json()` helpers** — Extracted shared utilities from `recommend_settings()` for reuse by the new structured compare function
|
| 107 |
-
|
| 108 |
-
### UI Improvements
|
| 109 |
-
- **AI Compare button hidden until mastered** — Now starts invisible and appears alongside the Download button only after mastering completes
|
| 110 |
-
- **AI comparison report moved above mastered playback** — Report text displays above the audio player for better reading flow
|
| 111 |
-
- **AI Reasoning display moved below upload** — AI Recommend analysis text now appears directly below the file upload block
|
| 112 |
-
- **Browser password autofill disabled** on the AI Access Key field to prevent the password manager from covering the Preset dropdown
|
| 113 |
-
|
| 114 |
-
---
|
| 115 |
-
|
| 116 |
-
## v3.6 — 2026-03-11
|
| 117 |
-
|
| 118 |
-
### Signal Flow Reorder — Compression Before Stereo Width
|
| 119 |
-
- **Multiband compression moved before stereo width** — Industry research (iZotope, Waves, FabFilter, Bob Katz, Berklee) shows ~90% consensus: dynamics processing should see the EQ'd signal without M/S side-channel energy affecting per-band behaviour. Stereo width now operates on the dynamically settled signal for stable, predictable imaging
|
| 120 |
-
- **New signal flow:** `Pre-Gain Drop (-18 LUFS) → HPF 15 Hz → EQ → Multiband Compression (3-band) → Stereo Width → LUFS Normalization → Output`
|
| 121 |
-
- **Old signal flow:** `... → EQ → Stereo Width → Multiband Compression → LUFS Normalization → Output`
|
| 122 |
-
|
| 123 |
-
### Updated Files
|
| 124 |
-
- **`dsp.py`** — Stage 3 = Multiband Compression, Stage 4 = Stereo Width (was reversed)
|
| 125 |
-
- **`analysis.py`** — AI signal flow prompts updated to match new stage order
|
| 126 |
-
- **`build_schematic.py`** — Schematic page layout, gear rack table, and signal flow diagram all updated; version bumped to v3.6
|
| 127 |
-
- **`ANALOG_SCHEMATIC.pdf`** — Regenerated with correct stage ordering
|
| 128 |
-
|
| 129 |
-
### UI Layout Rearrangement
|
| 130 |
-
- **"Apply AI Settings" button moved above "Master It!"** — One-click AI settings are now immediately visible before mastering
|
| 131 |
-
- **"AI Compare Original vs Master" button moved** to the old "Apply AI Settings" location (below sliders, above playback)
|
| 132 |
-
- **AI comparison report text** now displays directly below its trigger button
|
| 133 |
-
|
| 134 |
-
---
|
| 135 |
-
|
| 136 |
-
## v3.5.1 — 2026-03-11
|
| 137 |
-
|
| 138 |
-
### Bug Fix
|
| 139 |
-
- **AI Recommend "markdown string" fix** — Gemini was returning the JSON template placeholder `"markdown string"` literally instead of generating actual analysis. Updated the prompt template with an explicit example and instruction not to return it verbatim
|
| 140 |
-
|
| 141 |
-
### Schematic PDF v3.5
|
| 142 |
-
- **ANALOG_SCHEMATIC.pdf regenerated** with multiband compression architecture:
|
| 143 |
-
- Stage 4 now shows 3-band compressor with crossover sub-box, per-band parameter columns, and color-coded bands
|
| 144 |
-
- Gear rack table updated: 3U SSL G384 / Manley Vari-Mu / Maselec MLA-3 (was 2U single-band)
|
| 145 |
-
- Signal flow diagram updated: "Multiband Comp (3-Band)" replaces "Comp"
|
| 146 |
-
- Total rack space: 9U (was 8U)
|
| 147 |
-
- **`build_schematic.py` included** in repo for reproducible PDF generation
|
| 148 |
-
|
| 149 |
-
---
|
| 150 |
-
|
| 151 |
-
## v3.5 — 2026-03-11
|
| 152 |
-
|
| 153 |
-
### Multiband Compression
|
| 154 |
-
- **3-band multiband compression replaces single-band** — Signal is split at 200 Hz and 4 kHz using Linkwitz-Riley 4th-order crossovers (zero-phase, perfect reconstruction)
|
| 155 |
-
- **Per-band parameter curves** tuned to industry mastering best practices:
|
| 156 |
-
- **Low (< 200 Hz):** Firmest control, slow 80 ms attack (kick punch-through), ratio 1.2:1 → 2.5:1, threshold -16 → -24 dB
|
| 157 |
-
- **Mid (200 Hz – 4 kHz):** Lightest touch (vocal/instrument preservation), 30 ms attack, ratio 1.1:1 → 2.0:1, threshold -14 → -22 dB
|
| 158 |
-
- **High (> 4 kHz):** Barely compresses (harsh transient taming only), fast 10 ms attack, ratio 1.05:1 → 1.5:1, threshold -12 → -18 dB
|
| 159 |
-
- **Same single slider (0-100)** — No UI changes. Same true bypass at slider = 0. Slider drives all three bands in parallel
|
| 160 |
-
- **No makeup gain** — LUFS normalization handles output level (unchanged)
|
| 161 |
-
|
| 162 |
-
### AI Prompt Updates
|
| 163 |
-
- **Signal flow description updated** to reflect multiband architecture with per-band parameter ranges
|
| 164 |
-
- **AI comparison report now shows per-band compression parameters** (low/mid/high threshold, ratio, attack, release)
|
| 165 |
-
- **AI prompts updated** with full signal flow context including crossover frequencies and band-specific behavior
|
| 166 |
-
|
| 167 |
-
### Technical
|
| 168 |
-
- `linkwitz_riley_crossover()` made public in `stereo.py` (was private `_linkwitz_riley_crossover()`)
|
| 169 |
-
- New `map_multiband_compression()` function in `dsp.py`
|
| 170 |
-
- Old `map_compression()` retained as deprecated for backward compatibility
|
| 171 |
-
- No new pip dependencies — uses existing scipy + pedalboard
|
| 172 |
-
|
| 173 |
-
### Updated Signal Flow
|
| 174 |
-
```
|
| 175 |
-
Pre-Gain Drop (-18 LUFS) → HPF 15 Hz → EQ → Stereo Width → Multiband Compression (3-band) → LUFS Normalization → Output
|
| 176 |
-
```
|
| 177 |
-
|
| 178 |
-
---
|
| 179 |
-
|
| 180 |
-
## v3.4 — 2026-03-11
|
| 181 |
-
|
| 182 |
-
### Filter & Compression Fixes
|
| 183 |
-
- **HPF lowered from 20 Hz to 15 Hz** — Old 20 Hz cutoff caused ~0.6 dB loss at 35 Hz. New 15 Hz cutoff is negligible above 35 Hz (-0.1 dB) while still cleaning subsonic rumble
|
| 184 |
-
- **LPF at 20 kHz removed** — Was rolling off -1.2 dB at 15 kHz, fighting the 10 kHz high shelf in the air zone and muddying HF shaping. Source audio is already band-limited by sample rate (Nyquist)
|
| 185 |
-
- **Compressor now bypasses at slider = 0** — Previously ran a 1.1:1 ratio compressor even at zero. Now truly bypasses when compression slider is at 0
|
| 186 |
-
|
| 187 |
-
### Spectrum Visualization Overhaul
|
| 188 |
-
- **Level-aligned spectral comparison** — Mastered spectrum is aligned to the original's average level (100 Hz – 10 kHz passband), so the plot compares spectral *shape* instead of absolute loudness
|
| 189 |
-
- **Processing difference trace** — New green Δ dB subplot shows exactly what the mastering chain changed at each frequency (flat at 0 = no change, negative = cut, positive = boost)
|
| 190 |
-
- **Fixed false sub-bass loss display** — Old plot showed absolute levels, making the uniform loudness difference (e.g. -9 → -14 LUFS) look like disproportionate bass loss
|
| 191 |
-
|
| 192 |
-
### Updated Signal Flow
|
| 193 |
-
```
|
| 194 |
-
Pre-Gain Drop (-18 LUFS) → HPF 15 Hz → EQ → Stereo Width → Compression → LUFS Normalization → Output
|
| 195 |
-
```
|
| 196 |
-
|
| 197 |
-
---
|
| 198 |
-
|
| 199 |
-
## v3.3 — 2026-03-10
|
| 200 |
-
|
| 201 |
-
### AI Analysis (Gemini Pro 3.1)
|
| 202 |
-
- **AI Recommend button** — Analyzes uploaded audio (spectral profile, dynamics, stereo field) and recommends optimal mastering settings via Google Gemini Pro 3.1
|
| 203 |
-
- **Apply AI Settings** — One-click button to populate all 7 sliders with AI-recommended values
|
| 204 |
-
- **Post-master AI report** — After mastering, Gemini compares original vs mastered audio and provides a quality assessment with actionable feedback
|
| 205 |
-
- **Audio feature extraction** — New `analysis.py` module: spectral centroid, spectral rolloff, 6-band energy distribution, crest factor, dynamic range, stereo correlation
|
| 206 |
-
- **Graceful degradation** — If `GOOGLE_API_KEY` is not set, AI features show a helpful message instead of crashing
|
| 207 |
-
|
| 208 |
-
### Premium Access Key
|
| 209 |
-
- **AI features gated behind access key** — Users must enter a valid access key to use AI Recommend and post-master AI report
|
| 210 |
-
- Premium prompt directs users to the StudioAI Discord for access
|
| 211 |
-
- Access key validated against `AI_ACCESS_KEY` HF Space secret
|
| 212 |
-
|
| 213 |
-
### Technical
|
| 214 |
-
- **Gemini REST API** — Uses direct HTTP calls instead of the `google-generativeai` SDK (avoids 200 MB grpcio/protobuf dependency)
|
| 215 |
-
- No new pip dependencies — uses `requests` bundled with Gradio
|
| 216 |
-
|
| 217 |
-
---
|
| 218 |
-
|
| 219 |
-
## v3.2 — 2026-03-04
|
| 220 |
-
|
| 221 |
-
### Genre Expansion
|
| 222 |
-
- **6 new presets added:** Indie / Alt-Rock (Organic & Glued), R&B / Soul (Silky & Smooth), Lo-Fi / Chillhop (Dusty & Narrow), Jazz (Live & Dynamic), Reggae / Dub (Heavy Sound System), Synthwave / Retrowave (Analog & Driving)
|
| 223 |
-
- Total preset count: **14 genres**
|
| 224 |
-
|
| 225 |
-
### Limiter Removed
|
| 226 |
-
- **Removed Pedalboard safety limiter** — JUCE-based Limiter was applying automatic makeup gain, pushing output ~4 dB above the target LUFS (e.g. -10 LUFS when targeting -14). Signal flow now ends cleanly at LUFS normalization.
|
| 227 |
-
- Removed limiter gain reduction from stats display
|
| 228 |
-
|
| 229 |
-
### UI Fixes
|
| 230 |
-
- **Bass frequency slider expanded** — Range changed from 50–60 Hz to **40–100 Hz** to support genres like Reggae (80 Hz bass targeting)
|
| 231 |
-
- **Highs slider label corrected** — Changed from "Highs (6 kHz)" to "Highs (10 kHz)" to match the actual HighShelfFilter frequency
|
| 232 |
-
|
| 233 |
-
### Updated Signal Flow
|
| 234 |
-
```
|
| 235 |
-
Pre-Gain Drop (-18 LUFS) → HPF/LPF → EQ → Stereo Width → Compression → LUFS Normalization → Output
|
| 236 |
-
```
|
| 237 |
-
|
| 238 |
-
---
|
| 239 |
-
|
| 240 |
-
## v3.1 — 2026-03-03
|
| 241 |
-
|
| 242 |
-
### Research-Backed Preset Audit
|
| 243 |
-
All 8 presets audited against industry mastering best practices (iZotope, Waves, Sound On Sound, Mastering The Mix, Sage Audio, Nail The Mix, Attack Magazine, EDMProd, VI-Control).
|
| 244 |
-
|
| 245 |
-
- **Vintage Analog:** bass_freq_hz 50→80 Hz (80 Hz = warmth/body, 50 Hz = sub-rumble)
|
| 246 |
-
- **Hard Rock / Metal:** highs_db 1.0→0.5 (reduce harshness risk in fizz zone), bass_freq_hz 55→60 (kick thump sweet spot)
|
| 247 |
-
- **Acoustic & Vocal:** Renamed "Transparent" → "Clear & Present", mid_boost_db 1.5→1.0 (was most aggressive mid boost of any preset)
|
| 248 |
-
- **Hip-Hop / Boom Bap:** lows_db +0.5→-0.5 (cut low-mids, not boost — boom bap dips at 150 Hz), highs_db 0.0→-0.5 (rolled-off highs for signature lo-fi grit)
|
| 249 |
-
- **Cinematic / Orchestral:** stereo_width 130→110 (research warns against aggressive wideners on orchestral), lows_db 1.0→0.5 (orchestral EQ moves should be 0.25-0.5 dB)
|
| 250 |
-
- **New presets added:** Hip-Hop / Boom Bap (Punch & Grit), Cinematic / Orchestral (Dynamic & Wide)
|
| 251 |
-
|
| 252 |
-
### EQ: High Shelf Raised to 10 kHz
|
| 253 |
-
- **HighShelfFilter moved from 6 kHz to 10 kHz** — Research across all genres identifies 10-15 kHz as the "air" zone. The old 6 kHz shelf targeted upper-midrange presence/brightness instead of true air, and risked adding harshness in the 6-10 kHz fizz zone (especially problematic for metal and orchestral strings).
|
| 254 |
-
|
| 255 |
-
### Stereo Width: Frequency-Selective M/S
|
| 256 |
-
- **Bass below 200 Hz now stays untouched** — Added a Linkwitz-Riley 4th-order crossover at 200 Hz. Width adjustment (M/S encoding) applies only above the crossover. This keeps the low end tight and phase-coherent on club mono-sub systems and small speakers, matching the universal industry recommendation of "mono bass, wide highs."
|
| 257 |
-
|
| 258 |
-
---
|
| 259 |
-
|
| 260 |
-
## v3.0 — 2026-03-03
|
| 261 |
-
|
| 262 |
-
### Gain Staging Overhaul
|
| 263 |
-
- **Pre-gain drop to -18 LUFS** — Hot inputs (e.g. Suno at -9 LUFS) are normalized to -18 LUFS before any processing, preventing EQ boosts from clipping the working audio
|
| 264 |
-
- **Removed makeup gain** — LUFS normalization at the end handles volume; compressor makeup gain was redundant and caused cascading gain issues
|
| 265 |
-
- **Removed auto-gain reduction stage** — No longer needed with the pre-gain architecture
|
| 266 |
-
- **Simplified signal flow:** Pre-Gain Drop → Filters → EQ → Stereo Width → Compression → LUFS Normalize → Safety Limiter
|
| 267 |
-
|
| 268 |
-
### Compression Recalibration
|
| 269 |
-
- **Thresholds recalibrated for -18 LUFS working level** — Range changed from -6→-14 dB to **-14→-22 dB** so compression engages properly at the new internal level
|
| 270 |
-
- **Ratio range tightened** — 1.1:1 → 2.5:1 (was 1:1 → 4:1) for safer mastering glue
|
| 271 |
-
- **Attack fixed at 30 ms** (was 15 ms) to let transients breathe
|
| 272 |
-
- **Release now variable** — 250 ms (less compression) → 100 ms (more compression)
|
| 273 |
-
|
| 274 |
-
### Preset Retuning
|
| 275 |
-
- All 5 original presets retuned with new compression values calibrated for the -18 LUFS working level
|
| 276 |
-
- **New preset: Deep Ambient (Immersive & Sustained)** — Low compression (20), wide stereo (125%), air boost, sub-bass focus
|
| 277 |
-
|
| 278 |
-
### UI & Visualization
|
| 279 |
-
- **Download button moved** — Now appears directly under the Mastered playback box instead of at the bottom
|
| 280 |
-
- **Spectrum plot height** — Matched to waveform plot height (8x4)
|
| 281 |
-
- **Spectrum x-axis labels** — Changed from exponent notation to "10 Hz", "100 Hz", "1 kHz", "10 kHz"
|
| 282 |
-
- **Logo & Discord link** — Added base64-embedded sAI logo with Discord.gg/StudioAI link in header
|
| 283 |
-
|
| 284 |
-
### Statistics
|
| 285 |
-
- Removed auto-gain reduction from stats display (no longer exists)
|
| 286 |
-
- Gain staging table now shows only limiter gain reduction
|
| 287 |
-
|
| 288 |
-
---
|
| 289 |
-
|
| 290 |
## v2.3 — 2026-03-01
|
| 291 |
|
| 292 |
### Cleanup & HF Spaces Deployment
|
|
|
|
| 1 |
# Changelog — Audio Mastering Suite
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## v2.3 — 2026-03-01
|
| 4 |
|
| 5 |
### Cleanup & HF Spaces Deployment
|
PRESETS_IMPROVEMENTS.md
DELETED
|
@@ -1,407 +0,0 @@
|
|
| 1 |
-
# Code Improvement Suggestions for presets.py
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
The `presets.py` file contains audio mastering presets with good documentation but has several opportunities for refactoring, optimization, and better practices.
|
| 5 |
-
|
| 6 |
-
---
|
| 7 |
-
|
| 8 |
-
## 1. **Data Validation & Type Safety**
|
| 9 |
-
|
| 10 |
-
### Issue
|
| 11 |
-
No validation of preset values. Invalid values could cause runtime errors in the audio processing pipeline.
|
| 12 |
-
|
| 13 |
-
### Current Code
|
| 14 |
-
```python
|
| 15 |
-
PRESETS = {
|
| 16 |
-
"Modern Pop (Bright & Wide)": {
|
| 17 |
-
"compression": 80,
|
| 18 |
-
"stereo_width": 115,
|
| 19 |
-
},
|
| 20 |
-
}
|
| 21 |
-
```
|
| 22 |
-
|
| 23 |
-
### Recommendation
|
| 24 |
-
Add a `PresetConfig` dataclass or TypedDict with validation:
|
| 25 |
-
|
| 26 |
-
```python
|
| 27 |
-
from dataclasses import dataclass
|
| 28 |
-
from typing import TypedDict, Optional, Literal
|
| 29 |
-
|
| 30 |
-
class PresetConfig(TypedDict):
|
| 31 |
-
"""Type definition for preset configuration."""
|
| 32 |
-
lows_db: float
|
| 33 |
-
mid_boost_db: float
|
| 34 |
-
highs_db: float
|
| 35 |
-
bass_boost_db: float
|
| 36 |
-
bass_freq_hz: int
|
| 37 |
-
compression: int # 0-100
|
| 38 |
-
stereo_width: int # 0-200
|
| 39 |
-
|
| 40 |
-
@dataclass
|
| 41 |
-
class AudioPreset:
|
| 42 |
-
"""Validated audio preset with constraints."""
|
| 43 |
-
name: str
|
| 44 |
-
config: Optional[PresetConfig]
|
| 45 |
-
|
| 46 |
-
def __post_init__(self):
|
| 47 |
-
if self.config is None:
|
| 48 |
-
return
|
| 49 |
-
|
| 50 |
-
# Validate ranges
|
| 51 |
-
assert 0 <= self.config["compression"] <= 100, "Compression must be 0-100"
|
| 52 |
-
assert 0 <= self.config["stereo_width"] <= 200, "Stereo width must be 0-200"
|
| 53 |
-
assert 20 <= self.config["bass_freq_hz"] <= 200, "Bass freq must be 20-200 Hz"
|
| 54 |
-
assert -12 <= self.config["lows_db"] <= 12, "dB values must be -12 to +12"
|
| 55 |
-
```
|
| 56 |
-
|
| 57 |
-
**Benefits:**
|
| 58 |
-
- Type hints enable IDE autocomplete and catch errors early
|
| 59 |
-
- Runtime validation prevents invalid audio processing
|
| 60 |
-
- Self-documenting code
|
| 61 |
-
|
| 62 |
-
---
|
| 63 |
-
|
| 64 |
-
## 2. **Extract Magic Numbers to Constants**
|
| 65 |
-
|
| 66 |
-
### Issue
|
| 67 |
-
Hardcoded values (compression ranges, frequency limits, dB ranges) scattered throughout.
|
| 68 |
-
|
| 69 |
-
### Current Code
|
| 70 |
-
```python
|
| 71 |
-
"compression": 80, # Ratio ~2.2:1, Rel 130ms
|
| 72 |
-
"stereo_width": 115,
|
| 73 |
-
```
|
| 74 |
-
|
| 75 |
-
### Recommendation
|
| 76 |
-
```python
|
| 77 |
-
# At the top of the file
|
| 78 |
-
class AudioConstraints:
|
| 79 |
-
"""Valid ranges for audio parameters."""
|
| 80 |
-
COMPRESSION_MIN = 0
|
| 81 |
-
COMPRESSION_MAX = 100
|
| 82 |
-
STEREO_WIDTH_MIN = 0
|
| 83 |
-
STEREO_WIDTH_MAX = 200
|
| 84 |
-
BASS_FREQ_MIN_HZ = 20
|
| 85 |
-
BASS_FREQ_MAX_HZ = 200
|
| 86 |
-
DB_MIN = -12
|
| 87 |
-
DB_MAX = 12
|
| 88 |
-
|
| 89 |
-
# Then use in validation
|
| 90 |
-
assert AudioConstraints.COMPRESSION_MIN <= config["compression"] <= AudioConstraints.COMPRESSION_MAX
|
| 91 |
-
```
|
| 92 |
-
|
| 93 |
-
**Benefits:**
|
| 94 |
-
- Single source of truth for constraints
|
| 95 |
-
- Easier to maintain and update limits
|
| 96 |
-
- Self-documenting
|
| 97 |
-
|
| 98 |
-
---
|
| 99 |
-
|
| 100 |
-
## 3. **Separate Metadata from Configuration**
|
| 101 |
-
|
| 102 |
-
### Issue
|
| 103 |
-
Comments explaining compression ratios and release times are mixed with data. This metadata should be structured.
|
| 104 |
-
|
| 105 |
-
### Current Code
|
| 106 |
-
```python
|
| 107 |
-
"compression": 80, # Ratio ~2.2:1, Rel 130ms (Snappy, cohesive glue)
|
| 108 |
-
"stereo_width": 115,
|
| 109 |
-
```
|
| 110 |
-
|
| 111 |
-
### Recommendation
|
| 112 |
-
```python
|
| 113 |
-
PRESETS_METADATA = {
|
| 114 |
-
"Modern Pop (Bright & Wide)": {
|
| 115 |
-
"description": "Bright, punchy, and wide stereo field",
|
| 116 |
-
"use_cases": ["Pop", "Dance", "Electronic"],
|
| 117 |
-
"compression_details": {
|
| 118 |
-
"value": 80,
|
| 119 |
-
"ratio": "~2.2:1",
|
| 120 |
-
"release_ms": 130,
|
| 121 |
-
"character": "Snappy, cohesive glue"
|
| 122 |
-
},
|
| 123 |
-
"stereo_width_details": {
|
| 124 |
-
"value": 115,
|
| 125 |
-
"character": "Keeps the band feeling 'in the room'"
|
| 126 |
-
}
|
| 127 |
-
}
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
PRESETS = {
|
| 131 |
-
"-- None --": None,
|
| 132 |
-
"Modern Pop (Bright & Wide)": {
|
| 133 |
-
"lows_db": 0.0,
|
| 134 |
-
"mid_boost_db": 1.0,
|
| 135 |
-
"highs_db": 1.5,
|
| 136 |
-
"bass_boost_db": 1.0,
|
| 137 |
-
"bass_freq_hz": 60,
|
| 138 |
-
"compression": 80,
|
| 139 |
-
"stereo_width": 115,
|
| 140 |
-
},
|
| 141 |
-
}
|
| 142 |
-
```
|
| 143 |
-
|
| 144 |
-
**Benefits:**
|
| 145 |
-
- Cleaner data structure
|
| 146 |
-
- Metadata can be used for UI tooltips/help
|
| 147 |
-
- Easier to maintain documentation separately
|
| 148 |
-
|
| 149 |
-
---
|
| 150 |
-
|
| 151 |
-
## 4. **Create a Preset Factory/Builder Pattern**
|
| 152 |
-
|
| 153 |
-
### Issue
|
| 154 |
-
Repetitive dictionary creation with potential for inconsistency.
|
| 155 |
-
|
| 156 |
-
### Recommendation
|
| 157 |
-
```python
|
| 158 |
-
def create_preset(
|
| 159 |
-
lows_db: float,
|
| 160 |
-
mid_boost_db: float,
|
| 161 |
-
highs_db: float,
|
| 162 |
-
bass_boost_db: float,
|
| 163 |
-
bass_freq_hz: int,
|
| 164 |
-
compression: int,
|
| 165 |
-
stereo_width: int,
|
| 166 |
-
) -> PresetConfig:
|
| 167 |
-
"""Factory function to create and validate presets."""
|
| 168 |
-
config: PresetConfig = {
|
| 169 |
-
"lows_db": lows_db,
|
| 170 |
-
"mid_boost_db": mid_boost_db,
|
| 171 |
-
"highs_db": highs_db,
|
| 172 |
-
"bass_boost_db": bass_boost_db,
|
| 173 |
-
"bass_freq_hz": bass_freq_hz,
|
| 174 |
-
"compression": compression,
|
| 175 |
-
"stereo_width": stereo_width,
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
# Validation happens here
|
| 179 |
-
AudioPreset("temp", config) # Triggers __post_init__ validation
|
| 180 |
-
return config
|
| 181 |
-
|
| 182 |
-
PRESETS = {
|
| 183 |
-
"-- None --": None,
|
| 184 |
-
"Modern Pop (Bright & Wide)": create_preset(
|
| 185 |
-
lows_db=0.0,
|
| 186 |
-
mid_boost_db=1.0,
|
| 187 |
-
highs_db=1.5,
|
| 188 |
-
bass_boost_db=1.0,
|
| 189 |
-
bass_freq_hz=60,
|
| 190 |
-
compression=80,
|
| 191 |
-
stereo_width=115,
|
| 192 |
-
),
|
| 193 |
-
}
|
| 194 |
-
```
|
| 195 |
-
|
| 196 |
-
**Benefits:**
|
| 197 |
-
- Automatic validation on creation
|
| 198 |
-
- Consistent structure
|
| 199 |
-
- Easier to refactor later
|
| 200 |
-
|
| 201 |
-
---
|
| 202 |
-
|
| 203 |
-
## 5. **Add Preset Lookup & Utility Functions**
|
| 204 |
-
|
| 205 |
-
### Issue
|
| 206 |
-
No helper functions to work with presets programmatically.
|
| 207 |
-
|
| 208 |
-
### Recommendation
|
| 209 |
-
```python
|
| 210 |
-
def get_preset(name: str) -> Optional[PresetConfig]:
|
| 211 |
-
"""Retrieve a preset by name."""
|
| 212 |
-
return PRESETS.get(name)
|
| 213 |
-
|
| 214 |
-
def list_presets() -> list[str]:
|
| 215 |
-
"""Get all available preset names."""
|
| 216 |
-
return [name for name in PRESETS.keys() if name != "-- None --"]
|
| 217 |
-
|
| 218 |
-
def get_preset_info(name: str) -> dict:
|
| 219 |
-
"""Get preset with metadata."""
|
| 220 |
-
if name not in PRESETS:
|
| 221 |
-
raise ValueError(f"Preset '{name}' not found")
|
| 222 |
-
|
| 223 |
-
return {
|
| 224 |
-
"name": name,
|
| 225 |
-
"config": PRESETS[name],
|
| 226 |
-
"metadata": PRESETS_METADATA.get(name, {})
|
| 227 |
-
}
|
| 228 |
-
```
|
| 229 |
-
|
| 230 |
-
**Benefits:**
|
| 231 |
-
- Encapsulation
|
| 232 |
-
- Easier to test
|
| 233 |
-
- Prevents direct dictionary access errors
|
| 234 |
-
|
| 235 |
-
---
|
| 236 |
-
|
| 237 |
-
## 6. **Add Documentation & Docstrings**
|
| 238 |
-
|
| 239 |
-
### Issue
|
| 240 |
-
Module-level documentation is minimal.
|
| 241 |
-
|
| 242 |
-
### Recommendation
|
| 243 |
-
```python
|
| 244 |
-
"""Preset definitions for the audio mastering suite.
|
| 245 |
-
|
| 246 |
-
This module contains predefined audio mastering configurations optimized for
|
| 247 |
-
different music genres and production styles. Each preset includes:
|
| 248 |
-
- EQ settings (lows, mids, highs)
|
| 249 |
-
- Bass boost parameters
|
| 250 |
-
- Compression settings
|
| 251 |
-
- Stereo width
|
| 252 |
-
|
| 253 |
-
Presets are validated on load to ensure all values are within acceptable ranges.
|
| 254 |
-
|
| 255 |
-
Example:
|
| 256 |
-
>>> preset = get_preset("Modern Pop (Bright & Wide)")
|
| 257 |
-
>>> if preset:
|
| 258 |
-
... apply_mastering(audio, preset)
|
| 259 |
-
"""
|
| 260 |
-
```
|
| 261 |
-
|
| 262 |
-
---
|
| 263 |
-
|
| 264 |
-
## 7. **Consider Configuration File Format**
|
| 265 |
-
|
| 266 |
-
### Issue
|
| 267 |
-
Hardcoding presets in Python limits flexibility.
|
| 268 |
-
|
| 269 |
-
### Recommendation
|
| 270 |
-
Consider supporting JSON/YAML for easier editing:
|
| 271 |
-
|
| 272 |
-
```python
|
| 273 |
-
import json
|
| 274 |
-
from pathlib import Path
|
| 275 |
-
|
| 276 |
-
def load_presets_from_file(filepath: Path) -> dict:
|
| 277 |
-
"""Load presets from JSON file."""
|
| 278 |
-
with open(filepath) as f:
|
| 279 |
-
return json.load(f)
|
| 280 |
-
|
| 281 |
-
# presets.json
|
| 282 |
-
{
|
| 283 |
-
"Modern Pop (Bright & Wide)": {
|
| 284 |
-
"lows_db": 0.0,
|
| 285 |
-
"mid_boost_db": 1.0,
|
| 286 |
-
...
|
| 287 |
-
}
|
| 288 |
-
}
|
| 289 |
-
```
|
| 290 |
-
|
| 291 |
-
**Benefits:**
|
| 292 |
-
- Users can create custom presets without code changes
|
| 293 |
-
- Easier to share presets
|
| 294 |
-
- Decouples data from code
|
| 295 |
-
|
| 296 |
-
---
|
| 297 |
-
|
| 298 |
-
## 8. **Add Preset Comparison & Diff Utilities**
|
| 299 |
-
|
| 300 |
-
### Issue
|
| 301 |
-
No way to compare presets or understand differences.
|
| 302 |
-
|
| 303 |
-
### Recommendation
|
| 304 |
-
```python
|
| 305 |
-
def compare_presets(name1: str, name2: str) -> dict:
|
| 306 |
-
"""Compare two presets and show differences."""
|
| 307 |
-
p1 = get_preset(name1)
|
| 308 |
-
p2 = get_preset(name2)
|
| 309 |
-
|
| 310 |
-
if not p1 or not p2:
|
| 311 |
-
raise ValueError("One or both presets not found")
|
| 312 |
-
|
| 313 |
-
differences = {}
|
| 314 |
-
for key in p1.keys():
|
| 315 |
-
if p1[key] != p2[key]:
|
| 316 |
-
differences[key] = {
|
| 317 |
-
"preset1": p1[key],
|
| 318 |
-
"preset2": p2[key],
|
| 319 |
-
"delta": p1[key] - p2[key]
|
| 320 |
-
}
|
| 321 |
-
|
| 322 |
-
return differences
|
| 323 |
-
```
|
| 324 |
-
|
| 325 |
-
---
|
| 326 |
-
|
| 327 |
-
## 9. **Add Preset Versioning & Changelog**
|
| 328 |
-
|
| 329 |
-
### Issue
|
| 330 |
-
No way to track preset changes over time.
|
| 331 |
-
|
| 332 |
-
### Recommendation
|
| 333 |
-
```python
|
| 334 |
-
PRESET_VERSION = "1.0.0"
|
| 335 |
-
|
| 336 |
-
PRESET_CHANGELOG = {
|
| 337 |
-
"1.0.0": "Initial release with 8 genre presets",
|
| 338 |
-
"1.1.0": "Adjusted compression ratios for better transparency",
|
| 339 |
-
}
|
| 340 |
-
```
|
| 341 |
-
|
| 342 |
-
---
|
| 343 |
-
|
| 344 |
-
## 10. **Specific Improvement for the Snippet**
|
| 345 |
-
|
| 346 |
-
The specific code snippet you provided:
|
| 347 |
-
```python
|
| 348 |
-
"compression": 45, # Moderate glue. Holds the band together without crushing dynamics.
|
| 349 |
-
"stereo_width": 105, # Keeps the band feeling "in the room"
|
| 350 |
-
```
|
| 351 |
-
|
| 352 |
-
### Improvements:
|
| 353 |
-
|
| 354 |
-
1. **Extract comment metadata:**
|
| 355 |
-
```python
|
| 356 |
-
"compression": 45,
|
| 357 |
-
"stereo_width": 105,
|
| 358 |
-
```
|
| 359 |
-
With metadata stored separately:
|
| 360 |
-
```python
|
| 361 |
-
"compression_details": {
|
| 362 |
-
"value": 45,
|
| 363 |
-
"character": "Moderate glue. Holds the band together without crushing dynamics."
|
| 364 |
-
}
|
| 365 |
-
```
|
| 366 |
-
|
| 367 |
-
2. **Add inline type hints in docstring:**
|
| 368 |
-
```python
|
| 369 |
-
"""
|
| 370 |
-
Preset configuration with validated audio parameters.
|
| 371 |
-
|
| 372 |
-
compression (int): 0-100, where higher = more aggressive glue
|
| 373 |
-
stereo_width (int): 0-200, where 100 = mono, 200 = maximum width
|
| 374 |
-
"""
|
| 375 |
-
```
|
| 376 |
-
|
| 377 |
-
3. **Consider semantic naming:**
|
| 378 |
-
```python
|
| 379 |
-
# Instead of generic "compression", consider:
|
| 380 |
-
"compression_amount": 45, # More explicit
|
| 381 |
-
"stereo_width_percent": 105, # Clearer units
|
| 382 |
-
```
|
| 383 |
-
|
| 384 |
-
---
|
| 385 |
-
|
| 386 |
-
## Summary of Recommendations (Priority Order)
|
| 387 |
-
|
| 388 |
-
| Priority | Recommendation | Impact |
|
| 389 |
-
|----------|---|---|
|
| 390 |
-
| **High** | Add type hints & validation | Prevents runtime errors |
|
| 391 |
-
| **High** | Extract magic numbers to constants | Maintainability |
|
| 392 |
-
| **Medium** | Add utility functions | Usability |
|
| 393 |
-
| **Medium** | Separate metadata from config | Code clarity |
|
| 394 |
-
| **Low** | Support external config files | Flexibility |
|
| 395 |
-
| **Low** | Add comparison utilities | Developer experience |
|
| 396 |
-
|
| 397 |
-
---
|
| 398 |
-
|
| 399 |
-
## Implementation Roadmap
|
| 400 |
-
|
| 401 |
-
1. **Phase 1:** Add TypedDict/dataclass with validation
|
| 402 |
-
2. **Phase 2:** Extract constants and add utility functions
|
| 403 |
-
3. **Phase 3:** Separate metadata into dedicated structure
|
| 404 |
-
4. **Phase 4:** Add comprehensive docstrings
|
| 405 |
-
5. **Phase 5:** Consider external config file support
|
| 406 |
-
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -26,25 +26,25 @@ A web-based audio mastering tool built with Gradio and Spotify's Pedalboard DSP
|
|
| 26 |
|
| 27 |
- **3-Band EQ** - Lows (200 Hz shelf), Mids (1.2 kHz peak), Highs (6 kHz shelf), each +/-3 dB
|
| 28 |
- **Bass Boost** - Sub-bass enhancement (0-3 dB) with adjustable frequency (50-60 Hz)
|
| 29 |
-
- **Compression** -
|
| 30 |
-
- **Limiter** -
|
| 31 |
- **Stereo Width** - M/S matrix stereo image control (80%-150%)
|
| 32 |
-
- **LUFS Targeting** -
|
| 33 |
-
- **
|
| 34 |
-
- **Genre Presets** - Modern Pop, Vintage Analog, Heavy Trap/EDM, Hard Rock/Metal, Acoustic & Vocal, Deep Ambient
|
| 35 |
- **Visualization** - Before/after waveform and spectrum comparison
|
| 36 |
-
- **Statistics** - Original vs mastered LUFS, true peak,
|
| 37 |
|
| 38 |
## Signal Flow
|
| 39 |
|
| 40 |
```
|
| 41 |
-
|
| 42 |
-
-> HPF 20 Hz + LPF 20 kHz (12 dB/oct)
|
| 43 |
-> EQ (Bass Boost, Lows, Highs, Mids)
|
| 44 |
-> Stereo Width (M/S processing)
|
| 45 |
-
-> Compression
|
| 46 |
-
->
|
| 47 |
-
->
|
|
|
|
|
|
|
| 48 |
```
|
| 49 |
|
| 50 |
## Usage
|
|
|
|
| 26 |
|
| 27 |
- **3-Band EQ** - Lows (200 Hz shelf), Mids (1.2 kHz peak), Highs (6 kHz shelf), each +/-3 dB
|
| 28 |
- **Bass Boost** - Sub-bass enhancement (0-3 dB) with adjustable frequency (50-60 Hz)
|
| 29 |
+
- **Compression** - Variable ratio (1:1 to 4:1) and threshold (-12 to -18 dB)
|
| 30 |
+
- **Limiter** - Brick-wall at -1 dB with auto-gain reduction to keep limiter work under 4 dB
|
| 31 |
- **Stereo Width** - M/S matrix stereo image control (80%-150%)
|
| 32 |
+
- **LUFS Targeting** - Strict post-chain normalization to streaming (-14) or CD (-11) standards
|
| 33 |
+
- **Genre Presets** - Modern Pop, Vintage Analog, Heavy Trap/EDM, Hard Rock/Metal, Acoustic & Vocal
|
|
|
|
| 34 |
- **Visualization** - Before/after waveform and spectrum comparison
|
| 35 |
+
- **Statistics** - Original vs mastered LUFS, true peak, gain staging metrics
|
| 36 |
|
| 37 |
## Signal Flow
|
| 38 |
|
| 39 |
```
|
| 40 |
+
HPF 20 Hz + LPF 20 kHz (12 dB/oct)
|
|
|
|
| 41 |
-> EQ (Bass Boost, Lows, Highs, Mids)
|
| 42 |
-> Stereo Width (M/S processing)
|
| 43 |
+
-> Compression + Makeup Gain
|
| 44 |
+
-> Auto Gain Reduction (caps limiter at 4 dB)
|
| 45 |
+
-> Limiter (-1 dB ceiling)
|
| 46 |
+
-> LUFS Normalization (pyloudnorm)
|
| 47 |
+
-> Safety Limiter (if true peaks exceed ceiling)
|
| 48 |
```
|
| 49 |
|
| 50 |
## Usage
|
analysis.py
DELETED
|
@@ -1,1194 +0,0 @@
|
|
| 1 |
-
# Build: 2026-03-21T15:25:58.145163+00:00
|
| 2 |
-
"""AI-powered audio analysis using Gemini Pro — feature extraction and recommendations."""
|
| 3 |
-
|
| 4 |
-
import json
|
| 5 |
-
import os
|
| 6 |
-
import numpy as np
|
| 7 |
-
from scipy.signal import welch
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
# ---------------------------------------------------------------------------
|
| 11 |
-
# Audio feature extraction
|
| 12 |
-
# ---------------------------------------------------------------------------
|
| 13 |
-
|
| 14 |
-
_BANDS = [
|
| 15 |
-
("Sub-bass", 20, 60),
|
| 16 |
-
("Bass", 60, 250),
|
| 17 |
-
("Low-Mids", 250, 500),
|
| 18 |
-
("Mids", 500, 2000),
|
| 19 |
-
("Upper-Mids", 2000, 6000),
|
| 20 |
-
("Highs", 6000, 20000),
|
| 21 |
-
]
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def extract_features(audio, sample_rate):
|
| 25 |
-
"""Extract audio features for AI analysis (basic — used by Auto Master).
|
| 26 |
-
|
| 27 |
-
Args:
|
| 28 |
-
audio: numpy array, shape (samples,) or (samples, channels).
|
| 29 |
-
sample_rate: int.
|
| 30 |
-
|
| 31 |
-
Returns:
|
| 32 |
-
dict with spectral, dynamic, and stereo measurements.
|
| 33 |
-
"""
|
| 34 |
-
# Convert to mono for spectral analysis
|
| 35 |
-
if audio.ndim == 2:
|
| 36 |
-
mono = audio.mean(axis=1)
|
| 37 |
-
else:
|
| 38 |
-
mono = audio
|
| 39 |
-
|
| 40 |
-
# --- Spectral analysis via Welch ---
|
| 41 |
-
nperseg = min(8192, len(mono))
|
| 42 |
-
freqs, psd = welch(mono, fs=sample_rate, nperseg=nperseg)
|
| 43 |
-
|
| 44 |
-
# Spectral centroid
|
| 45 |
-
total_energy = np.sum(psd)
|
| 46 |
-
if total_energy > 0:
|
| 47 |
-
spectral_centroid = float(np.sum(freqs * psd) / total_energy)
|
| 48 |
-
else:
|
| 49 |
-
spectral_centroid = 0.0
|
| 50 |
-
|
| 51 |
-
# Spectral rolloff (85%)
|
| 52 |
-
cumulative = np.cumsum(psd)
|
| 53 |
-
if total_energy > 0:
|
| 54 |
-
rolloff_idx = np.searchsorted(cumulative, 0.85 * total_energy)
|
| 55 |
-
spectral_rolloff = float(freqs[min(rolloff_idx, len(freqs) - 1)])
|
| 56 |
-
else:
|
| 57 |
-
spectral_rolloff = 0.0
|
| 58 |
-
|
| 59 |
-
# Band energy distribution (dB) — use float() to avoid numpy float32
|
| 60 |
-
band_energy = {}
|
| 61 |
-
for name, lo, hi in _BANDS:
|
| 62 |
-
mask = (freqs >= lo) & (freqs < hi)
|
| 63 |
-
band_rms = float(np.sqrt(np.mean(psd[mask]))) if np.any(mask) else 0.0
|
| 64 |
-
if band_rms > 0:
|
| 65 |
-
band_energy[name] = round(20.0 * np.log10(band_rms), 1)
|
| 66 |
-
else:
|
| 67 |
-
band_energy[name] = -100.0
|
| 68 |
-
|
| 69 |
-
# --- Dynamics (cast to Python float for JSON serialization) ---
|
| 70 |
-
rms = float(np.sqrt(np.mean(mono ** 2)))
|
| 71 |
-
peak = float(np.max(np.abs(mono)))
|
| 72 |
-
|
| 73 |
-
rms_db = round(20.0 * np.log10(rms), 1) if rms > 0 else -100.0
|
| 74 |
-
peak_db = round(20.0 * np.log10(peak), 1) if peak > 0 else -100.0
|
| 75 |
-
crest_factor = round(peak_db - rms_db, 1)
|
| 76 |
-
dynamic_range = crest_factor # simplified: same as crest factor for full-file
|
| 77 |
-
|
| 78 |
-
# --- Stereo correlation ---
|
| 79 |
-
is_mono = audio.ndim == 1 or audio.shape[1] == 1
|
| 80 |
-
if not is_mono:
|
| 81 |
-
left = audio[:, 0]
|
| 82 |
-
right = audio[:, 1]
|
| 83 |
-
correlation = np.corrcoef(left, right)[0, 1]
|
| 84 |
-
stereo_correlation = round(float(correlation), 3)
|
| 85 |
-
else:
|
| 86 |
-
stereo_correlation = None
|
| 87 |
-
|
| 88 |
-
# --- Loudness (reuse existing functions, lazy import) ---
|
| 89 |
-
from loudness import measure_loudness, measure_true_peak
|
| 90 |
-
lufs = measure_loudness(audio, sample_rate)
|
| 91 |
-
true_peak = measure_true_peak(audio, sample_rate)
|
| 92 |
-
|
| 93 |
-
return {
|
| 94 |
-
"spectral_centroid_hz": round(float(spectral_centroid), 1),
|
| 95 |
-
"spectral_rolloff_hz": round(float(spectral_rolloff), 1),
|
| 96 |
-
"band_energy": band_energy,
|
| 97 |
-
"rms_db": float(rms_db),
|
| 98 |
-
"peak_db": float(peak_db),
|
| 99 |
-
"crest_factor_db": float(crest_factor),
|
| 100 |
-
"dynamic_range_db": float(dynamic_range),
|
| 101 |
-
"stereo_correlation": stereo_correlation,
|
| 102 |
-
"lufs": round(float(lufs), 1) if not np.isinf(lufs) else -100.0,
|
| 103 |
-
"true_peak_dbtp": float(true_peak),
|
| 104 |
-
"is_mono": is_mono,
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# ---------------------------------------------------------------------------
|
| 109 |
-
# Detailed feature extraction — Super AI mode
|
| 110 |
-
# ---------------------------------------------------------------------------
|
| 111 |
-
|
| 112 |
-
# 24 analysis bands for fine-grained spectral view
|
| 113 |
-
_DETAIL_BANDS = [
|
| 114 |
-
("20-40", 20, 40),
|
| 115 |
-
("40-60", 40, 60),
|
| 116 |
-
("60-100", 60, 100),
|
| 117 |
-
("100-150", 100, 150),
|
| 118 |
-
("150-200", 150, 200),
|
| 119 |
-
("200-300", 200, 300),
|
| 120 |
-
("300-400", 300, 400),
|
| 121 |
-
("400-600", 400, 600),
|
| 122 |
-
("600-800", 600, 800),
|
| 123 |
-
("800-1k", 800, 1000),
|
| 124 |
-
("1k-1.5k", 1000, 1500),
|
| 125 |
-
("1.5k-2k", 1500, 2000),
|
| 126 |
-
("2k-3k", 2000, 3000),
|
| 127 |
-
("3k-4k", 3000, 4000),
|
| 128 |
-
("4k-5k", 4000, 5000),
|
| 129 |
-
("5k-6k", 5000, 6000),
|
| 130 |
-
("6k-8k", 6000, 8000),
|
| 131 |
-
("8k-10k", 8000, 10000),
|
| 132 |
-
("10k-12k", 10000, 12000),
|
| 133 |
-
("12k-16k", 12000, 16000),
|
| 134 |
-
("16k-20k", 16000, 20000),
|
| 135 |
-
]
|
| 136 |
-
|
| 137 |
-
# 3 compression bands matching the DSP crossover defaults
|
| 138 |
-
_COMP_BANDS = [
|
| 139 |
-
("low", 20, 200),
|
| 140 |
-
("mid", 200, 4000),
|
| 141 |
-
("high", 4000, 20000),
|
| 142 |
-
]
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
def extract_features_detailed(audio, sample_rate):
|
| 146 |
-
"""Extract rich spectral + dynamic features for Super AI mode.
|
| 147 |
-
|
| 148 |
-
Builds on extract_features() and adds:
|
| 149 |
-
- 24-band spectral profile (fine-grained EQ map)
|
| 150 |
-
- Spectral peak/resonance detection (top problematic frequencies)
|
| 151 |
-
- Per-compression-band dynamics (RMS, peak, crest factor)
|
| 152 |
-
- Spectral flatness (tonal vs noisy character)
|
| 153 |
-
- Spectral tilt (bass-heavy vs bright)
|
| 154 |
-
- Short-time dynamic variation (verse vs chorus energy)
|
| 155 |
-
- Per-band stereo correlation
|
| 156 |
-
|
| 157 |
-
All numpy — runs in milliseconds on CPU.
|
| 158 |
-
"""
|
| 159 |
-
from scipy.signal import find_peaks
|
| 160 |
-
|
| 161 |
-
base = extract_features(audio, sample_rate)
|
| 162 |
-
|
| 163 |
-
if audio.ndim == 2:
|
| 164 |
-
mono = audio.mean(axis=1)
|
| 165 |
-
else:
|
| 166 |
-
mono = audio
|
| 167 |
-
|
| 168 |
-
# --- High-resolution spectral analysis ---
|
| 169 |
-
nperseg = min(16384, len(mono))
|
| 170 |
-
freqs, psd = welch(mono, fs=sample_rate, nperseg=nperseg)
|
| 171 |
-
|
| 172 |
-
# 24-band spectral profile (dB)
|
| 173 |
-
spectral_profile = {}
|
| 174 |
-
for name, lo, hi in _DETAIL_BANDS:
|
| 175 |
-
mask = (freqs >= lo) & (freqs < hi)
|
| 176 |
-
if np.any(mask):
|
| 177 |
-
band_rms = float(np.sqrt(np.mean(psd[mask])))
|
| 178 |
-
spectral_profile[name] = round(20.0 * np.log10(max(band_rms, 1e-12)), 1)
|
| 179 |
-
else:
|
| 180 |
-
spectral_profile[name] = -100.0
|
| 181 |
-
|
| 182 |
-
# --- Spectral peaks / resonances ---
|
| 183 |
-
# Smooth the PSD, find prominent peaks
|
| 184 |
-
psd_db = 10.0 * np.log10(np.maximum(psd, 1e-20))
|
| 185 |
-
# Use a wider window for smoothing to avoid noise peaks
|
| 186 |
-
kernel_size = min(31, len(psd_db) // 4)
|
| 187 |
-
if kernel_size % 2 == 0:
|
| 188 |
-
kernel_size += 1
|
| 189 |
-
if kernel_size >= 3:
|
| 190 |
-
kernel = np.ones(kernel_size) / kernel_size
|
| 191 |
-
psd_smooth = np.convolve(psd_db, kernel, mode="same")
|
| 192 |
-
else:
|
| 193 |
-
psd_smooth = psd_db
|
| 194 |
-
|
| 195 |
-
# Find peaks that stand out above the smoothed curve
|
| 196 |
-
prominence_threshold = 3.0 # at least 3 dB above neighbors
|
| 197 |
-
peak_indices, peak_props = find_peaks(
|
| 198 |
-
psd_db,
|
| 199 |
-
prominence=prominence_threshold,
|
| 200 |
-
distance=max(1, int(50 / (freqs[1] - freqs[0]))) # at least 50 Hz apart
|
| 201 |
-
)
|
| 202 |
-
|
| 203 |
-
# Sort by prominence and take top 8
|
| 204 |
-
if len(peak_indices) > 0:
|
| 205 |
-
prominences = peak_props["prominences"]
|
| 206 |
-
top_idx = np.argsort(prominences)[::-1][:8]
|
| 207 |
-
resonances = []
|
| 208 |
-
for idx in top_idx:
|
| 209 |
-
pi = peak_indices[idx]
|
| 210 |
-
if freqs[pi] >= 30: # skip sub-bass noise
|
| 211 |
-
resonances.append({
|
| 212 |
-
"freq_hz": round(float(freqs[pi]), 1),
|
| 213 |
-
"level_db": round(float(psd_db[pi]), 1),
|
| 214 |
-
"prominence_db": round(float(prominences[idx]), 1),
|
| 215 |
-
})
|
| 216 |
-
else:
|
| 217 |
-
resonances = []
|
| 218 |
-
|
| 219 |
-
# --- Spectral flatness (Wiener entropy) ---
|
| 220 |
-
# 1.0 = white noise, 0.0 = pure tone
|
| 221 |
-
psd_pos = psd[psd > 0]
|
| 222 |
-
if len(psd_pos) > 0:
|
| 223 |
-
geo_mean = np.exp(np.mean(np.log(psd_pos)))
|
| 224 |
-
arith_mean = np.mean(psd_pos)
|
| 225 |
-
spectral_flatness = round(float(geo_mean / arith_mean), 4)
|
| 226 |
-
else:
|
| 227 |
-
spectral_flatness = 0.0
|
| 228 |
-
|
| 229 |
-
# --- Spectral tilt (slope of energy across frequency) ---
|
| 230 |
-
# Negative = bass-heavy, positive = bright
|
| 231 |
-
if len(freqs) > 1 and np.any(psd > 0):
|
| 232 |
-
log_freqs = np.log10(np.maximum(freqs[1:], 1.0)) # skip DC
|
| 233 |
-
log_psd = 10.0 * np.log10(np.maximum(psd[1:], 1e-20))
|
| 234 |
-
coeffs = np.polyfit(log_freqs, log_psd, 1)
|
| 235 |
-
spectral_tilt = round(float(coeffs[0]), 2) # dB/decade
|
| 236 |
-
else:
|
| 237 |
-
spectral_tilt = 0.0
|
| 238 |
-
|
| 239 |
-
# --- Per-compression-band dynamics ---
|
| 240 |
-
comp_band_dynamics = {}
|
| 241 |
-
for name, lo, hi in _COMP_BANDS:
|
| 242 |
-
mask = (freqs >= lo) & (freqs < hi)
|
| 243 |
-
if np.any(mask):
|
| 244 |
-
band_psd = psd[mask]
|
| 245 |
-
band_rms = float(np.sqrt(np.mean(band_psd)))
|
| 246 |
-
band_peak = float(np.sqrt(np.max(band_psd)))
|
| 247 |
-
rms_db = round(20.0 * np.log10(max(band_rms, 1e-12)), 1)
|
| 248 |
-
peak_db = round(20.0 * np.log10(max(band_peak, 1e-12)), 1)
|
| 249 |
-
comp_band_dynamics[name] = {
|
| 250 |
-
"rms_db": rms_db,
|
| 251 |
-
"peak_db": peak_db,
|
| 252 |
-
"crest_db": round(peak_db - rms_db, 1),
|
| 253 |
-
}
|
| 254 |
-
else:
|
| 255 |
-
comp_band_dynamics[name] = {"rms_db": -100.0, "peak_db": -100.0, "crest_db": 0.0}
|
| 256 |
-
|
| 257 |
-
# --- Short-time dynamic variation ---
|
| 258 |
-
# Split audio into ~4-second chunks and measure RMS of each
|
| 259 |
-
chunk_samples = int(4.0 * sample_rate)
|
| 260 |
-
n_chunks = max(1, len(mono) // chunk_samples)
|
| 261 |
-
chunk_rms_list = []
|
| 262 |
-
for i in range(n_chunks):
|
| 263 |
-
chunk = mono[i * chunk_samples : (i + 1) * chunk_samples]
|
| 264 |
-
c_rms = float(np.sqrt(np.mean(chunk ** 2)))
|
| 265 |
-
if c_rms > 0:
|
| 266 |
-
chunk_rms_list.append(20.0 * np.log10(c_rms))
|
| 267 |
-
else:
|
| 268 |
-
chunk_rms_list.append(-100.0)
|
| 269 |
-
|
| 270 |
-
if len(chunk_rms_list) > 1:
|
| 271 |
-
chunk_arr = np.array(chunk_rms_list)
|
| 272 |
-
dynamic_variation = {
|
| 273 |
-
"min_rms_db": round(float(np.min(chunk_arr)), 1),
|
| 274 |
-
"max_rms_db": round(float(np.max(chunk_arr)), 1),
|
| 275 |
-
"range_db": round(float(np.max(chunk_arr) - np.min(chunk_arr)), 1),
|
| 276 |
-
"std_db": round(float(np.std(chunk_arr)), 2),
|
| 277 |
-
"n_chunks": n_chunks,
|
| 278 |
-
}
|
| 279 |
-
else:
|
| 280 |
-
dynamic_variation = {
|
| 281 |
-
"min_rms_db": chunk_rms_list[0] if chunk_rms_list else -100.0,
|
| 282 |
-
"max_rms_db": chunk_rms_list[0] if chunk_rms_list else -100.0,
|
| 283 |
-
"range_db": 0.0, "std_db": 0.0, "n_chunks": 1,
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
# --- Per-band stereo correlation ---
|
| 287 |
-
is_mono = audio.ndim == 1 or audio.shape[1] == 1
|
| 288 |
-
stereo_band_corr = {}
|
| 289 |
-
if not is_mono:
|
| 290 |
-
from scipy.signal import butter, sosfilt
|
| 291 |
-
|
| 292 |
-
left = audio[:, 0].astype(np.float64)
|
| 293 |
-
right = audio[:, 1].astype(np.float64)
|
| 294 |
-
|
| 295 |
-
band_edges = [(20, 200), (200, 2000), (2000, 8000), (8000, min(20000, sample_rate // 2 - 1))]
|
| 296 |
-
band_names = ["low", "low_mid", "high_mid", "high"]
|
| 297 |
-
|
| 298 |
-
for bname, lo, hi in zip(band_names, *zip(*band_edges)):
|
| 299 |
-
try:
|
| 300 |
-
sos = butter(4, [lo, hi], btype="band", fs=sample_rate, output="sos")
|
| 301 |
-
l_filt = sosfilt(sos, left)
|
| 302 |
-
r_filt = sosfilt(sos, right)
|
| 303 |
-
corr = np.corrcoef(l_filt, r_filt)[0, 1]
|
| 304 |
-
stereo_band_corr[bname] = round(float(corr), 3)
|
| 305 |
-
except Exception:
|
| 306 |
-
stereo_band_corr[bname] = None
|
| 307 |
-
|
| 308 |
-
# --- Merge into base features ---
|
| 309 |
-
base["spectral_profile_24band"] = spectral_profile
|
| 310 |
-
base["resonances"] = resonances
|
| 311 |
-
base["spectral_flatness"] = spectral_flatness
|
| 312 |
-
base["spectral_tilt_db_per_decade"] = spectral_tilt
|
| 313 |
-
base["comp_band_dynamics"] = comp_band_dynamics
|
| 314 |
-
base["dynamic_variation"] = dynamic_variation
|
| 315 |
-
base["stereo_band_correlation"] = stereo_band_corr if not is_mono else None
|
| 316 |
-
|
| 317 |
-
return base
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
# ---------------------------------------------------------------------------
|
| 321 |
-
# Gemini API wrapper
|
| 322 |
-
# ---------------------------------------------------------------------------
|
| 323 |
-
|
| 324 |
-
class GeminiUnavailableError(Exception):
|
| 325 |
-
"""Raised when Gemini API is unavailable after retries."""
|
| 326 |
-
pass
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
def _call_gemini(system_prompt, user_prompt):
|
| 330 |
-
"""Call Gemini 2.5 Pro via OpenRouter (preferred) or Google direct API.
|
| 331 |
-
|
| 332 |
-
Uses OPENROUTER_API_KEY if set, otherwise falls back to GOOGLE_API_KEY.
|
| 333 |
-
Retries up to 3 times on server errors (5xx) with escalating delays.
|
| 334 |
-
Raises GeminiUnavailableError on persistent failure.
|
| 335 |
-
"""
|
| 336 |
-
import time
|
| 337 |
-
import requests as _requests
|
| 338 |
-
|
| 339 |
-
openrouter_key = os.environ.get("OPENROUTER_API_KEY")
|
| 340 |
-
google_key = os.environ.get("GOOGLE_API_KEY")
|
| 341 |
-
|
| 342 |
-
if not openrouter_key and not google_key:
|
| 343 |
-
return None
|
| 344 |
-
|
| 345 |
-
if openrouter_key:
|
| 346 |
-
# OpenRouter — OpenAI-compatible format
|
| 347 |
-
url = "https://openrouter.ai/api/v1/chat/completions"
|
| 348 |
-
headers = {
|
| 349 |
-
"Authorization": f"Bearer {openrouter_key}",
|
| 350 |
-
"Content-Type": "application/json",
|
| 351 |
-
}
|
| 352 |
-
payload = {
|
| 353 |
-
"model": "google/gemini-2.5-pro",
|
| 354 |
-
"messages": [
|
| 355 |
-
{"role": "system", "content": system_prompt},
|
| 356 |
-
{"role": "user", "content": user_prompt},
|
| 357 |
-
],
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
def _parse_response(data):
|
| 361 |
-
return data["choices"][0]["message"]["content"]
|
| 362 |
-
else:
|
| 363 |
-
# Google direct API
|
| 364 |
-
url = (
|
| 365 |
-
"https://generativelanguage.googleapis.com/v1beta/models/"
|
| 366 |
-
f"gemini-2.5-pro:generateContent?key={google_key}"
|
| 367 |
-
)
|
| 368 |
-
headers = {"Content-Type": "application/json"}
|
| 369 |
-
payload = {
|
| 370 |
-
"system_instruction": {"parts": [{"text": system_prompt}]},
|
| 371 |
-
"contents": [{"role": "user", "parts": [{"text": user_prompt}]}],
|
| 372 |
-
}
|
| 373 |
-
|
| 374 |
-
def _parse_response(data):
|
| 375 |
-
return data["candidates"][0]["content"]["parts"][0]["text"]
|
| 376 |
-
|
| 377 |
-
last_error = None
|
| 378 |
-
retry_delays = [10, 20, 30] # 3 retries: wait 10s, 20s, 30s
|
| 379 |
-
for attempt in range(4): # initial + 3 retries
|
| 380 |
-
try:
|
| 381 |
-
resp = _requests.post(url, headers=headers, json=payload, timeout=120)
|
| 382 |
-
resp.raise_for_status()
|
| 383 |
-
data = resp.json()
|
| 384 |
-
return _parse_response(data)
|
| 385 |
-
except _requests.exceptions.HTTPError as e:
|
| 386 |
-
last_error = e
|
| 387 |
-
status = getattr(resp, "status_code", 0)
|
| 388 |
-
if status >= 500 and attempt < 3:
|
| 389 |
-
time.sleep(retry_delays[attempt])
|
| 390 |
-
continue
|
| 391 |
-
break
|
| 392 |
-
except Exception as e:
|
| 393 |
-
last_error = e
|
| 394 |
-
if attempt == 0:
|
| 395 |
-
time.sleep(5)
|
| 396 |
-
continue
|
| 397 |
-
break
|
| 398 |
-
|
| 399 |
-
provider = "OpenRouter" if openrouter_key else "Google Gemini"
|
| 400 |
-
raise GeminiUnavailableError(
|
| 401 |
-
f"**{provider} is temporarily unavailable.**\n\n"
|
| 402 |
-
"This is an issue with the AI provider's servers, not with StudioAI.\n\n"
|
| 403 |
-
"Please try again in a few minutes. If the problem persists, "
|
| 404 |
-
"try again in a few hours.\n\n"
|
| 405 |
-
"No usage credit was consumed for this attempt.\n\n"
|
| 406 |
-
f"*Technical details: {last_error}*"
|
| 407 |
-
)
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
# ---------------------------------------------------------------------------
|
| 411 |
-
# Phase 1: AI-recommended settings
|
| 412 |
-
# ---------------------------------------------------------------------------
|
| 413 |
-
|
| 414 |
-
_SIGNAL_FLOW = """
|
| 415 |
-
SIGNAL FLOW (fixed processing order):
|
| 416 |
-
1. PRE-GAIN DROP — Input is normalized to -18 LUFS internal working level (stepped attenuator). This prevents EQ clipping on hot masters.
|
| 417 |
-
2. HPF 15 Hz — Always-on 2nd-order Butterworth high-pass filter (12 dB/oct). Subsonic cleanup only; -3 dB at 15 Hz, negligible loss above 35 Hz. There is NO low-pass filter — source is band-limited by sample rate (Nyquist).
|
| 418 |
-
3. 4-BAND PARAMETRIC EQ (user-adjustable):
|
| 419 |
-
- Bass Boost: Peak filter (Q=2.0), variable center 40-100 Hz, range 0 to +3.0 dB, step 0.5 dB
|
| 420 |
-
- Lows: Low shelf at 200 Hz (Q=1.0), range -3.0 to +3.0 dB, step 0.5 dB
|
| 421 |
-
- Highs: High shelf at 10 kHz (Q=0.7, gentle slope), range -3.0 to +3.0 dB, step 0.5 dB — this is the "air" band, no LPF to fight it
|
| 422 |
-
- Mids: Peak filter at 1.2 kHz (Q=1.0, wide bell), range -3.0 to +3.0 dB, step 0.1 dB
|
| 423 |
-
4. MULTIBAND COMPRESSION — 3-band dynamics processing (no makeup gain):
|
| 424 |
-
- Placed before stereo width so the compressor sees EQ'd audio without M/S side-channel energy affecting per-band behaviour.
|
| 425 |
-
- Two Linkwitz-Riley 4th-order crossovers split the signal at 200 Hz and 4 kHz.
|
| 426 |
-
- LOGARITHMIC SLIDER CURVE: The 0-100 slider uses a quadratic (t²) mapping — the bottom half of the slider (0-50) covers the transparent-to-light range, while aggressive compression is concentrated in the top 30% (70-100). This gives fine control in the musical "sweet spot."
|
| 427 |
-
- LOW band (< 200 Hz): Firmest control. Attack scales 80→20ms (lets kick breathe at low settings, catches bass transients at high). Ratio 1.2:1→3.0:1, threshold -16→-24 dB, release 200→120ms.
|
| 428 |
-
- MID band (200 Hz – 4 kHz): Musical peak control. Attack scales 30→10ms (transparent at low settings, tames snare/vocal peaks at high). Ratio 1.1:1→2.5:1, threshold -14→-24 dB, release 150→100ms.
|
| 429 |
-
- HIGH band (> 4 kHz): Transient control. Attack scales 10→3ms (light de-essing at low, catches cymbal/click transients at high). Ratio 1.05:1→2.0:1, threshold -12→-22 dB, release 80→40ms.
|
| 430 |
-
- TRUE BYPASS when slider = 0 (no compressor in chain at all).
|
| 431 |
-
- Higher compression values actively reduce the crest factor (peak-to-loudness ratio), which allows LUFS normalization to push louder without the true peak ceiling pulling level back down.
|
| 432 |
-
- Bands are summed back to full-range after compression. No makeup gain — LUFS normalization handles level.
|
| 433 |
-
5. STEREO WIDTH — Frequency-selective M/S matrix (after dynamics for stable imaging):
|
| 434 |
-
- Linkwitz-Riley 4th-order crossover at 200 Hz splits signal into low band and high band
|
| 435 |
-
- Low band (< 200 Hz): untouched — keeps bass mono-safe
|
| 436 |
-
- High band (≥ 200 Hz): M/S encode → width scaling → M/S decode
|
| 437 |
-
- Energy-preserving: mid_scale = sqrt(2/(1+w²)), side_scale = w × mid_scale
|
| 438 |
-
- Range: 80% (narrow) to 150% (wide). 100% = no change. Clip protection after summing.
|
| 439 |
-
6. LUFS NORMALIZATION — Measures integrated loudness (ITU-R BS.1770-4, K-weighted, gated) and applies uniform linear gain to hit the target LUFS exactly. Targets: -14 (streaming), -11 (CD), or custom.
|
| 440 |
-
7. SOFT CLIPPER — Piecewise tanh saturation after LUFS normalization. A knee sits 2 dB below the -0.1 dBTP ceiling. Everything below the knee is perfectly linear (zero processing). Only the tips of peaks above the knee are shaped with a tanh curve that asymptotes to the ceiling. This is NOT a limiter — it's analog-style waveshaping that typically affects only the top 1-2 dB of the loudest transients. LUFS is preserved (transient tips contribute almost nothing to integrated loudness) while true-peak is reduced significantly.
|
| 441 |
-
8. TRUE PEAK CEILING (safety net) — After the soft clipper, the true peak (4x oversampled, ITU-R BS.1770) is measured. If any residual inter-sample peaks still exceed -0.1 dBTP, the entire signal is scaled down by exactly the overshoot. This rarely engages thanks to the soft clipper, but guarantees compliance.
|
| 442 |
-
"""
|
| 443 |
-
|
| 444 |
-
_RECOMMEND_SYSTEM = f"""You are an expert audio mastering engineer. Analyze the audio measurements below and recommend optimal mastering settings for this tool.
|
| 445 |
-
|
| 446 |
-
{_SIGNAL_FLOW}
|
| 447 |
-
|
| 448 |
-
AVAILABLE CONTROLS (these are the ONLY parameters you can recommend):
|
| 449 |
-
- lows_db: Low shelf at 200 Hz, -3.0 to +3.0 dB
|
| 450 |
-
- mid_boost_db: Peak at 1.2 kHz (Q=1.0), -3.0 to +3.0 dB
|
| 451 |
-
- highs_db: High shelf at 10 kHz (Q=0.7), -3.0 to +3.0 dB
|
| 452 |
-
- bass_boost_db: Peak (Q=2.0), 0 to +3.0 dB
|
| 453 |
-
- bass_freq_hz: Center freq for bass boost, 40-100 Hz
|
| 454 |
-
- compression: 0 (bypass/off) to 100 (heavy). 0 = true bypass (no processing)
|
| 455 |
-
- stereo_width: 80-150%. 100 = no change. Only affects frequencies above 200 Hz.
|
| 456 |
-
|
| 457 |
-
IMPORTANT CONTEXT:
|
| 458 |
-
- The 15 Hz HPF is always active and cannot be adjusted — do not try to compensate for it.
|
| 459 |
-
- There is no LPF, so the 10 kHz high shelf has full authority over the air band with no interference.
|
| 460 |
-
- LUFS normalization at the end restores loudness automatically — do not worry about overall level, focus on spectral balance and dynamics.
|
| 461 |
-
- TRUE PEAK CEILING: A -0.1 dBTP ceiling is enforced after LUFS normalization. If the audio has a high crest factor (large peaks relative to loudness), the ceiling will pull the final level below the target LUFS. To allow the track to hit the target LUFS, recommend enough compression to reduce the crest factor. Look at the crest_factor_db measurement — values above ~10 dB suggest compression in the 40-70 range; above ~14 dB may need 60-85.
|
| 462 |
-
- The compression slider uses a LOGARITHMIC (quadratic) curve. Slider values 0-50 cover subtle/transparent compression. Values 50-75 are moderate. Values 75-100 are aggressive. Recommend accordingly — a slider value of 30 is very light, 50 is moderate, 70+ is firm.
|
| 463 |
-
- If the audio already sounds well-balanced, recommend conservative or zero settings. Not everything needs processing.
|
| 464 |
-
|
| 465 |
-
Return ONLY a valid JSON object with these exact keys. The "reasoning" field must contain your actual markdown explanation (3-5 bullet points explaining why you chose these values):
|
| 466 |
-
{{
|
| 467 |
-
"lows_db": number,
|
| 468 |
-
"mid_boost_db": number,
|
| 469 |
-
"highs_db": number,
|
| 470 |
-
"bass_boost_db": number,
|
| 471 |
-
"bass_freq_hz": integer,
|
| 472 |
-
"compression": integer,
|
| 473 |
-
"stereo_width": integer,
|
| 474 |
-
"reasoning": "### AI Analysis\\n- **Lows:** reason for lows_db choice\\n- **Highs:** reason for highs_db choice\\n- ... (write your actual analysis here, do NOT return this template literally)"
|
| 475 |
-
}}
|
| 476 |
-
|
| 477 |
-
Keep values within the valid ranges. Be conservative — subtle moves are better than aggressive ones."""
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
def _clamp_settings(d):
|
| 481 |
-
"""Clamp AI-returned slider values to valid ranges in-place and return *d*."""
|
| 482 |
-
d["lows_db"] = max(-3.0, min(3.0, float(d.get("lows_db", 0))))
|
| 483 |
-
d["mid_boost_db"] = max(-3.0, min(3.0, float(d.get("mid_boost_db", 0))))
|
| 484 |
-
d["highs_db"] = max(-3.0, min(3.0, float(d.get("highs_db", 0))))
|
| 485 |
-
d["bass_boost_db"] = max(0, min(3.0, float(d.get("bass_boost_db", 0))))
|
| 486 |
-
d["bass_freq_hz"] = max(40, min(100, int(d.get("bass_freq_hz", 60))))
|
| 487 |
-
d["compression"] = max(0, min(100, int(d.get("compression", 50))))
|
| 488 |
-
d["stereo_width"] = max(80, min(150, int(d.get("stereo_width", 100))))
|
| 489 |
-
return d
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
def _strip_json(text):
|
| 493 |
-
"""Strip markdown code fences from a JSON response and parse it."""
|
| 494 |
-
text = text.strip()
|
| 495 |
-
if text.startswith("```"):
|
| 496 |
-
lines = text.split("\n")
|
| 497 |
-
text = "\n".join(lines[1:-1])
|
| 498 |
-
return json.loads(text)
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
def recommend_settings(audio_path):
|
| 502 |
-
"""Analyze raw audio and return AI-recommended mastering settings.
|
| 503 |
-
|
| 504 |
-
Args:
|
| 505 |
-
audio_path: path to the uploaded audio file.
|
| 506 |
-
|
| 507 |
-
Returns:
|
| 508 |
-
dict with recommended slider values and reasoning markdown,
|
| 509 |
-
or None if AI is unavailable.
|
| 510 |
-
"""
|
| 511 |
-
from dsp import load_audio
|
| 512 |
-
audio, sr = load_audio(audio_path)
|
| 513 |
-
features = extract_features(audio, sr)
|
| 514 |
-
|
| 515 |
-
user_prompt = f"""Analyze this audio and recommend mastering settings:
|
| 516 |
-
|
| 517 |
-
**Audio Measurements:**
|
| 518 |
-
- Integrated Loudness: {features['lufs']} LUFS
|
| 519 |
-
- True Peak: {features['true_peak_dbtp']} dBTP
|
| 520 |
-
- RMS Level: {features['rms_db']} dB
|
| 521 |
-
- Crest Factor: {features['crest_factor_db']} dB
|
| 522 |
-
- Spectral Centroid: {features['spectral_centroid_hz']} Hz
|
| 523 |
-
- Spectral Rolloff (85%): {features['spectral_rolloff_hz']} Hz
|
| 524 |
-
- Stereo Correlation: {features['stereo_correlation'] if features['stereo_correlation'] is not None else 'N/A (mono)'}
|
| 525 |
-
- Mono: {features['is_mono']}
|
| 526 |
-
|
| 527 |
-
**Band Energy (dB):**
|
| 528 |
-
{chr(10).join(f'- {k}: {v} dB' for k, v in features['band_energy'].items())}
|
| 529 |
-
|
| 530 |
-
Return the JSON object with recommended settings."""
|
| 531 |
-
|
| 532 |
-
response = _call_gemini(_RECOMMEND_SYSTEM, user_prompt)
|
| 533 |
-
if response is None:
|
| 534 |
-
return None
|
| 535 |
-
|
| 536 |
-
# Parse JSON from response (Gemini may wrap it in markdown code fence)
|
| 537 |
-
try:
|
| 538 |
-
result = _strip_json(response)
|
| 539 |
-
_clamp_settings(result)
|
| 540 |
-
|
| 541 |
-
if "reasoning" not in result:
|
| 542 |
-
result["reasoning"] = "*No explanation provided.*"
|
| 543 |
-
|
| 544 |
-
return result
|
| 545 |
-
except (json.JSONDecodeError, KeyError, TypeError):
|
| 546 |
-
return {"reasoning": response, "parse_error": True}
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
# ---------------------------------------------------------------------------
|
| 550 |
-
# Phase 2: Post-master comparison report
|
| 551 |
-
# ---------------------------------------------------------------------------
|
| 552 |
-
|
| 553 |
-
_COMPARE_SYSTEM = f"""You are an expert audio mastering engineer reviewing a completed master. You are evaluating the output of a specific mastering tool with the following architecture:
|
| 554 |
-
|
| 555 |
-
{_SIGNAL_FLOW}
|
| 556 |
-
|
| 557 |
-
IMPORTANT — When assessing the master:
|
| 558 |
-
- The 15 Hz HPF is always active. Any sub-bass roll-off below ~30 Hz is intentional subsonic cleanup, NOT a problem. Do not flag it.
|
| 559 |
-
- There is no LPF. The full spectrum above the HPF is passed through, so the 10 kHz high shelf has full authority over the air band.
|
| 560 |
-
- LUFS normalization is the final stage — it applies uniform linear gain. Loudness differences between original and mastered are intentional (target LUFS). Focus on spectral shape and dynamics, not absolute level.
|
| 561 |
-
- Compression at slider=0 means TRUE BYPASS (compressor was not in the chain at all). Do not comment on compression characteristics if it was bypassed.
|
| 562 |
-
- Stereo width only affects frequencies above 200 Hz (Linkwitz-Riley crossover). Bass mono-compatibility is always preserved.
|
| 563 |
-
- When suggesting improvements, ONLY recommend changes to the 7 available controls (lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz, compression 0-100, stereo_width 80-150%). Do not suggest changes the tool cannot make (e.g., adjusting per-band attack times, changing crossover frequencies, changing the HPF frequency). The multiband compression is automatic — the user only controls the single 0-100 slider.
|
| 564 |
-
- TRUE PEAK CEILING: If the mastered true peak is at -0.1 dBTP and the LUFS is below target, the peak ceiling pulled the level down. The fix is more compression (higher slider value) to reduce crest factor, NOT removing the ceiling. Mention this trade-off when relevant.
|
| 565 |
-
- The compression slider uses a LOGARITHMIC (quadratic) curve: 0-50 = subtle/transparent, 50-75 = moderate, 75-100 = aggressive. Factor this into your slider recommendations.
|
| 566 |
-
|
| 567 |
-
Format your response as markdown with these sections:
|
| 568 |
-
### Overall Assessment
|
| 569 |
-
(1-2 sentences — was the mastering effective for the material?)
|
| 570 |
-
|
| 571 |
-
### What Worked Well
|
| 572 |
-
(bullet points referencing specific measurement changes)
|
| 573 |
-
|
| 574 |
-
### Suggested Improvements
|
| 575 |
-
(bullet points with specific slider value recommendations using the 7 available controls. If the master is good, say so — not every master needs changes.)
|
| 576 |
-
|
| 577 |
-
### Technical Notes
|
| 578 |
-
(any concerns about dynamics, phase coherence, or frequency balance that the available controls could address)
|
| 579 |
-
|
| 580 |
-
Be concise and specific. Reference actual measurement deltas between original and mastered."""
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
def compare_master(original, mastered, sample_rate, settings_dict, history=None):
|
| 584 |
-
"""Compare original vs mastered audio and return AI quality report.
|
| 585 |
-
|
| 586 |
-
Args:
|
| 587 |
-
original: numpy array of original audio.
|
| 588 |
-
mastered: numpy array of mastered audio.
|
| 589 |
-
sample_rate: int.
|
| 590 |
-
settings_dict: dict with the mastering settings that were applied.
|
| 591 |
-
history: list of dicts from previous analyses (optional).
|
| 592 |
-
|
| 593 |
-
Returns:
|
| 594 |
-
str: markdown-formatted comparison report, or fallback message.
|
| 595 |
-
"""
|
| 596 |
-
orig_features = extract_features(original, sample_rate)
|
| 597 |
-
mast_features = extract_features(mastered, sample_rate)
|
| 598 |
-
|
| 599 |
-
# Build the multiband compression details from slider value
|
| 600 |
-
from dsp import map_multiband_compression
|
| 601 |
-
comp_val = settings_dict.get("compression", 50)
|
| 602 |
-
band_params = map_multiband_compression(comp_val)
|
| 603 |
-
|
| 604 |
-
def _fmt_band(params):
|
| 605 |
-
return (f"threshold {params[0]:.1f} dB, ratio {params[1]:.2f}:1, "
|
| 606 |
-
f"attack {params[2]:.0f} ms, release {params[3]:.0f} ms")
|
| 607 |
-
|
| 608 |
-
history_text = _format_history(history or [])
|
| 609 |
-
|
| 610 |
-
user_prompt = f"""Compare the original and mastered audio:
|
| 611 |
-
|
| 612 |
-
**ORIGINAL Audio:**
|
| 613 |
-
- Loudness: {orig_features['lufs']} LUFS | True Peak: {orig_features['true_peak_dbtp']} dBTP
|
| 614 |
-
- RMS: {orig_features['rms_db']} dB | Crest Factor: {orig_features['crest_factor_db']} dB
|
| 615 |
-
- Spectral Centroid: {orig_features['spectral_centroid_hz']} Hz | Rolloff: {orig_features['spectral_rolloff_hz']} Hz
|
| 616 |
-
- Stereo Correlation: {orig_features['stereo_correlation'] if orig_features['stereo_correlation'] is not None else 'N/A (mono)'}
|
| 617 |
-
- Band Energy: {json.dumps(orig_features['band_energy'])}
|
| 618 |
-
|
| 619 |
-
**MASTERED Audio:**
|
| 620 |
-
- Loudness: {mast_features['lufs']} LUFS | True Peak: {mast_features['true_peak_dbtp']} dBTP
|
| 621 |
-
- RMS: {mast_features['rms_db']} dB | Crest Factor: {mast_features['crest_factor_db']} dB
|
| 622 |
-
- Spectral Centroid: {mast_features['spectral_centroid_hz']} Hz | Rolloff: {mast_features['spectral_rolloff_hz']} Hz
|
| 623 |
-
- Stereo Correlation: {mast_features['stereo_correlation'] if mast_features['stereo_correlation'] is not None else 'N/A (mono)'}
|
| 624 |
-
- Band Energy: {json.dumps(mast_features['band_energy'])}
|
| 625 |
-
|
| 626 |
-
**Settings Applied:**
|
| 627 |
-
- Lows (200 Hz shelf): {settings_dict.get('lows_db', 0)} dB
|
| 628 |
-
- Mids (1.2 kHz peak): {settings_dict.get('mid_boost_db', 0)} dB
|
| 629 |
-
- Highs (10 kHz shelf): {settings_dict.get('highs_db', 0)} dB
|
| 630 |
-
- Bass Boost: {settings_dict.get('bass_boost_db', 0)} dB @ {settings_dict.get('bass_freq_hz', 60)} Hz
|
| 631 |
-
- Compression: slider {comp_val}/100 (multiband, 3 bands)
|
| 632 |
-
- Low (< 200 Hz): {_fmt_band(band_params['low'])}
|
| 633 |
-
- Mid (200 Hz-4 kHz): {_fmt_band(band_params['mid'])}
|
| 634 |
-
- High (> 4 kHz): {_fmt_band(band_params['high'])}
|
| 635 |
-
- Stereo Width: {settings_dict.get('stereo_width', 100)}%
|
| 636 |
-
- Target LUFS: {settings_dict.get('target_lufs', -14)}{history_text}"""
|
| 637 |
-
|
| 638 |
-
response = _call_gemini(_COMPARE_SYSTEM, user_prompt)
|
| 639 |
-
if response is None:
|
| 640 |
-
return "*Set GOOGLE_API_KEY to enable AI comparison report.*"
|
| 641 |
-
return response
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
# ---------------------------------------------------------------------------
|
| 645 |
-
# Phase 3: Structured comparison (for Auto Master loop)
|
| 646 |
-
# ---------------------------------------------------------------------------
|
| 647 |
-
|
| 648 |
-
_COMPARE_STRUCTURED_SYSTEM = f"""You are an expert audio mastering engineer reviewing a completed master. You are evaluating the output of a specific mastering tool with the following architecture:
|
| 649 |
-
|
| 650 |
-
{_SIGNAL_FLOW}
|
| 651 |
-
|
| 652 |
-
IMPORTANT — When assessing the master:
|
| 653 |
-
- The 15 Hz HPF is always active. Any sub-bass roll-off below ~30 Hz is intentional subsonic cleanup, NOT a problem. Do not flag it.
|
| 654 |
-
- There is no LPF. The full spectrum above the HPF is passed through, so the 10 kHz high shelf has full authority over the air band.
|
| 655 |
-
- LUFS normalization is the final stage — it applies uniform linear gain. Loudness differences between original and mastered are intentional (target LUFS). Focus on spectral shape and dynamics, not absolute level.
|
| 656 |
-
- Compression at slider=0 means TRUE BYPASS (compressor was not in the chain at all). Do not comment on compression characteristics if it was bypassed.
|
| 657 |
-
- Stereo width only affects frequencies above 200 Hz (Linkwitz-Riley crossover). Bass mono-compatibility is always preserved.
|
| 658 |
-
- When suggesting improvements, ONLY recommend changes to the 7 available controls. Do not suggest changes the tool cannot make (e.g., adjusting per-band attack times, changing crossover frequencies, changing the HPF frequency). The multiband compression is automatic — the user only controls the single 0-100 slider.
|
| 659 |
-
- TRUE PEAK CEILING: If the mastered true peak is at -0.1 dBTP and the LUFS is below target, the peak ceiling pulled the level down. The fix is more compression (higher slider value) to reduce crest factor, NOT removing the ceiling. Adjust your revised compression value accordingly.
|
| 660 |
-
- The compression slider uses a LOGARITHMIC (quadratic) curve: 0-50 = subtle/transparent, 50-75 = moderate, 75-100 = aggressive. Factor this into your slider recommendations.
|
| 661 |
-
|
| 662 |
-
Return ONLY a valid JSON object (no markdown fences, no extra text) with these exact keys:
|
| 663 |
-
{{
|
| 664 |
-
"lows_db": <number, -3.0 to +3.0>,
|
| 665 |
-
"mid_boost_db": <number, -3.0 to +3.0>,
|
| 666 |
-
"highs_db": <number, -3.0 to +3.0>,
|
| 667 |
-
"bass_boost_db": <number, 0 to +3.0>,
|
| 668 |
-
"bass_freq_hz": <integer, 40 to 100>,
|
| 669 |
-
"compression": <integer, 0 to 100>,
|
| 670 |
-
"stereo_width": <integer, 80 to 150>,
|
| 671 |
-
"report": "<your full markdown comparison report here — Overall Assessment, What Worked Well, Suggested Improvements, Technical Notes>"
|
| 672 |
-
}}
|
| 673 |
-
|
| 674 |
-
The numeric values should be your REVISED recommended settings for a re-master based on what you hear in the measurements.
|
| 675 |
-
The "report" field should contain the full markdown analysis.
|
| 676 |
-
Be concise and specific. Reference actual measurement deltas.
|
| 677 |
-
|
| 678 |
-
Do NOT return the template above literally — fill in your actual analysis and values."""
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
def _format_history(history):
|
| 682 |
-
"""Format analysis history for inclusion in prompts."""
|
| 683 |
-
if not history:
|
| 684 |
-
return ""
|
| 685 |
-
lines = ["\n\n**PREVIOUS ANALYSIS HISTORY** (oldest first — use this to avoid recommending settings that already failed or oscillating between values):"]
|
| 686 |
-
for i, entry in enumerate(history, 1):
|
| 687 |
-
lines.append(f"\n--- Pass {i} ---")
|
| 688 |
-
lines.append(f"Settings tried: {json.dumps({k: v for k, v in entry.get('settings', {}).items() if k != 'target_lufs'})}")
|
| 689 |
-
lines.append(f"Result: LUFS={entry.get('lufs', '?')}, True Peak={entry.get('true_peak', '?')} dBTP, Crest Factor={entry.get('crest_factor', '?')} dB")
|
| 690 |
-
if entry.get("summary"):
|
| 691 |
-
lines.append(f"AI assessment: {entry['summary']}")
|
| 692 |
-
lines.append("\nIMPORTANT: Do NOT oscillate. If a previous pass moved a setting in one direction and it helped, continue refining in that direction. If it didn't help, try a DIFFERENT approach rather than reverting to a value that was already tried.")
|
| 693 |
-
return "\n".join(lines)
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
def compare_master_structured(original, mastered, sample_rate, settings_dict,
|
| 697 |
-
history=None):
|
| 698 |
-
"""Compare original vs mastered and return structured values + report.
|
| 699 |
-
|
| 700 |
-
Same analysis as compare_master() but returns a dict with revised slider
|
| 701 |
-
values and a markdown report, for use in the Auto Master loop.
|
| 702 |
-
|
| 703 |
-
Args:
|
| 704 |
-
history: list of dicts from previous analyses (optional). Each entry
|
| 705 |
-
has keys: settings, lufs, true_peak, crest_factor, summary.
|
| 706 |
-
|
| 707 |
-
Returns:
|
| 708 |
-
dict with keys: lows_db, mid_boost_db, highs_db, bass_boost_db,
|
| 709 |
-
bass_freq_hz, compression, stereo_width, report.
|
| 710 |
-
On parse error: {"report": raw_text, "parse_error": True}.
|
| 711 |
-
On API failure: None.
|
| 712 |
-
"""
|
| 713 |
-
orig_features = extract_features(original, sample_rate)
|
| 714 |
-
mast_features = extract_features(mastered, sample_rate)
|
| 715 |
-
|
| 716 |
-
from dsp import map_multiband_compression
|
| 717 |
-
comp_val = settings_dict.get("compression", 50)
|
| 718 |
-
band_params = map_multiband_compression(comp_val)
|
| 719 |
-
|
| 720 |
-
def _fmt_band(params):
|
| 721 |
-
return (f"threshold {params[0]:.1f} dB, ratio {params[1]:.2f}:1, "
|
| 722 |
-
f"attack {params[2]:.0f} ms, release {params[3]:.0f} ms")
|
| 723 |
-
|
| 724 |
-
history_text = _format_history(history or [])
|
| 725 |
-
|
| 726 |
-
user_prompt = f"""Compare the original and mastered audio and return your revised settings as JSON:
|
| 727 |
-
|
| 728 |
-
**ORIGINAL Audio:**
|
| 729 |
-
- Loudness: {orig_features['lufs']} LUFS | True Peak: {orig_features['true_peak_dbtp']} dBTP
|
| 730 |
-
- RMS: {orig_features['rms_db']} dB | Crest Factor: {orig_features['crest_factor_db']} dB
|
| 731 |
-
- Spectral Centroid: {orig_features['spectral_centroid_hz']} Hz | Rolloff: {orig_features['spectral_rolloff_hz']} Hz
|
| 732 |
-
- Stereo Correlation: {orig_features['stereo_correlation'] if orig_features['stereo_correlation'] is not None else 'N/A (mono)'}
|
| 733 |
-
- Band Energy: {json.dumps(orig_features['band_energy'])}
|
| 734 |
-
|
| 735 |
-
**MASTERED Audio:**
|
| 736 |
-
- Loudness: {mast_features['lufs']} LUFS | True Peak: {mast_features['true_peak_dbtp']} dBTP
|
| 737 |
-
- RMS: {mast_features['rms_db']} dB | Crest Factor: {mast_features['crest_factor_db']} dB
|
| 738 |
-
- Spectral Centroid: {mast_features['spectral_centroid_hz']} Hz | Rolloff: {mast_features['spectral_rolloff_hz']} Hz
|
| 739 |
-
- Stereo Correlation: {mast_features['stereo_correlation'] if mast_features['stereo_correlation'] is not None else 'N/A (mono)'}
|
| 740 |
-
- Band Energy: {json.dumps(mast_features['band_energy'])}
|
| 741 |
-
|
| 742 |
-
**Settings Applied:**
|
| 743 |
-
- Lows (200 Hz shelf): {settings_dict.get('lows_db', 0)} dB
|
| 744 |
-
- Mids (1.2 kHz peak): {settings_dict.get('mid_boost_db', 0)} dB
|
| 745 |
-
- Highs (10 kHz shelf): {settings_dict.get('highs_db', 0)} dB
|
| 746 |
-
- Bass Boost: {settings_dict.get('bass_boost_db', 0)} dB @ {settings_dict.get('bass_freq_hz', 60)} Hz
|
| 747 |
-
- Compression: slider {comp_val}/100 (multiband, 3 bands)
|
| 748 |
-
- Low (< 200 Hz): {_fmt_band(band_params['low'])}
|
| 749 |
-
- Mid (200 Hz-4 kHz): {_fmt_band(band_params['mid'])}
|
| 750 |
-
- High (> 4 kHz): {_fmt_band(band_params['high'])}
|
| 751 |
-
- Stereo Width: {settings_dict.get('stereo_width', 100)}%
|
| 752 |
-
- Target LUFS: {settings_dict.get('target_lufs', -14)}{history_text}
|
| 753 |
-
|
| 754 |
-
Return the JSON object with your revised settings and comparison report."""
|
| 755 |
-
|
| 756 |
-
response = _call_gemini(_COMPARE_STRUCTURED_SYSTEM, user_prompt)
|
| 757 |
-
if response is None:
|
| 758 |
-
return None
|
| 759 |
-
|
| 760 |
-
try:
|
| 761 |
-
result = _strip_json(response)
|
| 762 |
-
_clamp_settings(result)
|
| 763 |
-
if "report" not in result:
|
| 764 |
-
result["report"] = "*No report provided.*"
|
| 765 |
-
return result
|
| 766 |
-
except (json.JSONDecodeError, KeyError, TypeError):
|
| 767 |
-
return {"report": response, "parse_error": True}
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
# ---------------------------------------------------------------------------
|
| 771 |
-
# Super AI mode — full parametric control
|
| 772 |
-
# ---------------------------------------------------------------------------
|
| 773 |
-
|
| 774 |
-
_SUPER_SIGNAL_FLOW = """
|
| 775 |
-
AUDIO ANALYSIS DATA YOU RECEIVE:
|
| 776 |
-
You will receive detailed measurements for each audio file, including:
|
| 777 |
-
- Standard: LUFS, true peak, RMS, crest factor, spectral centroid, spectral rolloff, stereo correlation
|
| 778 |
-
- 24-Band Spectral Profile: Fine-grained energy (dB) across 21 frequency bands from 20 Hz to 20 kHz.
|
| 779 |
-
USE THIS to make precise EQ decisions — you can see exactly where energy buildups, dips, and imbalances are.
|
| 780 |
-
- Spectral Resonances: Top 8 most prominent spectral peaks with frequency, level, and prominence (dB above neighbors).
|
| 781 |
-
USE THIS to identify harsh or ringing frequencies that need surgical EQ cuts.
|
| 782 |
-
- Spectral Flatness: 0.0 = pure tonal, 1.0 = white noise. Tells you how tonal vs noisy the material is.
|
| 783 |
-
- Spectral Tilt: dB/decade slope. Negative = bass-heavy, positive = bright. Guides overall tonal balance decisions.
|
| 784 |
-
- Per-Compression-Band Dynamics: RMS, peak, and crest factor for each of the 3 compression bands (low/mid/high).
|
| 785 |
-
USE THIS to set compression thresholds and ratios per band — you can see which bands need taming.
|
| 786 |
-
- Dynamic Variation: Min/max/range/std of RMS across 4-second chunks of the track.
|
| 787 |
-
Tells you how much the track varies (quiet verse vs loud chorus). High range = preserve dynamics. Low range = already compressed.
|
| 788 |
-
- Per-Band Stereo Correlation: Correlation for low, low-mid, high-mid, and high frequency bands.
|
| 789 |
-
USE THIS to make stereo width decisions — low correlation = wide, high = narrow/mono.
|
| 790 |
-
|
| 791 |
-
SIGNAL FLOW (fixed processing order — you control ALL parameters):
|
| 792 |
-
1. PRE-GAIN DROP — Input is normalized to -18 LUFS (automatic, not adjustable).
|
| 793 |
-
2. HIGH-PASS FILTER — Adjustable cutoff (10-80 Hz). Default 15 Hz, 12 dB/oct Butterworth.
|
| 794 |
-
3. 6-BAND FULLY PARAMETRIC EQ — Each band is independently configurable:
|
| 795 |
-
- band1 through band6: type (peak/low_shelf/high_shelf), frequency (20-20000 Hz), gain (-6 to +6 dB), Q (0.1-10.0)
|
| 796 |
-
You can use any combination of shelf and peak filters at any frequency. The normal UI locks these to fixed frequencies and ±3 dB — you are NOT limited to that. You have full parametric EQ control.
|
| 797 |
-
Set gain_db to 0 on any band you don't need — unused bands are bypassed automatically.
|
| 798 |
-
4. MULTIBAND COMPRESSION — 3-band dynamics with per-band control:
|
| 799 |
-
- crossover_low: adjustable crossover frequency for low/mid split (default 200 Hz)
|
| 800 |
-
- crossover_high: adjustable crossover frequency for mid/high split (default 4000 Hz)
|
| 801 |
-
- Each band (low, mid, high) has independently adjustable: threshold (-20 to 0 dB), ratio (1.0-20.0), attack_ms (0.1-200 ms), release_ms (10-500 ms)
|
| 802 |
-
- Ratio 1.0 = bypass for that band
|
| 803 |
-
- No makeup gain — LUFS normalization restores level.
|
| 804 |
-
5. STEREO WIDTH — M/S matrix, frequency-selective (crossover at 200 Hz, bass stays mono). Range 80-150%.
|
| 805 |
-
6. LUFS NORMALIZATION — Automatic to target LUFS (fixed by user, DO NOT change).
|
| 806 |
-
7. SOFT CLIPPER — Piecewise tanh saturation, knee 2 dB below -0.1 dBTP ceiling. Always active. Linear below knee, tanh above. This is a safety net — NOT a creative tool.
|
| 807 |
-
8. TRUE PEAK CEILING — Safety net at -0.1 dBTP. Scales signal down if residual peaks exceed ceiling.
|
| 808 |
-
|
| 809 |
-
CONSTRAINTS (DO NOT VIOLATE):
|
| 810 |
-
- Target LUFS is fixed by the user. Do not change it.
|
| 811 |
-
- The soft clipper and true peak ceiling must remain as-is (automatic safety nets).
|
| 812 |
-
- You cannot add new processing stages — only adjust the parameters described above.
|
| 813 |
-
|
| 814 |
-
TRUE PEAK GUIDANCE (IMPORTANT):
|
| 815 |
-
- True peak between -1.0 and -0.1 dBTP is the IDEAL goal, but it is NOT always achievable.
|
| 816 |
-
- Source material that is already heavily limited or compressed (e.g., AI-generated tracks from Suno, Udio, etc.) has a very low crest factor (peak-to-loudness ratio). When such material is normalized DOWN to a streaming LUFS target (e.g., -14 LUFS), the true peak will naturally drop well below -1.0 dBTP. This is correct and expected behavior.
|
| 817 |
-
- DO NOT over-compress or crush dynamics to try to force the true peak higher. Dynamics preservation is MORE important than hitting a specific true peak number.
|
| 818 |
-
- If the source material has a low crest factor, acknowledge this in your analysis and accept the true peak wherever it naturally lands after LUFS normalization.
|
| 819 |
-
- Only use compression for tonal shaping and dynamic control — NEVER to artificially raise the true peak.
|
| 820 |
-
"""
|
| 821 |
-
|
| 822 |
-
_SUPER_RECOMMEND_SYSTEM = f"""You are a world-class audio mastering engineer with decades of experience. You have FULL control over every parameter in the mastering chain. Analyze the audio measurements and recommend optimal settings.
|
| 823 |
-
|
| 824 |
-
{_SUPER_SIGNAL_FLOW}
|
| 825 |
-
|
| 826 |
-
MASTERING PHILOSOPHY:
|
| 827 |
-
- LESS IS MORE. A great master sounds like a better version of the original, not a different song.
|
| 828 |
-
- Most EQ moves should be ±1 to ±2 dB. Moves beyond ±3 dB are RARE and require strong justification.
|
| 829 |
-
- If the source audio already sounds good in a frequency range, LEAVE IT ALONE. Do not EQ for the sake of EQ.
|
| 830 |
-
- Use surgical EQ moves — small cuts are often more effective than boosts.
|
| 831 |
-
- Compression thresholds should be set so compression only engages on peaks, NOT constantly. A threshold of -35 dB or lower means the compressor is always compressing — that destroys dynamics. Typical mastering thresholds are -15 to -8 dB.
|
| 832 |
-
- Match compression to the genre and dynamic character of the material.
|
| 833 |
-
- Preserve the artist's intent — enhance, don't transform.
|
| 834 |
-
- True peak between -1.0 and -0.1 dBTP is ideal, but do NOT sacrifice dynamics to achieve it. If the source is already heavily compressed, the true peak may land below -1.0 dBTP at the target LUFS — that is acceptable.
|
| 835 |
-
|
| 836 |
-
TONAL DIRECTION (apply to all masters):
|
| 837 |
-
- Aim for a slightly WARM overall tone — a SUBTLE richness in the low-mids (200-500 Hz) and smooth, non-harsh highs. This means maybe +0.5 to +1.5 dB shelf, NOT +3 dB or more.
|
| 838 |
-
- High shelf boosts above +1.5 dB will make the master sound harsh and brittle — avoid this.
|
| 839 |
-
|
| 840 |
-
HEAVY, UNCONSTRAINED BASS — Protect low-end punch and transient impact (40-100 Hz) at all costs:
|
| 841 |
-
- Rule 1: Prioritize additive EQ (Band 1/Band 2) for bass weight. DO NOT cut any frequencies below 150 Hz with EQ. The HPF already handles rumble removal. Any EQ band targeting frequencies below 150 Hz should have POSITIVE gain (boost) or be bypassed (0 dB). Cutting sub-bass removes the punch and weight from the track.
|
| 842 |
-
- Rule 2: DO NOT over-compress the < 200 Hz band. If the source crest factor is already low, default the Low-Band Compressor to BYPASS (Ratio 1:1) or use a very slow attack (>60 ms) so the kick drum transient escapes untouched.
|
| 843 |
-
- Rule 3: The sub-bass should feel physical, anchored, and wide open.
|
| 844 |
-
- Rule 4: HPF cutoff must stay at or below 25 Hz for bass-heavy material. Only raise it above 30 Hz if measurements show significant rumble below 20 Hz.
|
| 845 |
-
|
| 846 |
-
Return ONLY a valid JSON object with this exact structure (no markdown fences):
|
| 847 |
-
{{
|
| 848 |
-
"hpf_freq": <float, 10-80>,
|
| 849 |
-
"eq": {{
|
| 850 |
-
"band1": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 851 |
-
"band2": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 852 |
-
"band3": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 853 |
-
"band4": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 854 |
-
"band5": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 855 |
-
"band6": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}}
|
| 856 |
-
}},
|
| 857 |
-
"compression": {{
|
| 858 |
-
"low": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},
|
| 859 |
-
"mid": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},
|
| 860 |
-
"high": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}}
|
| 861 |
-
}},
|
| 862 |
-
"crossover_low": <float Hz>,
|
| 863 |
-
"crossover_high": <float Hz>,
|
| 864 |
-
"stereo_width": <int, 80-150>,
|
| 865 |
-
"reasoning": "### AI Analysis\\n- **EQ:** reason for EQ choices\\n- **Dynamics:** reason for compression settings\\n- **Stereo:** reason for width choice\\n(write your ACTUAL analysis — do NOT return this template literally)"
|
| 866 |
-
}}
|
| 867 |
-
|
| 868 |
-
Be musical and intentional. Every parameter should have a reason."""
|
| 869 |
-
|
| 870 |
-
_SUPER_COMPARE_SYSTEM = f"""You are a world-class audio mastering engineer reviewing a completed master. You have FULL control over every parameter and can make surgical adjustments.
|
| 871 |
-
|
| 872 |
-
{_SUPER_SIGNAL_FLOW}
|
| 873 |
-
|
| 874 |
-
REVIEW GUIDELINES:
|
| 875 |
-
- Compare original vs mastered measurements carefully.
|
| 876 |
-
- Make VERY SMALL adjustments — typically ±0.5 dB EQ tweaks or 1-2 dB threshold changes. If you changed a parameter by more than ±1 dB on the previous pass, do NOT change it again unless the measurements clearly show a problem.
|
| 877 |
-
- If something sounds good, LEAVE IT ALONE. The best revision is often the smallest one.
|
| 878 |
-
- Compression thresholds should be -15 to -8 dB for mastering. If you see a threshold below -20 dB, raise it — that compressor is over-compressing.
|
| 879 |
-
- Focus on what the measurements tell you: spectral balance, dynamics, stereo image.
|
| 880 |
-
- True peak between -1.0 and -0.1 dBTP is ideal, but do NOT over-compress to force it. If the source has a low crest factor, accept the true peak wherever it lands naturally.
|
| 881 |
-
- LUFS target is fixed — do not try to change it.
|
| 882 |
-
- Reference the previous analysis history to avoid oscillating between settings.
|
| 883 |
-
- Each revision should be a refinement, not a reset. Aim for 1-2 parameter changes per pass, not 5+.
|
| 884 |
-
|
| 885 |
-
TONAL DIRECTION (maintain across all revisions):
|
| 886 |
-
- The master should have a slightly WARM overall tone — SUBTLE richness in the low-mids (200-500 Hz) and smooth, non-harsh highs. Avoid clinical or brittle sound.
|
| 887 |
-
- High shelf boosts above +1.5 dB will make the master harsh — pull them back if present.
|
| 888 |
-
|
| 889 |
-
HEAVY, UNCONSTRAINED BASS — Protect low-end punch and transient impact (40-100 Hz) at all costs:
|
| 890 |
-
- Rule 1: Prioritize additive EQ (Band 1/Band 2) for bass weight. DO NOT cut any frequencies below 150 Hz with EQ. If a previous pass cut sub-bass, UNDO that cut (set gain to 0 or positive). Cutting sub-bass removes punch and weight.
|
| 891 |
-
- Rule 2: DO NOT over-compress the < 200 Hz band. If the source crest factor is already low, default the Low-Band Compressor to BYPASS (Ratio 1:1) or use a very slow attack (>60 ms) so the kick drum transient escapes untouched.
|
| 892 |
-
- Rule 3: The sub-bass should feel physical, anchored, and wide open.
|
| 893 |
-
- Rule 4: HPF cutoff must stay at or below 25 Hz for bass-heavy material. Only raise it above 30 Hz if measurements show significant rumble below 20 Hz.
|
| 894 |
-
|
| 895 |
-
CREST FACTOR CHECK (passes 2-4):
|
| 896 |
-
- If the crest factor in the < 200 Hz range decreases between passes, you have over-compressed the kick drum. BACK OFF the Low-Band compressor ratio or lengthen the attack time. Do not lose the punch.
|
| 897 |
-
- Sidechain Emulation: Treat the Low-Band compressor as if it has a 100 Hz HPF on its sidechain detector. Do not let sustained sub-bass notes clamp down on the rhythmic transients.
|
| 898 |
-
|
| 899 |
-
Return ONLY a valid JSON object with this exact structure (no markdown fences):
|
| 900 |
-
{{
|
| 901 |
-
"hpf_freq": <float, 10-80>,
|
| 902 |
-
"eq": {{
|
| 903 |
-
"band1": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 904 |
-
"band2": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 905 |
-
"band3": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 906 |
-
"band4": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 907 |
-
"band5": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}},
|
| 908 |
-
"band6": {{"type": "<peak|low_shelf|high_shelf>", "freq": <float Hz>, "gain_db": <float>, "q": <float>}}
|
| 909 |
-
}},
|
| 910 |
-
"compression": {{
|
| 911 |
-
"low": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},
|
| 912 |
-
"mid": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}},
|
| 913 |
-
"high": {{"threshold": <float dB>, "ratio": <float>, "attack_ms": <float>, "release_ms": <float>}}
|
| 914 |
-
}},
|
| 915 |
-
"crossover_low": <float Hz>,
|
| 916 |
-
"crossover_high": <float Hz>,
|
| 917 |
-
"stereo_width": <int, 80-150>,
|
| 918 |
-
"report": "<your full markdown comparison report — Overall Assessment, What Worked Well, Suggested Improvements, Technical Notes>"
|
| 919 |
-
}}
|
| 920 |
-
|
| 921 |
-
The numeric values should be your REVISED settings for a re-master. Make small, targeted adjustments.
|
| 922 |
-
The "report" field must contain your actual markdown analysis."""
|
| 923 |
-
|
| 924 |
-
_SUPER_FINAL_REPORT_SYSTEM = f"""You are a world-class audio mastering engineer writing a final quality report. You are evaluating whether a master meets professional distribution standards.
|
| 925 |
-
|
| 926 |
-
{_SUPER_SIGNAL_FLOW}
|
| 927 |
-
|
| 928 |
-
TONAL DIRECTION (evaluate against these goals):
|
| 929 |
-
- The desired outcome is a slightly WARM overall tone with smooth, non-harsh highs.
|
| 930 |
-
- HEAVY, UNCONSTRAINED BASS — the low end (40-100 Hz) should feel physical, punchy, and anchored. Evaluate whether the kick drum transients survived the compression stage. If sub-bass was cut by EQ or crushed by compression, flag it as a failure.
|
| 931 |
-
- HPF should be at or below 25 Hz for bass-heavy material.
|
| 932 |
-
- Evaluate whether the final master achieves this tonal character.
|
| 933 |
-
|
| 934 |
-
Write a comprehensive final report in markdown format covering:
|
| 935 |
-
|
| 936 |
-
### Overall Assessment
|
| 937 |
-
(Was the mastering effective? Does it meet professional standards? Does it achieve the desired warm tone with enhanced bass?)
|
| 938 |
-
|
| 939 |
-
### Spectral Balance
|
| 940 |
-
(Evaluate frequency balance — low end warmth, midrange richness, high end smoothness)
|
| 941 |
-
|
| 942 |
-
### Dynamics & Loudness
|
| 943 |
-
(LUFS, true peak compliance, crest factor, dynamic range preservation)
|
| 944 |
-
|
| 945 |
-
### Stereo Image
|
| 946 |
-
(Width, mono compatibility, balance)
|
| 947 |
-
|
| 948 |
-
### Processing Summary
|
| 949 |
-
(What the mastering chain did — EQ moves, compression character, etc.)
|
| 950 |
-
|
| 951 |
-
### Verdict
|
| 952 |
-
(Pass/fail for streaming distribution. Any remaining concerns?)
|
| 953 |
-
|
| 954 |
-
Be specific. Reference actual measurements. This is the FINAL report — no suggestions for changes, just an honest evaluation of the finished master."""
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
def _clamp_super_params(d):
|
| 958 |
-
"""Clamp Super AI parameters to safe ranges."""
|
| 959 |
-
d["hpf_freq"] = max(10.0, min(80.0, float(d.get("hpf_freq", 15.0))))
|
| 960 |
-
d["stereo_width"] = max(80, min(150, int(d.get("stereo_width", 100))))
|
| 961 |
-
d["crossover_low"] = max(80.0, min(500.0, float(d.get("crossover_low", 200.0))))
|
| 962 |
-
d["crossover_high"] = max(1000.0, min(10000.0, float(d.get("crossover_high", 4000.0))))
|
| 963 |
-
|
| 964 |
-
eq = d.get("eq", {})
|
| 965 |
-
for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):
|
| 966 |
-
band = eq.get(bk, {})
|
| 967 |
-
band["freq"] = max(20.0, min(20000.0, float(band.get("freq", 1000))))
|
| 968 |
-
band["gain_db"] = max(-6.0, min(6.0, float(band.get("gain_db", 0))))
|
| 969 |
-
band["q"] = max(0.1, min(10.0, float(band.get("q", 1.0))))
|
| 970 |
-
if band.get("type") not in ("peak", "low_shelf", "high_shelf"):
|
| 971 |
-
band["type"] = "peak"
|
| 972 |
-
eq[bk] = band
|
| 973 |
-
d["eq"] = eq
|
| 974 |
-
|
| 975 |
-
comp = d.get("compression", {})
|
| 976 |
-
for bk in ("low", "mid", "high"):
|
| 977 |
-
bp = comp.get(bk, {})
|
| 978 |
-
bp["threshold"] = max(-20.0, min(0.0, float(bp.get("threshold", -14.0))))
|
| 979 |
-
bp["ratio"] = max(1.0, min(20.0, float(bp.get("ratio", 1.0))))
|
| 980 |
-
bp["attack_ms"] = max(0.1, min(200.0, float(bp.get("attack_ms", 30.0))))
|
| 981 |
-
bp["release_ms"] = max(10.0, min(500.0, float(bp.get("release_ms", 150.0))))
|
| 982 |
-
comp[bk] = bp
|
| 983 |
-
d["compression"] = comp
|
| 984 |
-
|
| 985 |
-
return d
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
def _format_super_settings(params):
|
| 989 |
-
"""Format Super AI parameters into readable text for prompts."""
|
| 990 |
-
eq = params.get("eq", {})
|
| 991 |
-
comp = params.get("compression", {})
|
| 992 |
-
lines = [
|
| 993 |
-
f"- HPF: {params.get('hpf_freq', 15)} Hz",
|
| 994 |
-
]
|
| 995 |
-
for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):
|
| 996 |
-
b = eq.get(bk, {})
|
| 997 |
-
if abs(b.get("gain_db", 0)) < 0.01:
|
| 998 |
-
lines.append(f"- EQ {bk}: bypassed (0 dB)")
|
| 999 |
-
else:
|
| 1000 |
-
lines.append(f"- EQ {bk}: {b.get('type','peak')} @ {b.get('freq',1000)} Hz, "
|
| 1001 |
-
f"{b.get('gain_db',0):+.1f} dB, Q={b.get('q',1.0):.2f}")
|
| 1002 |
-
lines.append(f"- Crossovers: {params.get('crossover_low', 200)} Hz / {params.get('crossover_high', 4000)} Hz")
|
| 1003 |
-
for bk in ("low", "mid", "high"):
|
| 1004 |
-
bp = comp.get(bk, {})
|
| 1005 |
-
lines.append(f"- Comp {bk}: threshold {bp.get('threshold',-14):.1f} dB, "
|
| 1006 |
-
f"ratio {bp.get('ratio',1.0):.2f}:1, "
|
| 1007 |
-
f"attack {bp.get('attack_ms',30):.1f} ms, "
|
| 1008 |
-
f"release {bp.get('release_ms',150):.1f} ms")
|
| 1009 |
-
lines.append(f"- Stereo Width: {params.get('stereo_width', 100)}%")
|
| 1010 |
-
return "\n".join(lines)
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
def _format_detailed_features(features, label="Audio"):
|
| 1014 |
-
"""Format detailed features from extract_features_detailed() for prompts."""
|
| 1015 |
-
lines = [
|
| 1016 |
-
f"**{label} — Core Measurements:**",
|
| 1017 |
-
f"- Loudness: {features['lufs']} LUFS | True Peak: {features['true_peak_dbtp']} dBTP",
|
| 1018 |
-
f"- RMS: {features['rms_db']} dB | Crest Factor: {features['crest_factor_db']} dB",
|
| 1019 |
-
f"- Spectral Centroid: {features['spectral_centroid_hz']} Hz | Rolloff: {features['spectral_rolloff_hz']} Hz",
|
| 1020 |
-
f"- Spectral Flatness: {features.get('spectral_flatness', 'N/A')} (0=tonal, 1=noise)",
|
| 1021 |
-
f"- Spectral Tilt: {features.get('spectral_tilt_db_per_decade', 'N/A')} dB/decade (negative=bass-heavy, positive=bright)",
|
| 1022 |
-
f"- Stereo Correlation: {features['stereo_correlation'] if features['stereo_correlation'] is not None else 'N/A (mono)'}",
|
| 1023 |
-
]
|
| 1024 |
-
|
| 1025 |
-
# 24-band spectral profile
|
| 1026 |
-
profile = features.get("spectral_profile_24band")
|
| 1027 |
-
if profile:
|
| 1028 |
-
lines.append(f"\n**{label} — 24-Band Spectral Profile (dB):**")
|
| 1029 |
-
for band, val in profile.items():
|
| 1030 |
-
bar = "█" * max(0, int((val + 60) / 2)) if val > -60 else ""
|
| 1031 |
-
lines.append(f" {band:>8s}: {val:>7.1f} dB {bar}")
|
| 1032 |
-
|
| 1033 |
-
# Resonances
|
| 1034 |
-
resonances = features.get("resonances", [])
|
| 1035 |
-
if resonances:
|
| 1036 |
-
lines.append(f"\n**{label} — Spectral Resonances (peaks above neighbors):**")
|
| 1037 |
-
for r in resonances:
|
| 1038 |
-
lines.append(f" {r['freq_hz']:>8.1f} Hz: {r['level_db']:+.1f} dB "
|
| 1039 |
-
f"(prominence: {r['prominence_db']:.1f} dB)")
|
| 1040 |
-
|
| 1041 |
-
# Per-compression-band dynamics
|
| 1042 |
-
cbd = features.get("comp_band_dynamics")
|
| 1043 |
-
if cbd:
|
| 1044 |
-
lines.append(f"\n**{label} — Per-Compression-Band Dynamics:**")
|
| 1045 |
-
for band_name in ("low", "mid", "high"):
|
| 1046 |
-
bd = cbd.get(band_name, {})
|
| 1047 |
-
lines.append(f" {band_name:>4s}: RMS {bd.get('rms_db', '?')} dB, "
|
| 1048 |
-
f"Peak {bd.get('peak_db', '?')} dB, "
|
| 1049 |
-
f"Crest {bd.get('crest_db', '?')} dB")
|
| 1050 |
-
|
| 1051 |
-
# Dynamic variation
|
| 1052 |
-
dv = features.get("dynamic_variation")
|
| 1053 |
-
if dv:
|
| 1054 |
-
lines.append(f"\n**{label} — Dynamic Variation (4-sec chunks, {dv.get('n_chunks', '?')} chunks):**")
|
| 1055 |
-
lines.append(f" RMS range: {dv.get('min_rms_db', '?')} to {dv.get('max_rms_db', '?')} dB "
|
| 1056 |
-
f"(span: {dv.get('range_db', '?')} dB, σ: {dv.get('std_db', '?')} dB)")
|
| 1057 |
-
|
| 1058 |
-
# Per-band stereo correlation
|
| 1059 |
-
sbc = features.get("stereo_band_correlation")
|
| 1060 |
-
if sbc:
|
| 1061 |
-
lines.append(f"\n**{label} — Per-Band Stereo Correlation:**")
|
| 1062 |
-
for band_name in ("low", "low_mid", "high_mid", "high"):
|
| 1063 |
-
val = sbc.get(band_name)
|
| 1064 |
-
lines.append(f" {band_name:>8s}: {val if val is not None else 'N/A'}")
|
| 1065 |
-
|
| 1066 |
-
# Original 6-band energy for backward compat
|
| 1067 |
-
lines.append(f"\n**{label} — 6-Band Energy Summary:**")
|
| 1068 |
-
for k, v in features.get("band_energy", {}).items():
|
| 1069 |
-
lines.append(f" {k}: {v} dB")
|
| 1070 |
-
|
| 1071 |
-
return "\n".join(lines)
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
def _format_super_history(history):
|
| 1075 |
-
"""Format Super AI analysis history for prompts."""
|
| 1076 |
-
if not history:
|
| 1077 |
-
return ""
|
| 1078 |
-
lines = ["\n\n**PREVIOUS ANALYSIS HISTORY** (use this to avoid oscillating — refine, don't reset):"]
|
| 1079 |
-
for i, entry in enumerate(history, 1):
|
| 1080 |
-
lines.append(f"\n--- Pass {i} ---")
|
| 1081 |
-
lines.append(f"Settings:\n{_format_super_settings(entry.get('params', {}))}")
|
| 1082 |
-
lines.append(f"Result: LUFS={entry.get('lufs', '?')}, True Peak={entry.get('true_peak', '?')} dBTP, "
|
| 1083 |
-
f"Crest Factor={entry.get('crest_factor', '?')} dB")
|
| 1084 |
-
if entry.get("summary"):
|
| 1085 |
-
lines.append(f"AI assessment: {entry['summary']}")
|
| 1086 |
-
lines.append("\nIMPORTANT: Do NOT oscillate. Refine incrementally. If a setting helped, keep it and fine-tune. "
|
| 1087 |
-
"If it didn't help, try a DIFFERENT approach rather than reverting.")
|
| 1088 |
-
return "\n".join(lines)
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
def super_ai_recommend(audio_path):
|
| 1092 |
-
"""Analyze raw audio and return full-parametric AI mastering settings.
|
| 1093 |
-
|
| 1094 |
-
Returns:
|
| 1095 |
-
dict with full parameter set + reasoning, or None.
|
| 1096 |
-
"""
|
| 1097 |
-
from dsp import load_audio
|
| 1098 |
-
audio, sr = load_audio(audio_path)
|
| 1099 |
-
features = extract_features_detailed(audio, sr)
|
| 1100 |
-
|
| 1101 |
-
user_prompt = f"""Analyze this audio and recommend full mastering parameters:
|
| 1102 |
-
|
| 1103 |
-
{_format_detailed_features(features, "INPUT")}
|
| 1104 |
-
|
| 1105 |
-
Return the JSON object with your recommended full parameter set.
|
| 1106 |
-
Use the 24-band spectral profile to make precise EQ decisions.
|
| 1107 |
-
Use the resonances to identify frequencies that need surgical cuts.
|
| 1108 |
-
Use the per-band dynamics to set compression thresholds and ratios.
|
| 1109 |
-
Use the dynamic variation to decide how aggressively to compress."""
|
| 1110 |
-
|
| 1111 |
-
response = _call_gemini(_SUPER_RECOMMEND_SYSTEM, user_prompt)
|
| 1112 |
-
if response is None:
|
| 1113 |
-
return None
|
| 1114 |
-
|
| 1115 |
-
try:
|
| 1116 |
-
result = _strip_json(response)
|
| 1117 |
-
_clamp_super_params(result)
|
| 1118 |
-
if "reasoning" not in result:
|
| 1119 |
-
result["reasoning"] = "*No explanation provided.*"
|
| 1120 |
-
return result
|
| 1121 |
-
except (json.JSONDecodeError, KeyError, TypeError):
|
| 1122 |
-
return {"reasoning": response, "parse_error": True}
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
def super_ai_compare(original, mastered, sample_rate, params, target_lufs,
|
| 1126 |
-
history=None):
|
| 1127 |
-
"""Compare original vs mastered with full-parametric revision.
|
| 1128 |
-
|
| 1129 |
-
Returns:
|
| 1130 |
-
dict with revised full params + report, or None.
|
| 1131 |
-
"""
|
| 1132 |
-
orig_features = extract_features_detailed(original, sample_rate)
|
| 1133 |
-
mast_features = extract_features_detailed(mastered, sample_rate)
|
| 1134 |
-
history_text = _format_super_history(history or [])
|
| 1135 |
-
|
| 1136 |
-
user_prompt = f"""Compare original vs mastered audio and return revised full parameters:
|
| 1137 |
-
|
| 1138 |
-
{_format_detailed_features(orig_features, "ORIGINAL")}
|
| 1139 |
-
|
| 1140 |
-
{_format_detailed_features(mast_features, "MASTERED")}
|
| 1141 |
-
|
| 1142 |
-
**Settings Applied:**
|
| 1143 |
-
{_format_super_settings(params)}
|
| 1144 |
-
- Target LUFS: {target_lufs}{history_text}
|
| 1145 |
-
|
| 1146 |
-
Return the JSON with your REVISED full parameter set and comparison report.
|
| 1147 |
-
Make SMALL, incremental adjustments — refine what's working, fix what isn't.
|
| 1148 |
-
Compare the 24-band profiles to see exactly where the EQ moved things.
|
| 1149 |
-
Compare per-band dynamics to evaluate compression effectiveness.
|
| 1150 |
-
Check if resonances were tamed or if new ones were introduced."""
|
| 1151 |
-
|
| 1152 |
-
response = _call_gemini(_SUPER_COMPARE_SYSTEM, user_prompt)
|
| 1153 |
-
if response is None:
|
| 1154 |
-
return None
|
| 1155 |
-
|
| 1156 |
-
try:
|
| 1157 |
-
result = _strip_json(response)
|
| 1158 |
-
_clamp_super_params(result)
|
| 1159 |
-
if "report" not in result:
|
| 1160 |
-
result["report"] = "*No report provided.*"
|
| 1161 |
-
return result
|
| 1162 |
-
except (json.JSONDecodeError, KeyError, TypeError):
|
| 1163 |
-
return {"report": response, "parse_error": True}
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
def super_ai_final_report(original, mastered, sample_rate, params, target_lufs,
|
| 1167 |
-
history=None):
|
| 1168 |
-
"""Generate a final quality assessment report (no new settings).
|
| 1169 |
-
|
| 1170 |
-
Returns:
|
| 1171 |
-
str: markdown report.
|
| 1172 |
-
"""
|
| 1173 |
-
orig_features = extract_features_detailed(original, sample_rate)
|
| 1174 |
-
mast_features = extract_features_detailed(mastered, sample_rate)
|
| 1175 |
-
history_text = _format_super_history(history or [])
|
| 1176 |
-
|
| 1177 |
-
user_prompt = f"""Write a final mastering quality report for this completed master:
|
| 1178 |
-
|
| 1179 |
-
{_format_detailed_features(orig_features, "ORIGINAL")}
|
| 1180 |
-
|
| 1181 |
-
{_format_detailed_features(mast_features, "MASTERED")}
|
| 1182 |
-
|
| 1183 |
-
**Final Settings Applied:**
|
| 1184 |
-
{_format_super_settings(params)}
|
| 1185 |
-
- Target LUFS: {target_lufs}{history_text}
|
| 1186 |
-
|
| 1187 |
-
Write the final quality report. No suggestions — just an honest assessment of whether this master meets professional standards.
|
| 1188 |
-
Reference specific frequency bands and measurements from the detailed analysis above."""
|
| 1189 |
-
|
| 1190 |
-
response = _call_gemini(_SUPER_FINAL_REPORT_SYSTEM, user_prompt)
|
| 1191 |
-
if response is None:
|
| 1192 |
-
return "*AI final report unavailable.*"
|
| 1193 |
-
return response
|
| 1194 |
-
# v4.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,233 +1,15 @@
|
|
| 1 |
"""Audio Mastering Suite — Gradio web application."""
|
| 2 |
|
| 3 |
-
import os
|
| 4 |
-
import gc
|
| 5 |
-
import time
|
| 6 |
import gradio as gr
|
| 7 |
-
from
|
| 8 |
-
from dsp import master_audio, master_audio_full
|
| 9 |
from presets import PRESETS
|
| 10 |
from visualization import plot_waveform_comparison, plot_spectrum_comparison
|
| 11 |
-
# Lazy-imported inside callbacks to avoid slowing app startup
|
| 12 |
-
# from analysis import recommend_settings, compare_master
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
_PRICING_URL = "https://huggingface.co/spaces/AnimalMonk/studio-ai-pricing"
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def _dsp_settings_md(dsp):
|
| 19 |
-
"""Format actual DSP parameters as a markdown table."""
|
| 20 |
-
if not dsp:
|
| 21 |
-
return ""
|
| 22 |
-
|
| 23 |
-
lines = [
|
| 24 |
-
"### DSP Settings",
|
| 25 |
-
"| Parameter | Value |",
|
| 26 |
-
"|---|---|",
|
| 27 |
-
f"| **HPF** | {dsp.get('hpf_freq', 15):.0f} Hz |",
|
| 28 |
-
]
|
| 29 |
-
|
| 30 |
-
# EQ — could be list (manual) or dict with band1-4 (super)
|
| 31 |
-
eq = dsp.get("eq", {})
|
| 32 |
-
if isinstance(eq, list):
|
| 33 |
-
for i, e in enumerate(eq):
|
| 34 |
-
lines.append(
|
| 35 |
-
f"| **EQ {i+1}** | {e.get('type','peak')} @ "
|
| 36 |
-
f"{e.get('freq',1000):.0f} Hz, {e.get('gain_db',0):+.1f} dB, "
|
| 37 |
-
f"Q={e.get('q',1.0):.2f} |"
|
| 38 |
-
)
|
| 39 |
-
if not eq:
|
| 40 |
-
lines.append("| **EQ** | flat (no adjustments) |")
|
| 41 |
-
else:
|
| 42 |
-
for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):
|
| 43 |
-
b = eq.get(bk, {})
|
| 44 |
-
g = b.get("gain_db", 0)
|
| 45 |
-
if abs(g) < 0.01:
|
| 46 |
-
lines.append(f"| **EQ {bk}** | bypassed |")
|
| 47 |
-
else:
|
| 48 |
-
lines.append(
|
| 49 |
-
f"| **EQ {bk}** | {b.get('type','peak')} @ "
|
| 50 |
-
f"{b.get('freq',1000):.0f} Hz, {g:+.1f} dB, "
|
| 51 |
-
f"Q={b.get('q',1.0):.2f} |"
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
# Crossovers
|
| 55 |
-
if "crossover_low" in dsp:
|
| 56 |
-
lines.append(
|
| 57 |
-
f"| **Crossovers** | {dsp.get('crossover_low',200):.0f} / "
|
| 58 |
-
f"{dsp.get('crossover_high',4000):.0f} Hz |"
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
# Compression
|
| 62 |
-
comp = dsp.get("compression", {})
|
| 63 |
-
if comp:
|
| 64 |
-
for bk in ("low", "mid", "high"):
|
| 65 |
-
bp = comp.get(bk, {})
|
| 66 |
-
r = bp.get("ratio", 1.0)
|
| 67 |
-
if r <= 1.0:
|
| 68 |
-
lines.append(f"| **Comp {bk}** | bypassed |")
|
| 69 |
-
else:
|
| 70 |
-
lines.append(
|
| 71 |
-
f"| **Comp {bk}** | {bp.get('threshold',-14):.0f} dB, "
|
| 72 |
-
f"{r:.1f}:1, atk {bp.get('attack_ms',30):.0f} ms, "
|
| 73 |
-
f"rel {bp.get('release_ms',150):.0f} ms |"
|
| 74 |
-
)
|
| 75 |
-
else:
|
| 76 |
-
lines.append("| **Compression** | bypassed |")
|
| 77 |
-
|
| 78 |
-
lines.append(f"| **Stereo Width** | {dsp.get('stereo_width', 100)}% |")
|
| 79 |
-
return "\n".join(lines)
|
| 80 |
-
|
| 81 |
-
_PREMIUM_MSG = (
|
| 82 |
-
"**AI features require a StudioAI API key.**\n\n"
|
| 83 |
-
f"**[Get an API Key]({_PRICING_URL})** — plans start at $9/month.\n\n"
|
| 84 |
-
"*Manual mastering is always free.*"
|
| 85 |
-
)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def _check_ai_key(user_key, consume=False):
|
| 89 |
-
"""Validate user-provided AI access key via Unkey.
|
| 90 |
-
|
| 91 |
-
Args:
|
| 92 |
-
user_key: The key string from the user.
|
| 93 |
-
consume: If True, decrement one usage credit. If False, just validate
|
| 94 |
-
without consuming (cost=0).
|
| 95 |
-
|
| 96 |
-
Returns:
|
| 97 |
-
(valid: bool, error_msg: str, remaining: int|None)
|
| 98 |
-
"""
|
| 99 |
-
if not user_key:
|
| 100 |
-
return (False, _PREMIUM_MSG, None)
|
| 101 |
-
key = user_key.strip()
|
| 102 |
-
|
| 103 |
-
# Legacy key support (transition period)
|
| 104 |
-
for env_var in ("AI_ACCESS_KEY", "AI_ACCESS_KEY_2"):
|
| 105 |
-
expected = os.environ.get(env_var, "")
|
| 106 |
-
if expected and key == expected.strip():
|
| 107 |
-
return (True, "", None) # legacy keys — no usage limit
|
| 108 |
-
|
| 109 |
-
# Unkey verification — checks key validity and rate limits
|
| 110 |
-
unkey_api_id = os.environ.get("UNKEY_API_ID", "")
|
| 111 |
-
if not unkey_api_id:
|
| 112 |
-
# Unkey not configured — fall back to rejecting unknown keys
|
| 113 |
-
return (False, _PREMIUM_MSG, None)
|
| 114 |
-
|
| 115 |
-
try:
|
| 116 |
-
import requests as _req
|
| 117 |
-
unkey_root = os.environ.get("UNKEY_ROOT_KEY", "")
|
| 118 |
-
auth_headers = {
|
| 119 |
-
"Content-Type": "application/json",
|
| 120 |
-
"Authorization": f"Bearer {unkey_root}",
|
| 121 |
-
}
|
| 122 |
-
cost = 1 if consume else 0
|
| 123 |
-
|
| 124 |
-
# Try with rate limits first (subscription keys with monthly_masters)
|
| 125 |
-
resp = _req.post(
|
| 126 |
-
"https://api.unkey.com/v2/keys.verifyKey",
|
| 127 |
-
headers=auth_headers,
|
| 128 |
-
json={"key": key, "ratelimits": [{"name": "monthly_masters", "cost": cost}]},
|
| 129 |
-
timeout=5,
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
# If rate limit doesn't exist on this key, Unkey returns 412.
|
| 133 |
-
# Fall back to credits-only verification.
|
| 134 |
-
if resp.status_code == 412:
|
| 135 |
-
resp = _req.post(
|
| 136 |
-
"https://api.unkey.com/v2/keys.verifyKey",
|
| 137 |
-
headers=auth_headers,
|
| 138 |
-
json={"key": key},
|
| 139 |
-
timeout=5,
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
-
data = resp.json()
|
| 143 |
-
except Exception:
|
| 144 |
-
return (True, "", None) # fail open on network error
|
| 145 |
-
|
| 146 |
-
result = data.get("data", data)
|
| 147 |
-
|
| 148 |
-
if not result.get("valid", False):
|
| 149 |
-
code = result.get("code", "UNKNOWN")
|
| 150 |
-
msgs = {
|
| 151 |
-
"NOT_FOUND": f"Invalid API key. [Get one here]({_PRICING_URL}).",
|
| 152 |
-
"RATE_LIMITED": (
|
| 153 |
-
"You've used all your AI masters this month.\n\n"
|
| 154 |
-
f"[Upgrade your plan]({_PRICING_URL}) for more."
|
| 155 |
-
),
|
| 156 |
-
"USAGE_EXCEEDED": (
|
| 157 |
-
"You've used all your AI masters this month.\n\n"
|
| 158 |
-
f"[Upgrade your plan]({_PRICING_URL}) for more."
|
| 159 |
-
),
|
| 160 |
-
"DISABLED": "Your API key has been disabled. Contact support.",
|
| 161 |
-
"EXPIRED": f"Your subscription has expired. [Renew here]({_PRICING_URL}).",
|
| 162 |
-
}
|
| 163 |
-
return (False, msgs.get(code, f"Key validation failed ({code})."), 0)
|
| 164 |
-
|
| 165 |
-
# Check remaining from either rate limits or credits
|
| 166 |
-
remaining = None
|
| 167 |
-
ratelimits = result.get("ratelimits", [])
|
| 168 |
-
for rl in ratelimits:
|
| 169 |
-
if rl.get("name") == "monthly_masters":
|
| 170 |
-
remaining = rl.get("remaining")
|
| 171 |
-
break
|
| 172 |
-
if remaining is None:
|
| 173 |
-
remaining = result.get("credits", result.get("remaining"))
|
| 174 |
-
|
| 175 |
-
return (True, "", remaining)
|
| 176 |
|
| 177 |
|
| 178 |
# ---------------------------------------------------------------------------
|
| 179 |
# Callbacks
|
| 180 |
# ---------------------------------------------------------------------------
|
| 181 |
|
| 182 |
-
def check_key_status(user_key):
|
| 183 |
-
"""Check key validity and remaining uses without consuming one."""
|
| 184 |
-
if not user_key or not user_key.strip():
|
| 185 |
-
return ""
|
| 186 |
-
key = user_key.strip()
|
| 187 |
-
|
| 188 |
-
# Legacy keys
|
| 189 |
-
for env_var in ("AI_ACCESS_KEY", "AI_ACCESS_KEY_2"):
|
| 190 |
-
expected = os.environ.get(env_var, "")
|
| 191 |
-
if expected and key == expected.strip():
|
| 192 |
-
return "**Key valid** — unlimited uses (legacy key)"
|
| 193 |
-
|
| 194 |
-
unkey_api_id = os.environ.get("UNKEY_API_ID", "")
|
| 195 |
-
if not unkey_api_id:
|
| 196 |
-
return ""
|
| 197 |
-
|
| 198 |
-
try:
|
| 199 |
-
import requests as _req
|
| 200 |
-
resp = _req.post(
|
| 201 |
-
"https://api.unkey.com/v2/keys.verifyKey",
|
| 202 |
-
headers={"Authorization": f"Bearer {os.environ.get('UNKEY_ROOT_KEY', '')}"},
|
| 203 |
-
json={"key": key, "ratelimits": [{"name": "monthly_masters", "cost": 0}]},
|
| 204 |
-
timeout=5,
|
| 205 |
-
)
|
| 206 |
-
data = resp.json()
|
| 207 |
-
result = data.get("data", data)
|
| 208 |
-
except Exception:
|
| 209 |
-
return ""
|
| 210 |
-
|
| 211 |
-
if not result.get("valid", False):
|
| 212 |
-
code = result.get("code", "UNKNOWN")
|
| 213 |
-
if code == "NOT_FOUND":
|
| 214 |
-
return f"**Invalid key.** [Get one here]({_PRICING_URL})"
|
| 215 |
-
elif code in ("RATE_LIMITED", "USAGE_EXCEEDED"):
|
| 216 |
-
return "**No masters remaining this month.** [Upgrade]({_PRICING_URL})"
|
| 217 |
-
elif code == "DISABLED":
|
| 218 |
-
return "**Key disabled.** Contact support."
|
| 219 |
-
return f"**Key error:** {code}"
|
| 220 |
-
|
| 221 |
-
tier = result.get("meta", {}).get("tier", "unknown")
|
| 222 |
-
ratelimits = result.get("ratelimits", [])
|
| 223 |
-
for rl in ratelimits:
|
| 224 |
-
if rl.get("name") == "monthly_masters":
|
| 225 |
-
remaining = rl.get("remaining", "?")
|
| 226 |
-
limit = rl.get("limit", "?")
|
| 227 |
-
return f"**{tier.capitalize()}** — {remaining}/{limit} AI masters remaining this month"
|
| 228 |
-
return f"**{tier.capitalize()}** — key valid"
|
| 229 |
-
|
| 230 |
-
|
| 231 |
def apply_preset(preset_name):
|
| 232 |
"""Update all sliders when a preset is selected."""
|
| 233 |
if preset_name == "-- None --" or preset_name not in PRESETS or PRESETS[preset_name] is None:
|
|
@@ -253,77 +35,6 @@ def toggle_custom_lufs(target_choice):
|
|
| 253 |
return gr.update(visible=(target_choice == "Custom"))
|
| 254 |
|
| 255 |
|
| 256 |
-
def _lock_btns():
|
| 257 |
-
"""Return 6 gr.Button updates to disable all AI/action buttons."""
|
| 258 |
-
return tuple(gr.Button(interactive=False) for _ in range(6))
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
def _unlock_btns():
|
| 262 |
-
"""Return 6 gr.Button updates to re-enable all AI/action buttons."""
|
| 263 |
-
return tuple(gr.Button(interactive=True) for _ in range(6))
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
def ai_recommend(audio_path, ai_access_key):
|
| 267 |
-
"""Analyze raw audio and return AI-recommended settings + reasoning.
|
| 268 |
-
|
| 269 |
-
Yields 14 values: 7 sliders + ai_values_state + ai_reasoning_display + 5 buttons.
|
| 270 |
-
"""
|
| 271 |
-
_skip = gr.Slider()
|
| 272 |
-
_no_change = (_skip, _skip, _skip, _skip, _skip, _skip, _skip, {})
|
| 273 |
-
|
| 274 |
-
valid, err, _remaining = _check_ai_key(ai_access_key)
|
| 275 |
-
if not valid:
|
| 276 |
-
yield (*_no_change, err, *_unlock_btns())
|
| 277 |
-
return
|
| 278 |
-
|
| 279 |
-
if audio_path is None:
|
| 280 |
-
raise gr.Error("Please upload an audio file first.")
|
| 281 |
-
|
| 282 |
-
# Immediate feedback
|
| 283 |
-
yield (*_no_change, "# \u26a0\ufe0f AI WORKING \u2014 PLEASE WAIT\n\n---\n\n### \u23f3 Analyzing audio with Gemini\u2026\n*This takes 15-30 seconds.*",
|
| 284 |
-
*_lock_btns(),
|
| 285 |
-
)
|
| 286 |
-
|
| 287 |
-
from analysis import recommend_settings
|
| 288 |
-
result = recommend_settings(audio_path)
|
| 289 |
-
|
| 290 |
-
if result is None:
|
| 291 |
-
yield (*_no_change, "*Set GOOGLE_API_KEY to enable AI recommendations.*", *_unlock_btns())
|
| 292 |
-
return
|
| 293 |
-
|
| 294 |
-
if result.get("parse_error"):
|
| 295 |
-
yield (*_no_change, result.get("reasoning", "*Could not parse AI response.*"), *_unlock_btns())
|
| 296 |
-
return
|
| 297 |
-
|
| 298 |
-
yield (
|
| 299 |
-
gr.Slider(label=f"Lows (200 Hz) | AI: {result['lows_db']:+.1f} dB"),
|
| 300 |
-
gr.Slider(label=f"Mids (1.2 kHz) | AI: {result['mid_boost_db']:+.1f} dB"),
|
| 301 |
-
gr.Slider(label=f"Highs (10 kHz) | AI: {result['highs_db']:+.1f} dB"),
|
| 302 |
-
gr.Slider(label=f"Bass Boost (dB) | AI: {result['bass_boost_db']:.1f} dB"),
|
| 303 |
-
gr.Slider(label=f"Bass Freq (Hz) | AI: {result['bass_freq_hz']} Hz"),
|
| 304 |
-
gr.Slider(label=f"Compression | AI: {result['compression']}"),
|
| 305 |
-
gr.Slider(label=f"Stereo Width (%) | AI: {result['stereo_width']}%"),
|
| 306 |
-
result,
|
| 307 |
-
result.get("reasoning", ""),
|
| 308 |
-
*_unlock_btns(),
|
| 309 |
-
)
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
def apply_ai(ai_values):
|
| 313 |
-
"""Populate sliders with AI-recommended values, reset labels."""
|
| 314 |
-
if not ai_values:
|
| 315 |
-
raise gr.Error("No AI recommendations yet. Click 'AI Recommend' first.")
|
| 316 |
-
return (
|
| 317 |
-
gr.Slider(value=ai_values["lows_db"], label="Lows (200 Hz)"),
|
| 318 |
-
gr.Slider(value=ai_values["mid_boost_db"], label="Mids (1.2 kHz)"),
|
| 319 |
-
gr.Slider(value=ai_values["highs_db"], label="Highs (10 kHz)"),
|
| 320 |
-
gr.Slider(value=ai_values["bass_boost_db"], label="Bass Boost (dB)"),
|
| 321 |
-
gr.Slider(value=ai_values["bass_freq_hz"], label="Bass Boost Frequency (Hz)"),
|
| 322 |
-
gr.Slider(value=ai_values["compression"], label="Compression: Less <-> More"),
|
| 323 |
-
gr.Slider(value=ai_values["stereo_width"], label="Stereo Width (%)"),
|
| 324 |
-
)
|
| 325 |
-
|
| 326 |
-
|
| 327 |
def process(audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz,
|
| 328 |
comp_val, width, target_choice, custom_lufs):
|
| 329 |
"""Run the mastering pipeline and return all outputs."""
|
|
@@ -338,650 +49,40 @@ def process(audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_fre
|
|
| 338 |
else:
|
| 339 |
target = float(custom_lufs)
|
| 340 |
|
|
|
|
| 341 |
output_path, original, mastered, sr, stats = master_audio(
|
| 342 |
audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz,
|
| 343 |
-
comp_val, width, target,
|
| 344 |
)
|
| 345 |
|
| 346 |
# Plots
|
| 347 |
waveform_fig = plot_waveform_comparison(original, mastered, sr)
|
| 348 |
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
|
| 349 |
|
| 350 |
-
# Stats markdown
|
| 351 |
mono_note = "\n\n*Input is mono — stereo width adjustment was skipped.*" if stats["mono"] else ""
|
| 352 |
-
|
| 353 |
stats_md = (
|
| 354 |
"### Loudness Statistics\n"
|
| 355 |
"| | LUFS | True Peak (dBTP) |\n"
|
| 356 |
"|---|---|---|\n"
|
| 357 |
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
|
| 358 |
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
)
|
| 361 |
|
| 362 |
-
# Store data for on-demand AI comparison
|
| 363 |
-
master_data = {
|
| 364 |
-
"original": original,
|
| 365 |
-
"mastered": mastered,
|
| 366 |
-
"sr": sr,
|
| 367 |
-
"settings": {
|
| 368 |
-
"lows_db": lows_db,
|
| 369 |
-
"mid_boost_db": mid_boost_db,
|
| 370 |
-
"highs_db": highs_db,
|
| 371 |
-
"bass_boost_db": bass_boost_db,
|
| 372 |
-
"bass_freq_hz": bass_freq_hz,
|
| 373 |
-
"compression": comp_val,
|
| 374 |
-
"stereo_width": width,
|
| 375 |
-
"target_lufs": target,
|
| 376 |
-
},
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
return (
|
| 380 |
output_path,
|
| 381 |
waveform_fig, spectrum_fig,
|
| 382 |
-
|
| 383 |
-
gr.Button("AI Compare Original vs Master", variant="secondary", visible=True),
|
| 384 |
gr.DownloadButton("Download Mastered File", value=output_path, visible=True),
|
| 385 |
-
master_data,
|
| 386 |
-
"",
|
| 387 |
-
)
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
def _slider_updates(values):
|
| 391 |
-
"""Return 7 gr.Slider updates from a settings dict."""
|
| 392 |
-
return (
|
| 393 |
-
gr.Slider(value=values["lows_db"]),
|
| 394 |
-
gr.Slider(value=values["mid_boost_db"]),
|
| 395 |
-
gr.Slider(value=values["highs_db"]),
|
| 396 |
-
gr.Slider(value=values["bass_boost_db"]),
|
| 397 |
-
gr.Slider(value=values["bass_freq_hz"]),
|
| 398 |
-
gr.Slider(value=values["compression"]),
|
| 399 |
-
gr.Slider(value=values["stereo_width"]),
|
| 400 |
-
)
|
| 401 |
-
|
| 402 |
-
_NO_SLIDER_CHANGE = tuple(gr.update() for _ in range(7))
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
def _make_history_entry(settings_dict, mast_features, summary=""):
|
| 406 |
-
"""Build a history entry dict for analysis context."""
|
| 407 |
-
return {
|
| 408 |
-
"settings": {k: v for k, v in settings_dict.items() if k != "target_lufs"},
|
| 409 |
-
"lufs": mast_features.get("lufs", "?"),
|
| 410 |
-
"true_peak": mast_features.get("true_peak_dbtp", "?"),
|
| 411 |
-
"crest_factor": mast_features.get("crest_factor_db", "?"),
|
| 412 |
-
"summary": summary,
|
| 413 |
-
}
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
def ai_compare(master_data, ai_access_key, history):
|
| 417 |
-
"""Run AI comparison report on demand (costs Gemini tokens).
|
| 418 |
-
|
| 419 |
-
Yields 15 values: 7 sliders + ai_values_state + ai_report_display
|
| 420 |
-
+ analysis_history_state + 5 buttons.
|
| 421 |
-
"""
|
| 422 |
-
SKIP = gr.update()
|
| 423 |
-
valid, err, _remaining = _check_ai_key(ai_access_key)
|
| 424 |
-
if not valid:
|
| 425 |
-
yield (
|
| 426 |
-
*_NO_SLIDER_CHANGE,
|
| 427 |
-
SKIP,
|
| 428 |
-
err,
|
| 429 |
-
SKIP, # history unchanged
|
| 430 |
-
*_unlock_btns(),
|
| 431 |
-
)
|
| 432 |
-
return
|
| 433 |
-
|
| 434 |
-
if not master_data:
|
| 435 |
-
raise gr.Error("Master a track first, then click AI Compare.")
|
| 436 |
-
|
| 437 |
-
# Immediate feedback -- lock buttons + prominent indicator
|
| 438 |
-
yield (
|
| 439 |
-
*_NO_SLIDER_CHANGE,
|
| 440 |
-
SKIP,
|
| 441 |
-
"# \u26a0\ufe0f AI WORKING \u2014 PLEASE WAIT\n\n---\n\n### \u23f3 Comparing original vs master\u2026\n*This takes 15-30 seconds.*",
|
| 442 |
-
SKIP, # history unchanged
|
| 443 |
-
*_lock_btns(),
|
| 444 |
-
)
|
| 445 |
-
try:
|
| 446 |
-
from analysis import compare_master_structured, extract_features
|
| 447 |
-
result = compare_master_structured(
|
| 448 |
-
master_data["original"],
|
| 449 |
-
master_data["mastered"],
|
| 450 |
-
master_data["sr"],
|
| 451 |
-
master_data["settings"],
|
| 452 |
-
history=history,
|
| 453 |
-
)
|
| 454 |
-
if result is None or result.get("parse_error"):
|
| 455 |
-
report = result.get("report", "*AI comparison unavailable.*") if result else "*AI unavailable.*"
|
| 456 |
-
yield (
|
| 457 |
-
*_NO_SLIDER_CHANGE,
|
| 458 |
-
SKIP,
|
| 459 |
-
"---\n### AI Post-Master Analysis\n" + report,
|
| 460 |
-
SKIP, # history unchanged
|
| 461 |
-
*_unlock_btns(),
|
| 462 |
-
)
|
| 463 |
-
else:
|
| 464 |
-
report = result.get("report", "*No report provided.*")
|
| 465 |
-
# Add this analysis to history
|
| 466 |
-
mast_features = extract_features(master_data["mastered"], master_data["sr"])
|
| 467 |
-
new_history = list(history) + [_make_history_entry(
|
| 468 |
-
master_data["settings"], mast_features,
|
| 469 |
-
summary=report[:200] if report else "",
|
| 470 |
-
)]
|
| 471 |
-
yield (
|
| 472 |
-
*_slider_updates(result),
|
| 473 |
-
result,
|
| 474 |
-
"---\n### AI Post-Master Analysis\n" + report,
|
| 475 |
-
new_history,
|
| 476 |
-
*_unlock_btns(),
|
| 477 |
-
)
|
| 478 |
-
except Exception as e:
|
| 479 |
-
yield (
|
| 480 |
-
*_NO_SLIDER_CHANGE,
|
| 481 |
-
SKIP,
|
| 482 |
-
f"---\n### AI Post-Master Analysis\n*AI comparison unavailable: {e}*",
|
| 483 |
-
SKIP, # history unchanged
|
| 484 |
-
*_unlock_btns(),
|
| 485 |
-
)
|
| 486 |
-
|
| 487 |
-
def auto_master(audio_path, ai_access_key, target_choice, custom_lufs):
|
| 488 |
-
"""Iterative AI-driven mastering loop (generator).
|
| 489 |
-
|
| 490 |
-
Flow: AI Recommend → Master → AI Compare (×3 with revisions).
|
| 491 |
-
Yields progress to 24 outputs at each step (17 + 5 lockable buttons + history + dsp).
|
| 492 |
-
"""
|
| 493 |
-
gc.collect() # Free any previous run's data before starting
|
| 494 |
-
SKIP = gr.update()
|
| 495 |
-
_SLIDER_KEYS = ("lows_db", "mid_boost_db", "highs_db",
|
| 496 |
-
"bass_boost_db", "bass_freq_hz", "compression", "stereo_width")
|
| 497 |
-
|
| 498 |
-
def _yield_progress(progress, sliders=None, ai_vals=SKIP,
|
| 499 |
-
audio=SKIP, wf=SKIP, sp=SKIP, dsp=SKIP, stats=SKIP,
|
| 500 |
-
cmp_btn=SKIP, dl=SKIP, mdata=SKIP, report=SKIP,
|
| 501 |
-
btns=None, history=SKIP):
|
| 502 |
-
"""Build the 24-element tuple for each yield."""
|
| 503 |
-
s = sliders or (SKIP,) * 7
|
| 504 |
-
b = btns or (SKIP,) * 5
|
| 505 |
-
return (*s, ai_vals, progress,
|
| 506 |
-
audio, wf, sp, dsp, stats, cmp_btn, dl, mdata, report, *b, history)
|
| 507 |
-
|
| 508 |
-
def _slider_updates(vals):
|
| 509 |
-
"""Return 7 gr.Slider updates from a values dict."""
|
| 510 |
-
return (
|
| 511 |
-
gr.Slider(value=vals["lows_db"]),
|
| 512 |
-
gr.Slider(value=vals["mid_boost_db"]),
|
| 513 |
-
gr.Slider(value=vals["highs_db"]),
|
| 514 |
-
gr.Slider(value=vals["bass_boost_db"]),
|
| 515 |
-
gr.Slider(value=vals["bass_freq_hz"]),
|
| 516 |
-
gr.Slider(value=vals["compression"]),
|
| 517 |
-
gr.Slider(value=vals["stereo_width"]),
|
| 518 |
-
)
|
| 519 |
-
|
| 520 |
-
# --- Validation ---
|
| 521 |
-
valid, err, _remaining = _check_ai_key(ai_access_key)
|
| 522 |
-
if not valid:
|
| 523 |
-
yield _yield_progress(
|
| 524 |
-
err,
|
| 525 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 526 |
-
)
|
| 527 |
-
return
|
| 528 |
-
|
| 529 |
-
if audio_path is None:
|
| 530 |
-
raise gr.Error("Please upload an audio file first.")
|
| 531 |
-
|
| 532 |
-
if not os.path.exists(audio_path):
|
| 533 |
-
raise gr.Error("Audio file not found — please re-upload your file.")
|
| 534 |
-
|
| 535 |
-
# Resolve target LUFS
|
| 536 |
-
if target_choice == "-14 (Streaming)":
|
| 537 |
-
target = -14.0
|
| 538 |
-
elif target_choice == "-11 (CD)":
|
| 539 |
-
target = -11.0
|
| 540 |
-
else:
|
| 541 |
-
target = float(custom_lufs)
|
| 542 |
-
|
| 543 |
-
# === STEP 1: AI Recommend ===
|
| 544 |
-
P = "## \U0001f3db Auto Master\n\n" # progress lines (clean, no analysis text)
|
| 545 |
-
P += "**Pass 1 of 3**\n\n"
|
| 546 |
-
P += "\u23f3 *Analyzing audio with Gemini...*"
|
| 547 |
-
yield _yield_progress(P, history=[])
|
| 548 |
-
|
| 549 |
-
from analysis import recommend_settings, compare_master_structured, compare_master, extract_features
|
| 550 |
-
loop_history = [] # accumulate history across passes
|
| 551 |
-
try:
|
| 552 |
-
result = recommend_settings(audio_path)
|
| 553 |
-
except GeminiUnavailableError as e:
|
| 554 |
-
yield _yield_progress(
|
| 555 |
-
str(e),
|
| 556 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 557 |
-
)
|
| 558 |
-
return
|
| 559 |
-
|
| 560 |
-
if result is None:
|
| 561 |
-
yield _yield_progress("*Set GOOGLE_API_KEY to enable AI recommendations.*")
|
| 562 |
-
return
|
| 563 |
-
if result.get("parse_error"):
|
| 564 |
-
yield _yield_progress(result.get("reasoning", "*Could not parse AI response.*"))
|
| 565 |
-
return
|
| 566 |
-
|
| 567 |
-
values = {k: result[k] for k in _SLIDER_KEYS}
|
| 568 |
-
|
| 569 |
-
P += "\n\n\u2705 AI recommended initial settings"
|
| 570 |
-
P += "\n\n\u23f3 *Mastering with initial settings...*"
|
| 571 |
-
yield _yield_progress(P, sliders=_slider_updates(values), ai_vals=values)
|
| 572 |
-
|
| 573 |
-
# === STEP 2: Master pass 1 ===
|
| 574 |
-
output_path, original, mastered, sr, stats = master_audio(
|
| 575 |
-
audio_path,
|
| 576 |
-
values["lows_db"], values["mid_boost_db"], values["highs_db"],
|
| 577 |
-
values["bass_boost_db"], values["bass_freq_hz"],
|
| 578 |
-
values["compression"], values["stereo_width"], target,
|
| 579 |
)
|
| 580 |
|
| 581 |
-
P += "\n\n\u2705 Pass 1 mastered"
|
| 582 |
-
P += "\n\n\u23f3 *Waiting before next AI call...*"
|
| 583 |
-
yield _yield_progress(P)
|
| 584 |
-
time.sleep(15)
|
| 585 |
-
|
| 586 |
-
P += "\n\n\u23f3 *AI analyzing master and recommending adjustments...*"
|
| 587 |
-
yield _yield_progress(P)
|
| 588 |
-
|
| 589 |
-
# === STEP 3: AI Compare (structured) — iteration 1 ===
|
| 590 |
-
settings_dict = {**values, "target_lufs": target}
|
| 591 |
-
mast_feat = extract_features(mastered, sr)
|
| 592 |
-
loop_history.append(_make_history_entry(settings_dict, mast_feat, "Pass 1 initial"))
|
| 593 |
-
try:
|
| 594 |
-
result2 = compare_master_structured(original, mastered, sr, settings_dict,
|
| 595 |
-
history=loop_history)
|
| 596 |
-
except GeminiUnavailableError as e:
|
| 597 |
-
yield _yield_progress(
|
| 598 |
-
str(e),
|
| 599 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 600 |
-
)
|
| 601 |
-
return
|
| 602 |
-
|
| 603 |
-
if result2 is None or result2.get("parse_error"):
|
| 604 |
-
P += "\n\n\u26a0\ufe0f Could not parse AI adjustments \u2014 keeping current settings"
|
| 605 |
-
else:
|
| 606 |
-
values = {k: result2[k] for k in _SLIDER_KEYS}
|
| 607 |
-
P += "\n\n\u2705 AI suggested adjustments applied"
|
| 608 |
-
del result2
|
| 609 |
-
|
| 610 |
-
P += "\n\n---\n\n**Pass 2 of 3**\n\n"
|
| 611 |
-
P += "\u23f3 *Re-mastering with revised settings...*"
|
| 612 |
-
yield _yield_progress(P, sliders=_slider_updates(values), ai_vals=values)
|
| 613 |
-
|
| 614 |
-
# === STEP 4: Master pass 2 ===
|
| 615 |
-
output_path, original, mastered, sr, stats = master_audio(
|
| 616 |
-
audio_path,
|
| 617 |
-
values["lows_db"], values["mid_boost_db"], values["highs_db"],
|
| 618 |
-
values["bass_boost_db"], values["bass_freq_hz"],
|
| 619 |
-
values["compression"], values["stereo_width"], target,
|
| 620 |
-
)
|
| 621 |
-
|
| 622 |
-
P += "\n\n\u2705 Pass 2 mastered"
|
| 623 |
-
P += "\n\n\u23f3 *Waiting before next AI call...*"
|
| 624 |
-
yield _yield_progress(P)
|
| 625 |
-
time.sleep(15)
|
| 626 |
-
|
| 627 |
-
P += "\n\n\u23f3 *AI analyzing and recommending final adjustments...*"
|
| 628 |
-
yield _yield_progress(P)
|
| 629 |
-
|
| 630 |
-
# === STEP 5: AI Compare (structured) — iteration 2 ===
|
| 631 |
-
settings_dict = {**values, "target_lufs": target}
|
| 632 |
-
mast_feat = extract_features(mastered, sr)
|
| 633 |
-
loop_history.append(_make_history_entry(settings_dict, mast_feat, "Pass 2"))
|
| 634 |
-
try:
|
| 635 |
-
result3 = compare_master_structured(original, mastered, sr, settings_dict,
|
| 636 |
-
history=loop_history)
|
| 637 |
-
except GeminiUnavailableError as e:
|
| 638 |
-
yield _yield_progress(
|
| 639 |
-
str(e),
|
| 640 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 641 |
-
)
|
| 642 |
-
return
|
| 643 |
-
|
| 644 |
-
if result3 is None or result3.get("parse_error"):
|
| 645 |
-
P += "\n\n\u26a0\ufe0f Could not parse AI adjustments \u2014 keeping current settings"
|
| 646 |
-
else:
|
| 647 |
-
values = {k: result3[k] for k in _SLIDER_KEYS}
|
| 648 |
-
P += "\n\n\u2705 AI suggested final adjustments applied"
|
| 649 |
-
del result3
|
| 650 |
-
|
| 651 |
-
P += "\n\n---\n\n**Pass 3 of 3**\n\n"
|
| 652 |
-
P += "\u23f3 *Final mastering pass...*"
|
| 653 |
-
yield _yield_progress(P, sliders=_slider_updates(values), ai_vals=values)
|
| 654 |
-
|
| 655 |
-
# === STEP 6: Master pass 3 (final) ===
|
| 656 |
-
output_path, original, mastered, sr, stats = master_audio(
|
| 657 |
-
audio_path,
|
| 658 |
-
values["lows_db"], values["mid_boost_db"], values["highs_db"],
|
| 659 |
-
values["bass_boost_db"], values["bass_freq_hz"],
|
| 660 |
-
values["compression"], values["stereo_width"], target,
|
| 661 |
-
)
|
| 662 |
-
|
| 663 |
-
P += "\n\n\u2705 Final master complete"
|
| 664 |
-
P += "\n\n\u23f3 *Waiting before final AI call...*"
|
| 665 |
-
yield _yield_progress(P)
|
| 666 |
-
time.sleep(15)
|
| 667 |
-
|
| 668 |
-
P += "\n\n\u23f3 *Running final AI analysis...*"
|
| 669 |
-
yield _yield_progress(P)
|
| 670 |
-
|
| 671 |
-
# === STEP 7: Final AI Compare (markdown only) ===
|
| 672 |
-
mast_feat = extract_features(mastered, sr)
|
| 673 |
-
loop_history.append(_make_history_entry({**values, "target_lufs": target}, mast_feat, "Pass 3 final"))
|
| 674 |
-
try:
|
| 675 |
-
final_report = compare_master(
|
| 676 |
-
original, mastered, sr, {**values, "target_lufs": target},
|
| 677 |
-
history=loop_history,
|
| 678 |
-
)
|
| 679 |
-
except GeminiUnavailableError as e:
|
| 680 |
-
# Final report is non-critical — mastering is done
|
| 681 |
-
final_report = str(e)
|
| 682 |
-
except Exception as e:
|
| 683 |
-
final_report = f"*AI comparison unavailable: {e}*"
|
| 684 |
-
|
| 685 |
-
# Generate plots and stats
|
| 686 |
-
waveform_fig = plot_waveform_comparison(original, mastered, sr)
|
| 687 |
-
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
|
| 688 |
-
|
| 689 |
-
mono_note = "\n\n*Input is mono \u2014 stereo width adjustment was skipped.*" if stats["mono"] else ""
|
| 690 |
-
dsp_md = _dsp_settings_md(stats.get("dsp", {}))
|
| 691 |
-
stats_md = (
|
| 692 |
-
"### Loudness Statistics\n"
|
| 693 |
-
"| | LUFS | True Peak (dBTP) |\n"
|
| 694 |
-
"|---|---|---|\n"
|
| 695 |
-
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
|
| 696 |
-
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
|
| 697 |
-
f"{mono_note}"
|
| 698 |
-
)
|
| 699 |
-
|
| 700 |
-
master_data = {
|
| 701 |
-
"original": original,
|
| 702 |
-
"mastered": mastered,
|
| 703 |
-
"sr": sr,
|
| 704 |
-
"settings": {**values, "target_lufs": target},
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
# Consume one usage credit now that mastering succeeded
|
| 708 |
-
_check_ai_key(ai_access_key, consume=True)
|
| 709 |
-
|
| 710 |
-
P += "\n\n\u2705 **Auto Master complete!** 3 passes, 4 AI analyses."
|
| 711 |
-
|
| 712 |
-
yield (
|
| 713 |
-
gr.Slider(value=values["lows_db"], label="Lows (200 Hz)"),
|
| 714 |
-
gr.Slider(value=values["mid_boost_db"], label="Mids (1.2 kHz)"),
|
| 715 |
-
gr.Slider(value=values["highs_db"], label="Highs (10 kHz)"),
|
| 716 |
-
gr.Slider(value=values["bass_boost_db"], label="Bass Boost (dB)"),
|
| 717 |
-
gr.Slider(value=values["bass_freq_hz"], label="Bass Boost Frequency (Hz)"),
|
| 718 |
-
gr.Slider(value=values["compression"], label="Compression: Less <-> More"),
|
| 719 |
-
gr.Slider(value=values["stereo_width"], label="Stereo Width (%)"),
|
| 720 |
-
values, # ai_values_state
|
| 721 |
-
P, # ai_reasoning_display
|
| 722 |
-
output_path, # ab_player
|
| 723 |
-
waveform_fig, # waveform_plot
|
| 724 |
-
spectrum_fig, # spectrum_plot
|
| 725 |
-
dsp_md, # dsp_display
|
| 726 |
-
stats_md, # stats_display
|
| 727 |
-
gr.Button("AI Compare Original vs Master",
|
| 728 |
-
variant="secondary", visible=True), # ai_compare_btn
|
| 729 |
-
gr.DownloadButton("Download Mastered File",
|
| 730 |
-
value=output_path, visible=True), # download_file
|
| 731 |
-
master_data, # master_data_state
|
| 732 |
-
"---\n### AI Post-Master Analysis\n" + final_report, # ai_report_display
|
| 733 |
-
gr.Button(interactive=True), # master_btn
|
| 734 |
-
gr.Button(interactive=True), # apply_ai_btn
|
| 735 |
-
gr.Button(interactive=True), # ai_recommend_btn
|
| 736 |
-
gr.Button(interactive=True), # auto_master_btn
|
| 737 |
-
gr.Button(interactive=True), # super_ai_btn
|
| 738 |
-
loop_history, # analysis_history_state
|
| 739 |
-
)
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
def super_ai_master(audio_path, ai_access_key, target_choice, custom_lufs):
|
| 743 |
-
"""Super AI mastering loop with full parametric control (generator).
|
| 744 |
-
|
| 745 |
-
Flow:
|
| 746 |
-
Pass 1: AI analyzes raw audio → full-parametric settings → master
|
| 747 |
-
Pass 2: AI compares + adjusts ALL params (small moves) → master
|
| 748 |
-
Pass 3: Same as pass 2
|
| 749 |
-
Pass 4: Same as pass 2
|
| 750 |
-
Pass 5: Final AI compare — report only, no suggestions.
|
| 751 |
-
|
| 752 |
-
Yields progress to 24 outputs at each step (17 + 5 lockable buttons + history + dsp).
|
| 753 |
-
"""
|
| 754 |
-
gc.collect() # Free any previous run's data before starting
|
| 755 |
-
SKIP = gr.update()
|
| 756 |
-
|
| 757 |
-
def _yield_progress(progress, sliders=None, ai_vals=SKIP,
|
| 758 |
-
audio=SKIP, wf=SKIP, sp=SKIP, dsp=SKIP, stats=SKIP,
|
| 759 |
-
cmp_btn=SKIP, dl=SKIP, mdata=SKIP, report=SKIP,
|
| 760 |
-
btns=None, history=SKIP):
|
| 761 |
-
"""Build the 24-element tuple for each yield."""
|
| 762 |
-
s = sliders or (SKIP,) * 7
|
| 763 |
-
b = btns or (SKIP,) * 5
|
| 764 |
-
return (*s, ai_vals, progress,
|
| 765 |
-
audio, wf, sp, dsp, stats, cmp_btn, dl, mdata, report, *b, history)
|
| 766 |
-
|
| 767 |
-
# --- Validation ---
|
| 768 |
-
valid, err, _remaining = _check_ai_key(ai_access_key)
|
| 769 |
-
if not valid:
|
| 770 |
-
yield _yield_progress(
|
| 771 |
-
err,
|
| 772 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 773 |
-
)
|
| 774 |
-
return
|
| 775 |
-
|
| 776 |
-
if audio_path is None:
|
| 777 |
-
raise gr.Error("Please upload an audio file first.")
|
| 778 |
-
|
| 779 |
-
if not os.path.exists(audio_path):
|
| 780 |
-
raise gr.Error("Audio file not found — please re-upload your file.")
|
| 781 |
-
|
| 782 |
-
# Resolve target LUFS
|
| 783 |
-
if target_choice == "-14 (Streaming)":
|
| 784 |
-
target = -14.0
|
| 785 |
-
elif target_choice == "-11 (CD)":
|
| 786 |
-
target = -11.0
|
| 787 |
-
else:
|
| 788 |
-
target = float(custom_lufs)
|
| 789 |
-
|
| 790 |
-
# === PASS 1: AI full-parametric recommendation ===
|
| 791 |
-
P = "## \U0001f9e0 Super AI Master (beta)\n\n"
|
| 792 |
-
P += "**Pass 1 of 5** — AI analyzing audio with full parametric control\n\n"
|
| 793 |
-
P += "\u23f3 *Deep analysis with Gemini...*"
|
| 794 |
-
yield _yield_progress(P, history=[])
|
| 795 |
-
|
| 796 |
-
from analysis import (super_ai_recommend, super_ai_compare,
|
| 797 |
-
super_ai_final_report, extract_features)
|
| 798 |
-
|
| 799 |
-
loop_history = []
|
| 800 |
-
try:
|
| 801 |
-
result = super_ai_recommend(audio_path)
|
| 802 |
-
except GeminiUnavailableError as e:
|
| 803 |
-
yield _yield_progress(
|
| 804 |
-
str(e),
|
| 805 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 806 |
-
)
|
| 807 |
-
return
|
| 808 |
-
|
| 809 |
-
if result is None:
|
| 810 |
-
yield _yield_progress("*Set GOOGLE_API_KEY to enable AI recommendations.*")
|
| 811 |
-
return
|
| 812 |
-
if result.get("parse_error"):
|
| 813 |
-
yield _yield_progress(result.get("reasoning", "*Could not parse AI response.*"))
|
| 814 |
-
return
|
| 815 |
-
|
| 816 |
-
# Extract the full parameter set (everything except reasoning)
|
| 817 |
-
params = {k: v for k, v in result.items() if k != "reasoning"}
|
| 818 |
-
|
| 819 |
-
P += "\n\n\u2705 AI recommended initial settings"
|
| 820 |
-
P += "\n\n\u23f3 *Mastering with full parametric settings...*"
|
| 821 |
-
yield _yield_progress(P)
|
| 822 |
-
|
| 823 |
-
# Master pass 1
|
| 824 |
-
output_path, original, mastered, sr, stats = master_audio_full(
|
| 825 |
-
audio_path, params, target,
|
| 826 |
-
)
|
| 827 |
-
|
| 828 |
-
mast_feat = extract_features(mastered, sr)
|
| 829 |
-
loop_history.append({
|
| 830 |
-
"params": params,
|
| 831 |
-
"lufs": mast_feat.get("lufs", "?"),
|
| 832 |
-
"true_peak": mast_feat.get("true_peak_dbtp", "?"),
|
| 833 |
-
"crest_factor": mast_feat.get("crest_factor_db", "?"),
|
| 834 |
-
"summary": "Pass 1 initial recommendation",
|
| 835 |
-
})
|
| 836 |
-
|
| 837 |
-
P += "\n\n\u2705 Pass 1 mastered"
|
| 838 |
-
|
| 839 |
-
# === PASSES 2-4: AI compare + adjust + master ===
|
| 840 |
-
for pass_num in range(2, 5):
|
| 841 |
-
P += f"\n\n---\n\n**Pass {pass_num} of 5** — AI comparing and refining\n\n"
|
| 842 |
-
P += "\u23f3 *Waiting before next AI call...*"
|
| 843 |
-
yield _yield_progress(P)
|
| 844 |
-
time.sleep(15)
|
| 845 |
-
|
| 846 |
-
P += "\n\n\u23f3 *AI analyzing master and adjusting parameters...*"
|
| 847 |
-
yield _yield_progress(P)
|
| 848 |
-
|
| 849 |
-
try:
|
| 850 |
-
result_cmp = super_ai_compare(
|
| 851 |
-
original, mastered, sr, params, target,
|
| 852 |
-
history=loop_history,
|
| 853 |
-
)
|
| 854 |
-
except GeminiUnavailableError as e:
|
| 855 |
-
yield _yield_progress(
|
| 856 |
-
str(e),
|
| 857 |
-
btns=tuple(gr.Button(interactive=True) for _ in range(5)),
|
| 858 |
-
)
|
| 859 |
-
return
|
| 860 |
-
|
| 861 |
-
if result_cmp is None or result_cmp.get("parse_error"):
|
| 862 |
-
P += "\n\n\u26a0\ufe0f Could not parse AI adjustments — keeping current settings"
|
| 863 |
-
else:
|
| 864 |
-
params = {k: v for k, v in result_cmp.items() if k != "report"}
|
| 865 |
-
P += "\n\n\u2705 AI adjustments applied"
|
| 866 |
-
|
| 867 |
-
P += f"\n\n\u23f3 *Re-mastering pass {pass_num}...*"
|
| 868 |
-
yield _yield_progress(P)
|
| 869 |
-
|
| 870 |
-
output_path, original, mastered, sr, stats = master_audio_full(
|
| 871 |
-
audio_path, params, target,
|
| 872 |
-
)
|
| 873 |
-
|
| 874 |
-
mast_feat = extract_features(mastered, sr)
|
| 875 |
-
report_snippet = ""
|
| 876 |
-
if result_cmp and not result_cmp.get("parse_error"):
|
| 877 |
-
report_snippet = result_cmp.get("report", "")[:200]
|
| 878 |
-
loop_history.append({
|
| 879 |
-
"params": params,
|
| 880 |
-
"lufs": mast_feat.get("lufs", "?"),
|
| 881 |
-
"true_peak": mast_feat.get("true_peak_dbtp", "?"),
|
| 882 |
-
"crest_factor": mast_feat.get("crest_factor_db", "?"),
|
| 883 |
-
"summary": f"Pass {pass_num}: {report_snippet}",
|
| 884 |
-
})
|
| 885 |
-
|
| 886 |
-
P += f"\n\n\u2705 Pass {pass_num} mastered"
|
| 887 |
-
|
| 888 |
-
# === PASS 5: Final report only ===
|
| 889 |
-
P += "\n\n---\n\n**Pass 5 of 5** — Final quality assessment\n\n"
|
| 890 |
-
P += "\u23f3 *Waiting before final AI call...*"
|
| 891 |
-
yield _yield_progress(P)
|
| 892 |
-
time.sleep(15)
|
| 893 |
-
|
| 894 |
-
P += "\n\n\u23f3 *Running final AI evaluation...*"
|
| 895 |
-
yield _yield_progress(P)
|
| 896 |
-
|
| 897 |
-
try:
|
| 898 |
-
final_report = super_ai_final_report(
|
| 899 |
-
original, mastered, sr, params, target,
|
| 900 |
-
history=loop_history,
|
| 901 |
-
)
|
| 902 |
-
except GeminiUnavailableError as e:
|
| 903 |
-
# Pass 5 is report-only — mastering is done, so we can still
|
| 904 |
-
# show results even if the report fails. Don't abort here.
|
| 905 |
-
final_report = str(e)
|
| 906 |
-
except Exception as e:
|
| 907 |
-
final_report = f"*AI final report unavailable: {e}*"
|
| 908 |
-
|
| 909 |
-
# Generate plots and stats
|
| 910 |
-
waveform_fig = plot_waveform_comparison(original, mastered, sr)
|
| 911 |
-
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
|
| 912 |
-
|
| 913 |
-
mono_note = "\n\n*Input is mono — stereo width adjustment was skipped.*" if stats["mono"] else ""
|
| 914 |
-
dsp_md = _dsp_settings_md(stats.get("dsp", {}))
|
| 915 |
-
stats_md = (
|
| 916 |
-
"### Loudness Statistics\n"
|
| 917 |
-
"| | LUFS | True Peak (dBTP) |\n"
|
| 918 |
-
"|---|---|---|\n"
|
| 919 |
-
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
|
| 920 |
-
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
|
| 921 |
-
f"{mono_note}"
|
| 922 |
-
)
|
| 923 |
-
|
| 924 |
-
# Super AI uses full parametric — don't map back to sliders
|
| 925 |
-
# (sliders have fixed ranges that can't represent the full parameter space)
|
| 926 |
-
master_data = {
|
| 927 |
-
"original": original,
|
| 928 |
-
"mastered": mastered,
|
| 929 |
-
"sr": sr,
|
| 930 |
-
"settings": {"target_lufs": target},
|
| 931 |
-
}
|
| 932 |
-
|
| 933 |
-
# Consume one usage credit now that mastering succeeded
|
| 934 |
-
_check_ai_key(ai_access_key, consume=True)
|
| 935 |
-
|
| 936 |
-
P += "\n\n\u2705 **Super AI Master complete!** 5 passes, full parametric control."
|
| 937 |
-
|
| 938 |
-
# Build the full settings report for display
|
| 939 |
-
eq = params.get("eq", {})
|
| 940 |
-
comp = params.get("compression", {})
|
| 941 |
-
settings_report = "---\n### Super AI — Applied Settings\n"
|
| 942 |
-
settings_report += f"**HPF:** {params.get('hpf_freq', 15)} Hz\n\n"
|
| 943 |
-
settings_report += "**EQ:**\n"
|
| 944 |
-
for bk in ("band1", "band2", "band3", "band4", "band5", "band6"):
|
| 945 |
-
b = eq.get(bk, {})
|
| 946 |
-
if abs(b.get("gain_db", 0)) < 0.01:
|
| 947 |
-
settings_report += f"- {bk}: bypassed\n"
|
| 948 |
-
else:
|
| 949 |
-
settings_report += (f"- {bk}: {b.get('type','peak')} @ {b.get('freq',1000):.0f} Hz, "
|
| 950 |
-
f"{b.get('gain_db',0):+.1f} dB, Q={b.get('q',1.0):.2f}\n")
|
| 951 |
-
settings_report += f"\n**Crossovers:** {params.get('crossover_low', 200)} / {params.get('crossover_high', 4000)} Hz\n\n"
|
| 952 |
-
settings_report += "**Compression:**\n"
|
| 953 |
-
for bk in ("low", "mid", "high"):
|
| 954 |
-
bp = comp.get(bk, {})
|
| 955 |
-
settings_report += (f"- {bk}: threshold {bp.get('threshold',-14):.1f} dB, "
|
| 956 |
-
f"ratio {bp.get('ratio',1.0):.1f}:1, "
|
| 957 |
-
f"attack {bp.get('attack_ms',30):.1f} ms, "
|
| 958 |
-
f"release {bp.get('release_ms',150):.1f} ms\n")
|
| 959 |
-
settings_report += f"\n**Stereo Width:** {params.get('stereo_width', 100)}%\n\n"
|
| 960 |
-
|
| 961 |
-
# Final yield — Super AI doesn't update sliders (params exceed slider ranges)
|
| 962 |
-
SKIP = gr.update()
|
| 963 |
-
yield (
|
| 964 |
-
SKIP, SKIP, SKIP, SKIP, SKIP, SKIP, SKIP, # leave sliders unchanged
|
| 965 |
-
{}, # ai_values_state
|
| 966 |
-
P, # ai_reasoning_display
|
| 967 |
-
output_path, # ab_player
|
| 968 |
-
waveform_fig, # waveform_plot
|
| 969 |
-
spectrum_fig, # spectrum_plot
|
| 970 |
-
dsp_md, # dsp_display
|
| 971 |
-
stats_md, # stats_display
|
| 972 |
-
gr.Button("AI Compare Original vs Master",
|
| 973 |
-
variant="secondary", visible=True), # ai_compare_btn
|
| 974 |
-
gr.DownloadButton("Download Mastered File",
|
| 975 |
-
value=output_path, visible=True), # download_file
|
| 976 |
-
master_data, # master_data_state
|
| 977 |
-
settings_report + "---\n### AI Final Report\n" + final_report, # ai_report_display
|
| 978 |
-
gr.Button(interactive=True), # master_btn
|
| 979 |
-
gr.Button(interactive=True), # apply_ai_btn
|
| 980 |
-
gr.Button(interactive=True), # ai_recommend_btn
|
| 981 |
-
gr.Button(interactive=True), # auto_master_btn
|
| 982 |
-
gr.Button(interactive=True), # super_ai_btn
|
| 983 |
-
loop_history, # analysis_history_state
|
| 984 |
-
)
|
| 985 |
|
| 986 |
|
| 987 |
# ---------------------------------------------------------------------------
|
|
@@ -989,33 +90,15 @@ def super_ai_master(audio_path, ai_access_key, target_choice, custom_lufs):
|
|
| 989 |
# ---------------------------------------------------------------------------
|
| 990 |
|
| 991 |
with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
| 992 |
-
gr.
|
| 993 |
-
'<div style="display:flex;align-items:center;gap:16px;">'
|
| 994 |
-
'<img src="data:image/png;base64,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" alt="s.AI logo" style="height:80px;border-radius:12px;">'
|
| 995 |
-
'<div>'
|
| 996 |
-
'<h1 style="margin:0;">Audio Mastering Suite</h1>'
|
| 997 |
-
'<p style="margin:4px 0 0 0;font-size:0.9em;">by AnimalMonk | '
|
| 998 |
-
'<a href="https://discord.gg/StudioAI" target="_blank">Join us on Discord</a></p>'
|
| 999 |
-
'</div></div>'
|
| 1000 |
-
)
|
| 1001 |
|
| 1002 |
-
# --- Preset
|
| 1003 |
with gr.Row():
|
| 1004 |
preset_dropdown = gr.Dropdown(
|
| 1005 |
label="Preset",
|
| 1006 |
choices=list(PRESETS.keys()),
|
| 1007 |
value="-- None --",
|
| 1008 |
)
|
| 1009 |
-
ai_access_input = gr.Textbox(
|
| 1010 |
-
label="AI Access Key",
|
| 1011 |
-
placeholder="Enter key to unlock AI",
|
| 1012 |
-
type="password",
|
| 1013 |
-
scale=1,
|
| 1014 |
-
autofocus=False,
|
| 1015 |
-
elem_id="ai-access-key",
|
| 1016 |
-
)
|
| 1017 |
-
ai_recommend_btn = gr.Button("AI Recommend", variant="secondary")
|
| 1018 |
-
auto_master_btn = gr.Button("Auto Master", variant="primary")
|
| 1019 |
target_dropdown = gr.Dropdown(
|
| 1020 |
label="Target LUFS",
|
| 1021 |
choices=["-14 (Streaming)", "-11 (CD)", "Custom"],
|
|
@@ -1027,31 +110,8 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1027 |
visible=False,
|
| 1028 |
)
|
| 1029 |
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
key_status_display = gr.Markdown("")
|
| 1033 |
-
|
| 1034 |
-
gr.HTML(
|
| 1035 |
-
f'<p style="margin:0;font-size:14px;color:#ccc;">'
|
| 1036 |
-
f'<a href="{_PRICING_URL}" target="_blank" rel="noopener" '
|
| 1037 |
-
f'style="color:#7c6cf0;font-weight:bold;text-decoration:underline;">'
|
| 1038 |
-
f'Get an API Key</a>'
|
| 1039 |
-
f' — plans start at $9/month. '
|
| 1040 |
-
f'<em>Manual mastering is always free.</em></p>'
|
| 1041 |
-
)
|
| 1042 |
-
|
| 1043 |
-
# Disable browser password autofill on the AI key field
|
| 1044 |
-
gr.HTML("""<script>
|
| 1045 |
-
document.addEventListener('DOMContentLoaded', function() {
|
| 1046 |
-
var el = document.querySelector('#ai-access-key input');
|
| 1047 |
-
if (el) { el.autocomplete = 'off'; el.setAttribute('autocomplete', 'off'); }
|
| 1048 |
-
});
|
| 1049 |
-
// Gradio may render late — retry after a short delay
|
| 1050 |
-
setTimeout(function() {
|
| 1051 |
-
var el = document.querySelector('#ai-access-key input');
|
| 1052 |
-
if (el) { el.autocomplete = 'off'; el.setAttribute('autocomplete', 'off'); }
|
| 1053 |
-
}, 1500);
|
| 1054 |
-
</script>""", visible=False)
|
| 1055 |
|
| 1056 |
# --- Upload (full width) ---
|
| 1057 |
audio_input = gr.Audio(
|
|
@@ -1060,12 +120,6 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1060 |
sources=["upload"],
|
| 1061 |
)
|
| 1062 |
|
| 1063 |
-
# --- AI Recommendations (below upload) ---
|
| 1064 |
-
ai_reasoning_display = gr.Markdown(value="", visible=True)
|
| 1065 |
-
|
| 1066 |
-
# --- Apply AI Recommended Settings (below AI reasoning) ---
|
| 1067 |
-
apply_ai_btn = gr.Button("Apply AI Recommended Settings", variant="secondary")
|
| 1068 |
-
|
| 1069 |
# --- Control sliders ---
|
| 1070 |
with gr.Row():
|
| 1071 |
lows_slider = gr.Slider(
|
|
@@ -1077,7 +131,7 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1077 |
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
|
| 1078 |
)
|
| 1079 |
highs_slider = gr.Slider(
|
| 1080 |
-
label="Highs (
|
| 1081 |
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
|
| 1082 |
)
|
| 1083 |
|
|
@@ -1089,7 +143,7 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1089 |
)
|
| 1090 |
bass_freq_slider = gr.Slider(
|
| 1091 |
label="Bass Boost Frequency (Hz)",
|
| 1092 |
-
minimum=
|
| 1093 |
)
|
| 1094 |
with gr.Column():
|
| 1095 |
comp_slider = gr.Slider(
|
|
@@ -1101,30 +155,16 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1101 |
minimum=80, maximum=150, value=100, step=1,
|
| 1102 |
)
|
| 1103 |
|
| 1104 |
-
# --- State for AI values, master data, and analysis history ---
|
| 1105 |
-
master_data_state = gr.State({})
|
| 1106 |
-
ai_values_state = gr.State({})
|
| 1107 |
-
analysis_history_state = gr.State([])
|
| 1108 |
-
|
| 1109 |
-
# --- Master button ---
|
| 1110 |
-
master_btn = gr.Button("Master It!", variant="primary", size="lg")
|
| 1111 |
-
|
| 1112 |
-
# --- AI Post-Master Comparison (above playback) ---
|
| 1113 |
-
ai_report_display = gr.Markdown(value="", visible=True, label="AI Analysis")
|
| 1114 |
-
|
| 1115 |
# --- Playback ---
|
| 1116 |
ab_player = gr.Audio(label="Mastered", interactive=False)
|
| 1117 |
-
ai_compare_btn = gr.Button("AI Compare Original vs Master", variant="secondary", visible=False)
|
| 1118 |
-
download_file = gr.DownloadButton("Download Mastered File", visible=False)
|
| 1119 |
|
| 1120 |
# --- Visualization ---
|
| 1121 |
with gr.Row():
|
| 1122 |
waveform_plot = gr.Plot(label="Waveform Comparison")
|
| 1123 |
spectrum_plot = gr.Plot(label="Spectrum Comparison")
|
| 1124 |
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
stats_display = gr.Markdown()
|
| 1128 |
|
| 1129 |
# --- Event wiring ---
|
| 1130 |
preset_dropdown.change(
|
|
@@ -1141,36 +181,6 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1141 |
outputs=[custom_lufs_input],
|
| 1142 |
)
|
| 1143 |
|
| 1144 |
-
ai_access_input.change(
|
| 1145 |
-
check_key_status,
|
| 1146 |
-
inputs=[ai_access_input],
|
| 1147 |
-
outputs=[key_status_display],
|
| 1148 |
-
)
|
| 1149 |
-
|
| 1150 |
-
ai_recommend_btn.click(
|
| 1151 |
-
ai_recommend,
|
| 1152 |
-
inputs=[audio_input, ai_access_input],
|
| 1153 |
-
outputs=[
|
| 1154 |
-
lows_slider, mid_boost_slider, highs_slider,
|
| 1155 |
-
bass_boost_slider, bass_freq_slider,
|
| 1156 |
-
comp_slider, width_slider,
|
| 1157 |
-
ai_values_state,
|
| 1158 |
-
ai_reasoning_display,
|
| 1159 |
-
master_btn, apply_ai_btn, ai_recommend_btn,
|
| 1160 |
-
auto_master_btn, ai_compare_btn, super_ai_btn,
|
| 1161 |
-
],
|
| 1162 |
-
)
|
| 1163 |
-
|
| 1164 |
-
apply_ai_btn.click(
|
| 1165 |
-
apply_ai,
|
| 1166 |
-
inputs=[ai_values_state],
|
| 1167 |
-
outputs=[
|
| 1168 |
-
lows_slider, mid_boost_slider, highs_slider,
|
| 1169 |
-
bass_boost_slider, bass_freq_slider,
|
| 1170 |
-
comp_slider, width_slider,
|
| 1171 |
-
],
|
| 1172 |
-
)
|
| 1173 |
-
|
| 1174 |
master_btn.click(
|
| 1175 |
process,
|
| 1176 |
inputs=[
|
|
@@ -1182,72 +192,11 @@ with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
|
| 1182 |
outputs=[
|
| 1183 |
ab_player,
|
| 1184 |
waveform_plot, spectrum_plot,
|
| 1185 |
-
|
| 1186 |
-
ai_compare_btn, download_file,
|
| 1187 |
-
master_data_state,
|
| 1188 |
-
ai_report_display,
|
| 1189 |
-
],
|
| 1190 |
-
)
|
| 1191 |
-
|
| 1192 |
-
ai_compare_btn.click(
|
| 1193 |
-
ai_compare,
|
| 1194 |
-
inputs=[master_data_state, ai_access_input, analysis_history_state],
|
| 1195 |
-
outputs=[
|
| 1196 |
-
lows_slider, mid_boost_slider, highs_slider,
|
| 1197 |
-
bass_boost_slider, bass_freq_slider,
|
| 1198 |
-
comp_slider, width_slider,
|
| 1199 |
-
ai_values_state,
|
| 1200 |
-
ai_report_display,
|
| 1201 |
-
analysis_history_state,
|
| 1202 |
-
master_btn, apply_ai_btn, ai_recommend_btn,
|
| 1203 |
-
auto_master_btn, ai_compare_btn, super_ai_btn,
|
| 1204 |
-
],
|
| 1205 |
-
)
|
| 1206 |
-
|
| 1207 |
-
auto_master_btn.click(
|
| 1208 |
-
auto_master,
|
| 1209 |
-
inputs=[audio_input, ai_access_input, target_dropdown, custom_lufs_input],
|
| 1210 |
-
outputs=[
|
| 1211 |
-
lows_slider, mid_boost_slider, highs_slider,
|
| 1212 |
-
bass_boost_slider, bass_freq_slider,
|
| 1213 |
-
comp_slider, width_slider,
|
| 1214 |
-
ai_values_state,
|
| 1215 |
-
ai_reasoning_display,
|
| 1216 |
-
ab_player,
|
| 1217 |
-
waveform_plot, spectrum_plot,
|
| 1218 |
-
dsp_display, stats_display,
|
| 1219 |
-
ai_compare_btn, download_file,
|
| 1220 |
-
master_data_state,
|
| 1221 |
-
ai_report_display,
|
| 1222 |
-
master_btn, apply_ai_btn, ai_recommend_btn,
|
| 1223 |
-
auto_master_btn, super_ai_btn,
|
| 1224 |
-
analysis_history_state,
|
| 1225 |
],
|
| 1226 |
)
|
| 1227 |
|
| 1228 |
-
super_ai_btn.click(
|
| 1229 |
-
super_ai_master,
|
| 1230 |
-
inputs=[audio_input, ai_access_input, target_dropdown, custom_lufs_input],
|
| 1231 |
-
outputs=[
|
| 1232 |
-
lows_slider, mid_boost_slider, highs_slider,
|
| 1233 |
-
bass_boost_slider, bass_freq_slider,
|
| 1234 |
-
comp_slider, width_slider,
|
| 1235 |
-
ai_values_state,
|
| 1236 |
-
ai_reasoning_display,
|
| 1237 |
-
ab_player,
|
| 1238 |
-
waveform_plot, spectrum_plot,
|
| 1239 |
-
dsp_display, stats_display,
|
| 1240 |
-
ai_compare_btn, download_file,
|
| 1241 |
-
master_data_state,
|
| 1242 |
-
ai_report_display,
|
| 1243 |
-
master_btn, apply_ai_btn, ai_recommend_btn,
|
| 1244 |
-
auto_master_btn, super_ai_btn,
|
| 1245 |
-
analysis_history_state,
|
| 1246 |
-
],
|
| 1247 |
-
)
|
| 1248 |
|
| 1249 |
|
| 1250 |
if __name__ == "__main__":
|
| 1251 |
-
|
| 1252 |
-
_os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 1253 |
-
demo.launch(server_name="0.0.0.0", server_port=7860, share=False, ssr_mode=False)
|
|
|
|
| 1 |
"""Audio Mastering Suite — Gradio web application."""
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
+
from dsp import master_audio
|
|
|
|
| 5 |
from presets import PRESETS
|
| 6 |
from visualization import plot_waveform_comparison, plot_spectrum_comparison
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
# ---------------------------------------------------------------------------
|
| 10 |
# Callbacks
|
| 11 |
# ---------------------------------------------------------------------------
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def apply_preset(preset_name):
|
| 14 |
"""Update all sliders when a preset is selected."""
|
| 15 |
if preset_name == "-- None --" or preset_name not in PRESETS or PRESETS[preset_name] is None:
|
|
|
|
| 35 |
return gr.update(visible=(target_choice == "Custom"))
|
| 36 |
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
def process(audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz,
|
| 39 |
comp_val, width, target_choice, custom_lufs):
|
| 40 |
"""Run the mastering pipeline and return all outputs."""
|
|
|
|
| 49 |
else:
|
| 50 |
target = float(custom_lufs)
|
| 51 |
|
| 52 |
+
# Master (limiter hardcoded to -1 dB)
|
| 53 |
output_path, original, mastered, sr, stats = master_audio(
|
| 54 |
audio_path, lows_db, mid_boost_db, highs_db, bass_boost_db, bass_freq_hz,
|
| 55 |
+
comp_val, -1.0, width, target,
|
| 56 |
)
|
| 57 |
|
| 58 |
# Plots
|
| 59 |
waveform_fig = plot_waveform_comparison(original, mastered, sr)
|
| 60 |
spectrum_fig = plot_spectrum_comparison(original, mastered, sr)
|
| 61 |
|
| 62 |
+
# Stats markdown
|
| 63 |
mono_note = "\n\n*Input is mono — stereo width adjustment was skipped.*" if stats["mono"] else ""
|
| 64 |
+
warning_note = f"\n\n**Warning:** {stats['warning']}" if stats.get("warning") else ""
|
| 65 |
stats_md = (
|
| 66 |
"### Loudness Statistics\n"
|
| 67 |
"| | LUFS | True Peak (dBTP) |\n"
|
| 68 |
"|---|---|---|\n"
|
| 69 |
f"| **Original** | {stats['orig_lufs']:.1f} | {stats['orig_peak']:.1f} |\n"
|
| 70 |
f"| **Mastered** | {stats['mast_lufs']:.1f} | {stats['mast_peak']:.1f} |\n\n"
|
| 71 |
+
"### Gain Staging\n"
|
| 72 |
+
"| Metric | dB |\n"
|
| 73 |
+
"|---|---|\n"
|
| 74 |
+
f"| Auto Gain Reduction | {stats['auto_gain_reduction']:.1f} |\n"
|
| 75 |
+
f"| Limiter Gain Reduction | {stats['limiter_gain_reduction']:.1f} |"
|
| 76 |
+
f"{mono_note}{warning_note}"
|
| 77 |
)
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
return (
|
| 80 |
output_path,
|
| 81 |
waveform_fig, spectrum_fig,
|
| 82 |
+
stats_md,
|
|
|
|
| 83 |
gr.DownloadButton("Download Mastered File", value=output_path, visible=True),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
)
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
# ---------------------------------------------------------------------------
|
|
|
|
| 90 |
# ---------------------------------------------------------------------------
|
| 91 |
|
| 92 |
with gr.Blocks(title="Audio Mastering Suite", theme=gr.themes.Soft()) as demo:
|
| 93 |
+
gr.Markdown("# Audio Mastering Suite\n##### by AnimalMonk")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
# --- Preset & Target LUFS (side by side) ---
|
| 96 |
with gr.Row():
|
| 97 |
preset_dropdown = gr.Dropdown(
|
| 98 |
label="Preset",
|
| 99 |
choices=list(PRESETS.keys()),
|
| 100 |
value="-- None --",
|
| 101 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
target_dropdown = gr.Dropdown(
|
| 103 |
label="Target LUFS",
|
| 104 |
choices=["-14 (Streaming)", "-11 (CD)", "Custom"],
|
|
|
|
| 110 |
visible=False,
|
| 111 |
)
|
| 112 |
|
| 113 |
+
# --- Master button ---
|
| 114 |
+
master_btn = gr.Button("Master It!", variant="primary", size="lg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
# --- Upload (full width) ---
|
| 117 |
audio_input = gr.Audio(
|
|
|
|
| 120 |
sources=["upload"],
|
| 121 |
)
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
# --- Control sliders ---
|
| 124 |
with gr.Row():
|
| 125 |
lows_slider = gr.Slider(
|
|
|
|
| 131 |
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
|
| 132 |
)
|
| 133 |
highs_slider = gr.Slider(
|
| 134 |
+
label="Highs (6 kHz)",
|
| 135 |
minimum=-3.0, maximum=3.0, value=0.0, step=0.1,
|
| 136 |
)
|
| 137 |
|
|
|
|
| 143 |
)
|
| 144 |
bass_freq_slider = gr.Slider(
|
| 145 |
label="Bass Boost Frequency (Hz)",
|
| 146 |
+
minimum=50, maximum=60, value=55, step=1,
|
| 147 |
)
|
| 148 |
with gr.Column():
|
| 149 |
comp_slider = gr.Slider(
|
|
|
|
| 155 |
minimum=80, maximum=150, value=100, step=1,
|
| 156 |
)
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
# --- Playback ---
|
| 159 |
ab_player = gr.Audio(label="Mastered", interactive=False)
|
|
|
|
|
|
|
| 160 |
|
| 161 |
# --- Visualization ---
|
| 162 |
with gr.Row():
|
| 163 |
waveform_plot = gr.Plot(label="Waveform Comparison")
|
| 164 |
spectrum_plot = gr.Plot(label="Spectrum Comparison")
|
| 165 |
|
| 166 |
+
stats_display = gr.Markdown()
|
| 167 |
+
download_file = gr.DownloadButton("Download Mastered File", visible=False)
|
|
|
|
| 168 |
|
| 169 |
# --- Event wiring ---
|
| 170 |
preset_dropdown.change(
|
|
|
|
| 181 |
outputs=[custom_lufs_input],
|
| 182 |
)
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
master_btn.click(
|
| 185 |
process,
|
| 186 |
inputs=[
|
|
|
|
| 192 |
outputs=[
|
| 193 |
ab_player,
|
| 194 |
waveform_plot, spectrum_plot,
|
| 195 |
+
stats_display, download_file,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
],
|
| 197 |
)
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
|
| 201 |
if __name__ == "__main__":
|
| 202 |
+
demo.launch()
|
|
|
|
|
|
build_schematic.py
DELETED
|
@@ -1,1064 +0,0 @@
|
|
| 1 |
-
"""Generate ANALOG_SCHEMATIC.pdf v4.2 — clean graphical layout."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
from reportlab.lib.pagesizes import letter
|
| 5 |
-
from reportlab.lib.units import inch
|
| 6 |
-
from reportlab.lib.colors import HexColor, black, white
|
| 7 |
-
from reportlab.pdfgen import canvas
|
| 8 |
-
from reportlab.pdfbase import pdfmetrics
|
| 9 |
-
from reportlab.pdfbase.ttfonts import TTFont
|
| 10 |
-
|
| 11 |
-
# ── Fonts ──────────────────────────────────────────────────────────────────
|
| 12 |
-
FONT_DIR = os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts")
|
| 13 |
-
for name, filename in [
|
| 14 |
-
("Cal", "calibri.ttf"),
|
| 15 |
-
("CalB", "calibrib.ttf"),
|
| 16 |
-
("CalI", "calibrii.ttf"),
|
| 17 |
-
("Con", "consola.ttf"),
|
| 18 |
-
]:
|
| 19 |
-
path = os.path.join(FONT_DIR, filename)
|
| 20 |
-
if os.path.exists(path):
|
| 21 |
-
pdfmetrics.registerFont(TTFont(name, path))
|
| 22 |
-
|
| 23 |
-
BODY = "Cal"
|
| 24 |
-
BOLD = "CalB"
|
| 25 |
-
ITALIC = "CalI"
|
| 26 |
-
MONO = "Con"
|
| 27 |
-
|
| 28 |
-
# ── Colors ─────────────────────────────────────────────────────────────────
|
| 29 |
-
BG_DARK = HexColor("#1a1a2e")
|
| 30 |
-
BG_STAGE = HexColor("#f4f4f8")
|
| 31 |
-
BG_SUB = HexColor("#ffffff")
|
| 32 |
-
ACCENT = HexColor("#3a6ea5")
|
| 33 |
-
ACCENT2 = HexColor("#6b4c9a")
|
| 34 |
-
BORDER = HexColor("#cccccc")
|
| 35 |
-
TEXT_DARK = HexColor("#1a1a1a")
|
| 36 |
-
TEXT_MID = HexColor("#555555")
|
| 37 |
-
TEXT_LIGHT = HexColor("#999999")
|
| 38 |
-
ARROW_CLR = HexColor("#3a6ea5")
|
| 39 |
-
WARM_RED = HexColor("#b5442d")
|
| 40 |
-
WARM_ORANGE = HexColor("#c47a20")
|
| 41 |
-
|
| 42 |
-
OUTPUT = "ANALOG_SCHEMATIC.pdf"
|
| 43 |
-
W, H = letter
|
| 44 |
-
M = 0.6 * inch # margin
|
| 45 |
-
|
| 46 |
-
# ── Drawing helpers ────────────────────────────────────────────────────────
|
| 47 |
-
|
| 48 |
-
def draw_arrow_down(c, x, y_top, y_bot, color=ARROW_CLR):
|
| 49 |
-
"""Draw a downward arrow from y_top to y_bot at x."""
|
| 50 |
-
c.setStrokeColor(color)
|
| 51 |
-
c.setLineWidth(1.5)
|
| 52 |
-
c.line(x, y_top, x, y_bot + 6)
|
| 53 |
-
# arrowhead
|
| 54 |
-
c.setFillColor(color)
|
| 55 |
-
p = c.beginPath()
|
| 56 |
-
p.moveTo(x, y_bot)
|
| 57 |
-
p.lineTo(x - 4, y_bot + 8)
|
| 58 |
-
p.lineTo(x + 4, y_bot + 8)
|
| 59 |
-
p.close()
|
| 60 |
-
c.drawPath(p, fill=1, stroke=0)
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def stage_box(c, x, y, w, h, title, content_fn, stage_num=None):
|
| 64 |
-
"""Draw a rounded stage box with title bar and call content_fn for body."""
|
| 65 |
-
r = 6
|
| 66 |
-
# Shadow
|
| 67 |
-
c.setFillColor(HexColor("#e0e0e0"))
|
| 68 |
-
c.roundRect(x + 2, y - 2, w, h, r, fill=1, stroke=0)
|
| 69 |
-
# Main box
|
| 70 |
-
c.setFillColor(BG_STAGE)
|
| 71 |
-
c.setStrokeColor(BORDER)
|
| 72 |
-
c.setLineWidth(0.75)
|
| 73 |
-
c.roundRect(x, y, w, h, r, fill=1, stroke=1)
|
| 74 |
-
# Title bar
|
| 75 |
-
title_h = 22
|
| 76 |
-
c.setFillColor(ACCENT)
|
| 77 |
-
c.roundRect(x, y + h - title_h, w, title_h, r, fill=1, stroke=0)
|
| 78 |
-
# Fix bottom corners of title bar
|
| 79 |
-
c.rect(x, y + h - title_h, w, r, fill=1, stroke=0)
|
| 80 |
-
# Title text
|
| 81 |
-
c.setFillColor(white)
|
| 82 |
-
c.setFont(BOLD, 10)
|
| 83 |
-
label = f"STAGE {stage_num}: {title}" if stage_num else title
|
| 84 |
-
c.drawString(x + 10, y + h - 16, label)
|
| 85 |
-
# Body area
|
| 86 |
-
if content_fn:
|
| 87 |
-
content_fn(c, x + 10, y + h - title_h - 6, w - 20)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def sub_box(c, x, y, w, h, title=None, color=None):
|
| 91 |
-
"""Draw a smaller inner box. Returns (x, y) of content start."""
|
| 92 |
-
c.setFillColor(color or BG_SUB)
|
| 93 |
-
c.setStrokeColor(HexColor("#dddddd"))
|
| 94 |
-
c.setLineWidth(0.5)
|
| 95 |
-
c.roundRect(x, y, w, h, 4, fill=1, stroke=1)
|
| 96 |
-
if title:
|
| 97 |
-
c.setFillColor(TEXT_DARK)
|
| 98 |
-
c.setFont(BOLD, 8.5)
|
| 99 |
-
c.drawString(x + 6, y + h - 12, title)
|
| 100 |
-
return x + 6, y + h - 24
|
| 101 |
-
return x + 6, y + h - 12
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def text(c, x, y, s, font=None, size=8.5, color=None):
|
| 105 |
-
"""Draw a string, return y below it."""
|
| 106 |
-
c.setFillColor(color or TEXT_DARK)
|
| 107 |
-
c.setFont(font or BODY, size)
|
| 108 |
-
c.drawString(x, y, s)
|
| 109 |
-
return y - size - 2
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def text_pair(c, x, y, label, value, lw=120):
|
| 113 |
-
"""Draw a bold label + normal value on the same line."""
|
| 114 |
-
c.setFillColor(TEXT_DARK)
|
| 115 |
-
c.setFont(BOLD, 8)
|
| 116 |
-
c.drawString(x, y, label)
|
| 117 |
-
c.setFont(BODY, 8)
|
| 118 |
-
c.drawString(x + lw, y, value)
|
| 119 |
-
return y - 12
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def equiv(c, x, y, gear):
|
| 123 |
-
"""Draw an 'Equivalent:' line in italic."""
|
| 124 |
-
c.setFillColor(TEXT_MID)
|
| 125 |
-
c.setFont(ITALIC, 7.5)
|
| 126 |
-
c.drawString(x, y, f"Analog equivalent: {gear}")
|
| 127 |
-
return y - 10
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 131 |
-
# PAGE 1
|
| 132 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 133 |
-
|
| 134 |
-
def page1(c):
|
| 135 |
-
# ── Title banner ──────────────────────────────────────────────────
|
| 136 |
-
bh = 52
|
| 137 |
-
c.setFillColor(BG_DARK)
|
| 138 |
-
c.rect(0, H - bh, W, bh, fill=1, stroke=0)
|
| 139 |
-
c.setFillColor(white)
|
| 140 |
-
c.setFont(BOLD, 16)
|
| 141 |
-
c.drawCentredString(W / 2, H - 24, "s.AI AUDIO MASTERING SUITE")
|
| 142 |
-
c.setFont(BODY, 10)
|
| 143 |
-
c.setFillColor(HexColor("#aaaacc"))
|
| 144 |
-
c.drawCentredString(W / 2, H - 42, "Analog Signal Flow Schematic \u2022 v4.2 \u2022 Stereo")
|
| 145 |
-
|
| 146 |
-
# ── I/O labels ────────────────────────────────────────────────────
|
| 147 |
-
top_y = H - bh - 18
|
| 148 |
-
c.setFillColor(TEXT_MID)
|
| 149 |
-
c.setFont(MONO, 7.5)
|
| 150 |
-
c.drawString(M, top_y, "INPUT: L/R XLR Balanced +4 dBu")
|
| 151 |
-
c.drawRightString(W - M, top_y, "OUTPUT: L/R XLR Balanced +4 dBu \u2022 24-bit WAV")
|
| 152 |
-
|
| 153 |
-
# ── Stage 1: Input Gain ───────────────────────────────────────────
|
| 154 |
-
s1_y = H - bh - 38
|
| 155 |
-
s1_h = 100
|
| 156 |
-
s1_x = M
|
| 157 |
-
s1_w = W - 2 * M
|
| 158 |
-
|
| 159 |
-
def s1_body(c, x, y, w):
|
| 160 |
-
y = text(c, x, y, "Stepped attenuator (resistor ladder, Elma gold-contact switch)", BODY, 8.5)
|
| 161 |
-
y -= 4
|
| 162 |
-
y = text(c, x, y, "Normalizes input to -18 LUFS internal working level.", BODY, 8.5)
|
| 163 |
-
y = text(c, x, y, "Prevents EQ stage clipping on hot masters (e.g. Suno at -9 LUFS).", BODY, 8.5)
|
| 164 |
-
y -= 4
|
| 165 |
-
y = text(c, x, y, "Stepped positions: -3 / -6 / -9 / -12 / -15 / -18 dB", MONO, 7.5, TEXT_MID)
|
| 166 |
-
y -= 4
|
| 167 |
-
y = equiv(c, x, y, "Dangerous Music Liaison / Crookwood M1")
|
| 168 |
-
|
| 169 |
-
stage_box(c, s1_x, s1_y - s1_h, s1_w, s1_h, "INPUT GAIN", s1_body, 1)
|
| 170 |
-
|
| 171 |
-
# Arrow
|
| 172 |
-
arr_x = W / 2
|
| 173 |
-
draw_arrow_down(c, arr_x, s1_y - s1_h, s1_y - s1_h - 22)
|
| 174 |
-
|
| 175 |
-
# ── Stage 2: Subsonic Filter + EQ ─────────────────────────────────
|
| 176 |
-
s2_y = s1_y - s1_h - 24
|
| 177 |
-
s2_h = 380
|
| 178 |
-
s2_x = M
|
| 179 |
-
s2_w = W - 2 * M
|
| 180 |
-
|
| 181 |
-
def s2_body(c, x, y, w):
|
| 182 |
-
# HPF sub-box
|
| 183 |
-
hpf_h = 72
|
| 184 |
-
hpf_x = x
|
| 185 |
-
hpf_w = w
|
| 186 |
-
bx, by = sub_box(c, hpf_x, y - hpf_h, hpf_w, hpf_h, "HPF 15 Hz")
|
| 187 |
-
by = text(c, bx, by, "2nd-order Butterworth (12 dB/oct) \u2022 Passive LC network", BODY, 8)
|
| 188 |
-
by -= 2
|
| 189 |
-
by = text(c, bx, by, "-3 dB at 15 Hz, negligible loss above 35 Hz (-0.1 dB)", BODY, 8)
|
| 190 |
-
by -= 2
|
| 191 |
-
by = text(c, bx, by, "LPF removed in v3.4 \u2014 source band-limited by sample rate;", BODY, 8, TEXT_MID)
|
| 192 |
-
by = text(c, bx, by, "old 20 kHz LPF fought the 10 kHz high shelf in the air zone", BODY, 8, TEXT_MID)
|
| 193 |
-
by -= 2
|
| 194 |
-
equiv(c, bx, by, "Sontec MEP-250A filter section")
|
| 195 |
-
|
| 196 |
-
y = y - hpf_h - 6
|
| 197 |
-
draw_arrow_down(c, x + w / 2, y + 4, y - 10)
|
| 198 |
-
y -= 14
|
| 199 |
-
|
| 200 |
-
# EQ sub-box
|
| 201 |
-
eq_h = 258
|
| 202 |
-
bx, by = sub_box(c, x, y - eq_h, w, eq_h, "6-BAND PARAMETRIC EQ")
|
| 203 |
-
by = text(c, bx, by, "Discrete Class-A op-amps, inductorless gyrator topology", ITALIC, 7.5, TEXT_MID)
|
| 204 |
-
by -= 6
|
| 205 |
-
|
| 206 |
-
bands = [
|
| 207 |
-
("BAND 1: BASS BOOST", "PeakFilter \u2022 Q = 2.0 \u2022 40\u2013100 Hz",
|
| 208 |
-
"Narrow peak, 0 to +3 dB \u2022 Twin-T notch / gyrator",
|
| 209 |
-
"Pultec EQP-1A low-end boost"),
|
| 210 |
-
("BAND 2: LOWS SHELF", "LowShelf \u2022 Q = 1.0 \u2022 200 Hz",
|
| 211 |
-
"Broad shelf below 200 Hz, \u00b13 dB \u2022 RC shelf network",
|
| 212 |
-
"Manley Massive Passive low band"),
|
| 213 |
-
("BAND 3: HIGHS SHELF", "HighShelf \u2022 Q = 0.7 \u2022 10 kHz",
|
| 214 |
-
"Broad shelf above 10 kHz (\"Air\" band), \u00b13 dB \u2022 Gentle slope",
|
| 215 |
-
"Maselec MEA-2 air band"),
|
| 216 |
-
("BAND 4: MIDS PEAK", "PeakFilter \u2022 Q = 1.0 \u2022 1.2 kHz",
|
| 217 |
-
"Wide bell centered at 1.2 kHz, \u00b13 dB \u2022 Gyrator parametric",
|
| 218 |
-
"GML 8200 / Sontec MEP-250A"),
|
| 219 |
-
]
|
| 220 |
-
|
| 221 |
-
for i, (name, params, desc, eq_gear) in enumerate(bands):
|
| 222 |
-
band_h = 46
|
| 223 |
-
band_y = by - band_h
|
| 224 |
-
band_color = HexColor("#f0f4fa") if i % 2 == 0 else HexColor("#faf0f4")
|
| 225 |
-
c.setFillColor(band_color)
|
| 226 |
-
c.roundRect(bx - 2, band_y, w - 12, band_h, 3, fill=1, stroke=0)
|
| 227 |
-
ty = band_y + band_h - 12
|
| 228 |
-
ty = text(c, bx + 2, ty, name, BOLD, 8)
|
| 229 |
-
ty = text(c, bx + 2, ty, params, MONO, 7, TEXT_MID)
|
| 230 |
-
ty = text(c, bx + 2, ty, desc, BODY, 7.5)
|
| 231 |
-
equiv(c, bx + 2, ty, eq_gear)
|
| 232 |
-
|
| 233 |
-
by = band_y - 4
|
| 234 |
-
|
| 235 |
-
stage_box(c, s2_x, s2_y - s2_h, s2_w, s2_h, "SUBSONIC FILTER + EQ SECTION", s2_body, 2)
|
| 236 |
-
|
| 237 |
-
# Arrow to page 2
|
| 238 |
-
draw_arrow_down(c, arr_x, s2_y - s2_h, s2_y - s2_h - 18)
|
| 239 |
-
c.setFillColor(TEXT_LIGHT)
|
| 240 |
-
c.setFont(ITALIC, 7.5)
|
| 241 |
-
c.drawCentredString(arr_x, s2_y - s2_h - 28, "continued on next page")
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 245 |
-
# PAGE 2
|
| 246 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 247 |
-
|
| 248 |
-
def page2(c):
|
| 249 |
-
arr_x = W / 2
|
| 250 |
-
|
| 251 |
-
# ── Stage 3: Multiband Compressor ────────────────────────────────
|
| 252 |
-
s3_y = H - 28
|
| 253 |
-
s3_h = 370
|
| 254 |
-
s3_x = M
|
| 255 |
-
s3_w = W - 2 * M
|
| 256 |
-
|
| 257 |
-
def s3_body(c, x, y, w):
|
| 258 |
-
# Slider curve note
|
| 259 |
-
y = text(c, x, y, "LOGARITHMIC SLIDER CURVE: t = (slider / 100)\u00b2 \u2014 "
|
| 260 |
-
"bottom half (0\u201350) is transparent-to-light, aggressive in top 30% (70\u2013100)",
|
| 261 |
-
BOLD, 7.5, WARM_RED)
|
| 262 |
-
y -= 4
|
| 263 |
-
|
| 264 |
-
# Crossover sub-box
|
| 265 |
-
xo_h = 38
|
| 266 |
-
bx, by = sub_box(c, x, y - xo_h, w, xo_h,
|
| 267 |
-
"LINKWITZ-RILEY CROSSOVERS (4th-order, 200 Hz + 4 kHz)")
|
| 268 |
-
by = text(c, bx, by, "Two cascaded 2nd-order Butterworth filters per split "
|
| 269 |
-
"\u2022 Zero-phase \u2022 Perfect reconstruction", BODY, 7.5)
|
| 270 |
-
y = y - xo_h - 6
|
| 271 |
-
|
| 272 |
-
# 3-band columns
|
| 273 |
-
col_w = (w - 30) / 3
|
| 274 |
-
band_data = [
|
| 275 |
-
("LOW BAND (< 200 Hz)", ACCENT2, [
|
| 276 |
-
("Threshold:", "-16 \u2500 -24 dB"),
|
| 277 |
-
("Ratio:", "1.2:1 \u2500 3.0:1"),
|
| 278 |
-
("Attack:", "80 \u2500 20 ms"),
|
| 279 |
-
("Release:", "200 \u2500 120 ms"),
|
| 280 |
-
], "Firmest control \u2014 attack scales\ndown to catch kick/bass transients",
|
| 281 |
-
"SSL G384 Bus Comp (low band)"),
|
| 282 |
-
("MID BAND (200 Hz \u2013 4 kHz)", ACCENT, [
|
| 283 |
-
("Threshold:", "-14 \u2500 -24 dB"),
|
| 284 |
-
("Ratio:", "1.1:1 \u2500 2.5:1"),
|
| 285 |
-
("Attack:", "30 \u2500 10 ms"),
|
| 286 |
-
("Release:", "150 \u2500 100 ms"),
|
| 287 |
-
], "Musical peak control \u2014 attack\ncatches snare/vocal peaks at high comp",
|
| 288 |
-
"Manley Vari-Mu (mid band)"),
|
| 289 |
-
("HIGH BAND (> 4 kHz)", HexColor("#2a7a5a"), [
|
| 290 |
-
("Threshold:", "-12 \u2500 -22 dB"),
|
| 291 |
-
("Ratio:", "1.05:1 \u2500 2.0:1"),
|
| 292 |
-
("Attack:", "10 \u2500 3 ms"),
|
| 293 |
-
("Release:", "80 \u2500 40 ms"),
|
| 294 |
-
], "Transient control \u2014 fast attack\ntames cymbal/click transients",
|
| 295 |
-
"Maselec MLA-3 (high band)"),
|
| 296 |
-
]
|
| 297 |
-
|
| 298 |
-
band_h = 155
|
| 299 |
-
for i, (title, color, params, desc, gear) in enumerate(band_data):
|
| 300 |
-
bx_col = x + i * (col_w + 15)
|
| 301 |
-
# Band box
|
| 302 |
-
c.setFillColor(HexColor("#f8f8fc") if i % 2 == 0 else HexColor("#faf8f0"))
|
| 303 |
-
c.setStrokeColor(HexColor("#dddddd"))
|
| 304 |
-
c.setLineWidth(0.5)
|
| 305 |
-
c.roundRect(bx_col, y - band_h, col_w, band_h, 4, fill=1, stroke=1)
|
| 306 |
-
# Band title
|
| 307 |
-
c.setFillColor(color)
|
| 308 |
-
c.roundRect(bx_col, y - 16, col_w, 16, 4, fill=1, stroke=0)
|
| 309 |
-
c.rect(bx_col, y - 16, col_w, 4, fill=1, stroke=0)
|
| 310 |
-
c.setFillColor(white)
|
| 311 |
-
c.setFont(BOLD, 7.5)
|
| 312 |
-
c.drawCentredString(bx_col + col_w / 2, y - 12, title)
|
| 313 |
-
# Parameters
|
| 314 |
-
py = y - 28
|
| 315 |
-
for label, val in params:
|
| 316 |
-
c.setFillColor(TEXT_DARK)
|
| 317 |
-
c.setFont(BOLD, 7)
|
| 318 |
-
c.drawString(bx_col + 6, py, label)
|
| 319 |
-
c.setFont(MONO, 7)
|
| 320 |
-
c.drawString(bx_col + 58, py, val)
|
| 321 |
-
py -= 11
|
| 322 |
-
# Description
|
| 323 |
-
py -= 4
|
| 324 |
-
c.setFillColor(TEXT_MID)
|
| 325 |
-
c.setFont(ITALIC, 7)
|
| 326 |
-
for dl in desc.split("\n"):
|
| 327 |
-
c.drawString(bx_col + 6, py, dl)
|
| 328 |
-
py -= 9
|
| 329 |
-
# Gear equiv
|
| 330 |
-
py -= 2
|
| 331 |
-
c.setFont(ITALIC, 6.5)
|
| 332 |
-
c.drawString(bx_col + 6, py, f"Equiv: {gear}")
|
| 333 |
-
|
| 334 |
-
y = y - band_h - 8
|
| 335 |
-
|
| 336 |
-
# VCA topology note
|
| 337 |
-
y = text(c, x, y, "VCA topology \u2022 RMS sidechain \u2022 "
|
| 338 |
-
"No makeup gain \u2014 LUFS normalization restores output level",
|
| 339 |
-
BODY, 7.5, TEXT_MID)
|
| 340 |
-
y -= 2
|
| 341 |
-
# All parameters scale note
|
| 342 |
-
y = text(c, x, y, "All parameters (including attack) scale with slider position via quadratic curve",
|
| 343 |
-
BOLD, 7.5, WARM_ORANGE)
|
| 344 |
-
y -= 4
|
| 345 |
-
# Bypass note
|
| 346 |
-
c.setFillColor(HexColor("#cc4444"))
|
| 347 |
-
c.setFont(BOLD, 8.5)
|
| 348 |
-
c.drawString(x, y, "TRUE BYPASS when slider = 0 (no processing)")
|
| 349 |
-
y -= 12
|
| 350 |
-
equiv(c, x, y, "SSL G384 / Manley Vari-Mu / Maselec MLA-3 (3-band)")
|
| 351 |
-
|
| 352 |
-
stage_box(c, s3_x, s3_y - s3_h, s3_w, s3_h, "MULTIBAND COMPRESSOR (3-Band)", s3_body, 3)
|
| 353 |
-
|
| 354 |
-
draw_arrow_down(c, arr_x, s3_y - s3_h, s3_y - s3_h - 20)
|
| 355 |
-
|
| 356 |
-
# ── Stage 4: Stereo Width ─────────────────────────────────────────
|
| 357 |
-
s4_y = s3_y - s3_h - 22
|
| 358 |
-
s4_h = 330
|
| 359 |
-
s4_x = M
|
| 360 |
-
s4_w = W - 2 * M
|
| 361 |
-
|
| 362 |
-
def s4_body(c, x, y, w):
|
| 363 |
-
# Crossover
|
| 364 |
-
cx_h = 48
|
| 365 |
-
bx, by = sub_box(c, x, y - cx_h, w, cx_h, "LINKWITZ-RILEY CROSSOVER (4th-order, 200 Hz)")
|
| 366 |
-
by = text(c, bx, by, "Two cascaded 2nd-order Butterworth filters", BODY, 8)
|
| 367 |
-
by -= 2
|
| 368 |
-
equiv(c, bx, by, "Rane AC 22B active crossover")
|
| 369 |
-
|
| 370 |
-
y = y - cx_h - 6
|
| 371 |
-
|
| 372 |
-
# Split into two columns
|
| 373 |
-
col_w = (w - 20) / 2
|
| 374 |
-
left_x = x
|
| 375 |
-
right_x = x + col_w + 20
|
| 376 |
-
|
| 377 |
-
# LOW BAND label
|
| 378 |
-
c.setFillColor(ACCENT2)
|
| 379 |
-
c.setFont(BOLD, 8)
|
| 380 |
-
c.drawCentredString(left_x + col_w / 2, y - 10, "LOW BAND (< 200 Hz)")
|
| 381 |
-
c.setFillColor(TEXT_MID)
|
| 382 |
-
c.setFont(BODY, 7.5)
|
| 383 |
-
c.drawCentredString(left_x + col_w / 2, y - 22, "Untouched \u2014 mono-safe bass")
|
| 384 |
-
|
| 385 |
-
# HIGH BAND label
|
| 386 |
-
c.setFillColor(ACCENT)
|
| 387 |
-
c.setFont(BOLD, 8)
|
| 388 |
-
c.drawCentredString(right_x + col_w / 2, y - 10, "HIGH BAND (\u2265 200 Hz)")
|
| 389 |
-
|
| 390 |
-
y -= 30
|
| 391 |
-
|
| 392 |
-
# M/S boxes on the right
|
| 393 |
-
ms_blocks = [
|
| 394 |
-
("M/S ENCODE", "Mid = (L + R) / 2\nSide = (L \u2212 R) / 2",
|
| 395 |
-
"Resistor summing matrix (matched 0.1% precision)"),
|
| 396 |
-
("WIDTH CONTROL", "Mid \u00d7 mid_scale\nSide \u00d7 side_scale",
|
| 397 |
-
"Energy-preserving: mid_scale = \u221a(2/(1+w\u00b2)) Range: 80\u2013150%"),
|
| 398 |
-
("M/S DECODE", "L = Mid + Side\nR = Mid \u2212 Side",
|
| 399 |
-
"Resistor summing matrix"),
|
| 400 |
-
]
|
| 401 |
-
|
| 402 |
-
for i, (name, formulas, desc) in enumerate(ms_blocks):
|
| 403 |
-
bh = 52
|
| 404 |
-
bx2, by2 = sub_box(c, right_x, y - bh, col_w, bh, name)
|
| 405 |
-
for line in formulas.split("\n"):
|
| 406 |
-
by2 = text(c, bx2, by2, line, MONO, 7.5, TEXT_MID)
|
| 407 |
-
text(c, bx2, by2, desc, BODY, 7, TEXT_MID)
|
| 408 |
-
if i < 2:
|
| 409 |
-
draw_arrow_down(c, right_x + col_w / 2, y - bh, y - bh - 10)
|
| 410 |
-
y = y - bh - 14
|
| 411 |
-
|
| 412 |
-
# Summing
|
| 413 |
-
y -= 6
|
| 414 |
-
c.setStrokeColor(ACCENT)
|
| 415 |
-
c.setLineWidth(1)
|
| 416 |
-
c.line(left_x + col_w / 2, y + 70, left_x + col_w / 2, y + 10) # low band vertical
|
| 417 |
-
c.line(left_x + col_w / 2, y + 10, right_x + col_w / 2, y + 10) # horizontal join
|
| 418 |
-
c.line(right_x + col_w / 2, y + 30, right_x + col_w / 2, y + 10)
|
| 419 |
-
# Plus symbol
|
| 420 |
-
cx = (left_x + col_w / 2 + right_x + col_w / 2) / 2
|
| 421 |
-
c.setFont(BOLD, 12)
|
| 422 |
-
c.setFillColor(ACCENT)
|
| 423 |
-
c.drawCentredString(cx, y + 6, "+")
|
| 424 |
-
|
| 425 |
-
draw_arrow_down(c, cx, y + 4, y - 8)
|
| 426 |
-
y -= 10
|
| 427 |
-
c.setFillColor(TEXT_MID)
|
| 428 |
-
c.setFont(BODY, 7.5)
|
| 429 |
-
c.drawCentredString(cx, y - 4, "Summing amp (clip protection if peak > 1.0)")
|
| 430 |
-
equiv(c, x, y - 18, "Brainworx bx_control V2")
|
| 431 |
-
|
| 432 |
-
stage_box(c, s4_x, s4_y - s4_h, s4_w, s4_h, "STEREO WIDTH \u2014 FREQUENCY-SELECTIVE M/S MATRIX", s4_body, 4)
|
| 433 |
-
|
| 434 |
-
draw_arrow_down(c, arr_x, s4_y - s4_h, s4_y - s4_h - 18)
|
| 435 |
-
c.setFillColor(TEXT_LIGHT)
|
| 436 |
-
c.setFont(ITALIC, 7.5)
|
| 437 |
-
c.drawCentredString(arr_x, s4_y - s4_h - 28, "continued on next page")
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 441 |
-
# PAGE 3
|
| 442 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 443 |
-
|
| 444 |
-
def page3(c):
|
| 445 |
-
arr_x = W / 2
|
| 446 |
-
|
| 447 |
-
# ── Stage 5: LUFS Normalization ──────────────────────────────────
|
| 448 |
-
s5_y = H - 28
|
| 449 |
-
s5_h = 130
|
| 450 |
-
s5_x = M
|
| 451 |
-
s5_w = W - 2 * M
|
| 452 |
-
|
| 453 |
-
def s5_body(c, x, y, w):
|
| 454 |
-
col_w = (w - 20) / 2
|
| 455 |
-
|
| 456 |
-
# LUFS Meter
|
| 457 |
-
bh = 52
|
| 458 |
-
bx, by = sub_box(c, x, y - bh, col_w, bh, "LUFS METER (ITU-R BS.1770)")
|
| 459 |
-
by = text(c, bx, by, "Integrated loudness measurement", BODY, 8)
|
| 460 |
-
by -= 2
|
| 461 |
-
by = text(c, bx, by, "K-weighted, gated per BS.1770-4", MONO, 7, TEXT_MID)
|
| 462 |
-
|
| 463 |
-
# Output Gain Trim
|
| 464 |
-
rx = x + col_w + 20
|
| 465 |
-
bx2, by2 = sub_box(c, rx, y - bh, col_w, bh, "OUTPUT GAIN TRIM")
|
| 466 |
-
by2 = text(c, bx2, by2, "Stepped relay attenuator", BODY, 8)
|
| 467 |
-
by2 = text(c, bx2, by2, "Applies gain delta to hit target LUFS exactly", BODY, 8)
|
| 468 |
-
by2 -= 2
|
| 469 |
-
by2 = text(c, bx2, by2, "Targets: -14 (Streaming) / -11 (CD) / Custom", MONO, 7, TEXT_MID)
|
| 470 |
-
|
| 471 |
-
# Arrow between them
|
| 472 |
-
c.setStrokeColor(ACCENT)
|
| 473 |
-
c.setLineWidth(1.5)
|
| 474 |
-
mid_y = y - bh / 2
|
| 475 |
-
c.line(x + col_w + 2, mid_y, rx - 2, mid_y)
|
| 476 |
-
c.setFillColor(ACCENT)
|
| 477 |
-
p = c.beginPath()
|
| 478 |
-
p.moveTo(rx - 2, mid_y)
|
| 479 |
-
p.lineTo(rx - 10, mid_y + 4)
|
| 480 |
-
p.lineTo(rx - 10, mid_y - 4)
|
| 481 |
-
p.close()
|
| 482 |
-
c.drawPath(p, fill=1, stroke=0)
|
| 483 |
-
|
| 484 |
-
y -= bh + 8
|
| 485 |
-
equiv(c, x, y, "Dorrough 40-A loudness meter + Dangerous Music Monitor ST")
|
| 486 |
-
|
| 487 |
-
stage_box(c, s5_x, s5_y - s5_h, s5_w, s5_h, "OUTPUT LEVEL \u2014 LUFS NORMALIZATION", s5_body, 5)
|
| 488 |
-
|
| 489 |
-
draw_arrow_down(c, arr_x, s5_y - s5_h, s5_y - s5_h - 18)
|
| 490 |
-
|
| 491 |
-
# ── Stage 6: Soft Clipper ────────────────────────────────────────
|
| 492 |
-
s6_y = s5_y - s5_h - 20
|
| 493 |
-
s6_h = 155
|
| 494 |
-
s6_x = M
|
| 495 |
-
s6_w = W - 2 * M
|
| 496 |
-
|
| 497 |
-
def s6_body(c, x, y, w):
|
| 498 |
-
y = text(c, x, y, "Piecewise tanh saturation \u2014 analog-style waveshaping (NOT a limiter)", BOLD, 8.5)
|
| 499 |
-
y -= 4
|
| 500 |
-
|
| 501 |
-
col_w = (w - 20) / 2
|
| 502 |
-
|
| 503 |
-
# Left: How it works
|
| 504 |
-
bh = 80
|
| 505 |
-
bx, by = sub_box(c, x, y - bh, col_w, bh, "TRANSFER FUNCTION")
|
| 506 |
-
by = text(c, bx, by, "Ceiling: -0.1 dBTP (linear \u2248 0.9885)", MONO, 7, TEXT_MID)
|
| 507 |
-
by = text(c, bx, by, "Knee: 2 dB below ceiling", MONO, 7, TEXT_MID)
|
| 508 |
-
by -= 4
|
| 509 |
-
by = text(c, bx, by, "Below knee: perfectly linear (zero processing)", BODY, 7.5)
|
| 510 |
-
by = text(c, bx, by, "Above knee: tanh(excess / headroom) \u2192 asymptotes to ceiling", BODY, 7.5)
|
| 511 |
-
by -= 2
|
| 512 |
-
by = text(c, bx, by, "Only affects top 1\u20132 dB of loudest transient tips", BOLD, 7, WARM_RED)
|
| 513 |
-
|
| 514 |
-
# Right: Behavior
|
| 515 |
-
rx = x + col_w + 20
|
| 516 |
-
bx2, by2 = sub_box(c, rx, y - bh, col_w, bh, "BEHAVIOR")
|
| 517 |
-
by2 = text(c, bx2, by2, "Shaves peak tips without reducing LUFS", BODY, 7.5)
|
| 518 |
-
by2 = text(c, bx2, by2, "Allows normalization to reach target loudness", BODY, 7.5)
|
| 519 |
-
by2 -= 4
|
| 520 |
-
by2 = text(c, bx2, by2, "Reduces crest factor by 1\u20132 dB on hot material", MONO, 7, TEXT_MID)
|
| 521 |
-
by2 = text(c, bx2, by2, "Transparent on well-mixed sources", MONO, 7, TEXT_MID)
|
| 522 |
-
by2 -= 2
|
| 523 |
-
by2 = text(c, bx2, by2, "Always active (no bypass needed \u2014 linear below knee)", BOLD, 7, WARM_ORANGE)
|
| 524 |
-
|
| 525 |
-
y -= bh + 8
|
| 526 |
-
equiv(c, x, y, "Kush Omega N / Thermionic Culture Vulture (gentle saturation mode)")
|
| 527 |
-
|
| 528 |
-
stage_box(c, s6_x, s6_y - s6_h, s6_w, s6_h, "SOFT CLIPPER \u2014 PIECEWISE TANH SATURATION", s6_body, 6)
|
| 529 |
-
|
| 530 |
-
draw_arrow_down(c, arr_x, s6_y - s6_h, s6_y - s6_h - 18)
|
| 531 |
-
|
| 532 |
-
# ── Stage 7: True Peak Ceiling ───────────────────────────────────
|
| 533 |
-
s7_y = s6_y - s6_h - 20
|
| 534 |
-
s7_h = 105
|
| 535 |
-
s7_x = M
|
| 536 |
-
s7_w = W - 2 * M
|
| 537 |
-
|
| 538 |
-
def s7_body(c, x, y, w):
|
| 539 |
-
y = text(c, x, y, "Safety net \u2014 catches any residual inter-sample peaks the soft clipper didn't fully tame",
|
| 540 |
-
BODY, 8.5)
|
| 541 |
-
y -= 4
|
| 542 |
-
y = text(c, x, y, "Measures true peak via 4x oversampling per ITU-R BS.1770", MONO, 7.5, TEXT_MID)
|
| 543 |
-
y = text(c, x, y, "If true peak > -0.1 dBTP: scales entire signal down by overshoot amount", MONO, 7.5, TEXT_MID)
|
| 544 |
-
y -= 4
|
| 545 |
-
y = text(c, x, y, "Rarely engages thanks to soft clipper \u2014 typically < 0.1 dB correction", BODY, 8)
|
| 546 |
-
y -= 4
|
| 547 |
-
c.setFillColor(HexColor("#cc4444"))
|
| 548 |
-
c.setFont(BOLD, 8)
|
| 549 |
-
c.drawString(x, y, "GUARANTEES: True peak \u2264 -0.1 dBTP on all output")
|
| 550 |
-
y -= 12
|
| 551 |
-
equiv(c, x, y, "Dorrough 40-A peak meter + relay attenuator")
|
| 552 |
-
|
| 553 |
-
stage_box(c, s7_x, s7_y - s7_h, s7_w, s7_h, "TRUE PEAK CEILING (-0.1 dBTP)", s7_body, 7)
|
| 554 |
-
|
| 555 |
-
draw_arrow_down(c, arr_x, s7_y - s7_h, s7_y - s7_h - 18)
|
| 556 |
-
|
| 557 |
-
# Output file box
|
| 558 |
-
out_y = s7_y - s7_h - 20
|
| 559 |
-
out_w = 140
|
| 560 |
-
out_h = 30
|
| 561 |
-
out_x = arr_x - out_w / 2
|
| 562 |
-
c.setFillColor(BG_DARK)
|
| 563 |
-
c.roundRect(out_x, out_y - out_h, out_w, out_h, 6, fill=1, stroke=0)
|
| 564 |
-
c.setFillColor(white)
|
| 565 |
-
c.setFont(BOLD, 10)
|
| 566 |
-
c.drawCentredString(arr_x, out_y - 19, "24-bit WAV")
|
| 567 |
-
|
| 568 |
-
# ── Gear Rack Table ───────────────────────────────────────────────
|
| 569 |
-
table_y = out_y - out_h - 30
|
| 570 |
-
c.setFillColor(BG_DARK)
|
| 571 |
-
c.setFont(BOLD, 11)
|
| 572 |
-
c.drawString(M, table_y, "ANALOG GEAR RACK EQUIVALENT")
|
| 573 |
-
table_y -= 6
|
| 574 |
-
|
| 575 |
-
# Table
|
| 576 |
-
c.setStrokeColor(BORDER)
|
| 577 |
-
c.setLineWidth(0.5)
|
| 578 |
-
|
| 579 |
-
rows = [
|
| 580 |
-
("1U", "Dangerous Music Liaison", "Input gain / level matching"),
|
| 581 |
-
("2U", "Sontec MEP-250A", "HPF + 6-band parametric EQ"),
|
| 582 |
-
("3U", "SSL G384 / Manley Vari-Mu / Maselec MLA-3", "3-band multiband compression, bypass at 0"),
|
| 583 |
-
("1U", "Rane AC 22B", "Linkwitz-Riley crossover (200 Hz)"),
|
| 584 |
-
("1U", "Brainworx bx_control V2", "M/S encode / width / decode"),
|
| 585 |
-
("1U", "Dorrough 40-A + Dangerous ST", "LUFS metering + output gain trim"),
|
| 586 |
-
("1U", "Kush Omega N", "Piecewise tanh soft clipper (-0.1 dBTP ceiling)"),
|
| 587 |
-
("--", "Dorrough 40-A (peak section)", "True peak ceiling / safety net"),
|
| 588 |
-
]
|
| 589 |
-
|
| 590 |
-
col_x = [M, M + 30, M + 230]
|
| 591 |
-
row_h = 16
|
| 592 |
-
table_w = W - 2 * M
|
| 593 |
-
|
| 594 |
-
# Header
|
| 595 |
-
c.setFillColor(ACCENT)
|
| 596 |
-
c.rect(M, table_y - row_h, table_w, row_h, fill=1, stroke=0)
|
| 597 |
-
c.setFillColor(white)
|
| 598 |
-
c.setFont(BOLD, 7.5)
|
| 599 |
-
c.drawString(col_x[0] + 4, table_y - 12, "SIZE")
|
| 600 |
-
c.drawString(col_x[1] + 4, table_y - 12, "UNIT")
|
| 601 |
-
c.drawString(col_x[2] + 4, table_y - 12, "FUNCTION")
|
| 602 |
-
table_y -= row_h
|
| 603 |
-
|
| 604 |
-
for i, (size, unit, func) in enumerate(rows):
|
| 605 |
-
bg = HexColor("#f8f8fc") if i % 2 == 0 else white
|
| 606 |
-
c.setFillColor(bg)
|
| 607 |
-
c.rect(M, table_y - row_h, table_w, row_h, fill=1, stroke=0)
|
| 608 |
-
c.setStrokeColor(HexColor("#eeeeee"))
|
| 609 |
-
c.line(M, table_y - row_h, M + table_w, table_y - row_h)
|
| 610 |
-
c.setFillColor(TEXT_DARK)
|
| 611 |
-
c.setFont(BOLD, 7.5)
|
| 612 |
-
c.drawString(col_x[0] + 4, table_y - 12, size)
|
| 613 |
-
c.setFont(BODY, 7.5)
|
| 614 |
-
c.drawString(col_x[1] + 4, table_y - 12, unit)
|
| 615 |
-
c.setFont(BODY, 7.5)
|
| 616 |
-
c.drawString(col_x[2] + 4, table_y - 12, func)
|
| 617 |
-
table_y -= row_h
|
| 618 |
-
|
| 619 |
-
# Total row
|
| 620 |
-
c.setFillColor(BG_DARK)
|
| 621 |
-
c.rect(M, table_y - row_h, table_w, row_h, fill=1, stroke=0)
|
| 622 |
-
c.setFillColor(white)
|
| 623 |
-
c.setFont(BOLD, 8)
|
| 624 |
-
c.drawString(col_x[0] + 4, table_y - 12, "10U")
|
| 625 |
-
c.drawString(col_x[1] + 4, table_y - 12, "Total rack space")
|
| 626 |
-
|
| 627 |
-
# ── Signal flow summary ───────────────────────────────────────────
|
| 628 |
-
table_y -= row_h + 20
|
| 629 |
-
c.setFillColor(TEXT_MID)
|
| 630 |
-
c.setFont(BOLD, 8)
|
| 631 |
-
c.drawCentredString(W / 2, table_y, "SIGNAL FLOW")
|
| 632 |
-
|
| 633 |
-
table_y -= 14
|
| 634 |
-
flow_parts = [
|
| 635 |
-
"Pre-Gain\nDrop\n(-18 LUFS)",
|
| 636 |
-
"HPF\n15 Hz",
|
| 637 |
-
"EQ\n6-Band",
|
| 638 |
-
"Multiband\nComp\n(3-Band)",
|
| 639 |
-
"Stereo\nWidth\n(M/S)",
|
| 640 |
-
"LUFS\nNormalize",
|
| 641 |
-
"Soft\nClipper",
|
| 642 |
-
"True Peak\nCeiling",
|
| 643 |
-
"Output",
|
| 644 |
-
]
|
| 645 |
-
|
| 646 |
-
total_w = W - 2 * M
|
| 647 |
-
box_w = 56
|
| 648 |
-
gap = (total_w - len(flow_parts) * box_w) / (len(flow_parts) - 1)
|
| 649 |
-
bx_start = M
|
| 650 |
-
|
| 651 |
-
for i, part in enumerate(flow_parts):
|
| 652 |
-
bx = bx_start + i * (box_w + gap)
|
| 653 |
-
bh = 36
|
| 654 |
-
|
| 655 |
-
if i == len(flow_parts) - 1:
|
| 656 |
-
c.setFillColor(BG_DARK)
|
| 657 |
-
elif i == 6: # Soft Clipper
|
| 658 |
-
c.setFillColor(WARM_RED)
|
| 659 |
-
elif i == 7: # True Peak Ceiling
|
| 660 |
-
c.setFillColor(WARM_ORANGE)
|
| 661 |
-
else:
|
| 662 |
-
c.setFillColor(ACCENT if i % 2 == 0 else ACCENT2)
|
| 663 |
-
c.roundRect(bx, table_y - bh, box_w, bh, 4, fill=1, stroke=0)
|
| 664 |
-
|
| 665 |
-
c.setFillColor(white)
|
| 666 |
-
c.setFont(BOLD, 6.5)
|
| 667 |
-
lines = part.split("\n")
|
| 668 |
-
line_y = table_y - 9 - (len(lines) - 1) * 4
|
| 669 |
-
for line in lines:
|
| 670 |
-
c.drawCentredString(bx + box_w / 2, line_y, line)
|
| 671 |
-
line_y -= 8
|
| 672 |
-
|
| 673 |
-
# Arrow between boxes
|
| 674 |
-
if i < len(flow_parts) - 1:
|
| 675 |
-
ax1 = bx + box_w + 1
|
| 676 |
-
ax2 = bx + box_w + gap - 1
|
| 677 |
-
ay = table_y - bh / 2
|
| 678 |
-
c.setStrokeColor(TEXT_MID)
|
| 679 |
-
c.setLineWidth(1)
|
| 680 |
-
c.line(ax1, ay, ax2 - 5, ay)
|
| 681 |
-
c.setFillColor(TEXT_MID)
|
| 682 |
-
p = c.beginPath()
|
| 683 |
-
p.moveTo(ax2, ay)
|
| 684 |
-
p.lineTo(ax2 - 5, ay + 3)
|
| 685 |
-
p.lineTo(ax2 - 5, ay - 3)
|
| 686 |
-
p.close()
|
| 687 |
-
c.drawPath(p, fill=1, stroke=0)
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 691 |
-
# PAGE 4 — Super AI (beta) Workflow
|
| 692 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 693 |
-
|
| 694 |
-
SUPER_GREEN = HexColor("#1a7a3a")
|
| 695 |
-
SUPER_BLUE = HexColor("#2a5a9a")
|
| 696 |
-
SUPER_GOLD = HexColor("#8a6d1b")
|
| 697 |
-
|
| 698 |
-
def page4(c):
|
| 699 |
-
arr_x = W / 2
|
| 700 |
-
|
| 701 |
-
# ── Title banner ──────────────────────────────────────────────────
|
| 702 |
-
bh = 52
|
| 703 |
-
c.setFillColor(BG_DARK)
|
| 704 |
-
c.rect(0, H - bh, W, bh, fill=1, stroke=0)
|
| 705 |
-
c.setFillColor(white)
|
| 706 |
-
c.setFont(BOLD, 16)
|
| 707 |
-
c.drawCentredString(W / 2, H - 24, "SUPER AI (BETA) — 5-PASS MASTERING WORKFLOW")
|
| 708 |
-
c.setFont(BODY, 10)
|
| 709 |
-
c.setFillColor(HexColor("#aaaacc"))
|
| 710 |
-
c.drawCentredString(W / 2, H - 42,
|
| 711 |
-
"Full Parametric AI Control • Gemini 2.5 Pro • v4.2")
|
| 712 |
-
|
| 713 |
-
# ── 5-Pass Flow Diagram ───────────────────────────────────────────
|
| 714 |
-
flow_y = H - bh - 18
|
| 715 |
-
c.setFillColor(TEXT_DARK)
|
| 716 |
-
c.setFont(BOLD, 11)
|
| 717 |
-
c.drawString(M, flow_y, "5-PASS ITERATIVE REFINEMENT LOOP")
|
| 718 |
-
flow_y -= 8
|
| 719 |
-
|
| 720 |
-
passes = [
|
| 721 |
-
("PASS 1", "AI ANALYZE → MASTER",
|
| 722 |
-
"AI analyzes raw audio measurements (spectral profile, dynamics,\n"
|
| 723 |
-
"stereo field, crest factor). Returns full parametric settings.\n"
|
| 724 |
-
"DSP chain processes audio with AI-chosen parameters.",
|
| 725 |
-
SUPER_GREEN),
|
| 726 |
-
("PASS 2", "AI COMPARE → REVISE → RE-MASTER",
|
| 727 |
-
"AI compares original vs mastered measurements. Makes SMALL\n"
|
| 728 |
-
"incremental adjustments to all parameters. Re-masters with\n"
|
| 729 |
-
"revised settings. Full history context prevents oscillation.",
|
| 730 |
-
SUPER_BLUE),
|
| 731 |
-
("PASS 3", "AI COMPARE → REVISE → RE-MASTER",
|
| 732 |
-
"Same as Pass 2. Further refinement. AI references all prior\n"
|
| 733 |
-
"passes to avoid reverting previous improvements.",
|
| 734 |
-
SUPER_BLUE),
|
| 735 |
-
("PASS 4", "AI COMPARE → REVISE → RE-MASTER",
|
| 736 |
-
"Same as Pass 2–3. Final adjustment pass. AI makes the\n"
|
| 737 |
-
"smallest, most surgical changes at this stage.",
|
| 738 |
-
SUPER_BLUE),
|
| 739 |
-
("PASS 5", "FINAL REPORT (NO CHANGES)",
|
| 740 |
-
"AI compares final master against original. Writes a quality\n"
|
| 741 |
-
"report covering spectral balance, dynamics, stereo image,\n"
|
| 742 |
-
"and streaming compliance. No parameter changes — report only.",
|
| 743 |
-
SUPER_GOLD),
|
| 744 |
-
]
|
| 745 |
-
|
| 746 |
-
box_h = 60
|
| 747 |
-
gap = 6
|
| 748 |
-
for i, (label, title, desc, color) in enumerate(passes):
|
| 749 |
-
bx = M
|
| 750 |
-
by = flow_y - (i * (box_h + gap + 14))
|
| 751 |
-
bw = W - 2 * M
|
| 752 |
-
|
| 753 |
-
# Pass number badge
|
| 754 |
-
badge_w = 52
|
| 755 |
-
c.setFillColor(color)
|
| 756 |
-
c.roundRect(bx, by - box_h, badge_w, box_h, 4, fill=1, stroke=0)
|
| 757 |
-
c.setFillColor(white)
|
| 758 |
-
c.setFont(BOLD, 9)
|
| 759 |
-
c.drawCentredString(bx + badge_w / 2, by - box_h / 2 + 4, label)
|
| 760 |
-
|
| 761 |
-
# Content box
|
| 762 |
-
cx = bx + badge_w + 6
|
| 763 |
-
cw = bw - badge_w - 6
|
| 764 |
-
c.setFillColor(BG_STAGE)
|
| 765 |
-
c.setStrokeColor(BORDER)
|
| 766 |
-
c.setLineWidth(0.5)
|
| 767 |
-
c.roundRect(cx, by - box_h, cw, box_h, 4, fill=1, stroke=1)
|
| 768 |
-
|
| 769 |
-
# Title
|
| 770 |
-
c.setFillColor(color)
|
| 771 |
-
c.setFont(BOLD, 9)
|
| 772 |
-
c.drawString(cx + 8, by - 14, title)
|
| 773 |
-
|
| 774 |
-
# Description
|
| 775 |
-
c.setFillColor(TEXT_MID)
|
| 776 |
-
c.setFont(BODY, 7.5)
|
| 777 |
-
desc_y = by - 26
|
| 778 |
-
for line in desc.strip().split("\n"):
|
| 779 |
-
c.drawString(cx + 8, desc_y, line.strip())
|
| 780 |
-
desc_y -= 10
|
| 781 |
-
|
| 782 |
-
# Arrow between passes
|
| 783 |
-
if i < len(passes) - 1:
|
| 784 |
-
draw_arrow_down(c, bx + badge_w / 2, by - box_h,
|
| 785 |
-
by - box_h - gap - 12)
|
| 786 |
-
|
| 787 |
-
flow_y = by # track for next section
|
| 788 |
-
|
| 789 |
-
# ── AI-Controlled Parameters ──────────────────────────────────────
|
| 790 |
-
param_y = flow_y - box_h - gap - 30
|
| 791 |
-
param_h = 300
|
| 792 |
-
param_x = M
|
| 793 |
-
param_w = W - 2 * M
|
| 794 |
-
|
| 795 |
-
def param_body(c, x, y, w):
|
| 796 |
-
col_w = (w - 16) / 2
|
| 797 |
-
|
| 798 |
-
# LEFT COLUMN — EQ + HPF
|
| 799 |
-
left_x = x
|
| 800 |
-
y_left = y
|
| 801 |
-
|
| 802 |
-
# HPF
|
| 803 |
-
y_left = text(c, left_x, y_left, "HIGH-PASS FILTER", BOLD, 8.5, ACCENT)
|
| 804 |
-
y_left = text(c, left_x, y_left, "Cutoff: 10 – 80 Hz (default 15 Hz)", MONO, 7, TEXT_MID)
|
| 805 |
-
y_left = text(c, left_x, y_left, "12 dB/oct Butterworth", BODY, 7.5)
|
| 806 |
-
y_left -= 6
|
| 807 |
-
|
| 808 |
-
# EQ
|
| 809 |
-
y_left = text(c, left_x, y_left, "6-BAND FULLY PARAMETRIC EQ", BOLD, 8.5, ACCENT)
|
| 810 |
-
y_left = text(c, left_x, y_left, "Each band independently configurable:", BODY, 7.5)
|
| 811 |
-
y_left -= 2
|
| 812 |
-
eq_params = [
|
| 813 |
-
("Type:", "peak / low_shelf / high_shelf"),
|
| 814 |
-
("Frequency:", "20 – 20,000 Hz"),
|
| 815 |
-
("Gain:", "-6 to +6 dB"),
|
| 816 |
-
("Q:", "0.1 – 10.0"),
|
| 817 |
-
]
|
| 818 |
-
for label, val in eq_params:
|
| 819 |
-
c.setFillColor(TEXT_DARK)
|
| 820 |
-
c.setFont(BOLD, 7)
|
| 821 |
-
c.drawString(left_x + 8, y_left, label)
|
| 822 |
-
c.setFont(MONO, 7)
|
| 823 |
-
c.drawString(left_x + 75, y_left, val)
|
| 824 |
-
y_left -= 10
|
| 825 |
-
y_left -= 2
|
| 826 |
-
y_left = text(c, left_x, y_left, "Not limited to UI ±3 dB — full ±6 dB range", BOLD, 7, WARM_RED)
|
| 827 |
-
y_left -= 6
|
| 828 |
-
|
| 829 |
-
# Stereo
|
| 830 |
-
y_left = text(c, left_x, y_left, "STEREO WIDTH", BOLD, 8.5, ACCENT)
|
| 831 |
-
y_left = text(c, left_x, y_left, "Range: 80 – 150% (M/S matrix)", MONO, 7, TEXT_MID)
|
| 832 |
-
y_left = text(c, left_x, y_left, "Crossover at 200 Hz (fixed) — bass stays mono", BODY, 7.5)
|
| 833 |
-
|
| 834 |
-
# RIGHT COLUMN — Compression + Crossovers
|
| 835 |
-
right_x = x + col_w + 16
|
| 836 |
-
y_right = y
|
| 837 |
-
|
| 838 |
-
y_right = text(c, right_x, y_right, "MULTIBAND COMPRESSION (3-band)", BOLD, 8.5, ACCENT2)
|
| 839 |
-
y_right = text(c, right_x, y_right, "Per-band independent control:", BODY, 7.5)
|
| 840 |
-
y_right -= 2
|
| 841 |
-
|
| 842 |
-
comp_params = [
|
| 843 |
-
("Threshold:", "-20 to 0 dB"),
|
| 844 |
-
("Ratio:", "1.0 – 4.0 (1.0 = bypass)"),
|
| 845 |
-
("Attack:", "0.1 – 200 ms"),
|
| 846 |
-
("Release:", "10 – 500 ms"),
|
| 847 |
-
]
|
| 848 |
-
for label, val in comp_params:
|
| 849 |
-
c.setFillColor(TEXT_DARK)
|
| 850 |
-
c.setFont(BOLD, 7)
|
| 851 |
-
c.drawString(right_x + 8, y_right, label)
|
| 852 |
-
c.setFont(MONO, 7)
|
| 853 |
-
c.drawString(right_x + 75, y_right, val)
|
| 854 |
-
y_right -= 10
|
| 855 |
-
y_right -= 2
|
| 856 |
-
y_right = text(c, right_x, y_right,
|
| 857 |
-
"No makeup gain — LUFS normalization restores level",
|
| 858 |
-
BODY, 7.5, TEXT_MID)
|
| 859 |
-
y_right -= 8
|
| 860 |
-
|
| 861 |
-
y_right = text(c, right_x, y_right, "CROSSOVER FREQUENCIES", BOLD, 8.5, ACCENT2)
|
| 862 |
-
y_right = text(c, right_x, y_right, "Low/Mid split: 80 – 500 Hz (default 200 Hz)", MONO, 7, TEXT_MID)
|
| 863 |
-
y_right = text(c, right_x, y_right, "Mid/High split: 1,000 – 10,000 Hz (default 4 kHz)", MONO, 7, TEXT_MID)
|
| 864 |
-
y_right = text(c, right_x, y_right, "Linkwitz-Riley 4th-order (zero-phase, perfect reconstruction)", BODY, 7.5)
|
| 865 |
-
y_right -= 8
|
| 866 |
-
|
| 867 |
-
# Fixed stages note
|
| 868 |
-
y_right = text(c, right_x, y_right, "FIXED (NOT AI-CONTROLLED):", BOLD, 8.5, WARM_RED)
|
| 869 |
-
y_right = text(c, right_x, y_right, "• Pre-gain drop to -18 LUFS", BODY, 7.5)
|
| 870 |
-
y_right = text(c, right_x, y_right, "• LUFS target (set by user)", BODY, 7.5)
|
| 871 |
-
y_right = text(c, right_x, y_right, "• Soft clipper (-0.1 dBTP ceiling)", BODY, 7.5)
|
| 872 |
-
y_right = text(c, right_x, y_right, "• True peak ceiling (-0.1 dBTP)", BODY, 7.5)
|
| 873 |
-
y_right = text(c, right_x, y_right, "• Stereo width crossover (200 Hz)", BODY, 7.5)
|
| 874 |
-
|
| 875 |
-
stage_box(c, param_x, param_y - param_h, param_w, param_h,
|
| 876 |
-
"AI-CONTROLLED PARAMETERS — FULL PARAMETRIC RANGE", param_body)
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
def page5(c):
|
| 880 |
-
"""Page 5: AI Prompting Instructions & Philosophy."""
|
| 881 |
-
arr_x = W / 2
|
| 882 |
-
|
| 883 |
-
# ── Title banner ──────────────────────────────────────────────────
|
| 884 |
-
bh = 52
|
| 885 |
-
c.setFillColor(BG_DARK)
|
| 886 |
-
c.rect(0, H - bh, W, bh, fill=1, stroke=0)
|
| 887 |
-
c.setFillColor(white)
|
| 888 |
-
c.setFont(BOLD, 16)
|
| 889 |
-
c.drawCentredString(W / 2, H - 24, "SUPER AI — PROMPTING PHILOSOPHY")
|
| 890 |
-
c.setFont(BODY, 10)
|
| 891 |
-
c.setFillColor(HexColor("#aaaacc"))
|
| 892 |
-
c.drawCentredString(W / 2, H - 42,
|
| 893 |
-
"AI Mastering Engineer Instructions • v4.2")
|
| 894 |
-
|
| 895 |
-
y = H - bh - 24
|
| 896 |
-
|
| 897 |
-
# ── Mastering Philosophy ──────────────────────────────────────────
|
| 898 |
-
phil_h = 118
|
| 899 |
-
def phil_body(c, x, y, w):
|
| 900 |
-
points = [
|
| 901 |
-
"Listen to what the audio NEEDS, not what sounds impressive on paper.",
|
| 902 |
-
"Use surgical EQ moves — small cuts are often more effective than boosts.",
|
| 903 |
-
"Match compression to the genre and dynamic character of the material.",
|
| 904 |
-
"Preserve the artist's intent — enhance, don't transform.",
|
| 905 |
-
"Be musical and intentional. Every parameter should have a reason.",
|
| 906 |
-
"LESS IS MORE — use the minimum processing needed. Gentle moves preferred.",
|
| 907 |
-
]
|
| 908 |
-
for pt in points:
|
| 909 |
-
c.setFillColor(ACCENT)
|
| 910 |
-
c.setFont(BOLD, 8)
|
| 911 |
-
c.drawString(x, y, "•")
|
| 912 |
-
c.setFillColor(TEXT_DARK)
|
| 913 |
-
c.setFont(BODY, 8)
|
| 914 |
-
c.drawString(x + 12, y, pt)
|
| 915 |
-
y -= 14
|
| 916 |
-
y -= 4
|
| 917 |
-
c.setFillColor(TEXT_MID)
|
| 918 |
-
c.setFont(ITALIC, 7.5)
|
| 919 |
-
c.drawString(x, y, "The AI receives spectral centroid, rolloff, 6-band energy distribution, "
|
| 920 |
-
"crest factor, dynamic range, and stereo correlation.")
|
| 921 |
-
|
| 922 |
-
stage_box(c, M, y - phil_h, W - 2 * M, phil_h, "MASTERING PHILOSOPHY", phil_body)
|
| 923 |
-
y -= phil_h + 20
|
| 924 |
-
|
| 925 |
-
# ── Tonal Direction ───────────────────────────────────────────────
|
| 926 |
-
tone_h = 100
|
| 927 |
-
def tone_body(c, x, y, w):
|
| 928 |
-
col_w = (w - 20) / 2
|
| 929 |
-
|
| 930 |
-
# Warm
|
| 931 |
-
bh = 52
|
| 932 |
-
bx, by = sub_box(c, x, y - bh, col_w, bh, "SLIGHTLY WARM TONE")
|
| 933 |
-
by = text(c, bx, by, "Favor gentle richness in the low-mids (200–500 Hz)", BODY, 8)
|
| 934 |
-
by = text(c, bx, by, "Smooth, non-harsh highs — avoid clinical or brittle sound", BODY, 8)
|
| 935 |
-
by -= 2
|
| 936 |
-
text(c, bx, by, "Think: Neve console warmth, not Pultec mud", ITALIC, 7, TEXT_MID)
|
| 937 |
-
|
| 938 |
-
# Bass
|
| 939 |
-
rx = x + col_w + 20
|
| 940 |
-
bx2, by2 = sub_box(c, rx, y - bh, col_w, bh, "HEAVY, UNCONSTRAINED BASS")
|
| 941 |
-
by2 = text(c, bx2, by2, "Protect low-end punch and transients (40-100 Hz)", BODY, 8)
|
| 942 |
-
by2 = text(c, bx2, by2, "DO NOT over-compress <200 Hz. HPF max 25 Hz.", BODY, 8)
|
| 943 |
-
by2 -= 2
|
| 944 |
-
text(c, bx2, by2, "Sub-bass should feel physical, anchored, wide open", ITALIC, 7, TEXT_MID)
|
| 945 |
-
|
| 946 |
-
stage_box(c, M, y - tone_h, W - 2 * M, tone_h,
|
| 947 |
-
"TONAL DIRECTION (applied to all masters)", tone_body)
|
| 948 |
-
y -= tone_h + 20
|
| 949 |
-
|
| 950 |
-
# ── True Peak Guidance ────────────────────────────────────────────
|
| 951 |
-
tp_h = 148
|
| 952 |
-
def tp_body(c, x, y, w):
|
| 953 |
-
y = text(c, x, y, "IDEAL GOAL: True peak between -1.0 and -0.1 dBTP", BOLD, 9, SUPER_GREEN)
|
| 954 |
-
y -= 6
|
| 955 |
-
y = text(c, x, y, "However, this is a SOFT GOAL — not a hard constraint.", BOLD, 8.5, TEXT_DARK)
|
| 956 |
-
y -= 6
|
| 957 |
-
|
| 958 |
-
points = [
|
| 959 |
-
("Source awareness:", "AI-generated tracks (Suno, Udio, etc.) are already heavily "
|
| 960 |
-
"limited with very low crest factor."),
|
| 961 |
-
("Expected behavior:", "When normalized DOWN to a streaming LUFS target (e.g. -14), "
|
| 962 |
-
"the true peak will naturally drop well below -1.0 dBTP. This is correct."),
|
| 963 |
-
("Dynamics > True peak:", "DO NOT over-compress or crush dynamics to force the true "
|
| 964 |
-
"peak higher. Dynamics preservation is MORE important."),
|
| 965 |
-
("Compression purpose:", "Use compression for tonal shaping and dynamic control — "
|
| 966 |
-
"NEVER to artificially raise the true peak."),
|
| 967 |
-
]
|
| 968 |
-
for label, desc in points:
|
| 969 |
-
c.setFillColor(WARM_RED)
|
| 970 |
-
c.setFont(BOLD, 7.5)
|
| 971 |
-
c.drawString(x, y, label)
|
| 972 |
-
c.setFillColor(TEXT_DARK)
|
| 973 |
-
c.setFont(BODY, 7.5)
|
| 974 |
-
c.drawString(x + 100, y, desc)
|
| 975 |
-
y -= 20
|
| 976 |
-
|
| 977 |
-
stage_box(c, M, y - tp_h, W - 2 * M, tp_h,
|
| 978 |
-
"TRUE PEAK GUIDANCE", tp_body)
|
| 979 |
-
y -= tp_h + 20
|
| 980 |
-
|
| 981 |
-
# ── Review Guidelines (Compare Passes) ────────────────────────────
|
| 982 |
-
rev_h = 118
|
| 983 |
-
def rev_body(c, x, y, w):
|
| 984 |
-
points = [
|
| 985 |
-
"Compare original vs mastered measurements carefully.",
|
| 986 |
-
"Make SMALL, incremental adjustments — do not overhaul settings that are working.",
|
| 987 |
-
"If something sounds good, leave it alone.",
|
| 988 |
-
"Reference the previous analysis history to avoid oscillating between settings.",
|
| 989 |
-
"Each revision should be a refinement, not a reset.",
|
| 990 |
-
"LUFS target is fixed — do not try to change it.",
|
| 991 |
-
]
|
| 992 |
-
for pt in points:
|
| 993 |
-
c.setFillColor(ACCENT2)
|
| 994 |
-
c.setFont(BOLD, 8)
|
| 995 |
-
c.drawString(x, y, "•")
|
| 996 |
-
c.setFillColor(TEXT_DARK)
|
| 997 |
-
c.setFont(BODY, 8)
|
| 998 |
-
c.drawString(x + 12, y, pt)
|
| 999 |
-
y -= 14
|
| 1000 |
-
|
| 1001 |
-
stage_box(c, M, y - rev_h, W - 2 * M, rev_h,
|
| 1002 |
-
"COMPARE PASS GUIDELINES (Passes 2–4)", rev_body)
|
| 1003 |
-
y -= rev_h + 20
|
| 1004 |
-
|
| 1005 |
-
# ── Final Report ──────────────────────────────────────────────────
|
| 1006 |
-
rep_h = 118
|
| 1007 |
-
def rep_body(c, x, y, w):
|
| 1008 |
-
y = text(c, x, y, "Pass 5 produces a comprehensive quality report — NO parameter changes.",
|
| 1009 |
-
BOLD, 8.5, TEXT_DARK)
|
| 1010 |
-
y -= 6
|
| 1011 |
-
sections = [
|
| 1012 |
-
("Overall Assessment:", "Mastering effectiveness, professional standards, tonal goals met?"),
|
| 1013 |
-
("Spectral Balance:", "Low end warmth, midrange richness, high end smoothness"),
|
| 1014 |
-
("Dynamics & Loudness:", "LUFS compliance, true peak, crest factor, dynamic range"),
|
| 1015 |
-
("Stereo Image:", "Width, mono compatibility, balance"),
|
| 1016 |
-
("Processing Summary:", "What the chain did — EQ moves, compression character"),
|
| 1017 |
-
("Verdict:", "Pass/fail for streaming distribution"),
|
| 1018 |
-
]
|
| 1019 |
-
for label, desc in sections:
|
| 1020 |
-
c.setFillColor(SUPER_GOLD)
|
| 1021 |
-
c.setFont(BOLD, 7.5)
|
| 1022 |
-
c.drawString(x, y, label)
|
| 1023 |
-
c.setFillColor(TEXT_MID)
|
| 1024 |
-
c.setFont(BODY, 7.5)
|
| 1025 |
-
c.drawString(x + 130, y, desc)
|
| 1026 |
-
y -= 12
|
| 1027 |
-
|
| 1028 |
-
stage_box(c, M, y - rep_h, W - 2 * M, rep_h,
|
| 1029 |
-
"FINAL REPORT STRUCTURE (Pass 5)", rep_body)
|
| 1030 |
-
|
| 1031 |
-
# ── Footer ────────────────────────────────────────────────────────
|
| 1032 |
-
c.setFillColor(TEXT_LIGHT)
|
| 1033 |
-
c.setFont(ITALIC, 7)
|
| 1034 |
-
c.drawCentredString(W / 2, M - 10,
|
| 1035 |
-
"AI engine: Google Gemini 2.5 Pro • 5 API calls per master • "
|
| 1036 |
-
"Session-persistent history prevents oscillation")
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 1040 |
-
# BUILD
|
| 1041 |
-
# ══════════════════════════════════════════════════════════════════════════
|
| 1042 |
-
|
| 1043 |
-
def build():
|
| 1044 |
-
c = canvas.Canvas(OUTPUT, pagesize=letter)
|
| 1045 |
-
c.setTitle("s.AI Audio Mastering Suite \u2014 Analog Signal Flow Schematic v4.2")
|
| 1046 |
-
c.setAuthor("StudioAI")
|
| 1047 |
-
|
| 1048 |
-
page1(c)
|
| 1049 |
-
c.showPage()
|
| 1050 |
-
page2(c)
|
| 1051 |
-
c.showPage()
|
| 1052 |
-
page3(c)
|
| 1053 |
-
c.showPage()
|
| 1054 |
-
page4(c)
|
| 1055 |
-
c.showPage()
|
| 1056 |
-
page5(c)
|
| 1057 |
-
c.showPage()
|
| 1058 |
-
|
| 1059 |
-
c.save()
|
| 1060 |
-
print(f"Created {OUTPUT}")
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
if __name__ == "__main__":
|
| 1064 |
-
build()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dsp.py
CHANGED
|
@@ -3,14 +3,14 @@
|
|
| 3 |
import tempfile
|
| 4 |
import numpy as np
|
| 5 |
from pedalboard import (
|
| 6 |
-
Pedalboard, Compressor,
|
| 7 |
LowShelfFilter, HighShelfFilter, PeakFilter,
|
| 8 |
-
HighpassFilter,
|
| 9 |
)
|
| 10 |
from pedalboard.io import ReadableAudioFile
|
| 11 |
import soundfile as sf
|
| 12 |
|
| 13 |
-
from stereo import apply_stereo_width
|
| 14 |
from loudness import measure_loudness, measure_true_peak, normalize_to_lufs
|
| 15 |
|
| 16 |
|
|
@@ -18,85 +18,28 @@ from loudness import measure_loudness, measure_true_peak, normalize_to_lufs
|
|
| 18 |
# Slider-to-parameter mappings
|
| 19 |
# ---------------------------------------------------------------------------
|
| 20 |
|
| 21 |
-
# DEPRECATED — Retained for backward compat. Use map_multiband_compression().
|
| 22 |
def map_compression(comp_value):
|
| 23 |
-
"""Map compression slider (0 less to 100 more) to
|
| 24 |
-
Calibrated for a -18 LUFS internal working level.
|
| 25 |
|
| 26 |
Returns (threshold_db, ratio, attack_ms, release_ms).
|
| 27 |
"""
|
| 28 |
t = comp_value / 100.0 # 0.0 (less) → 1.0 (more)
|
| 29 |
|
| 30 |
-
# Threshold: -
|
| 31 |
-
threshold_db = -
|
| 32 |
|
| 33 |
-
# Ratio: 1
|
| 34 |
-
ratio = 1.
|
| 35 |
|
| 36 |
-
# Attack:
|
| 37 |
-
attack_ms =
|
| 38 |
|
| 39 |
-
# Release:
|
| 40 |
-
release_ms =
|
| 41 |
|
| 42 |
return threshold_db, ratio, attack_ms, release_ms
|
| 43 |
|
| 44 |
|
| 45 |
-
# ---------------------------------------------------------------------------
|
| 46 |
-
# Multiband compression parameter curves (v3.5)
|
| 47 |
-
# ---------------------------------------------------------------------------
|
| 48 |
-
|
| 49 |
-
# Crossover frequencies for 3-band split
|
| 50 |
-
_MB_CROSS_LOW = 200.0 # Hz — below this is "Low" band
|
| 51 |
-
_MB_CROSS_HIGH = 4000.0 # Hz — above this is "High" band
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def map_multiband_compression(comp_value):
|
| 55 |
-
"""Map compression slider (0-100) to per-band mastering parameters.
|
| 56 |
-
|
| 57 |
-
Three bands tuned to industry best practices:
|
| 58 |
-
Low (< 200 Hz) — Firmest control, slow attack lets kick punch through
|
| 59 |
-
Mid (200 Hz-4 kHz) — Lightest touch, preserve vocals/instruments
|
| 60 |
-
High (> 4 kHz) — Barely compresses, fast attack tames harsh transients
|
| 61 |
-
|
| 62 |
-
Each band returns (threshold_db, ratio, attack_ms, release_ms).
|
| 63 |
-
All curves calibrated for -18 LUFS internal working level.
|
| 64 |
-
|
| 65 |
-
Returns dict: {"low": (...), "mid": (...), "high": (...)}.
|
| 66 |
-
"""
|
| 67 |
-
# Logarithmic curve: bottom half of slider stays in transparent range,
|
| 68 |
-
# top half ramps into aggressive territory. t² gives ~50% of slider
|
| 69 |
-
# travel before reaching 25% of parameter range.
|
| 70 |
-
t = (comp_value / 100.0) ** 2 # 0→0, 50→0.25, 70→0.49, 100→1.0
|
| 71 |
-
|
| 72 |
-
# --- Low band (< 200 Hz): Firmest control ---
|
| 73 |
-
# Attack scales down so higher values catch kick/bass transients
|
| 74 |
-
low_threshold = -16.0 + t * (-8.0) # -16 → -24 dB
|
| 75 |
-
low_ratio = 1.2 + t * 1.8 # 1.2:1 → 3.0:1
|
| 76 |
-
low_attack = 80.0 + t * (-60.0) # 80 → 20 ms (grabs transients at high comp)
|
| 77 |
-
low_release = 200.0 + t * (-80.0) # 200 → 120 ms
|
| 78 |
-
|
| 79 |
-
# --- Mid band (200 Hz – 4 kHz): Musical peak control ---
|
| 80 |
-
# Attack scales from transparent to transient-taming
|
| 81 |
-
mid_threshold = -14.0 + t * (-10.0) # -14 → -24 dB
|
| 82 |
-
mid_ratio = 1.1 + t * 1.4 # 1.1:1 → 2.5:1
|
| 83 |
-
mid_attack = 30.0 + t * (-20.0) # 30 → 10 ms (catches snare/vocal peaks)
|
| 84 |
-
mid_release = 150.0 + t * (-50.0) # 150 → 100 ms (avoids pumping)
|
| 85 |
-
|
| 86 |
-
# --- High band (> 4 kHz): Transient control ---
|
| 87 |
-
# Already fast, gets faster and more aggressive
|
| 88 |
-
high_threshold = -12.0 + t * (-10.0) # -12 → -22 dB
|
| 89 |
-
high_ratio = 1.05 + t * 0.95 # 1.05:1 → 2.0:1
|
| 90 |
-
high_attack = 10.0 + t * (-7.0) # 10 → 3 ms (catches cymbal/click transients)
|
| 91 |
-
high_release = 80.0 + t * (-40.0) # 80 → 40 ms
|
| 92 |
-
|
| 93 |
-
return {
|
| 94 |
-
"low": (low_threshold, low_ratio, low_attack, low_release),
|
| 95 |
-
"mid": (mid_threshold, mid_ratio, mid_attack, mid_release),
|
| 96 |
-
"high": (high_threshold, high_ratio, high_attack, high_release),
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
|
| 100 |
# ---------------------------------------------------------------------------
|
| 101 |
# Audio I/O
|
| 102 |
# ---------------------------------------------------------------------------
|
|
@@ -124,15 +67,17 @@ def save_audio(audio, sample_rate):
|
|
| 124 |
# ---------------------------------------------------------------------------
|
| 125 |
|
| 126 |
def master_audio(input_path, lows_db, mid_boost_db, highs_db, bass_boost_db,
|
| 127 |
-
bass_freq_hz, comp_value, stereo_width, target_lufs):
|
| 128 |
"""Run the full mastering chain.
|
| 129 |
|
| 130 |
Signal flow:
|
| 131 |
-
1.
|
| 132 |
-
2.
|
| 133 |
-
3.
|
| 134 |
-
4.
|
| 135 |
-
5.
|
|
|
|
|
|
|
| 136 |
|
| 137 |
Returns:
|
| 138 |
(output_path, original_audio, mastered_audio, sample_rate, stats)
|
|
@@ -144,27 +89,14 @@ def master_audio(input_path, lows_db, mid_boost_db, highs_db, bass_boost_db,
|
|
| 144 |
orig_lufs = measure_loudness(original, sample_rate)
|
| 145 |
orig_peak = measure_true_peak(original, sample_rate)
|
| 146 |
|
| 147 |
-
# --- Stage 1:
|
| 148 |
-
# Normalize hot inputs (e.g. Suno at -9 LUFS) down to -18 LUFS
|
| 149 |
-
# so EQ boosts don't clip the working audio.
|
| 150 |
-
internal_target = -18.0
|
| 151 |
-
if not np.isinf(orig_lufs):
|
| 152 |
-
processed = normalize_to_lufs(
|
| 153 |
-
audio, sample_rate, orig_lufs, internal_target
|
| 154 |
-
).astype(np.float32)
|
| 155 |
-
else:
|
| 156 |
-
processed = audio.copy()
|
| 157 |
-
|
| 158 |
-
# --- Stage 2: Filtering & EQ ---
|
| 159 |
eq_effects = []
|
| 160 |
|
| 161 |
-
# Single HPF at
|
| 162 |
-
|
| 163 |
-
eq_effects.append(HighpassFilter(cutoff_frequency_hz=15.0))
|
| 164 |
|
| 165 |
-
# LPF
|
| 166 |
-
|
| 167 |
-
# in the air zone (10-20 kHz), muddying HF shaping.
|
| 168 |
|
| 169 |
# Bass boost (user controls gain and frequency)
|
| 170 |
if bass_boost_db > 0:
|
|
@@ -179,10 +111,10 @@ def master_audio(input_path, lows_db, mid_boost_db, highs_db, bass_boost_db,
|
|
| 179 |
eq_effects.append(LowShelfFilter(
|
| 180 |
cutoff_frequency_hz=200.0, gain_db=float(lows_db), q=1.0))
|
| 181 |
|
| 182 |
-
# Highs shelf
|
| 183 |
if highs_db != 0:
|
| 184 |
eq_effects.append(HighShelfFilter(
|
| 185 |
-
cutoff_frequency_hz=
|
| 186 |
|
| 187 |
# Mid boost/cut (Q=1.0 for wider, more musical presence)
|
| 188 |
if mid_boost_db != 0:
|
|
@@ -193,295 +125,68 @@ def master_audio(input_path, lows_db, mid_boost_db, highs_db, bass_boost_db,
|
|
| 193 |
))
|
| 194 |
|
| 195 |
eq_board = Pedalboard(eq_effects)
|
| 196 |
-
processed = eq_board(
|
| 197 |
-
|
| 198 |
-
# --- Stage
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
#
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
compressed_bands = []
|
| 217 |
-
for band_audio, band_key in [
|
| 218 |
-
(low_band, "low"), (mid_band, "mid"), (high_band, "high"),
|
| 219 |
-
]:
|
| 220 |
-
threshold, ratio, attack, release = band_params[band_key]
|
| 221 |
-
comp_board = Pedalboard([Compressor(
|
| 222 |
-
threshold_db=threshold,
|
| 223 |
-
ratio=ratio,
|
| 224 |
-
attack_ms=attack,
|
| 225 |
-
release_ms=release,
|
| 226 |
-
)])
|
| 227 |
-
compressed_bands.append(
|
| 228 |
-
comp_board(band_audio.T, sample_rate).T
|
| 229 |
-
)
|
| 230 |
-
|
| 231 |
-
# Recombine (perfect reconstruction from LR4 subtraction method)
|
| 232 |
-
processed = compressed_bands[0] + compressed_bands[1] + compressed_bands[2]
|
| 233 |
-
|
| 234 |
-
# --- Stage 4: Stereo width (after dynamics — imaging on settled signal) ---
|
| 235 |
-
processed = apply_stereo_width(processed, stereo_width, sample_rate)
|
| 236 |
-
|
| 237 |
-
# --- Stage 5: Target LUFS push ---
|
| 238 |
-
pre_norm_lufs = measure_loudness(processed, sample_rate)
|
| 239 |
-
if not np.isinf(pre_norm_lufs):
|
| 240 |
-
processed = normalize_to_lufs(
|
| 241 |
-
processed, sample_rate, pre_norm_lufs, target_lufs
|
| 242 |
-
).astype(np.float32)
|
| 243 |
-
|
| 244 |
-
# --- Stage 6: Soft clipper (piecewise tanh saturation) ---
|
| 245 |
-
# Perfectly linear below the knee; only the tips of peaks above it are
|
| 246 |
-
# shaped with a tanh curve that asymptotes to the ceiling. The knee
|
| 247 |
-
# sits 2 dB below the ceiling so only the loudest transient tips are
|
| 248 |
-
# affected — the vast majority of the signal passes through untouched.
|
| 249 |
-
ceiling_lin = 10.0 ** (-0.1 / 20.0) # -0.1 dBTP in linear ≈ 0.9885
|
| 250 |
-
peak_lin = np.max(np.abs(processed))
|
| 251 |
-
if peak_lin > ceiling_lin:
|
| 252 |
-
knee_lin = ceiling_lin * 10.0 ** (-2.0 / 20.0) # 2 dB below ceiling
|
| 253 |
-
headroom = ceiling_lin - knee_lin
|
| 254 |
-
abs_p = np.abs(processed)
|
| 255 |
-
above = abs_p > knee_lin
|
| 256 |
-
if np.any(above):
|
| 257 |
-
excess = abs_p[above] - knee_lin
|
| 258 |
-
clipped = knee_lin + headroom * np.tanh(excess / headroom)
|
| 259 |
-
processed[above] = (np.sign(processed[above]) * clipped).astype(np.float32)
|
| 260 |
-
|
| 261 |
-
# --- Stage 7: True peak ceiling (-0.1 dBTP) — safety net ---
|
| 262 |
-
# The soft clipper handles 99% of cases; this catches any residual
|
| 263 |
-
# inter-sample peaks that tanh didn't fully tame.
|
| 264 |
-
ceiling_dbtp = -0.1
|
| 265 |
-
tp = measure_true_peak(processed, sample_rate)
|
| 266 |
-
if tp > ceiling_dbtp:
|
| 267 |
-
overshoot_db = tp - ceiling_dbtp
|
| 268 |
-
processed = (processed * 10.0 ** (-overshoot_db / 20.0)).astype(np.float32)
|
| 269 |
-
|
| 270 |
-
# --- Final measurements ---
|
| 271 |
-
mast_lufs = measure_loudness(processed, sample_rate)
|
| 272 |
-
mast_peak = measure_true_peak(processed, sample_rate)
|
| 273 |
-
|
| 274 |
-
is_mono = original.ndim == 1 or original.shape[1] == 1
|
| 275 |
-
|
| 276 |
-
# Build actual DSP params used
|
| 277 |
-
dsp_params = {
|
| 278 |
-
"hpf_freq": 15.0,
|
| 279 |
-
"eq": [],
|
| 280 |
-
"stereo_width": stereo_width,
|
| 281 |
-
}
|
| 282 |
-
if bass_boost_db > 0:
|
| 283 |
-
dsp_params["eq"].append({"type": "peak", "freq": float(bass_freq_hz),
|
| 284 |
-
"gain_db": float(bass_boost_db), "q": 2.0})
|
| 285 |
-
if lows_db != 0:
|
| 286 |
-
dsp_params["eq"].append({"type": "low_shelf", "freq": 200.0,
|
| 287 |
-
"gain_db": float(lows_db), "q": 1.0})
|
| 288 |
-
if highs_db != 0:
|
| 289 |
-
dsp_params["eq"].append({"type": "high_shelf", "freq": 10000.0,
|
| 290 |
-
"gain_db": float(highs_db), "q": 0.7})
|
| 291 |
-
if mid_boost_db != 0:
|
| 292 |
-
dsp_params["eq"].append({"type": "peak", "freq": 1200.0,
|
| 293 |
-
"gain_db": float(mid_boost_db), "q": 1.0})
|
| 294 |
-
if comp_value > 0:
|
| 295 |
-
bp = map_multiband_compression(comp_value)
|
| 296 |
-
dsp_params["compression"] = {}
|
| 297 |
-
for bk in ("low", "mid", "high"):
|
| 298 |
-
t, r, a, rel = bp[bk]
|
| 299 |
-
dsp_params["compression"][bk] = {
|
| 300 |
-
"threshold": t, "ratio": r, "attack_ms": a, "release_ms": rel,
|
| 301 |
-
}
|
| 302 |
-
dsp_params["crossover_low"] = _MB_CROSS_LOW
|
| 303 |
-
dsp_params["crossover_high"] = _MB_CROSS_HIGH
|
| 304 |
-
|
| 305 |
-
stats = {
|
| 306 |
-
"orig_lufs": orig_lufs,
|
| 307 |
-
"orig_peak": orig_peak,
|
| 308 |
-
"mast_lufs": mast_lufs,
|
| 309 |
-
"mast_peak": mast_peak,
|
| 310 |
-
"mono": is_mono,
|
| 311 |
-
"dsp": dsp_params,
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
output_path = save_audio(processed, sample_rate)
|
| 315 |
-
return output_path, original, processed, sample_rate, stats
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
# ---------------------------------------------------------------------------
|
| 319 |
-
# Full-parameter mastering pipeline (Super AI mode)
|
| 320 |
-
# ---------------------------------------------------------------------------
|
| 321 |
-
|
| 322 |
-
def master_audio_full(input_path, params, target_lufs):
|
| 323 |
-
"""Run the full mastering chain with granular per-band control.
|
| 324 |
-
|
| 325 |
-
Unlike master_audio() which maps a single compression slider (0-100) to
|
| 326 |
-
per-band parameters, this function accepts ALL parameters directly —
|
| 327 |
-
including per-band EQ frequencies, Q values, and per-band compression
|
| 328 |
-
settings. This enables the AI to fine-tune every knob.
|
| 329 |
-
|
| 330 |
-
Args:
|
| 331 |
-
input_path: path to audio file.
|
| 332 |
-
params: dict with keys:
|
| 333 |
-
hpf_freq: float — high-pass filter cutoff in Hz (default 15)
|
| 334 |
-
eq: dict with band1..band6, each:
|
| 335 |
-
type: "peak" | "low_shelf" | "high_shelf"
|
| 336 |
-
freq: float Hz
|
| 337 |
-
gain_db: float dB
|
| 338 |
-
q: float Q factor
|
| 339 |
-
compression: dict with "low", "mid", "high", each:
|
| 340 |
-
threshold: float dB
|
| 341 |
-
ratio: float
|
| 342 |
-
attack_ms: float ms
|
| 343 |
-
release_ms: float ms
|
| 344 |
-
crossover_low: float Hz (default 200)
|
| 345 |
-
crossover_high: float Hz (default 4000)
|
| 346 |
-
stereo_width: int 80-150
|
| 347 |
-
target_lufs: float
|
| 348 |
-
|
| 349 |
-
Returns:
|
| 350 |
-
(output_path, original_audio, mastered_audio, sample_rate, stats)
|
| 351 |
-
"""
|
| 352 |
-
audio, sample_rate = load_audio(input_path)
|
| 353 |
-
original = audio.copy()
|
| 354 |
-
|
| 355 |
-
# --- Measure original loudness ---
|
| 356 |
-
orig_lufs = measure_loudness(original, sample_rate)
|
| 357 |
-
orig_peak = measure_true_peak(original, sample_rate)
|
| 358 |
-
|
| 359 |
-
# --- Stage 1: Pre-gain drop — create internal headroom ---
|
| 360 |
-
internal_target = -18.0
|
| 361 |
-
if not np.isinf(orig_lufs):
|
| 362 |
-
processed = normalize_to_lufs(
|
| 363 |
-
audio, sample_rate, orig_lufs, internal_target
|
| 364 |
-
).astype(np.float32)
|
| 365 |
else:
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
cutoff_frequency_hz=freq, gain_db=gain, q=q))
|
| 395 |
-
elif btype == "high_shelf":
|
| 396 |
-
eq_effects.append(HighShelfFilter(
|
| 397 |
-
cutoff_frequency_hz=freq, gain_db=gain, q=q))
|
| 398 |
-
else: # "peak"
|
| 399 |
-
eq_effects.append(PeakFilter(
|
| 400 |
-
cutoff_frequency_hz=freq, gain_db=gain, q=q))
|
| 401 |
-
|
| 402 |
-
eq_board = Pedalboard(eq_effects)
|
| 403 |
-
processed = eq_board(processed.T, sample_rate).T
|
| 404 |
-
|
| 405 |
-
# --- Stage 3: Multiband Compression (direct per-band params) ---
|
| 406 |
-
comp_config = params.get("compression", {})
|
| 407 |
-
has_comp = any(
|
| 408 |
-
comp_config.get(k, {}).get("ratio", 1.0) > 1.0
|
| 409 |
-
for k in ("low", "mid", "high")
|
| 410 |
-
)
|
| 411 |
-
|
| 412 |
-
if has_comp:
|
| 413 |
-
xo_low = float(params.get("crossover_low", 200.0))
|
| 414 |
-
xo_high = float(params.get("crossover_high", 4000.0))
|
| 415 |
-
|
| 416 |
-
# Split into 3 bands
|
| 417 |
-
low_band, upper = linkwitz_riley_crossover(
|
| 418 |
-
processed, sample_rate, xo_low,
|
| 419 |
-
)
|
| 420 |
-
mid_band, high_band = linkwitz_riley_crossover(
|
| 421 |
-
upper, sample_rate, xo_high,
|
| 422 |
-
)
|
| 423 |
-
|
| 424 |
-
compressed_bands = []
|
| 425 |
-
for band_audio, band_key in [
|
| 426 |
-
(low_band, "low"), (mid_band, "mid"), (high_band, "high"),
|
| 427 |
-
]:
|
| 428 |
-
bp = comp_config.get(band_key, {})
|
| 429 |
-
threshold = float(bp.get("threshold", -14.0))
|
| 430 |
-
ratio = float(bp.get("ratio", 1.0))
|
| 431 |
-
attack = float(bp.get("attack_ms", 30.0))
|
| 432 |
-
release = float(bp.get("release_ms", 150.0))
|
| 433 |
-
|
| 434 |
-
# Safety clamps
|
| 435 |
-
threshold = max(-40.0, min(0.0, threshold))
|
| 436 |
-
ratio = max(1.0, min(20.0, ratio))
|
| 437 |
-
attack = max(0.1, min(200.0, attack))
|
| 438 |
-
release = max(10.0, min(500.0, release))
|
| 439 |
-
|
| 440 |
-
if ratio > 1.0:
|
| 441 |
-
comp_board = Pedalboard([Compressor(
|
| 442 |
-
threshold_db=threshold,
|
| 443 |
-
ratio=ratio,
|
| 444 |
-
attack_ms=attack,
|
| 445 |
-
release_ms=release,
|
| 446 |
-
)])
|
| 447 |
-
compressed_bands.append(
|
| 448 |
-
comp_board(band_audio.T, sample_rate).T
|
| 449 |
-
)
|
| 450 |
-
else:
|
| 451 |
-
compressed_bands.append(band_audio)
|
| 452 |
-
|
| 453 |
-
processed = compressed_bands[0] + compressed_bands[1] + compressed_bands[2]
|
| 454 |
-
|
| 455 |
-
# --- Stage 4: Stereo width ---
|
| 456 |
-
stereo_width = int(params.get("stereo_width", 100))
|
| 457 |
-
processed = apply_stereo_width(processed, stereo_width, sample_rate)
|
| 458 |
-
|
| 459 |
-
# --- Stage 5: Target LUFS push ---
|
| 460 |
pre_norm_lufs = measure_loudness(processed, sample_rate)
|
| 461 |
if not np.isinf(pre_norm_lufs):
|
| 462 |
processed = normalize_to_lufs(
|
| 463 |
processed, sample_rate, pre_norm_lufs, target_lufs
|
| 464 |
).astype(np.float32)
|
| 465 |
|
| 466 |
-
# --- Stage
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
above = abs_p > knee_lin
|
| 474 |
-
if np.any(above):
|
| 475 |
-
excess = abs_p[above] - knee_lin
|
| 476 |
-
clipped = knee_lin + headroom * np.tanh(excess / headroom)
|
| 477 |
-
processed[above] = (np.sign(processed[above]) * clipped).astype(np.float32)
|
| 478 |
-
|
| 479 |
-
# --- Stage 7: True peak ceiling (-0.1 dBTP) — safety net ---
|
| 480 |
-
ceiling_dbtp = -0.1
|
| 481 |
-
tp = measure_true_peak(processed, sample_rate)
|
| 482 |
-
if tp > ceiling_dbtp:
|
| 483 |
-
overshoot_db = tp - ceiling_dbtp
|
| 484 |
-
processed = (processed * 10.0 ** (-overshoot_db / 20.0)).astype(np.float32)
|
| 485 |
|
| 486 |
# --- Final measurements ---
|
| 487 |
mast_lufs = measure_loudness(processed, sample_rate)
|
|
@@ -489,13 +194,20 @@ def master_audio_full(input_path, params, target_lufs):
|
|
| 489 |
|
| 490 |
is_mono = original.ndim == 1 or original.shape[1] == 1
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
stats = {
|
| 493 |
"orig_lufs": orig_lufs,
|
| 494 |
"orig_peak": orig_peak,
|
| 495 |
"mast_lufs": mast_lufs,
|
| 496 |
"mast_peak": mast_peak,
|
| 497 |
"mono": is_mono,
|
| 498 |
-
"
|
|
|
|
|
|
|
| 499 |
}
|
| 500 |
|
| 501 |
output_path = save_audio(processed, sample_rate)
|
|
|
|
| 3 |
import tempfile
|
| 4 |
import numpy as np
|
| 5 |
from pedalboard import (
|
| 6 |
+
Pedalboard, Compressor, Gain, Limiter,
|
| 7 |
LowShelfFilter, HighShelfFilter, PeakFilter,
|
| 8 |
+
HighpassFilter, LowpassFilter,
|
| 9 |
)
|
| 10 |
from pedalboard.io import ReadableAudioFile
|
| 11 |
import soundfile as sf
|
| 12 |
|
| 13 |
+
from stereo import apply_stereo_width
|
| 14 |
from loudness import measure_loudness, measure_true_peak, normalize_to_lufs
|
| 15 |
|
| 16 |
|
|
|
|
| 18 |
# Slider-to-parameter mappings
|
| 19 |
# ---------------------------------------------------------------------------
|
| 20 |
|
|
|
|
| 21 |
def map_compression(comp_value):
|
| 22 |
+
"""Map compression slider (0 less to 100 more) to compressor params.
|
|
|
|
| 23 |
|
| 24 |
Returns (threshold_db, ratio, attack_ms, release_ms).
|
| 25 |
"""
|
| 26 |
t = comp_value / 100.0 # 0.0 (less) → 1.0 (more)
|
| 27 |
|
| 28 |
+
# Threshold: -12.0 dB (less) → -18.0 dB (more)
|
| 29 |
+
threshold_db = -12.0 + t * (-18.0 - (-12.0))
|
| 30 |
|
| 31 |
+
# Ratio: 1:1 (less) → 4:1 (more)
|
| 32 |
+
ratio = 1.0 + t * 3.0
|
| 33 |
|
| 34 |
+
# Attack: 15 ms (fixed)
|
| 35 |
+
attack_ms = 15.0
|
| 36 |
|
| 37 |
+
# Release: 120 ms (fixed)
|
| 38 |
+
release_ms = 120.0
|
| 39 |
|
| 40 |
return threshold_db, ratio, attack_ms, release_ms
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
# ---------------------------------------------------------------------------
|
| 44 |
# Audio I/O
|
| 45 |
# ---------------------------------------------------------------------------
|
|
|
|
| 67 |
# ---------------------------------------------------------------------------
|
| 68 |
|
| 69 |
def master_audio(input_path, lows_db, mid_boost_db, highs_db, bass_boost_db,
|
| 70 |
+
bass_freq_hz, comp_value, limiter_ceiling, stereo_width, target_lufs):
|
| 71 |
"""Run the full mastering chain.
|
| 72 |
|
| 73 |
Signal flow:
|
| 74 |
+
1. HPF 20 Hz + LPF 20 kHz (12 dB/oct each)
|
| 75 |
+
2. EQ (bass boost, lows shelf, highs shelf, mid peak)
|
| 76 |
+
3. Stereo width (M/S processing — before dynamics)
|
| 77 |
+
4. Compression + makeup gain
|
| 78 |
+
5. LUFS normalization (single gain adjustment)
|
| 79 |
+
6. Auto-reduce if limiter would work > 4 dB
|
| 80 |
+
7. Final limiter (single pass)
|
| 81 |
|
| 82 |
Returns:
|
| 83 |
(output_path, original_audio, mastered_audio, sample_rate, stats)
|
|
|
|
| 89 |
orig_lufs = measure_loudness(original, sample_rate)
|
| 90 |
orig_peak = measure_true_peak(original, sample_rate)
|
| 91 |
|
| 92 |
+
# --- Stage 1: Filtering & EQ ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
eq_effects = []
|
| 94 |
|
| 95 |
+
# Single HPF at 20 Hz (12 dB/oct)
|
| 96 |
+
eq_effects.append(HighpassFilter(cutoff_frequency_hz=20.0))
|
|
|
|
| 97 |
|
| 98 |
+
# Single LPF at 20 kHz (12 dB/oct)
|
| 99 |
+
eq_effects.append(LowpassFilter(cutoff_frequency_hz=20000.0))
|
|
|
|
| 100 |
|
| 101 |
# Bass boost (user controls gain and frequency)
|
| 102 |
if bass_boost_db > 0:
|
|
|
|
| 111 |
eq_effects.append(LowShelfFilter(
|
| 112 |
cutoff_frequency_hz=200.0, gain_db=float(lows_db), q=1.0))
|
| 113 |
|
| 114 |
+
# Highs shelf
|
| 115 |
if highs_db != 0:
|
| 116 |
eq_effects.append(HighShelfFilter(
|
| 117 |
+
cutoff_frequency_hz=6000.0, gain_db=float(highs_db), q=0.7))
|
| 118 |
|
| 119 |
# Mid boost/cut (Q=1.0 for wider, more musical presence)
|
| 120 |
if mid_boost_db != 0:
|
|
|
|
| 125 |
))
|
| 126 |
|
| 127 |
eq_board = Pedalboard(eq_effects)
|
| 128 |
+
processed = eq_board(audio.T, sample_rate).T
|
| 129 |
+
|
| 130 |
+
# --- Stage 2: Stereo width (before dynamics to catch peak changes) ---
|
| 131 |
+
processed = apply_stereo_width(processed, stereo_width)
|
| 132 |
+
|
| 133 |
+
# --- Stage 3: Compression + Makeup Gain ---
|
| 134 |
+
threshold, ratio, attack, release = map_compression(comp_value)
|
| 135 |
+
|
| 136 |
+
comp_effects = []
|
| 137 |
+
comp_effects.append(Compressor(
|
| 138 |
+
threshold_db=threshold,
|
| 139 |
+
ratio=ratio,
|
| 140 |
+
attack_ms=attack,
|
| 141 |
+
release_ms=release,
|
| 142 |
+
))
|
| 143 |
+
|
| 144 |
+
# Makeup gain (estimated from compression amount)
|
| 145 |
+
if ratio > 1.0:
|
| 146 |
+
makeup_db = abs(threshold) * (1.0 - 1.0 / ratio) * 0.4
|
| 147 |
+
makeup_db = min(makeup_db, 12.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
else:
|
| 149 |
+
makeup_db = 0.0
|
| 150 |
+
comp_effects.append(Gain(gain_db=makeup_db))
|
| 151 |
+
|
| 152 |
+
comp_board = Pedalboard(comp_effects)
|
| 153 |
+
processed = comp_board(processed.T, sample_rate).T
|
| 154 |
+
|
| 155 |
+
# --- Stage 4: Auto-reduce gain to keep limiter under 4 dB reduction ---
|
| 156 |
+
pre_limiter_peak = measure_true_peak(processed, sample_rate)
|
| 157 |
+
would_reduce = max(pre_limiter_peak - limiter_ceiling, 0.0)
|
| 158 |
+
auto_gain_reduction = 0.0
|
| 159 |
+
max_limiter_reduction = 4.0
|
| 160 |
+
|
| 161 |
+
if would_reduce > max_limiter_reduction:
|
| 162 |
+
auto_gain_reduction = would_reduce - max_limiter_reduction
|
| 163 |
+
reduction_board = Pedalboard([Gain(gain_db=-auto_gain_reduction)])
|
| 164 |
+
processed = reduction_board(processed.T, sample_rate).T
|
| 165 |
+
|
| 166 |
+
# --- Stage 5: Limiter ---
|
| 167 |
+
# Measure actual peak going into limiter (after auto-reduce)
|
| 168 |
+
limiter_input_peak = measure_true_peak(processed, sample_rate)
|
| 169 |
+
limiter_gain_reduction = max(limiter_input_peak - limiter_ceiling, 0.0)
|
| 170 |
+
|
| 171 |
+
limiter_board = Pedalboard([
|
| 172 |
+
Limiter(threshold_db=limiter_ceiling, release_ms=100.0)
|
| 173 |
+
])
|
| 174 |
+
processed = limiter_board(processed.T, sample_rate).T
|
| 175 |
+
|
| 176 |
+
# --- Stage 6: Strict LUFS normalization (post-processing) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
pre_norm_lufs = measure_loudness(processed, sample_rate)
|
| 178 |
if not np.isinf(pre_norm_lufs):
|
| 179 |
processed = normalize_to_lufs(
|
| 180 |
processed, sample_rate, pre_norm_lufs, target_lufs
|
| 181 |
).astype(np.float32)
|
| 182 |
|
| 183 |
+
# --- Stage 7: Safety catch — limiter for stray true peaks ---
|
| 184 |
+
post_norm_peak = measure_true_peak(processed, sample_rate)
|
| 185 |
+
if post_norm_peak > limiter_ceiling:
|
| 186 |
+
safety_limiter = Pedalboard([
|
| 187 |
+
Limiter(threshold_db=limiter_ceiling, release_ms=100.0)
|
| 188 |
+
])
|
| 189 |
+
processed = safety_limiter(processed.T, sample_rate).T
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
# --- Final measurements ---
|
| 192 |
mast_lufs = measure_loudness(processed, sample_rate)
|
|
|
|
| 194 |
|
| 195 |
is_mono = original.ndim == 1 or original.shape[1] == 1
|
| 196 |
|
| 197 |
+
# Warning if auto gain reduction exceeded 2 dB
|
| 198 |
+
warning = ""
|
| 199 |
+
if auto_gain_reduction > 2.0:
|
| 200 |
+
warning = "Consider reducing EQ and Bass Boost settings and try again"
|
| 201 |
+
|
| 202 |
stats = {
|
| 203 |
"orig_lufs": orig_lufs,
|
| 204 |
"orig_peak": orig_peak,
|
| 205 |
"mast_lufs": mast_lufs,
|
| 206 |
"mast_peak": mast_peak,
|
| 207 |
"mono": is_mono,
|
| 208 |
+
"auto_gain_reduction": auto_gain_reduction,
|
| 209 |
+
"limiter_gain_reduction": limiter_gain_reduction,
|
| 210 |
+
"warning": warning,
|
| 211 |
}
|
| 212 |
|
| 213 |
output_path = save_audio(processed, sample_rate)
|
launch.bat
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
@echo off
|
| 2 |
-
title StudioAI Audio Mastering Suite - Local
|
| 3 |
-
echo Starting StudioAI Audio Mastering Suite...
|
| 4 |
-
echo.
|
| 5 |
-
|
| 6 |
-
set GOOGLE_API_KEY=AIzaSyCgdP4-cAnLwbNikVz04u-al4px4LzTGFM
|
| 7 |
-
set AI_ACCESS_KEY=StudioAI#!mastering41
|
| 8 |
-
set "AI_ACCESS_KEY_2=OFiOIFJdoheio&474Fieu"
|
| 9 |
-
set UNKEY_API_ID=api_pNZfzVKBwVEx
|
| 10 |
-
set UNKEY_ROOT_KEY=unkey_3ZFmunwPRAvA5hrobow6UKGa
|
| 11 |
-
|
| 12 |
-
cd /d "%~dp0"
|
| 13 |
-
|
| 14 |
-
:loop
|
| 15 |
-
echo.
|
| 16 |
-
echo [%date% %time%] Launching...
|
| 17 |
-
python app.py
|
| 18 |
-
echo.
|
| 19 |
-
echo [%date% %time%] Process exited. Restarting in 3 seconds...
|
| 20 |
-
timeout /t 3 /nobreak >nul
|
| 21 |
-
goto loop
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
presets.py
CHANGED
|
@@ -4,142 +4,52 @@ PRESETS = {
|
|
| 4 |
"-- None --": None,
|
| 5 |
|
| 6 |
"Modern Pop (Bright & Wide)": {
|
| 7 |
-
"lows_db": 0.
|
| 8 |
"mid_boost_db": 1.0,
|
| 9 |
-
"highs_db":
|
| 10 |
"bass_boost_db": 1.0,
|
| 11 |
"bass_freq_hz": 60,
|
| 12 |
-
"compression":
|
| 13 |
"stereo_width": 115,
|
| 14 |
},
|
| 15 |
|
| 16 |
"Vintage Analog (Warm & Intimate)": {
|
| 17 |
-
"lows_db":
|
| 18 |
"mid_boost_db": 0.0,
|
| 19 |
"highs_db": -1.5,
|
| 20 |
-
"bass_boost_db": 1.
|
| 21 |
-
"bass_freq_hz":
|
| 22 |
-
"compression":
|
| 23 |
"stereo_width": 95,
|
| 24 |
},
|
| 25 |
|
| 26 |
"Heavy Trap / EDM (Aggressive Bass)": {
|
| 27 |
-
"lows_db":
|
| 28 |
"mid_boost_db": -0.5,
|
| 29 |
"highs_db": 1.0,
|
| 30 |
-
"bass_boost_db":
|
| 31 |
-
"bass_freq_hz":
|
| 32 |
-
"compression":
|
| 33 |
"stereo_width": 125,
|
| 34 |
},
|
| 35 |
|
| 36 |
"Hard Rock / Metal (Punch & Bite)": {
|
| 37 |
"lows_db": -0.5,
|
| 38 |
-
"mid_boost_db": 1.
|
| 39 |
-
"highs_db":
|
| 40 |
"bass_boost_db": 1.5,
|
| 41 |
-
"bass_freq_hz": 60,
|
| 42 |
-
"compression":
|
| 43 |
"stereo_width": 110,
|
| 44 |
},
|
| 45 |
|
| 46 |
-
"Acoustic & Vocal (
|
| 47 |
"lows_db": -0.5,
|
| 48 |
-
"mid_boost_db": 1.
|
| 49 |
"highs_db": 1.0,
|
| 50 |
"bass_boost_db": 0.0,
|
| 51 |
"bass_freq_hz": 55,
|
| 52 |
-
"compression":
|
| 53 |
-
"stereo_width":
|
| 54 |
-
},
|
| 55 |
-
|
| 56 |
-
"Deep Ambient (Immersive & Sustained)": {
|
| 57 |
-
"lows_db": -1.0, # Cut the mud to let the sub-bass breathe
|
| 58 |
-
"mid_boost_db": 0.0, # Flat mids to keep the soundscape distant
|
| 59 |
-
"highs_db": 1.5, # Boost the 'Air' for shimmering reverb tails
|
| 60 |
-
"bass_boost_db": 1.0, # Gentle lift to the existing heavy pad
|
| 61 |
-
"bass_freq_hz": 50, # Focus the weight low (Dread Zone/Gravity)
|
| 62 |
-
"compression": 20, # Low ratio, slow release. Invisible leveling, no pumping.
|
| 63 |
-
"stereo_width": 125, # Extra wide to wrap the sound around the listener
|
| 64 |
-
},
|
| 65 |
-
|
| 66 |
-
"Hip-Hop / Boom Bap (Punch & Grit)": {
|
| 67 |
-
"lows_db": -0.5, # Cut low-mids for clarity — boom bap dips around 150Hz
|
| 68 |
-
"mid_boost_db": 1.0, # Bring the rapper's vocal right to the front
|
| 69 |
-
"highs_db": -0.5, # Rolled-off highs for signature lo-fi/vinyl grit
|
| 70 |
-
"bass_boost_db": 1.5, # Add weight...
|
| 71 |
-
"bass_freq_hz": 60, # ...but at 60Hz to emphasize the 'thump' of the kick, not the sub-rumble
|
| 72 |
-
"compression": 75, # Snappy compression to make the drums knock
|
| 73 |
-
"stereo_width": 100, # Keep the beat dead center for maximum impact
|
| 74 |
-
},
|
| 75 |
-
|
| 76 |
-
"Cinematic / Orchestral (Dynamic & Wide)": {
|
| 77 |
-
"lows_db": 0.5, # Gentle weight for cellos/basses (0.5 dB — orchestral moves are subtle)
|
| 78 |
-
"mid_boost_db": -0.5, # Scoop slightly to push the orchestra "back" into the room
|
| 79 |
-
"highs_db": 0.5, # Subtle air for the strings and brass
|
| 80 |
-
"bass_boost_db": 0.0, # Let the natural low-end breathe without artificial boosting
|
| 81 |
-
"bass_freq_hz": 50,
|
| 82 |
-
"compression": 0, # ZERO compression. Preserve all natural dynamic swells and drops.
|
| 83 |
-
"stereo_width": 110, # Subtle widening — research warns against aggressive wideners on orchestral
|
| 84 |
-
},
|
| 85 |
-
|
| 86 |
-
"Indie / Alt-Rock (Organic & Glued)": {
|
| 87 |
-
"lows_db": 0.5, # Gentle body for the bass guitar
|
| 88 |
-
"mid_boost_db": 0.5, # Subtle push for guitars and vocal presence
|
| 89 |
-
"highs_db": 0.5, # Just a kiss of air for the cymbals
|
| 90 |
-
"bass_boost_db": 0.5, # A very light touch on the kick fundamental
|
| 91 |
-
"bass_freq_hz": 60,
|
| 92 |
-
"compression": 45, # Moderate glue. Holds the band together without crushing dynamics.
|
| 93 |
-
"stereo_width": 105, # Keeps the band feeling "in the room"
|
| 94 |
-
},
|
| 95 |
-
|
| 96 |
-
"R&B / Soul (Silky & Smooth)": {
|
| 97 |
-
"lows_db": 0.5, # Warm low-mid body without mud
|
| 98 |
-
"mid_boost_db": 1.0, # Smooth, intimate vocal presence
|
| 99 |
-
"highs_db": 0.5, # Silky top end, not overly bright or harsh
|
| 100 |
-
"bass_boost_db": 1.0, # Round, deep, controlled bass
|
| 101 |
-
"bass_freq_hz": 60,
|
| 102 |
-
"compression": 55, # Medium glue, smooth recovery for vocals
|
| 103 |
-
"stereo_width": 105, # Natural, immersive width
|
| 104 |
-
},
|
| 105 |
-
|
| 106 |
-
"Lo-Fi / Chillhop (Dusty & Narrow)": {
|
| 107 |
-
"lows_db": 1.5, # Thick, warm midrange for sample texture
|
| 108 |
-
"mid_boost_db": 0.0,
|
| 109 |
-
"highs_db": -2.5, # Aggressively rolled off for tape/vinyl aesthetic
|
| 110 |
-
"bass_boost_db": 1.0, # Warm, blunt kick thud
|
| 111 |
-
"bass_freq_hz": 60,
|
| 112 |
-
"compression": 35, # Gentle, optical-style leveling
|
| 113 |
-
"stereo_width": 90, # Narrow width emulating vintage samplers
|
| 114 |
-
},
|
| 115 |
-
|
| 116 |
-
"Jazz (Live & Dynamic)": {
|
| 117 |
-
"lows_db": -0.5, # Keep mud away from upright bass/piano overlap
|
| 118 |
-
"mid_boost_db": 1.0, # Bring horns and brushwork forward into the room
|
| 119 |
-
"highs_db": 0.5, # Natural acoustic air
|
| 120 |
-
"bass_boost_db": 0.0, # Zero artificial sub-bass; rely on the recording
|
| 121 |
-
"bass_freq_hz": 60,
|
| 122 |
-
"compression": 10, # Almost zero compression; absolute dynamic preservation
|
| 123 |
-
"stereo_width": 100, # True to the physical room
|
| 124 |
-
},
|
| 125 |
-
|
| 126 |
-
"Reggae / Dub (Heavy Sound System)": {
|
| 127 |
-
"lows_db": 1.0, # Thick low-mids for the rhythm section
|
| 128 |
-
"mid_boost_db": -1.0, # Scooped mids to make room for massive bass and tape delays
|
| 129 |
-
"highs_db": 0.5, # Slice through for hi-hats and echoes
|
| 130 |
-
"bass_boost_db": 2.5, # Massive sound-system weight
|
| 131 |
-
"bass_freq_hz": 80, # Targeted directly at the classic reggae bassline fundamental
|
| 132 |
-
"compression": 85, # Heavy pumping to lock in the off-beat groove
|
| 133 |
-
"stereo_width": 115, # Wide space for panning delays
|
| 134 |
-
},
|
| 135 |
-
|
| 136 |
-
"Synthwave / Retrowave (Analog & Driving)": {
|
| 137 |
-
"lows_db": 1.0, # Thick low-mids for analog synth body
|
| 138 |
-
"mid_boost_db": -0.5, # Slight scoop to carve out room for the kick and snare
|
| 139 |
-
"highs_db": 1.5, # Crisp, shimmering highs for 80s synth leads and hi-hats
|
| 140 |
-
"bass_boost_db": 1.5, # Driving, relentless synth-bass weight
|
| 141 |
-
"bass_freq_hz": 60, # Targeted at the punch of the Linndrum/707 kick
|
| 142 |
-
"compression": 75, # Heavy, driving glue to keep the grid tight and relentless
|
| 143 |
-
"stereo_width": 120, # Wide and cinematic, like a neon highway
|
| 144 |
},
|
| 145 |
-
}
|
|
|
|
| 4 |
"-- None --": None,
|
| 5 |
|
| 6 |
"Modern Pop (Bright & Wide)": {
|
| 7 |
+
"lows_db": 0.5,
|
| 8 |
"mid_boost_db": 1.0,
|
| 9 |
+
"highs_db": 2.0,
|
| 10 |
"bass_boost_db": 1.0,
|
| 11 |
"bass_freq_hz": 60,
|
| 12 |
+
"compression": 60,
|
| 13 |
"stereo_width": 115,
|
| 14 |
},
|
| 15 |
|
| 16 |
"Vintage Analog (Warm & Intimate)": {
|
| 17 |
+
"lows_db": 2.0,
|
| 18 |
"mid_boost_db": 0.0,
|
| 19 |
"highs_db": -1.5,
|
| 20 |
+
"bass_boost_db": 1.5,
|
| 21 |
+
"bass_freq_hz": 50,
|
| 22 |
+
"compression": 40,
|
| 23 |
"stereo_width": 95,
|
| 24 |
},
|
| 25 |
|
| 26 |
"Heavy Trap / EDM (Aggressive Bass)": {
|
| 27 |
+
"lows_db": 1.0,
|
| 28 |
"mid_boost_db": -0.5,
|
| 29 |
"highs_db": 1.0,
|
| 30 |
+
"bass_boost_db": 3.0,
|
| 31 |
+
"bass_freq_hz": 55,
|
| 32 |
+
"compression": 85,
|
| 33 |
"stereo_width": 125,
|
| 34 |
},
|
| 35 |
|
| 36 |
"Hard Rock / Metal (Punch & Bite)": {
|
| 37 |
"lows_db": -0.5,
|
| 38 |
+
"mid_boost_db": 1.5,
|
| 39 |
+
"highs_db": 1.0,
|
| 40 |
"bass_boost_db": 1.5,
|
| 41 |
+
"bass_freq_hz": 60,
|
| 42 |
+
"compression": 70,
|
| 43 |
"stereo_width": 110,
|
| 44 |
},
|
| 45 |
|
| 46 |
+
"Acoustic & Vocal (Transparent)": {
|
| 47 |
"lows_db": -0.5,
|
| 48 |
+
"mid_boost_db": 1.5,
|
| 49 |
"highs_db": 1.0,
|
| 50 |
"bass_boost_db": 0.0,
|
| 51 |
"bass_freq_hz": 55,
|
| 52 |
+
"compression": 15,
|
| 53 |
+
"stereo_width": 105,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
},
|
| 55 |
+
}
|
sAI.png
ADDED
|
Git LFS Details
|
stereo.py
CHANGED
|
@@ -1,44 +1,14 @@
|
|
| 1 |
-
"""Mid/Side stereo width processing
|
| 2 |
|
| 3 |
import numpy as np
|
| 4 |
-
from scipy.signal import butter, sosfiltfilt
|
| 5 |
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def linkwitz_riley_crossover(audio, sample_rate, crossover_hz):
|
| 12 |
-
"""Split audio into low and high bands using a Linkwitz-Riley 4th-order
|
| 13 |
-
crossover (two cascaded 2nd-order Butterworth filters).
|
| 14 |
-
|
| 15 |
-
Returns (low_band, high_band) each with the same shape as audio.
|
| 16 |
-
"""
|
| 17 |
-
nyquist = sample_rate / 2.0
|
| 18 |
-
# Clamp to valid range for the filter
|
| 19 |
-
freq = min(crossover_hz, nyquist * 0.95)
|
| 20 |
-
|
| 21 |
-
sos = butter(2, freq, btype='low', fs=sample_rate, output='sos')
|
| 22 |
-
|
| 23 |
-
# Linkwitz-Riley = two passes of Butterworth (sosfiltfilt = forward+backward
|
| 24 |
-
# = zero-phase, effectively 4th-order LR behaviour)
|
| 25 |
-
low = sosfiltfilt(sos, audio, axis=0).astype(np.float32)
|
| 26 |
-
high = (audio - low).astype(np.float32)
|
| 27 |
-
|
| 28 |
-
return low, high
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def apply_stereo_width(audio, width_percent, sample_rate=44100):
|
| 32 |
-
"""Apply stereo width adjustment using frequency-selective M/S encoding.
|
| 33 |
-
|
| 34 |
-
Bass below 200 Hz stays mono-preserving (no width change) to keep
|
| 35 |
-
the low end tight and phase-coherent on club/mono systems.
|
| 36 |
-
Width adjustment applies only to frequencies above the crossover.
|
| 37 |
|
| 38 |
Args:
|
| 39 |
audio: numpy array of shape (samples, 2), float32.
|
| 40 |
width_percent: 80 to 150. 100 = no change.
|
| 41 |
-
sample_rate: int, sample rate for crossover filter.
|
| 42 |
|
| 43 |
Returns:
|
| 44 |
numpy array of shape (samples, 2), float32.
|
|
@@ -48,19 +18,12 @@ def apply_stereo_width(audio, width_percent, sample_rate=44100):
|
|
| 48 |
|
| 49 |
width_factor = width_percent / 100.0
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
return audio
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
# Apply M/S width to HIGH band only
|
| 59 |
-
left_h = high[:, 0]
|
| 60 |
-
right_h = high[:, 1]
|
| 61 |
-
|
| 62 |
-
mid = (left_h + right_h) / 2.0
|
| 63 |
-
side = (left_h - right_h) / 2.0
|
| 64 |
|
| 65 |
# Energy-preserving scaling
|
| 66 |
mid_scale = np.sqrt(2.0 / (1.0 + width_factor ** 2))
|
|
@@ -69,13 +32,11 @@ def apply_stereo_width(audio, width_percent, sample_rate=44100):
|
|
| 69 |
mid_out = mid * mid_scale
|
| 70 |
side_out = side * side_scale
|
| 71 |
|
|
|
|
| 72 |
left_out = mid_out + side_out
|
| 73 |
right_out = mid_out - side_out
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# Recombine: untouched low band + widened high band
|
| 78 |
-
result = low + high_widened
|
| 79 |
|
| 80 |
# Prevent clipping from width expansion
|
| 81 |
peak = np.max(np.abs(result))
|
|
|
|
| 1 |
+
"""Mid/Side stereo width processing."""
|
| 2 |
|
| 3 |
import numpy as np
|
|
|
|
| 4 |
|
| 5 |
|
| 6 |
+
def apply_stereo_width(audio, width_percent):
|
| 7 |
+
"""Apply stereo width adjustment using M/S encoding.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
Args:
|
| 10 |
audio: numpy array of shape (samples, 2), float32.
|
| 11 |
width_percent: 80 to 150. 100 = no change.
|
|
|
|
| 12 |
|
| 13 |
Returns:
|
| 14 |
numpy array of shape (samples, 2), float32.
|
|
|
|
| 18 |
|
| 19 |
width_factor = width_percent / 100.0
|
| 20 |
|
| 21 |
+
left = audio[:, 0]
|
| 22 |
+
right = audio[:, 1]
|
|
|
|
| 23 |
|
| 24 |
+
# Encode to M/S
|
| 25 |
+
mid = (left + right) / 2.0
|
| 26 |
+
side = (left - right) / 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Energy-preserving scaling
|
| 29 |
mid_scale = np.sqrt(2.0 / (1.0 + width_factor ** 2))
|
|
|
|
| 32 |
mid_out = mid * mid_scale
|
| 33 |
side_out = side * side_scale
|
| 34 |
|
| 35 |
+
# Decode back to L/R
|
| 36 |
left_out = mid_out + side_out
|
| 37 |
right_out = mid_out - side_out
|
| 38 |
|
| 39 |
+
result = np.column_stack([left_out, right_out])
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
# Prevent clipping from width expansion
|
| 42 |
peak = np.max(np.abs(result))
|
visualization.py
CHANGED
|
@@ -54,17 +54,11 @@ def plot_waveform_comparison(original, mastered, sample_rate):
|
|
| 54 |
|
| 55 |
|
| 56 |
def plot_spectrum_comparison(original, mastered, sample_rate):
|
| 57 |
-
"""Create
|
| 58 |
-
and a difference trace showing the processing's spectral impact.
|
| 59 |
-
|
| 60 |
-
The mastered spectrum is level-aligned to the original so the plot
|
| 61 |
-
compares spectral *shape*, not overall loudness (LUFS stats handle that).
|
| 62 |
|
| 63 |
Returns a matplotlib Figure.
|
| 64 |
"""
|
| 65 |
-
fig,
|
| 66 |
-
2, 1, figsize=(8, 5), height_ratios=[3, 1], sharex=True,
|
| 67 |
-
)
|
| 68 |
|
| 69 |
orig_mono = _to_mono(original)
|
| 70 |
mast_mono = _to_mono(mastered)
|
|
@@ -88,38 +82,15 @@ def plot_spectrum_comparison(original, mastered, sample_rate):
|
|
| 88 |
freqs_o, spec_o = avg_spectrum(orig_mono, n_fft, sample_rate)
|
| 89 |
freqs_m, spec_m = avg_spectrum(mast_mono, n_fft, sample_rate)
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
label="Original")
|
| 101 |
-
ax_spec.plot(freqs_m, spec_m_aligned, color="#d94a4a", alpha=0.7,
|
| 102 |
-
linewidth=1, label="Mastered (level-aligned)")
|
| 103 |
-
ax_spec.set_ylabel("Magnitude (dB)")
|
| 104 |
-
ax_spec.set_title("Spectral Shape Comparison")
|
| 105 |
-
ax_spec.legend(loc="upper right", fontsize=8)
|
| 106 |
-
ax_spec.grid(True, alpha=0.3)
|
| 107 |
-
|
| 108 |
-
# --- Bottom: difference (mastered − original) ---
|
| 109 |
-
diff = spec_m_aligned - spec_o
|
| 110 |
-
ax_diff.plot(freqs_o, diff, color="#2ca02c", linewidth=1)
|
| 111 |
-
ax_diff.axhline(0, color="gray", linewidth=0.5, linestyle="--")
|
| 112 |
-
ax_diff.set_ylabel("Δ dB")
|
| 113 |
-
ax_diff.set_xlabel("Frequency")
|
| 114 |
-
ax_diff.set_title("Processing Difference (Mastered − Original)", fontsize=9)
|
| 115 |
-
ax_diff.set_ylim(-6, 6)
|
| 116 |
-
ax_diff.grid(True, alpha=0.3)
|
| 117 |
-
|
| 118 |
-
# Shared x-axis settings
|
| 119 |
-
ax_diff.set_xscale("log")
|
| 120 |
-
ax_diff.set_xlim(20, sample_rate / 2)
|
| 121 |
-
ax_diff.set_xticks([10, 100, 1000, 10000])
|
| 122 |
-
ax_diff.set_xticklabels(["10 Hz", "100 Hz", "1 kHz", "10 kHz"])
|
| 123 |
|
| 124 |
plt.tight_layout()
|
| 125 |
return fig
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
def plot_spectrum_comparison(original, mastered, sample_rate):
|
| 57 |
+
"""Create an overlaid frequency spectrum comparison.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
Returns a matplotlib Figure.
|
| 60 |
"""
|
| 61 |
+
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
|
|
|
|
|
|
|
| 62 |
|
| 63 |
orig_mono = _to_mono(original)
|
| 64 |
mast_mono = _to_mono(mastered)
|
|
|
|
| 82 |
freqs_o, spec_o = avg_spectrum(orig_mono, n_fft, sample_rate)
|
| 83 |
freqs_m, spec_m = avg_spectrum(mast_mono, n_fft, sample_rate)
|
| 84 |
|
| 85 |
+
ax.plot(freqs_o, spec_o, color="#4a90d9", alpha=0.7, linewidth=1, label="Original")
|
| 86 |
+
ax.plot(freqs_m, spec_m, color="#d94a4a", alpha=0.7, linewidth=1, label="Mastered")
|
| 87 |
+
ax.set_xscale("log")
|
| 88 |
+
ax.set_xlim(20, sample_rate / 2)
|
| 89 |
+
ax.set_xlabel("Frequency (Hz)")
|
| 90 |
+
ax.set_ylabel("Magnitude (dB)")
|
| 91 |
+
ax.set_title("Frequency Spectrum Comparison")
|
| 92 |
+
ax.legend()
|
| 93 |
+
ax.grid(True, alpha=0.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
plt.tight_layout()
|
| 96 |
return fig
|