Vibecode Masterchannel
track this build5 phases, 15 steps, beginner friendly0%Mastering is signal processing, and the open source world already solved a big chunk of it: reference matching, loudness normalization and true peak limiting are all off the shelf. An agent can wire matchering, pyloudnorm and ffmpeg into a local CLI that takes your mix plus a commercial reference and spits out a competitive master in an afternoon. What it cannot do is decide, with no reference, what your track should sound like: that judgment is the part these services trained on thousands of masters to fake. So the DIY build is genuinely usable if you already know which records you want to sound like, and mediocre if you don't. Also expect to babysit sample rates, mono compatibility and the occasional inter-sample peak.
You are building a lean indie version of Masterchannel. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Masterchannel · indie build A command line pipeline that measures your mix and a reference master, matches the spectral balance and the dynamic range towards the reference, normalises to a chosen loudness target with true peak headroom, and exports platform-ready files with a report showing every figure before and after. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg for measurement and rendering, plus a small DSP layer for matching | loudness measurement to the standard is solved and does not need reimplementing | | Runtime | Python 3.12 with numpy and a spectral analysis library | the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for | | Measurement | Integrated loudness, true peak, loudness range and a spectral profile | the report is the product; processing you cannot verify is guesswork | | Matching | Broad multiband tilt towards the reference, not a hundred narrow bands | a fine spectral match copies the reference's room and arrangement and sounds wrong on your track | | Output | Original untouched, masters plus a JSON report alongside | you will rerun with different settings and need to compare | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **ffmpeg with loudness measurement** · free - Why: Measurement to the standard and encoding both run through it, and reimplementing the measurement is not worth it. - Get it: brew install ffmpeg or your package manager. Confirm the loudness filter is present. - Verify: ffmpeg -filters lists loudnorm - [ ] **Python 3.12 with numerical libraries** · free - Why: Spectral analysis and matching are array arithmetic. - Get it: python.org or your package manager, then a virtual environment with numpy and a spectral analysis package. - Verify: python3 --version prints 3.12 or higher and numpy imports - [ ] **Three of your own mixes, and one you consider finished** · free - Why: Matching and dynamics work are only judgeable against real material, and you need something you already trust as a comparison in the final phase. - Get it: Your own work. Include one mix you know is too dynamic and one you suspect is too compressed, since those are the cases the tool must handle differently. - Verify: Three mixes plus a trusted master are available - [ ] **Reference masters you are entitled to analyse** · free with music you own - Why: Analysing a commercial track for your own reference is ordinary practice, and redistributing it is not. Keep references local and never ship them. - Get it: Tracks you own in a lossless format where possible, since a heavily compressed source distorts the spectral profile you are matching to. - Verify: At least two references analyse into profiles - [ ] **Monitoring you know well** · free if you already have monitoring - Why: Every decision here is finally judged by ear, and unfamiliar speakers will lead you to correct for the room rather than the track. - Get it: Whatever you have listened to enough to know. Familiarity matters more than price. - Verify: You can identify a familiar reference track's character on them ## Quick start ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. - Reference-free mastering: the service guesses a target for you, your script needs you to pick one - Genre-aware presets and the taste baked into a trained model - Stem mastering, vocal-forward variants and other per-track intelligence - A clean web UI with instant previews and revision history - Anything resembling a second opinion when your mix is the actual problem If one of those is essential to you, that is the reason to keep paying for Masterchannel, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Masterchannel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a mastering pipeline that matches a reference and hits a streaming loudness target to replace Masterchannel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Understand the loudness situation before you build, because it changes what mastering is for. Streaming services normalise playback, so a track mastered several decibels louder than the platform target is turned down on playback and arrives quieter in feel and flatter in dynamics than a quieter master would have. Mastering loud no longer wins anything and costs you the dynamics you crushed to get there. So the target is a stated loudness figure with true peak headroom, and the interesting work is matching a reference you admire in spectrum and dynamics rather than pushing a limiter. Build the measurement first and the processing second, so every claim the tool makes is one you can check. ### Stack (fixed, do not substitute) - Processing: ffmpeg for measurement and rendering, plus a small DSP layer for matching · loudness measurement to the standard is solved and does not need reimplementing - Runtime: Python 3.12 with numpy and a spectral analysis library · the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for - Measurement: Integrated loudness, true peak, loudness range and a spectral profile · the report is the product; processing you cannot verify is guesswork - Matching: Broad multiband tilt towards the reference, not a hundred narrow bands · a fine spectral match copies the reference's room and arrangement and sounds wrong on your track - Output: Original untouched, masters plus a JSON report alongside · you will rerun with different settings and need to compare ### Data model (create this before Phase 1) - tracks(id, source_path, sample_rate, channels, duration_ms, content_hash) - references(id, path, name, added_at, profile_json) · analysed once and reused across tracks - measurements(id, subject_kind, subject_id, stage, integrated_lufs, true_peak_dbtp, lra, spectral_bands_json, crest_db) · stage is before or after - jobs(id, track_id, reference_id, target_lufs, true_peak_db, match_strength, output_path, settings_hash, at) - The settings hash on every output is what makes a master reproducible, which matters when a client asks for a small change six months later. ### Phase 1 · Measure before you touch anything Build: Measure integrated loudness, true peak, loudness range and a coarse spectral profile for a file, without modifying it. Measure a reference master the same way. Print both side by side. Trap: Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. Done when: Analysis prints integrated loudness in LUFS, true peak in dBTP, loudness range in LU and a spectral profile in a small number of bands. A generated reference tone measures at its expected level. The source file checksum is unchanged after analysis. A reference and a mix print in a comparable form. Do not build yet: No matching, normalisation or export. ### Phase 2 · Matching a reference, gently Build: Compute the difference between your spectral profile and the reference's, apply it as a broad multiband correction with a strength control, and re-measure. Cap the correction per band so no band can move by an implausible amount. Trap: Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. Done when: Matching moves the spectral profile measurably towards the reference without any band exceeding the cap. Setting strength to zero produces a file measurably identical to the input. A reference in a very different genre produces a capped correction rather than an extreme one. The applied correction per band is recorded in the report. Do not build yet: No loudness normalisation or export yet. ### Phase 3 · Dynamics, treated as a budget Build: Compare loudness range against the reference and apply gentle compression only where yours is substantially wider. Set a floor below which the tool refuses to reduce range further, and report the range before and after. Trap: Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. Done when: A mix with a much wider range than the reference is brought closer without falling below the configured floor. A mix already close to the reference is left alone, verified by an unchanged range figure. Loudness range before and after appears in the report. The tool refuses to reduce range below the floor and says why. Do not build yet: No normalisation or export yet. ### Phase 4 · Loudness and true peak, done last Build: Normalise to the configured loudness target using a measured two pass approach with linear gain, and apply a true peak ceiling below zero. Re-measure the output and assert both figures. State the target and why it was chosen. Trap: Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. Done when: The output measures within a small tolerance of the target loudness and below the true peak ceiling, verified by re-running the analysis. Loudness range after normalisation is close to what it was before, showing gain rather than compression was used. Encoding the output to a lossy format does not exceed zero true peak. The target is recorded in the report. Do not build yet: No exports or batch yet. ### Phase 5 · Deliverables and the report Build: Export the master in a lossless format at the source rate plus any lossy versions you need, all from the same processed audio. Write a report with every figure before and after, the reference used and the settings hash. Trap: Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. Done when: Lossless and lossy outputs derive from the same processed audio, verified by comparing their measurements. The report lists every figure before and after with the reference and the settings hash. Re-running with the same settings produces byte-identical output. The source file is unmodified. Do not build yet: Batch and honesty checks come last. ### Phase 6 · Batch, references and the honest comparison Build: Process a directory as an album, keeping loudness consistent across tracks rather than normalising each independently. Build a reference library. Then compare your masters against a mastered version you trust and record what you hear. Trap: Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. Done when: An album processed together preserves relative loudness between tracks, verified by comparing per-track figures before and after. A reference library stores analysed profiles reusable across projects. A blind comparison against a trusted master has been done and the notes recorded. Per-track and album figures both appear in the report. Do not build yet: Nothing after this. ### Out of scope (and why) - The judgement of a mastering engineer, which is what you are actually paying for and which no reference matching reproduces. - Fixing a mix. Mastering moves a finished mix a small distance; a problem in the mix stays a problem and should go back to the mix. - Stem mastering and the corrective work that needs access to individual parts. - Distribution and the platform-specific delivery requirements that come with it. ### README must contain - That streaming platforms normalise playback, so mastering louder than the target gains nothing - The loudness target and true peak ceiling used, and why headroom is left below zero - That spectral matching uses broad bands with caps, and why fine matching sounds wrong - That loudness is moved with gain rather than compression, with the range figures to prove it - That album processing preserves relative loudness between tracks ===== AGENTS.md ===== # Agent instructions · Masterchannel indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg for measurement and rendering, plus a small DSP layer for matching, Python 3.12 with numpy and a spectral analysis library, Integrated loudness, true peak, loudness range and a spectral profile, Broad multiband tilt towards the reference, not a hundred narrow bands, Original untouched, masters plus a JSON report alongside. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ===== BUILD_PLAN.md ===== # Build plan · Masterchannel A command line pipeline that measures your mix and a reference master, matches the spectral balance and the dynamic range towards the reference, normalises to a chosen loudness target with true peak headroom, and exports platform-ready files with a report showing every figure before and after. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Measure before you touch anything You can describe a mix and a reference in the same numbers. ### Steps 1. Measure loudness to the standard Parse the structured output rather than the human readable text, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` 2. Compute a coarse spectral profile A handful of bands, averaged over the track. Fine resolution invites over-matching later. Files: `src/spectrum.py` 3. Print mix and reference together The comparison is what the whole tool is for. Make it the first thing it does. Files: `src/report.py` ### Done when - [ ] Analysis prints loudness, true peak, range and a spectral profile - [ ] A generated tone measures at its expected level - [ ] The source checksum is unchanged - [ ] Mix and reference print in comparable form ### Watch out - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. ## Phase 2 · Matching a reference, gently Your mix moves towards the reference's balance without taking on its arrangement. ### Steps 1. Compute the band difference Your profile against the reference's, in a small number of bands. Files: `src/match.py` 2. Apply with a strength control and caps Strength scales the whole correction; caps limit any single band. Both are needed. Files: `src/match.py` 3. Re-measure and record Every applied correction in the report, so the processing is inspectable rather than magic. Files: `src/report.py` ### Done when - [ ] Matching moves the profile towards the reference - [ ] No band exceeds the configured cap - [ ] Zero strength produces an identical measurement - [ ] Applied corrections are recorded per band ### Watch out - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. ## Phase 3 · Dynamics, treated as a budget The loudness range moves towards the reference without being crushed to get there. ### Steps 1. Compare loudness range Yours against the reference. The gap, if any, is what justifies compression. Files: `src/dynamics.py` 2. Compress only to close a real gap With a floor. Below it the tool refuses rather than continuing. Files: `src/dynamics.py` 3. Report range before and after The number that tells you whether you crushed it. Files: `src/report.py` ### Done when - [ ] A wide-range mix is brought closer without passing the floor - [ ] A mix near the reference is left alone - [ ] Range before and after appears in the report - [ ] The floor produces a refusal with a stated reason ### Watch out - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. ## Phase 4 · Loudness and true peak, done last The master lands on its target with headroom that survives encoding. ### Steps 1. Normalise with measured values Two pass with linear correction, so a fixed gain is applied rather than dynamic processing. Files: `src/normalise.py` 2. Leave true peak headroom Below zero, because lossy encoding introduces peaks above the original samples. Files: `src/normalise.py` 3. Verify after encoding Encode to a lossy format and measure again. This is the check that justifies the headroom. Files: `src/verify.py` ### Done when - [ ] Output measures within tolerance of the target - [ ] Loudness range survives normalisation - [ ] A lossy encode does not exceed zero true peak - [ ] The target is recorded in the report ### Watch out - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. ## Phase 5 · Deliverables and the report One run produces every file you need and the evidence that it was done properly. ### Steps 1. Process once, encode many One processed master, several encodings from it. Never reprocess per format. Files: `src/export.py` 2. Write the full report Before and after, reference, settings hash. This is what makes the master defensible. Files: `src/report.py` 3. Assert determinism Same input and settings, same bytes. Pin anything random. Files: `tests/test_determinism.py` ### Done when - [ ] Lossless and lossy outputs derive from the same audio - [ ] The report lists all figures, reference and settings hash - [ ] Reruns are byte-identical - [ ] The source is unmodified ### Watch out - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ## Not in this build - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. ## After v1, if you want it - Mid and side processing, which is where a small amount of width adjustment lives - A null test against the source, which shows exactly what the processing changed - Per-platform variants once you know the targets differ enough to matter ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. TARGET_LUFS=-14 # Required. Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. TRUE_PEAK_DB=-1.0 # Required. How far towards the reference to move. Full strength copies the reference's arrangement. MATCH_STRENGTH=0.6 # Required. Maximum correction per band. The cap that prevents an extreme match. MATCH_BAND_CAP_DB=3.0 # Required. Loudness range floor. Below it the tool refuses to compress further. MIN_LRA=6 # Required. Where masters and reports are written. Never the source directory. OUTPUT_DIR=./masters
You are building a lean indie version of Masterchannel. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Masterchannel · indie build A command line pipeline that measures your mix and a reference master, matches the spectral balance and the dynamic range towards the reference, normalises to a chosen loudness target with true peak headroom, and exports platform-ready files with a report showing every figure before and after. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg for measurement and rendering, plus a small DSP layer for matching | loudness measurement to the standard is solved and does not need reimplementing | | Runtime | Python 3.12 with numpy and a spectral analysis library | the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for | | Measurement | Integrated loudness, true peak, loudness range and a spectral profile | the report is the product; processing you cannot verify is guesswork | | Matching | Broad multiband tilt towards the reference, not a hundred narrow bands | a fine spectral match copies the reference's room and arrangement and sounds wrong on your track | | Output | Original untouched, masters plus a JSON report alongside | you will rerun with different settings and need to compare | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **ffmpeg with loudness measurement** · free - Why: Measurement to the standard and encoding both run through it, and reimplementing the measurement is not worth it. - Get it: brew install ffmpeg or your package manager. Confirm the loudness filter is present. - Verify: ffmpeg -filters lists loudnorm - [ ] **Python 3.12 with numerical libraries** · free - Why: Spectral analysis and matching are array arithmetic. - Get it: python.org or your package manager, then a virtual environment with numpy and a spectral analysis package. - Verify: python3 --version prints 3.12 or higher and numpy imports - [ ] **Three of your own mixes, and one you consider finished** · free - Why: Matching and dynamics work are only judgeable against real material, and you need something you already trust as a comparison in the final phase. - Get it: Your own work. Include one mix you know is too dynamic and one you suspect is too compressed, since those are the cases the tool must handle differently. - Verify: Three mixes plus a trusted master are available - [ ] **Reference masters you are entitled to analyse** · free with music you own - Why: Analysing a commercial track for your own reference is ordinary practice, and redistributing it is not. Keep references local and never ship them. - Get it: Tracks you own in a lossless format where possible, since a heavily compressed source distorts the spectral profile you are matching to. - Verify: At least two references analyse into profiles - [ ] **Monitoring you know well** · free if you already have monitoring - Why: Every decision here is finally judged by ear, and unfamiliar speakers will lead you to correct for the room rather than the track. - Get it: Whatever you have listened to enough to know. Familiarity matters more than price. - Verify: You can identify a familiar reference track's character on them ## Quick start ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. - Reference-free mastering: the service guesses a target for you, your script needs you to pick one - Genre-aware presets and the taste baked into a trained model - Stem mastering, vocal-forward variants and other per-track intelligence - A clean web UI with instant previews and revision history - Anything resembling a second opinion when your mix is the actual problem If one of those is essential to you, that is the reason to keep paying for Masterchannel, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Masterchannel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a mastering pipeline that matches a reference and hits a streaming loudness target to replace Masterchannel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Understand the loudness situation before you build, because it changes what mastering is for. Streaming services normalise playback, so a track mastered several decibels louder than the platform target is turned down on playback and arrives quieter in feel and flatter in dynamics than a quieter master would have. Mastering loud no longer wins anything and costs you the dynamics you crushed to get there. So the target is a stated loudness figure with true peak headroom, and the interesting work is matching a reference you admire in spectrum and dynamics rather than pushing a limiter. Build the measurement first and the processing second, so every claim the tool makes is one you can check. ### Stack (fixed, do not substitute) - Processing: ffmpeg for measurement and rendering, plus a small DSP layer for matching · loudness measurement to the standard is solved and does not need reimplementing - Runtime: Python 3.12 with numpy and a spectral analysis library · the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for - Measurement: Integrated loudness, true peak, loudness range and a spectral profile · the report is the product; processing you cannot verify is guesswork - Matching: Broad multiband tilt towards the reference, not a hundred narrow bands · a fine spectral match copies the reference's room and arrangement and sounds wrong on your track - Output: Original untouched, masters plus a JSON report alongside · you will rerun with different settings and need to compare ### Data model (create this before Phase 1) - tracks(id, source_path, sample_rate, channels, duration_ms, content_hash) - references(id, path, name, added_at, profile_json) · analysed once and reused across tracks - measurements(id, subject_kind, subject_id, stage, integrated_lufs, true_peak_dbtp, lra, spectral_bands_json, crest_db) · stage is before or after - jobs(id, track_id, reference_id, target_lufs, true_peak_db, match_strength, output_path, settings_hash, at) - The settings hash on every output is what makes a master reproducible, which matters when a client asks for a small change six months later. ### Phase 1 · Measure before you touch anything Build: Measure integrated loudness, true peak, loudness range and a coarse spectral profile for a file, without modifying it. Measure a reference master the same way. Print both side by side. Trap: Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. Done when: Analysis prints integrated loudness in LUFS, true peak in dBTP, loudness range in LU and a spectral profile in a small number of bands. A generated reference tone measures at its expected level. The source file checksum is unchanged after analysis. A reference and a mix print in a comparable form. Do not build yet: No matching, normalisation or export. ### Phase 2 · Matching a reference, gently Build: Compute the difference between your spectral profile and the reference's, apply it as a broad multiband correction with a strength control, and re-measure. Cap the correction per band so no band can move by an implausible amount. Trap: Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. Done when: Matching moves the spectral profile measurably towards the reference without any band exceeding the cap. Setting strength to zero produces a file measurably identical to the input. A reference in a very different genre produces a capped correction rather than an extreme one. The applied correction per band is recorded in the report. Do not build yet: No loudness normalisation or export yet. ### Phase 3 · Dynamics, treated as a budget Build: Compare loudness range against the reference and apply gentle compression only where yours is substantially wider. Set a floor below which the tool refuses to reduce range further, and report the range before and after. Trap: Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. Done when: A mix with a much wider range than the reference is brought closer without falling below the configured floor. A mix already close to the reference is left alone, verified by an unchanged range figure. Loudness range before and after appears in the report. The tool refuses to reduce range below the floor and says why. Do not build yet: No normalisation or export yet. ### Phase 4 · Loudness and true peak, done last Build: Normalise to the configured loudness target using a measured two pass approach with linear gain, and apply a true peak ceiling below zero. Re-measure the output and assert both figures. State the target and why it was chosen. Trap: Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. Done when: The output measures within a small tolerance of the target loudness and below the true peak ceiling, verified by re-running the analysis. Loudness range after normalisation is close to what it was before, showing gain rather than compression was used. Encoding the output to a lossy format does not exceed zero true peak. The target is recorded in the report. Do not build yet: No exports or batch yet. ### Phase 5 · Deliverables and the report Build: Export the master in a lossless format at the source rate plus any lossy versions you need, all from the same processed audio. Write a report with every figure before and after, the reference used and the settings hash. Trap: Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. Done when: Lossless and lossy outputs derive from the same processed audio, verified by comparing their measurements. The report lists every figure before and after with the reference and the settings hash. Re-running with the same settings produces byte-identical output. The source file is unmodified. Do not build yet: Batch and honesty checks come last. ### Phase 6 · Batch, references and the honest comparison Build: Process a directory as an album, keeping loudness consistent across tracks rather than normalising each independently. Build a reference library. Then compare your masters against a mastered version you trust and record what you hear. Trap: Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. Done when: An album processed together preserves relative loudness between tracks, verified by comparing per-track figures before and after. A reference library stores analysed profiles reusable across projects. A blind comparison against a trusted master has been done and the notes recorded. Per-track and album figures both appear in the report. Do not build yet: Nothing after this. ### Out of scope (and why) - The judgement of a mastering engineer, which is what you are actually paying for and which no reference matching reproduces. - Fixing a mix. Mastering moves a finished mix a small distance; a problem in the mix stays a problem and should go back to the mix. - Stem mastering and the corrective work that needs access to individual parts. - Distribution and the platform-specific delivery requirements that come with it. ### README must contain - That streaming platforms normalise playback, so mastering louder than the target gains nothing - The loudness target and true peak ceiling used, and why headroom is left below zero - That spectral matching uses broad bands with caps, and why fine matching sounds wrong - That loudness is moved with gain rather than compression, with the range figures to prove it - That album processing preserves relative loudness between tracks ===== AGENTS.md ===== # Agent instructions · Masterchannel indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg for measurement and rendering, plus a small DSP layer for matching, Python 3.12 with numpy and a spectral analysis library, Integrated loudness, true peak, loudness range and a spectral profile, Broad multiband tilt towards the reference, not a hundred narrow bands, Original untouched, masters plus a JSON report alongside. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ===== BUILD_PLAN.md ===== # Build plan · Masterchannel A command line pipeline that measures your mix and a reference master, matches the spectral balance and the dynamic range towards the reference, normalises to a chosen loudness target with true peak headroom, and exports platform-ready files with a report showing every figure before and after. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Measure before you touch anything You can describe a mix and a reference in the same numbers. ### Steps 1. Measure loudness to the standard Parse the structured output rather than the human readable text, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` 2. Compute a coarse spectral profile A handful of bands, averaged over the track. Fine resolution invites over-matching later. Files: `src/spectrum.py` 3. Print mix and reference together The comparison is what the whole tool is for. Make it the first thing it does. Files: `src/report.py` ### Done when - [ ] Analysis prints loudness, true peak, range and a spectral profile - [ ] A generated tone measures at its expected level - [ ] The source checksum is unchanged - [ ] Mix and reference print in comparable form ### Watch out - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. ## Phase 2 · Matching a reference, gently Your mix moves towards the reference's balance without taking on its arrangement. ### Steps 1. Compute the band difference Your profile against the reference's, in a small number of bands. Files: `src/match.py` 2. Apply with a strength control and caps Strength scales the whole correction; caps limit any single band. Both are needed. Files: `src/match.py` 3. Re-measure and record Every applied correction in the report, so the processing is inspectable rather than magic. Files: `src/report.py` ### Done when - [ ] Matching moves the profile towards the reference - [ ] No band exceeds the configured cap - [ ] Zero strength produces an identical measurement - [ ] Applied corrections are recorded per band ### Watch out - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. ## Phase 3 · Dynamics, treated as a budget The loudness range moves towards the reference without being crushed to get there. ### Steps 1. Compare loudness range Yours against the reference. The gap, if any, is what justifies compression. Files: `src/dynamics.py` 2. Compress only to close a real gap With a floor. Below it the tool refuses rather than continuing. Files: `src/dynamics.py` 3. Report range before and after The number that tells you whether you crushed it. Files: `src/report.py` ### Done when - [ ] A wide-range mix is brought closer without passing the floor - [ ] A mix near the reference is left alone - [ ] Range before and after appears in the report - [ ] The floor produces a refusal with a stated reason ### Watch out - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. ## Phase 4 · Loudness and true peak, done last The master lands on its target with headroom that survives encoding. ### Steps 1. Normalise with measured values Two pass with linear correction, so a fixed gain is applied rather than dynamic processing. Files: `src/normalise.py` 2. Leave true peak headroom Below zero, because lossy encoding introduces peaks above the original samples. Files: `src/normalise.py` 3. Verify after encoding Encode to a lossy format and measure again. This is the check that justifies the headroom. Files: `src/verify.py` ### Done when - [ ] Output measures within tolerance of the target - [ ] Loudness range survives normalisation - [ ] A lossy encode does not exceed zero true peak - [ ] The target is recorded in the report ### Watch out - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. ## Phase 5 · Deliverables and the report One run produces every file you need and the evidence that it was done properly. ### Steps 1. Process once, encode many One processed master, several encodings from it. Never reprocess per format. Files: `src/export.py` 2. Write the full report Before and after, reference, settings hash. This is what makes the master defensible. Files: `src/report.py` 3. Assert determinism Same input and settings, same bytes. Pin anything random. Files: `tests/test_determinism.py` ### Done when - [ ] Lossless and lossy outputs derive from the same audio - [ ] The report lists all figures, reference and settings hash - [ ] Reruns are byte-identical - [ ] The source is unmodified ### Watch out - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ## Not in this build - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. ## After v1, if you want it - Mid and side processing, which is where a small amount of width adjustment lives - A null test against the source, which shows exactly what the processing changed - Per-platform variants once you know the targets differ enough to matter ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. TARGET_LUFS=-14 # Required. Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. TRUE_PEAK_DB=-1.0 # Required. How far towards the reference to move. Full strength copies the reference's arrangement. MATCH_STRENGTH=0.6 # Required. Maximum correction per band. The cap that prevents an extreme match. MATCH_BAND_CAP_DB=3.0 # Required. Loudness range floor. Below it the tool refuses to compress further. MIN_LRA=6 # Required. Where masters and reports are written. Never the source directory. OUTPUT_DIR=./masters
You are building a production product version of Masterchannel. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Masterchannel · product brief ## Problem Mastering is signal processing, and the open source world already solved a big chunk of it: reference matching, loudness normalization and true peak limiting are all off the shelf. An agent can wire matchering, pyloudnorm and ffmpeg into a local CLI that takes your mix plus a commercial reference and spits out a competitive master in an afternoon. What it cannot do is decide, with no reference, what your track should sound like: that judgment is the part these services trained on thousands of masters to fake. So the DIY build is genuinely usable if you already know which records you want to sound like, and mediocre if you don't. Also expect to babysit sample rates, mono compatibility and the occasional inter-sample peak. ## Product outcome A mastering pipeline whose every claim is a measured figure, which moves loudness with gain rather than compression, matches a reference as a direction rather than a copy, and produces reproducible masters with a report you could hand to anyone. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.11 and ffmpeg installed locally - At least one commercial reference track in a lossless format - Headphones or monitors you actually trust - Basic willingness to A/B and re-run with different references ## Explicit non-goals for v1 - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. - Reference-free mastering: the service guesses a target for you, your script needs you to pick one - Genre-aware presets and the taste baked into a trained model - Stem mastering, vocal-forward variants and other per-track intelligence - A clean web UI with instant previews and revision history - Anything resembling a second opinion when your mix is the actual problem ## Success criteria - Output loudness and true peak are verified by re-measurement - Loudness range is confirmed to survive normalisation - A lossy encode is confirmed not to exceed zero true peak - Album processing is confirmed to preserve relative loudness - Reruns are confirmed byte-identical ===== BRIEF.md ===== # Build brief · Masterchannel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a mastering pipeline that matches a reference and hits a streaming loudness target to replace Masterchannel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Understand the loudness situation before you build, because it changes what mastering is for. Streaming services normalise playback, so a track mastered several decibels louder than the platform target is turned down on playback and arrives quieter in feel and flatter in dynamics than a quieter master would have. Mastering loud no longer wins anything and costs you the dynamics you crushed to get there. So the target is a stated loudness figure with true peak headroom, and the interesting work is matching a reference you admire in spectrum and dynamics rather than pushing a limiter. Build the measurement first and the processing second, so every claim the tool makes is one you can check. ### Stack (fixed, do not substitute) - Processing: ffmpeg for measurement and rendering, plus a small DSP layer for matching · loudness measurement to the standard is solved and does not need reimplementing - Runtime: Python 3.12 with numpy and a spectral analysis library · the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for - Measurement: Integrated loudness, true peak, loudness range and a spectral profile · the report is the product; processing you cannot verify is guesswork - Matching: Broad multiband tilt towards the reference, not a hundred narrow bands · a fine spectral match copies the reference's room and arrangement and sounds wrong on your track - Output: Original untouched, masters plus a JSON report alongside · you will rerun with different settings and need to compare ### Data model (create this before Phase 1) - tracks(id, source_path, sample_rate, channels, duration_ms, content_hash) - references(id, path, name, added_at, profile_json) · analysed once and reused across tracks - measurements(id, subject_kind, subject_id, stage, integrated_lufs, true_peak_dbtp, lra, spectral_bands_json, crest_db) · stage is before or after - jobs(id, track_id, reference_id, target_lufs, true_peak_db, match_strength, output_path, settings_hash, at) - The settings hash on every output is what makes a master reproducible, which matters when a client asks for a small change six months later. ### Phase 1 · Measure before you touch anything Build: Measure integrated loudness, true peak, loudness range and a coarse spectral profile for a file, without modifying it. Measure a reference master the same way. Print both side by side. Trap: Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. Done when: Analysis prints integrated loudness in LUFS, true peak in dBTP, loudness range in LU and a spectral profile in a small number of bands. A generated reference tone measures at its expected level. The source file checksum is unchanged after analysis. A reference and a mix print in a comparable form. Do not build yet: No matching, normalisation or export. ### Phase 2 · Matching a reference, gently Build: Compute the difference between your spectral profile and the reference's, apply it as a broad multiband correction with a strength control, and re-measure. Cap the correction per band so no band can move by an implausible amount. Trap: Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. Done when: Matching moves the spectral profile measurably towards the reference without any band exceeding the cap. Setting strength to zero produces a file measurably identical to the input. A reference in a very different genre produces a capped correction rather than an extreme one. The applied correction per band is recorded in the report. Do not build yet: No loudness normalisation or export yet. ### Phase 3 · Dynamics, treated as a budget Build: Compare loudness range against the reference and apply gentle compression only where yours is substantially wider. Set a floor below which the tool refuses to reduce range further, and report the range before and after. Trap: Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. Done when: A mix with a much wider range than the reference is brought closer without falling below the configured floor. A mix already close to the reference is left alone, verified by an unchanged range figure. Loudness range before and after appears in the report. The tool refuses to reduce range below the floor and says why. Do not build yet: No normalisation or export yet. ### Phase 4 · Loudness and true peak, done last Build: Normalise to the configured loudness target using a measured two pass approach with linear gain, and apply a true peak ceiling below zero. Re-measure the output and assert both figures. State the target and why it was chosen. Trap: Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. Done when: The output measures within a small tolerance of the target loudness and below the true peak ceiling, verified by re-running the analysis. Loudness range after normalisation is close to what it was before, showing gain rather than compression was used. Encoding the output to a lossy format does not exceed zero true peak. The target is recorded in the report. Do not build yet: No exports or batch yet. ### Phase 5 · Deliverables and the report Build: Export the master in a lossless format at the source rate plus any lossy versions you need, all from the same processed audio. Write a report with every figure before and after, the reference used and the settings hash. Trap: Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. Done when: Lossless and lossy outputs derive from the same processed audio, verified by comparing their measurements. The report lists every figure before and after with the reference and the settings hash. Re-running with the same settings produces byte-identical output. The source file is unmodified. Do not build yet: Batch and honesty checks come last. ### Phase 6 · Batch, references and the honest comparison Build: Process a directory as an album, keeping loudness consistent across tracks rather than normalising each independently. Build a reference library. Then compare your masters against a mastered version you trust and record what you hear. Trap: Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. Done when: An album processed together preserves relative loudness between tracks, verified by comparing per-track figures before and after. A reference library stores analysed profiles reusable across projects. A blind comparison against a trusted master has been done and the notes recorded. Per-track and album figures both appear in the report. Do not build yet: Nothing after this. ### Out of scope (and why) - The judgement of a mastering engineer, which is what you are actually paying for and which no reference matching reproduces. - Fixing a mix. Mastering moves a finished mix a small distance; a problem in the mix stays a problem and should go back to the mix. - Stem mastering and the corrective work that needs access to individual parts. - Distribution and the platform-specific delivery requirements that come with it. ### README must contain - That streaming platforms normalise playback, so mastering louder than the target gains nothing - The loudness target and true peak ceiling used, and why headroom is left below zero - That spectral matching uses broad bands with caps, and why fine matching sounds wrong - That loudness is moved with gain rather than compression, with the range figures to prove it - That album processing preserves relative loudness between tracks ===== ARCHITECTURE.md ===== # Architecture · Masterchannel ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg for measurement and rendering, plus a small DSP layer for matching | loudness measurement to the standard is solved and does not need reimplementing | | Runtime | Python 3.12 with numpy and a spectral analysis library | the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for | | Measurement | Integrated loudness, true peak, loudness range and a spectral profile | the report is the product; processing you cannot verify is guesswork | | Matching | Broad multiband tilt towards the reference, not a hundred narrow bands | a fine spectral match copies the reference's room and arrangement and sounds wrong on your track | | Output | Original untouched, masters plus a JSON report alongside | you will rerun with different settings and need to compare | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Measure | Loudness, peak, range and spectrum. | Standard measurement, so any implementation producing the same units is fine. The report is the product. | | Match | Moving spectrum towards a reference. | Band count and caps are the tuning. Fine matching is the failure mode, not a refinement. | | Dynamics | Range comparison and gentle compression. | The floor is a quality decision rather than a limit, and it is what stops the tool undoing its own work. | | Normalise | Gain to target with headroom. | Two pass with linear correction. Single pass reintroduces the compression this build avoids. | | Album | Relative loudness across a set. | Any implementation, provided it normalises the set rather than each track. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_LUFS` · required · Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. - `TRUE_PEAK_DB` · required · Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. - `MATCH_STRENGTH` · required · How far towards the reference to move. Full strength copies the reference's arrangement. - `MATCH_BAND_CAP_DB` · required · Maximum correction per band. The cap that prevents an extreme match. - `MIN_LRA` · required · Loudness range floor. Below it the tool refuses to compress further. - `OUTPUT_DIR` · required · Where masters and reports are written. Never the source directory. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Masterchannel product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg for measurement and rendering, plus a small DSP layer for matching, Python 3.12 with numpy and a spectral analysis library, Integrated loudness, true peak, loudness range and a spectral profile, Broad multiband tilt towards the reference, not a hundred narrow bands, Original untouched, masters plus a JSON report alongside. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. - Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. ===== MILESTONES.md ===== # Delivery milestones · Masterchannel Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Measure before you touch anything You can describe a mix and a reference in the same numbers. ### Steps 1. Measure loudness to the standard Parse the structured output rather than the human readable text, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` 2. Compute a coarse spectral profile A handful of bands, averaged over the track. Fine resolution invites over-matching later. Files: `src/spectrum.py` 3. Print mix and reference together The comparison is what the whole tool is for. Make it the first thing it does. Files: `src/report.py` ### Done when - [ ] Analysis prints loudness, true peak, range and a spectral profile - [ ] A generated tone measures at its expected level - [ ] The source checksum is unchanged - [ ] Mix and reference print in comparable form ### Watch out - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. ## M2 · Matching a reference, gently Your mix moves towards the reference's balance without taking on its arrangement. ### Steps 1. Compute the band difference Your profile against the reference's, in a small number of bands. Files: `src/match.py` 2. Apply with a strength control and caps Strength scales the whole correction; caps limit any single band. Both are needed. Files: `src/match.py` 3. Re-measure and record Every applied correction in the report, so the processing is inspectable rather than magic. Files: `src/report.py` ### Done when - [ ] Matching moves the profile towards the reference - [ ] No band exceeds the configured cap - [ ] Zero strength produces an identical measurement - [ ] Applied corrections are recorded per band ### Watch out - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. ## M3 · Dynamics, treated as a budget The loudness range moves towards the reference without being crushed to get there. ### Steps 1. Compare loudness range Yours against the reference. The gap, if any, is what justifies compression. Files: `src/dynamics.py` 2. Compress only to close a real gap With a floor. Below it the tool refuses rather than continuing. Files: `src/dynamics.py` 3. Report range before and after The number that tells you whether you crushed it. Files: `src/report.py` ### Done when - [ ] A wide-range mix is brought closer without passing the floor - [ ] A mix near the reference is left alone - [ ] Range before and after appears in the report - [ ] The floor produces a refusal with a stated reason ### Watch out - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. ## M4 · Loudness and true peak, done last The master lands on its target with headroom that survives encoding. ### Steps 1. Normalise with measured values Two pass with linear correction, so a fixed gain is applied rather than dynamic processing. Files: `src/normalise.py` 2. Leave true peak headroom Below zero, because lossy encoding introduces peaks above the original samples. Files: `src/normalise.py` 3. Verify after encoding Encode to a lossy format and measure again. This is the check that justifies the headroom. Files: `src/verify.py` ### Done when - [ ] Output measures within tolerance of the target - [ ] Loudness range survives normalisation - [ ] A lossy encode does not exceed zero true peak - [ ] The target is recorded in the report ### Watch out - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. ## M5 · Deliverables and the report One run produces every file you need and the evidence that it was done properly. ### Steps 1. Process once, encode many One processed master, several encodings from it. Never reprocess per format. Files: `src/export.py` 2. Write the full report Before and after, reference, settings hash. This is what makes the master defensible. Files: `src/report.py` 3. Assert determinism Same input and settings, same bytes. Pin anything random. Files: `tests/test_determinism.py` ### Done when - [ ] Lossless and lossy outputs derive from the same audio - [ ] The report lists all figures, reference and settings hash - [ ] Reruns are byte-identical - [ ] The source is unmodified ### Watch out - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ## M6 · Batch, references and the honest comparison (production only) An album masters consistently, and you can tell whether the tool helped. ### Steps 1. Normalise the album, not the track One gain across the set, preserving relative levels. Per-track normalisation is the common mistake. Files: `src/album.py` 2. Build a reference library Analysed once, reused. Reference analysis is the slow part and does not change. Files: `src/references.py` 3. Compare honestly and write it down Against something you trust, listening rather than reading figures. Record what you heard. Files: `docs/comparison.md` ### Done when - [ ] Album processing preserves relative loudness - [ ] The reference library stores reusable profiles - [ ] A blind comparison has been done and recorded - [ ] Per-track and album figures both appear ### Watch out - Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. ===== OPERATIONS.md ===== # Operations · Masterchannel ## Backup Source mixes are irreplaceable and belong in a backup. Masters regenerate from sources plus the settings hash, which is why the hash is recorded on every output. ## Restore Restore sources and settings, then re-run and compare against a stored master. Byte equality is what proves the restore rather than the file merely existing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch work, watch the proportion of tracks hitting the range floor or the band cap. A high proportion means the reference is a poor match for the material rather than that the settings are wrong. ## Incident checklist A master delivered with the wrong settings is fully recoverable because sources are untouched and runs are reproducible, which is the reason for both rules and justifies itself the first time a client asks for a small change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Output loudness and true peak are verified by re-measurement - [ ] Loudness range is confirmed to survive normalisation - [ ] A lossy encode is confirmed not to exceed zero true peak - [ ] Album processing is confirmed to preserve relative loudness - [ ] Reruns are confirmed byte-identical ## Launch constraint Do not market omitted Masterchannel capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. TARGET_LUFS=-14 # Required. Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. TRUE_PEAK_DB=-1.0 # Required. How far towards the reference to move. Full strength copies the reference's arrangement. MATCH_STRENGTH=0.6 # Required. Maximum correction per band. The cap that prevents an extreme match. MATCH_BAND_CAP_DB=3.0 # Required. Loudness range floor. Below it the tool refuses to compress further. MIN_LRA=6 # Required. Where masters and reports are written. Never the source directory. OUTPUT_DIR=./masters
# Masterchannel · indie build A command line pipeline that measures your mix and a reference master, matches the spectral balance and the dynamic range towards the reference, normalises to a chosen loudness target with true peak headroom, and exports platform-ready files with a report showing every figure before and after. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg for measurement and rendering, plus a small DSP layer for matching | loudness measurement to the standard is solved and does not need reimplementing | | Runtime | Python 3.12 with numpy and a spectral analysis library | the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for | | Measurement | Integrated loudness, true peak, loudness range and a spectral profile | the report is the product; processing you cannot verify is guesswork | | Matching | Broad multiband tilt towards the reference, not a hundred narrow bands | a fine spectral match copies the reference's room and arrangement and sounds wrong on your track | | Output | Original untouched, masters plus a JSON report alongside | you will rerun with different settings and need to compare | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **ffmpeg with loudness measurement** · free - Why: Measurement to the standard and encoding both run through it, and reimplementing the measurement is not worth it. - Get it: brew install ffmpeg or your package manager. Confirm the loudness filter is present. - Verify: ffmpeg -filters lists loudnorm - [ ] **Python 3.12 with numerical libraries** · free - Why: Spectral analysis and matching are array arithmetic. - Get it: python.org or your package manager, then a virtual environment with numpy and a spectral analysis package. - Verify: python3 --version prints 3.12 or higher and numpy imports - [ ] **Three of your own mixes, and one you consider finished** · free - Why: Matching and dynamics work are only judgeable against real material, and you need something you already trust as a comparison in the final phase. - Get it: Your own work. Include one mix you know is too dynamic and one you suspect is too compressed, since those are the cases the tool must handle differently. - Verify: Three mixes plus a trusted master are available - [ ] **Reference masters you are entitled to analyse** · free with music you own - Why: Analysing a commercial track for your own reference is ordinary practice, and redistributing it is not. Keep references local and never ship them. - Get it: Tracks you own in a lossless format where possible, since a heavily compressed source distorts the spectral profile you are matching to. - Verify: At least two references analyse into profiles - [ ] **Monitoring you know well** · free if you already have monitoring - Why: Every decision here is finally judged by ear, and unfamiliar speakers will lead you to correct for the room rather than the track. - Get it: Whatever you have listened to enough to know. Familiarity matters more than price. - Verify: You can identify a familiar reference track's character on them ## Quick start ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. - Reference-free mastering: the service guesses a target for you, your script needs you to pick one - Genre-aware presets and the taste baked into a trained model - Stem mastering, vocal-forward variants and other per-track intelligence - A clean web UI with instant previews and revision history - Anything resembling a second opinion when your mix is the actual problem If one of those is essential to you, that is the reason to keep paying for Masterchannel, and the README should say so rather than pretend.
# Build brief · Masterchannel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a mastering pipeline that matches a reference and hits a streaming loudness target to replace Masterchannel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Understand the loudness situation before you build, because it changes what mastering is for. Streaming services normalise playback, so a track mastered several decibels louder than the platform target is turned down on playback and arrives quieter in feel and flatter in dynamics than a quieter master would have. Mastering loud no longer wins anything and costs you the dynamics you crushed to get there. So the target is a stated loudness figure with true peak headroom, and the interesting work is matching a reference you admire in spectrum and dynamics rather than pushing a limiter. Build the measurement first and the processing second, so every claim the tool makes is one you can check. ### Stack (fixed, do not substitute) - Processing: ffmpeg for measurement and rendering, plus a small DSP layer for matching · loudness measurement to the standard is solved and does not need reimplementing - Runtime: Python 3.12 with numpy and a spectral analysis library · the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for - Measurement: Integrated loudness, true peak, loudness range and a spectral profile · the report is the product; processing you cannot verify is guesswork - Matching: Broad multiband tilt towards the reference, not a hundred narrow bands · a fine spectral match copies the reference's room and arrangement and sounds wrong on your track - Output: Original untouched, masters plus a JSON report alongside · you will rerun with different settings and need to compare ### Data model (create this before Phase 1) - tracks(id, source_path, sample_rate, channels, duration_ms, content_hash) - references(id, path, name, added_at, profile_json) · analysed once and reused across tracks - measurements(id, subject_kind, subject_id, stage, integrated_lufs, true_peak_dbtp, lra, spectral_bands_json, crest_db) · stage is before or after - jobs(id, track_id, reference_id, target_lufs, true_peak_db, match_strength, output_path, settings_hash, at) - The settings hash on every output is what makes a master reproducible, which matters when a client asks for a small change six months later. ### Phase 1 · Measure before you touch anything Build: Measure integrated loudness, true peak, loudness range and a coarse spectral profile for a file, without modifying it. Measure a reference master the same way. Print both side by side. Trap: Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. Done when: Analysis prints integrated loudness in LUFS, true peak in dBTP, loudness range in LU and a spectral profile in a small number of bands. A generated reference tone measures at its expected level. The source file checksum is unchanged after analysis. A reference and a mix print in a comparable form. Do not build yet: No matching, normalisation or export. ### Phase 2 · Matching a reference, gently Build: Compute the difference between your spectral profile and the reference's, apply it as a broad multiband correction with a strength control, and re-measure. Cap the correction per band so no band can move by an implausible amount. Trap: Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. Done when: Matching moves the spectral profile measurably towards the reference without any band exceeding the cap. Setting strength to zero produces a file measurably identical to the input. A reference in a very different genre produces a capped correction rather than an extreme one. The applied correction per band is recorded in the report. Do not build yet: No loudness normalisation or export yet. ### Phase 3 · Dynamics, treated as a budget Build: Compare loudness range against the reference and apply gentle compression only where yours is substantially wider. Set a floor below which the tool refuses to reduce range further, and report the range before and after. Trap: Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. Done when: A mix with a much wider range than the reference is brought closer without falling below the configured floor. A mix already close to the reference is left alone, verified by an unchanged range figure. Loudness range before and after appears in the report. The tool refuses to reduce range below the floor and says why. Do not build yet: No normalisation or export yet. ### Phase 4 · Loudness and true peak, done last Build: Normalise to the configured loudness target using a measured two pass approach with linear gain, and apply a true peak ceiling below zero. Re-measure the output and assert both figures. State the target and why it was chosen. Trap: Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. Done when: The output measures within a small tolerance of the target loudness and below the true peak ceiling, verified by re-running the analysis. Loudness range after normalisation is close to what it was before, showing gain rather than compression was used. Encoding the output to a lossy format does not exceed zero true peak. The target is recorded in the report. Do not build yet: No exports or batch yet. ### Phase 5 · Deliverables and the report Build: Export the master in a lossless format at the source rate plus any lossy versions you need, all from the same processed audio. Write a report with every figure before and after, the reference used and the settings hash. Trap: Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. Done when: Lossless and lossy outputs derive from the same processed audio, verified by comparing their measurements. The report lists every figure before and after with the reference and the settings hash. Re-running with the same settings produces byte-identical output. The source file is unmodified. Do not build yet: Batch and honesty checks come last. ### Phase 6 · Batch, references and the honest comparison Build: Process a directory as an album, keeping loudness consistent across tracks rather than normalising each independently. Build a reference library. Then compare your masters against a mastered version you trust and record what you hear. Trap: Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. Done when: An album processed together preserves relative loudness between tracks, verified by comparing per-track figures before and after. A reference library stores analysed profiles reusable across projects. A blind comparison against a trusted master has been done and the notes recorded. Per-track and album figures both appear in the report. Do not build yet: Nothing after this. ### Out of scope (and why) - The judgement of a mastering engineer, which is what you are actually paying for and which no reference matching reproduces. - Fixing a mix. Mastering moves a finished mix a small distance; a problem in the mix stays a problem and should go back to the mix. - Stem mastering and the corrective work that needs access to individual parts. - Distribution and the platform-specific delivery requirements that come with it. ### README must contain - That streaming platforms normalise playback, so mastering louder than the target gains nothing - The loudness target and true peak ceiling used, and why headroom is left below zero - That spectral matching uses broad bands with caps, and why fine matching sounds wrong - That loudness is moved with gain rather than compression, with the range figures to prove it - That album processing preserves relative loudness between tracks
# Agent instructions · Masterchannel indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: ffmpeg for measurement and rendering, plus a small DSP layer for matching, Python 3.12 with numpy and a spectral analysis library, Integrated loudness, true peak, loudness range and a spectral profile, Broad multiband tilt towards the reference, not a hundred narrow bands, Original untouched, masters plus a JSON report alongside. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that.
# Build plan · Masterchannel A command line pipeline that measures your mix and a reference master, matches the spectral balance and the dynamic range towards the reference, normalises to a chosen loudness target with true peak headroom, and exports platform-ready files with a report showing every figure before and after. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Measure before you touch anything You can describe a mix and a reference in the same numbers. ### Steps 1. Measure loudness to the standard Parse the structured output rather than the human readable text, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` 2. Compute a coarse spectral profile A handful of bands, averaged over the track. Fine resolution invites over-matching later. Files: `src/spectrum.py` 3. Print mix and reference together The comparison is what the whole tool is for. Make it the first thing it does. Files: `src/report.py` ### Done when - [ ] Analysis prints loudness, true peak, range and a spectral profile - [ ] A generated tone measures at its expected level - [ ] The source checksum is unchanged - [ ] Mix and reference print in comparable form ### Watch out - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. ## Phase 2 · Matching a reference, gently Your mix moves towards the reference's balance without taking on its arrangement. ### Steps 1. Compute the band difference Your profile against the reference's, in a small number of bands. Files: `src/match.py` 2. Apply with a strength control and caps Strength scales the whole correction; caps limit any single band. Both are needed. Files: `src/match.py` 3. Re-measure and record Every applied correction in the report, so the processing is inspectable rather than magic. Files: `src/report.py` ### Done when - [ ] Matching moves the profile towards the reference - [ ] No band exceeds the configured cap - [ ] Zero strength produces an identical measurement - [ ] Applied corrections are recorded per band ### Watch out - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. ## Phase 3 · Dynamics, treated as a budget The loudness range moves towards the reference without being crushed to get there. ### Steps 1. Compare loudness range Yours against the reference. The gap, if any, is what justifies compression. Files: `src/dynamics.py` 2. Compress only to close a real gap With a floor. Below it the tool refuses rather than continuing. Files: `src/dynamics.py` 3. Report range before and after The number that tells you whether you crushed it. Files: `src/report.py` ### Done when - [ ] A wide-range mix is brought closer without passing the floor - [ ] A mix near the reference is left alone - [ ] Range before and after appears in the report - [ ] The floor produces a refusal with a stated reason ### Watch out - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. ## Phase 4 · Loudness and true peak, done last The master lands on its target with headroom that survives encoding. ### Steps 1. Normalise with measured values Two pass with linear correction, so a fixed gain is applied rather than dynamic processing. Files: `src/normalise.py` 2. Leave true peak headroom Below zero, because lossy encoding introduces peaks above the original samples. Files: `src/normalise.py` 3. Verify after encoding Encode to a lossy format and measure again. This is the check that justifies the headroom. Files: `src/verify.py` ### Done when - [ ] Output measures within tolerance of the target - [ ] Loudness range survives normalisation - [ ] A lossy encode does not exceed zero true peak - [ ] The target is recorded in the report ### Watch out - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. ## Phase 5 · Deliverables and the report One run produces every file you need and the evidence that it was done properly. ### Steps 1. Process once, encode many One processed master, several encodings from it. Never reprocess per format. Files: `src/export.py` 2. Write the full report Before and after, reference, settings hash. This is what makes the master defensible. Files: `src/report.py` 3. Assert determinism Same input and settings, same bytes. Pin anything random. Files: `tests/test_determinism.py` ### Done when - [ ] Lossless and lossy outputs derive from the same audio - [ ] The report lists all figures, reference and settings hash - [ ] Reruns are byte-identical - [ ] The source is unmodified ### Watch out - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ## Not in this build - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. ## After v1, if you want it - Mid and side processing, which is where a small amount of width adjustment lives - A null test against the source, which shows exactly what the processing changed - Per-platform variants once you know the targets differ enough to matter
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. TARGET_LUFS=-14 # Required. Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. TRUE_PEAK_DB=-1.0 # Required. How far towards the reference to move. Full strength copies the reference's arrangement. MATCH_STRENGTH=0.6 # Required. Maximum correction per band. The cap that prevents an extreme match. MATCH_BAND_CAP_DB=3.0 # Required. Loudness range floor. Below it the tool refuses to compress further. MIN_LRA=6 # Required. Where masters and reports are written. Never the source directory. OUTPUT_DIR=./masters
# Masterchannel · product brief ## Problem Mastering is signal processing, and the open source world already solved a big chunk of it: reference matching, loudness normalization and true peak limiting are all off the shelf. An agent can wire matchering, pyloudnorm and ffmpeg into a local CLI that takes your mix plus a commercial reference and spits out a competitive master in an afternoon. What it cannot do is decide, with no reference, what your track should sound like: that judgment is the part these services trained on thousands of masters to fake. So the DIY build is genuinely usable if you already know which records you want to sound like, and mediocre if you don't. Also expect to babysit sample rates, mono compatibility and the occasional inter-sample peak. ## Product outcome A mastering pipeline whose every claim is a measured figure, which moves loudness with gain rather than compression, matches a reference as a direction rather than a copy, and produces reproducible masters with a report you could hand to anyone. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.11 and ffmpeg installed locally - At least one commercial reference track in a lossless format - Headphones or monitors you actually trust - Basic willingness to A/B and re-run with different references ## Explicit non-goals for v1 - The judgement of a mastering engineer. - Fixing problems that belong in the mix. - Stem mastering and corrective work on individual parts. - Distribution and platform delivery requirements. - Reference-free mastering: the service guesses a target for you, your script needs you to pick one - Genre-aware presets and the taste baked into a trained model - Stem mastering, vocal-forward variants and other per-track intelligence - A clean web UI with instant previews and revision history - Anything resembling a second opinion when your mix is the actual problem ## Success criteria - Output loudness and true peak are verified by re-measurement - Loudness range is confirmed to survive normalisation - A lossy encode is confirmed not to exceed zero true peak - Album processing is confirmed to preserve relative loudness - Reruns are confirmed byte-identical
# Build brief · Masterchannel The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a mastering pipeline that matches a reference and hits a streaming loudness target to replace Masterchannel. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Understand the loudness situation before you build, because it changes what mastering is for. Streaming services normalise playback, so a track mastered several decibels louder than the platform target is turned down on playback and arrives quieter in feel and flatter in dynamics than a quieter master would have. Mastering loud no longer wins anything and costs you the dynamics you crushed to get there. So the target is a stated loudness figure with true peak headroom, and the interesting work is matching a reference you admire in spectrum and dynamics rather than pushing a limiter. Build the measurement first and the processing second, so every claim the tool makes is one you can check. ### Stack (fixed, do not substitute) - Processing: ffmpeg for measurement and rendering, plus a small DSP layer for matching · loudness measurement to the standard is solved and does not need reimplementing - Runtime: Python 3.12 with numpy and a spectral analysis library · the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for - Measurement: Integrated loudness, true peak, loudness range and a spectral profile · the report is the product; processing you cannot verify is guesswork - Matching: Broad multiband tilt towards the reference, not a hundred narrow bands · a fine spectral match copies the reference's room and arrangement and sounds wrong on your track - Output: Original untouched, masters plus a JSON report alongside · you will rerun with different settings and need to compare ### Data model (create this before Phase 1) - tracks(id, source_path, sample_rate, channels, duration_ms, content_hash) - references(id, path, name, added_at, profile_json) · analysed once and reused across tracks - measurements(id, subject_kind, subject_id, stage, integrated_lufs, true_peak_dbtp, lra, spectral_bands_json, crest_db) · stage is before or after - jobs(id, track_id, reference_id, target_lufs, true_peak_db, match_strength, output_path, settings_hash, at) - The settings hash on every output is what makes a master reproducible, which matters when a client asks for a small change six months later. ### Phase 1 · Measure before you touch anything Build: Measure integrated loudness, true peak, loudness range and a coarse spectral profile for a file, without modifying it. Measure a reference master the same way. Print both side by side. Trap: Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. Done when: Analysis prints integrated loudness in LUFS, true peak in dBTP, loudness range in LU and a spectral profile in a small number of bands. A generated reference tone measures at its expected level. The source file checksum is unchanged after analysis. A reference and a mix print in a comparable form. Do not build yet: No matching, normalisation or export. ### Phase 2 · Matching a reference, gently Build: Compute the difference between your spectral profile and the reference's, apply it as a broad multiband correction with a strength control, and re-measure. Cap the correction per band so no band can move by an implausible amount. Trap: Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. Done when: Matching moves the spectral profile measurably towards the reference without any band exceeding the cap. Setting strength to zero produces a file measurably identical to the input. A reference in a very different genre produces a capped correction rather than an extreme one. The applied correction per band is recorded in the report. Do not build yet: No loudness normalisation or export yet. ### Phase 3 · Dynamics, treated as a budget Build: Compare loudness range against the reference and apply gentle compression only where yours is substantially wider. Set a floor below which the tool refuses to reduce range further, and report the range before and after. Trap: Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. Done when: A mix with a much wider range than the reference is brought closer without falling below the configured floor. A mix already close to the reference is left alone, verified by an unchanged range figure. Loudness range before and after appears in the report. The tool refuses to reduce range below the floor and says why. Do not build yet: No normalisation or export yet. ### Phase 4 · Loudness and true peak, done last Build: Normalise to the configured loudness target using a measured two pass approach with linear gain, and apply a true peak ceiling below zero. Re-measure the output and assert both figures. State the target and why it was chosen. Trap: Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. Done when: The output measures within a small tolerance of the target loudness and below the true peak ceiling, verified by re-running the analysis. Loudness range after normalisation is close to what it was before, showing gain rather than compression was used. Encoding the output to a lossy format does not exceed zero true peak. The target is recorded in the report. Do not build yet: No exports or batch yet. ### Phase 5 · Deliverables and the report Build: Export the master in a lossless format at the source rate plus any lossy versions you need, all from the same processed audio. Write a report with every figure before and after, the reference used and the settings hash. Trap: Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. Done when: Lossless and lossy outputs derive from the same processed audio, verified by comparing their measurements. The report lists every figure before and after with the reference and the settings hash. Re-running with the same settings produces byte-identical output. The source file is unmodified. Do not build yet: Batch and honesty checks come last. ### Phase 6 · Batch, references and the honest comparison Build: Process a directory as an album, keeping loudness consistent across tracks rather than normalising each independently. Build a reference library. Then compare your masters against a mastered version you trust and record what you hear. Trap: Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended. Done when: An album processed together preserves relative loudness between tracks, verified by comparing per-track figures before and after. A reference library stores analysed profiles reusable across projects. A blind comparison against a trusted master has been done and the notes recorded. Per-track and album figures both appear in the report. Do not build yet: Nothing after this. ### Out of scope (and why) - The judgement of a mastering engineer, which is what you are actually paying for and which no reference matching reproduces. - Fixing a mix. Mastering moves a finished mix a small distance; a problem in the mix stays a problem and should go back to the mix. - Stem mastering and the corrective work that needs access to individual parts. - Distribution and the platform-specific delivery requirements that come with it. ### README must contain - That streaming platforms normalise playback, so mastering louder than the target gains nothing - The loudness target and true peak ceiling used, and why headroom is left below zero - That spectral matching uses broad bands with caps, and why fine matching sounds wrong - That loudness is moved with gain rather than compression, with the range figures to prove it - That album processing preserves relative loudness between tracks
# Architecture · Masterchannel ## Stack | Part | Choice | Why | | --- | --- | --- | | Processing | ffmpeg for measurement and rendering, plus a small DSP layer for matching | loudness measurement to the standard is solved and does not need reimplementing | | Runtime | Python 3.12 with numpy and a spectral analysis library | the matching is arithmetic over spectra and dynamics, which is what numerical libraries are for | | Measurement | Integrated loudness, true peak, loudness range and a spectral profile | the report is the product; processing you cannot verify is guesswork | | Matching | Broad multiband tilt towards the reference, not a hundred narrow bands | a fine spectral match copies the reference's room and arrangement and sounds wrong on your track | | Output | Original untouched, masters plus a JSON report alongside | you will rerun with different settings and need to compare | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Measure | Loudness, peak, range and spectrum. | Standard measurement, so any implementation producing the same units is fine. The report is the product. | | Match | Moving spectrum towards a reference. | Band count and caps are the tuning. Fine matching is the failure mode, not a refinement. | | Dynamics | Range comparison and gentle compression. | The floor is a quality decision rather than a limit, and it is what stops the tool undoing its own work. | | Normalise | Gain to target with headroom. | Two pass with linear correction. Single pass reintroduces the compression this build avoids. | | Album | Relative loudness across a set. | Any implementation, provided it normalises the set rather than each track. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `TARGET_LUFS` · required · Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. - `TRUE_PEAK_DB` · required · Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. - `MATCH_STRENGTH` · required · How far towards the reference to move. Full strength copies the reference's arrangement. - `MATCH_BAND_CAP_DB` · required · Maximum correction per band. The cap that prevents an extreme match. - `MIN_LRA` · required · Loudness range floor. Below it the tool refuses to compress further. - `OUTPUT_DIR` · required · Where masters and reports are written. Never the source directory. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Masterchannel product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: ffmpeg for measurement and rendering, plus a small DSP layer for matching, Python 3.12 with numpy and a spectral analysis library, Integrated loudness, true peak, loudness range and a spectral profile, Broad multiband tilt towards the reference, not a hundred narrow bands, Original untouched, masters plus a JSON report alongside. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. - Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended.
# Delivery milestones · Masterchannel Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Measure before you touch anything You can describe a mix and a reference in the same numbers. ### Steps 1. Measure loudness to the standard Parse the structured output rather than the human readable text, which changes between versions. Files: `src/measure.py` ```sh ffmpeg -i mix.wav -af loudnorm=print_format=json -f null - ``` 2. Compute a coarse spectral profile A handful of bands, averaged over the track. Fine resolution invites over-matching later. Files: `src/spectrum.py` 3. Print mix and reference together The comparison is what the whole tool is for. Make it the first thing it does. Files: `src/report.py` ### Done when - [ ] Analysis prints loudness, true peak, range and a spectral profile - [ ] A generated tone measures at its expected level - [ ] The source checksum is unchanged - [ ] Mix and reference print in comparable form ### Watch out - Peak and true peak are different: a file peaking at zero on a sample meter can clip audibly once encoded to a lossy format, because reconstruction between samples goes higher. Read the true peak figure, and leave headroom below zero for exactly that reason. ## M2 · Matching a reference, gently Your mix moves towards the reference's balance without taking on its arrangement. ### Steps 1. Compute the band difference Your profile against the reference's, in a small number of bands. Files: `src/match.py` 2. Apply with a strength control and caps Strength scales the whole correction; caps limit any single band. Both are needed. Files: `src/match.py` 3. Re-measure and record Every applied correction in the report, so the processing is inspectable rather than magic. Files: `src/report.py` ### Done when - [ ] Matching moves the profile towards the reference - [ ] No band exceeds the configured cap - [ ] Zero strength produces an identical measurement - [ ] Applied corrections are recorded per band ### Watch out - Matching a fine spectral curve copies the reference's specific arrangement: its vocal, its bass note choices, its room. The result sounds wrong because your track has different content. Use a small number of broad bands, cap the movement per band, and treat the reference as a direction rather than a destination. ## M3 · Dynamics, treated as a budget The loudness range moves towards the reference without being crushed to get there. ### Steps 1. Compare loudness range Yours against the reference. The gap, if any, is what justifies compression. Files: `src/dynamics.py` 2. Compress only to close a real gap With a floor. Below it the tool refuses rather than continuing. Files: `src/dynamics.py` 3. Report range before and after The number that tells you whether you crushed it. Files: `src/report.py` ### Done when - [ ] A wide-range mix is brought closer without passing the floor - [ ] A mix near the reference is left alone - [ ] Range before and after appears in the report - [ ] The floor produces a refusal with a stated reason ### Watch out - Reaching a loudness target by compressing rather than by gain is what produced a decade of tiring masters, and on a normalising platform it buys nothing at all: the platform turns you down anyway and you are left with the flattened dynamics. Move loudness with gain, and use compression only to close a genuine gap in range. ## M4 · Loudness and true peak, done last The master lands on its target with headroom that survives encoding. ### Steps 1. Normalise with measured values Two pass with linear correction, so a fixed gain is applied rather than dynamic processing. Files: `src/normalise.py` 2. Leave true peak headroom Below zero, because lossy encoding introduces peaks above the original samples. Files: `src/normalise.py` 3. Verify after encoding Encode to a lossy format and measure again. This is the check that justifies the headroom. Files: `src/verify.py` ### Done when - [ ] Output measures within tolerance of the target - [ ] Loudness range survives normalisation - [ ] A lossy encode does not exceed zero true peak - [ ] The target is recorded in the report ### Watch out - Single pass normalisation compresses dynamics to reach the target rather than applying a fixed gain, which undoes the care taken in the previous phase. Measure first, then apply the measured values with linear correction, and confirm the loudness range survived. ## M5 · Deliverables and the report One run produces every file you need and the evidence that it was done properly. ### Steps 1. Process once, encode many One processed master, several encodings from it. Never reprocess per format. Files: `src/export.py` 2. Write the full report Before and after, reference, settings hash. This is what makes the master defensible. Files: `src/report.py` 3. Assert determinism Same input and settings, same bytes. Pin anything random. Files: `tests/test_determinism.py` ### Done when - [ ] Lossless and lossy outputs derive from the same audio - [ ] The report lists all figures, reference and settings hash - [ ] Reruns are byte-identical - [ ] The source is unmodified ### Watch out - Producing the lossy version by re-processing rather than by encoding the finished master means the two differ in more than format, and the difference will be noticed by whoever compares them. Process once, encode from that. ## M6 · Batch, references and the honest comparison (production only) An album masters consistently, and you can tell whether the tool helped. ### Steps 1. Normalise the album, not the track One gain across the set, preserving relative levels. Per-track normalisation is the common mistake. Files: `src/album.py` 2. Build a reference library Analysed once, reused. Reference analysis is the slow part and does not change. Files: `src/references.py` 3. Compare honestly and write it down Against something you trust, listening rather than reading figures. Record what you heard. Files: `docs/comparison.md` ### Done when - [ ] Album processing preserves relative loudness - [ ] The reference library stores reusable profiles - [ ] A blind comparison has been done and recorded - [ ] Per-track and album figures both appear ### Watch out - Normalising each track of an album independently destroys the intended relative loudness between them, so the quiet interlude arrives as loud as the single. Measure the album as a whole and apply one gain across it, preserving the relationships the mix engineer intended.
# Operations · Masterchannel ## Backup Source mixes are irreplaceable and belong in a backup. Masters regenerate from sources plus the settings hash, which is why the hash is recorded on every output. ## Restore Restore sources and settings, then re-run and compare against a stored master. Byte equality is what proves the restore rather than the file merely existing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For batch work, watch the proportion of tracks hitting the range floor or the band cap. A high proportion means the reference is a poor match for the material rather than that the settings are wrong. ## Incident checklist A master delivered with the wrong settings is fully recoverable because sources are untouched and runs are reproducible, which is the reason for both rules and justifies itself the first time a client asks for a small change. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Output loudness and true peak are verified by re-measurement - [ ] Loudness range is confirmed to survive normalisation - [ ] A lossy encode is confirmed not to exceed zero true peak - [ ] Album processing is confirmed to preserve relative loudness - [ ] Reruns are confirmed byte-identical ## Launch constraint Do not market omitted Masterchannel capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Integrated loudness target. Around -14 matches common streaming normalisation; state your choice rather than inheriting a default. TARGET_LUFS=-14 # Required. Ceiling in dBTP. Headroom below zero so lossy encoding does not clip. TRUE_PEAK_DB=-1.0 # Required. How far towards the reference to move. Full strength copies the reference's arrangement. MATCH_STRENGTH=0.6 # Required. Maximum correction per band. The cap that prevents an extreme match. MATCH_BAND_CAP_DB=3.0 # Required. Loudness range floor. Below it the tool refuses to compress further. MIN_LRA=6 # Required. Where masters and reports are written. Never the source directory. OUTPUT_DIR=./masters
$ choose a build depth, inspect the files, then open the complete pack in your agent
Most people paying for AI mastering are not chasing the last two percent of fidelity, they are avoiding a decision. They have a mix, a release date, and no interest in learning about multiband compression or inter-sample true peaks. A web upload that returns something loud and balanced in ninety seconds is worth real money against that, and the reference-free convenience is exactly the part a local script does worst. Engineers and people with a strong reference library will get most of the value from the DIY build; everyone else will keep paying to skip the taste problem.
xReference-free mastering: the service guesses a target for you, your script needs you to pick one
xGenre-aware presets and the taste baked into a trained model
xStem mastering, vocal-forward variants and other per-track intelligence
xA clean web UI with instant previews and revision history
xAnything resembling a second opinion when your mix is the actual problem
Nothing worth pointing at. That's why the prompt exists.
Vibecode Masterchannel
Kinda. The core of Masterchannel is buildable in a weekend with the prompt on this page, but there are real gaps: Reference-free mastering: the service guesses a target for you, your script needs you to pick one, Genre-aware presets and the taste baked into a trained model. Read the honest list above before committing.
How much does Masterchannel cost?
Masterchannel costs about $29/month (Artist, checked 2026-08-18), which is $348 per year.
What do I lose by replacing Masterchannel?
Honestly: Reference-free mastering: the service guesses a target for you, your script needs you to pick one; Genre-aware presets and the taste baked into a trained model; Stem mastering, vocal-forward variants and other per-track intelligence; A clean web UI with instant previews and revision history; Anything resembling a second opinion when your mix is the actual problem. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Masterchannel?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.