Commit b4b70db
chore: release 0.91.0 (#358)
* chore: bump develop to 0.90.0-dev
* chore: add .worktrees/ to gitignore
* feat: add atomic_write_text() utility for crash-safe file writes
Refs #140
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use atomic writes for all handler file operations
Replaces direct path.write_text() with atomic_write_text() at 5 sites
to prevent file corruption from interrupted writes.
Updates two tests that mocked Path.write_text to instead mock
atomic_write_text at the correct module scope.
Refs #140
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove TOCTOU race in state lock acquisition
Replace mtime-based stale lock detection with exponential backoff.
fcntl.flock locks auto-release on process death, so stale detection
was unnecessary and introduced a race condition.
Refs #140
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace silent except-pass with specific types and logging
All 5 bare 'except Exception: pass' handlers now catch specific
exception types and log warnings so failures are diagnosable.
Refs #140
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: backup corrupted state.json and degrade to empty state
read_state() now creates a timestamped .corrupt backup when JSON
parsing fails, logs at ERROR level, and returns an empty dict instead
of None. Prevents silent cache poisoning.
Refs #140
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use staging directory for mastering to prevent orphaned files
master_album now writes to .mastering_staging/ and moves files to
mastered/ only after all tracks succeed. Staging is cleaned up on
failure so mastered/ never contains partial results.
Refs #140
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: detect stale plugin cache with missing/ghost skill registration (#234)
Add health_check MCP tool that combines venv and skill registration
checks into a single session-start call. Compares on-disk skills
against the Claude Code plugin cache to detect missing skills
(lyric-refiner, voice-checker, genre-creator) and ghost registrations
(ship). Integrates into session-start, diagnose, and test suite.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add missing mypy type arguments for dict returns
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add health-check skill for on-demand plugin health checks
Lets users run /bitwize-music:health-check anytime to check venv
packages and skill registration without a full session-start.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent health-check skill from using Bash instead of MCP tool
Haiku model was calling python3 -m mcp.cli via Bash instead of using
the MCP tool interface directly. Added explicit instruction to use
the tool interface only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add ToolSearch to health-check allowed-tools, spell out exact MCP call
Haiku needs ToolSearch to load deferred MCP tool schemas before
calling them. Also made the workflow steps explicit with the full
tool name to prevent the model from improvising with Bash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add Discord server link to about skill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add true peak limiting and TPDF dithering to mastering pipeline
Replace sample-peak limiter with ITU-R BS.1770-4 true peak detection
using 4x oversampling via scipy.signal.resample_poly. Add TPDF dither
as the final step before 16-bit quantization to eliminate correlated
truncation distortion. Add truepeak QC check to qc_tracks.
Closes #245
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: clarify stem import wording in README example
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add input length bounds to text analysis MCP tools
Add 50,000-character max-length validation to all text-input handlers:
check_homographs, check_explicit_content, scan_artist_names,
count_syllables, analyze_readability, analyze_rhyme_scheme, and
extract_distinctive_phrases. Returns a clear JSON error when exceeded.
Closes #242
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add negative prompting guidance to suno-engineer skill
Add per-genre exclude styles tables to genre-practices.md for Hip-Hop,
Alternative Rock, Electronic, Folk/Indie, and Country with concrete
instrument/element exclusions per arrangement type. Add output-focused
quality checklist item to verify exclusions were effective.
Closes #232
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: bump pypdf 6.9.2 → 6.10.0 (GHSA-3crg-w4f6-42mx)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add code of conduct
Be excellent to each other.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: wire saturation, lowpass, and stereo width into mix pipeline (#246) (#256)
Implement Phase 1 of audio enhancement processing: wire existing dead
preset fields (saturation_drive, lowpass_cutoff, stereo_width) that were
tuned across 120+ genre entries but never had implementation code.
- Add apply_saturation() — tanh soft saturation with level normalization
- Add apply_lowpass() — 2nd-order Butterworth lowpass (mirrors apply_highpass)
- Add _apply_character_effects() helper for consistent chain ordering
- Update enhance_stereo() to support negative amounts (narrowing)
- Wire effects into all 12 stem processors + full_mix per YAML chain spec
- All defaults are off (drive=0, cutoff=20000, width=1.0) — no behavior change
unless a genre preset overrides
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract hardcoded mastering params into configurable presets (#255)
* refactor: convert genre presets from tuples to dicts
load_genre_presets() now returns dict[str, dict[str, float]] instead of
dict[str, tuple[float, ...]]. Adds _PRESET_DEFAULTS with all 12 preset
keys including 7 newly exposed parameters. Updates all test assertions
from tuple destructuring to dict key access.
Refs: #251
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add compression, EQ, and dither defaults to genre-presets.yaml
Adds compress_ratio, compress_threshold, compress_attack, compress_release,
eq_highmid_freq, eq_highmid_q, eq_highs_freq, eq_highs_q, and dither_bits
to the defaults block. Individual genres inherit these automatically.
Closes the compress_ratio gap (was handled in code but missing from YAML).
Refs: #251
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: thread preset dict through master_track and _process_one_track
master_track() and _process_one_track() now accept an optional preset
dict. When provided, compression params (threshold, attack, release),
EQ params (freq, Q for both bands), and dither_bits are read from
the preset. Legacy positional args still work for backward compatibility.
Refs: #251
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: wire preset dict through main() with CLI overrides
main() now builds a full preset dict from defaults -> genre preset -> CLI
overrides, then passes it through the call chain. Adds 8 new CLI args:
--compress-threshold, --compress-attack, --compress-release,
--eq-highmid-freq, --eq-highmid-q, --eq-highs-freq, --eq-highs-q,
--dither-bits. Session banner shows all active parameters.
Refs: #251
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: update override example and skill docs with new preset fields
Documents all 12 configurable mastering parameters in the override
example file and updates mastering-engineer skill with new override
format examples. Removes stale boost_sub references.
Refs: #251
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update MCP handlers to use dict-based genre presets
The mastering preset refactor changed load_genre_presets() from returning
tuples to dicts, but the MCP audio handlers still used tuple unpacking.
Update both master_audio and master_album handlers to use dict key access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update test mocks to use dict-based genre presets
Test mocks were still returning tuple presets after the tuple→dict
refactor, causing 4 test failures in CI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add low-end EQ, stereo width, and 24-bit output to mastering (#252) (#257)
Add three new mastering capabilities:
- Low shelf EQ (eq_low_freq/eq_low_gain/eq_low_q) for bass shaping,
plus sub-bass HPF (eq_sub_cut_freq) for rumble removal
- Stereo width control (stereo_width) with optional bass mono fold
(stereo_bass_mono_freq) for club/PA coherence
- Selectable output bit depth (output_bits: 16 or 24) for platforms
that accept 24-bit (Tidal HiFi, Apple Lossless, Bandcamp)
All defaults are passthrough — no behavior change without explicit
preset or CLI override.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add look-ahead limiting, parallel compression, oversampling, and LRA (#253) (#258)
Upgrade the mastering dynamics chain with five new capabilities:
- Look-ahead limiter with smooth release envelope — pre-applies gain
reduction before transients hit, replacing reactive limiting
- Parallel compression (wet/dry blend via compress_mix) — preserves
dynamics while adding density
- Oversampled nonlinear processing (processing_oversample: 1/2/4) —
reduces aliasing from compression and limiting
- LRA measurement and targeting (target_lra) — measures loudness range
per EBU R128 short-term method
- Compressor makeup gain (compress_makeup) — compensates for
compression gain reduction
All defaults preserve existing behavior exactly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add DC offset removal, de-essing, sample rate conversion, and track gaps (#254) (#259)
Add four new mastering pipeline stages:
- DC offset removal (dc_filter_freq) — subsonic HPF as first processing
stage, removes DC bias that wastes headroom
- De-esser (deess_enabled/freq/bandwidth/threshold/ratio) — frequency-
selective compression for sibilance reduction, applied after EQ
- Sample rate conversion (output_sample_rate) — rational resampling via
polyphase FIR, applied after processing before dither
- Inter-track gap insertion (track_gap) — prepends silence for album
sequencing, applied after dither before write
All defaults preserve existing behavior. DC filter defaults to 5Hz
(always-on subsonic cleanup); all others default to off/bypass.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add sub-bass exciter and transient shaping to mix pipeline (#246 Phase 2) (#260)
Add two new per-stem effects:
- Sub-bass harmonic exciter (bass stem) — isolates sub-bass via lowpass,
generates upper harmonics via tanh + squaring waveshaping, highpasses
harmonics to keep only generated content, blends back. Makes bass
audible on small speakers.
- Transient shaper (drums/percussion stems) — dual-envelope detection
using the existing numba-accelerated envelope follower. Compares fast
envelope (transients) against slow envelope (sustain) to independently
boost/cut attack and sustain.
New preset fields: sub_bass_exciter, sub_bass_freq (bass),
transient_attack_db, transient_sustain_db (drums/percussion).
All defaults are off — no behavior change without explicit override.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Log failed optional module imports in processing helpers
* feat: add multiband compression and mid/side EQ to mastering (#246 Phase 3) (#261)
Add two mastering enhancements:
- 3-band multiband compression with Linkwitz-Riley crossovers — splits
signal into low/mid/high bands, compresses each independently with
per-band ratio and threshold, recombines. Replaces single-band
compression when enabled. Prevents pumping artifacts from bass
triggering full-band gain reduction.
- Mid/side EQ for frequency-selective stereo management — applies low
shelf and high shelf EQ to the side channel only. Negative low gain
narrows bass (mono bass coherence), positive high gain widens treble.
New preset fields: multiband_enabled, multiband_{low,high}_crossover,
multiband_{low,mid,high}_{ratio,threshold}, midside_{low,high}_{gain,freq}.
All defaults are off — no behavior change without explicit override.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add album loudness consistency and extended loudness metering (#254) (#263)
Complete remaining #254 items:
- Album-level loudness consistency (--album-consistency): two-pass
mastering that analyzes source LUFS across all tracks, then adjusts
per-track targets to keep the spread within tolerance. Quieter sources
get slightly higher targets, louder sources get slightly lower.
- Short-term and momentary loudness in analysis: sliding-window LUFS
measurements (3s short-term, 400ms momentary) per EBU R128. Reports
max short-term, max momentary, and short-term range in analysis output.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add linear-phase FIR EQ option to mastering pipeline (#254) (#264)
Add eq_linear_phase preset flag that switches EQ stages from minimum-phase
IIR biquads to linear-phase FIR filters via frequency-sampled design.
Zero phase distortion at the cost of higher latency (irrelevant for
offline mastering). Completes the final item from #254.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add iterative LRA targeting and missing --deess-bandwidth CLI arg (#265)
* fix: add iterative LRA targeting and missing --deess-bandwidth CLI arg
LRA targeting (#253): previously only measured and reported LRA without
acting on it. Now iteratively increases compression ratio (up to 5
iterations, capped at 8.0) when measured LRA exceeds target_lra,
re-running compression from a pre-compression checkpoint each iteration.
De-esser bandwidth (#254): adds missing --deess-bandwidth CLI arg to
match the other four de-esser CLI parameters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve mypy union-attr errors on pre_compress_data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve audit gaps across mastering issues #251-#254 (#266)
Bugs fixed:
- LRA targeting: parallel blend now downsamples before blending (was
shape mismatch when oversample > 1)
- LRA targeting: recalculate current_lufs after iterative re-compression
(was using stale pre-loop value for final normalization)
- LRA targeting: warn when audio is already over-compressed (LRA below
target) per #253 spec — skip expansion to avoid artifacts
- LRA targeting: handle compress_ratio=1.0 gracefully (start from 1.5)
Code gaps fixed:
- Wire eq_low_q through apply_low_shelf() (was defined but unused)
- Add --eq-low-q CLI arg
- Add 6 missing multiband CLI args (--multiband-{low,mid,high}-{ratio,threshold})
- Add 2 missing midside CLI args (--midside-{low,high}-freq)
- Wire all new CLI args into cli_overrides dict
Documentation gaps fixed:
- SKILL.md: expand "Available preset fields" from 12 to all 47 fields
- Override example: add all #253/#254 fields to header comment
- CHANGELOG: document mastering overhaul in [Unreleased]
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use eq_low_q in linear-phase and mid/side low shelf paths (#267)
The linear-phase low_shelf case in _design_linear_phase_eq() used
hardcoded alpha = sin(w0)/2*sqrt(2) instead of sin(w0)/(2*q), causing
a tonal mismatch with the IIR path when eq_low_q != 0.707.
Also passes q explicitly in apply_midside_eq() low shelf calls and
in the linear-phase low shelf call in master_track().
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: log failed optional module imports in processing helpers
* fix: add path traversal guards to MCP handler parameters (#274)
Add _is_path_confined() helper to _shared.py and apply it to all
user-supplied path parameters across MCP handlers: subfolder,
source_subfolder, track_filename, reference_filename, target_filename,
file_name, and songbook title. Validate color_hex/text_color against
hex regex before ffmpeg filter interpolation.
Closes #273
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: harden _normalize_slug against path traversal and fix CI injection (#276)
_normalize_slug now rejects inputs containing path separators (/,\),
null bytes, or traversal sequences (..) with a ValueError. This
single-point fix closes path traversal vectors in rename_album,
rename_track, resolve_path, and create_album_structure.
Defense-in-depth: added _is_path_confined checks in rename and
create_album_structure handlers before any shutil.move or mkdir.
Also moved github.head_ref in pr-target-check.yml from inline
interpolation to an env: block to prevent shell injection via
malicious fork branch names.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: harden optional module import helpers and callers (#277) (#278)
Narrow except clauses from Exception to (ImportError, OSError), add None
guards at all call sites, and restructure _check_anthemscore to avoid
double-logging by checking the helper return value before falling back.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add @zeel2104 to contributors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: establish external contributor attribution workflow
Add maintainer checklist item to PR template and CLAUDE.md rule so
external PR authors get added to README Contributors section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add discord link to readme
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: look-ahead limiter hits ceiling exactly on transients (#284)
The release-smoothed envelope was shifted backward by lookahead_samples and
applied directly, so at peak sample K the gain used was smoothed[K+lookahead]
— already partially released from the attack. Output overshot the ceiling by
roughly exp(-lookahead_ms/release_ms) in dB (~0.9 dB at the defaults).
Replace the shift with a rolling minimum over [i, i+lookahead_samples] so the
applied gain at each sample is bounded by the smallest gain required anywhere
in the lookahead window. Peaks now receive their full pre-computed attenuation
and the ceiling is hit within 0.01 dB.
Regression test covers transient + ceiling compliance, which the existing
steady-sine tests couldn't trigger.
Fixes #283
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: default mastering routing to polish-then-master (#281)
When the user asks to master an album, polish first via mix-engineer
then hand off to mastering-engineer. Opt out with phrase ("master only",
"skip polish", "already polished") or by having polished audio already
staged at {audio_root}/.../polished/.
Closes #280
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make QC click thresholds genre-aware (#287)
_check_clicks used a single hardcoded peak-to-RMS ratio (6.0) and fail
count (3) for every genre. Electronic, IDM, breakcore, trap, metal, and
glitch work FAILed click QC on intentional musical transients.
Promote click_peak_ratio and click_fail_count into genre-presets.yaml
defaults and add overrides for dense-transient genres: tier 1 (8.0, 15)
covers electronic/metal/trap; tier 2 (10.0, 30) covers IDM/breakcore/
glitch/footwork/speedcore/gabber. User mastering-presets.yaml overrides
still apply per the existing load_genre_presets merge. qc_tracks.py
accepts --genre, and the qc_audio MCP tool accepts an optional genre
argument that routes through to _check_clicks.
Closes #285
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: post-downsample true-peak guard closes SRC ripple overshoot (#288)
The look-ahead limiter (#284) hits the target ceiling exactly at the
oversampled rate, but the subsequent `scipy.signal.resample_poly`
downsample (and optional output SRC) has passband ripple that
re-introduces 0.1–0.9 dB of inter-sample overshoot above the ceiling at
the output rate.
Add one reactive `limit_peaks()` pass after SRC and before dither. A
single pass converges because `gain = ceiling / true_peak` is exact for
the measured peak and soft-clip absorbs any residual. `final_lufs` is
still measured at the processing rate (authoritative and pyln-
supported); `final_peak` is now measured at the output rate after the
guard so the returned value reflects what was written to disk.
Closes #286
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: analyze every stem per track in analyze_mix_issues stems mode (#272) (#293)
Previously sampled only the alphabetically first stem per track, so issues
in specific stems (muddy bass, harsh vocals, etc.) were missed. Now iterates
every stem and reports per-stem diagnostics under tracks[].stems[stem_name],
with track-level issues aggregated as the union across stems.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: genre-aware declick with stem-tier cubic repair (#289) (#291)
* refactor: remove_clicks returns (data, n_clicks) and accepts peak_ratio/repair
Extend remove_clicks() to support two detection modes (std-diff default,
windowed peak/rms matching qc_tracks._check_clicks) and two repair modes
(linear default, cubic spline for stem isolation). All three in-file call
sites updated to unpack the new tuple return. Backward-compatible: callers
that pass no new kwargs see identical audio output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: peak_ratio detection and cubic repair paths in remove_clicks
Adds TestRemoveClicksPeakRatio (4 tests) and TestRemoveClicksCubicRepair
(5 tests) covering the #289 windowed peak/rms detection path and the
cubic-spline stem-tier repair mode.
Also corrects _generate_click's background sine scale from 0.3× to 0.1×
amplitude: the original scale made the window RMS too high for the
peak/rms ratio to exceed 6.0 with any window-based approach, so the new
detection path could never fire. At 0.1× the window peak/rms ratio is
~10.2 (detected at 6.0, not at 50.0) while all existing std-path tests
continue to pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: tighten cubic repair amplitude bound and fix peak_ratio test naming
* feat: polish pipeline reads master click thresholds per genre
Add _resolve_master_click_thresholds() helper that looks up
click_peak_ratio / click_fail_count from mastering GENRE_PRESETS so
_get_stem_settings and _get_full_mix_settings overlay the same
detection semantics as the QC click detector (#289).
Mix-preset overrides win when already present; helper fails soft for
unknown genres. 4 new tests in TestMasterClickThresholdsThreaded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: tighten click-threshold helper docstring, test, and exception scope
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: drums/percussion processors use peak_ratio+cubic repair and report clicks
Wire click_peak_ratio into process_drums and process_percussion: when the
setting is present the processors call remove_clicks with peak_ratio+cubic
repair (stem-tier quality); fall back to the legacy std+linear path when
only click_threshold is supplied. Both processors now accept an optional
report dict and accumulate clicks_removed into it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: mix_track_stems reports clicks_removed per stem
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: mix_track_full reports clicks_removed and uses peak_ratio when genre is set
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: include clicks_removed in mix_track_full skip-path result
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: stem cubic repair differs from full-mix linear repair
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: hoist CubicSpline import and remove unreachable _repair_cubic fallback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: changelog entry for genre-aware declick and stem-tier repair (#289)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve mypy errors in declick machinery
- Narrow `peak_ratio` in `_detect_peak_ratio` via assert so mypy sees the
non-None closure value (caller `_process_channel` already guards).
- Dispatch `process_drums` / `process_percussion` directly from
`mix_track_stems` so mypy sees the `report=` kwarg on the matching
signature; other stems continue to route through `STEM_PROCESSORS`.
- Annotate `STEM_PROCESSORS` as `dict[str, Callable[..., Any]]` to cover
the drums/percussion signature asymmetry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: research Suno V5.5 and update reference guide (#279) (#294)
V5.5 (March 26, 2026) is backward-compatible with V5 — same boxes,
metatags, and sliders. Engine is more expressive; new features are
Voices (cloning), Custom Models (fine-tuning), and My Taste.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: polish pipeline enhancements (#269, #270, #271) (#295)
* feat: add per-track polish support to polish_audio (#271)
polish_audio now accepts an optional track_filename to process a single
track instead of the whole album, mirroring master_audio's granularity.
Stems mode matches the stem directory by filename stem; full-mix mode
matches the WAV filename directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: run full qc_track suite in polish_album verify stage (#270)
Replaces the lightweight peak/RMS/finite/clipping checks with the full
8-check qc_track suite (format, mono, phase, clipping, truepeak, clicks,
silence, spectral). Catches polish-introduced issues at the point they're
introduced rather than surfacing later in master_album pre-QC. Passes
genre through so click thresholds stay genre-aware.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add polish_and_master_album orchestrator tool (#269)
Combines polish_album() and master_album(source_subfolder="polished")
into a single MCP call, removing the manual handoff between the two
pipelines. Stops on failure at either phase. The individual tools remain
available for granular control (re-polish or re-master alone).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: accept FAIL verdict in polish_album verify assertion
The verify stage now runs the full qc_track suite, which can legitimately
FAIL on synthetic fixture audio (1.5s degenerate signal triggers spectral
or silence checks). The test's intent is to confirm the pipeline ran all
stages to completion, not that synthetic audio passes production QC.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pre-master QC no longer gates on truepeak/clicks/tinniness (#297) (#298)
The Stage 3 pre-master QC gate in polish_and_master_album was blocking
all tracks by running checks that mastering is designed to fix:
- truepeak: polished audio is pre-limiter; post-master verification
(Stage 5) is the real ceiling gate.
- clicks: polish already ran stem-tier declick (#289); residual
transients here false-positive on legitimate percussive content.
- spectral tinniness: mastering's cut_highmid EQ exists to tame
high-mid buildup; a hard FAIL blocks work the master can fix.
Pre-QC now passes an explicit checks list and spectral tinniness
returns WARN instead of FAIL. The six checks mastering cannot fix
(format, mono, phase, clipping, silence, spectral) remain active,
so the gate still catches real issues. Post-QC is unchanged.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: codec preview and mono fold-down QC artifacts (#296) (#299)
* feat: codec preview and mono fold-down QC artifacts (#296)
Adds a sibling `mastering_samples/` directory for operator-listening QC
artifacts so `mastered/` stays byte-identical to DistroKid uploads.
- Codec preview: renders 128 kbps AAC (.aac.m4a) per mastered track for
Bluetooth-path audition.
- Mono fold-down QC: sums stereo to mono, measures per-band / LUFS /
vocal-RMS / correlation deltas; a >6 dB band drop hard-fails the
pipeline with the offending frequency surfaced (phase cancellation
guard). Writes a `.MONO_FOLD.md` sidecar plus a listenable `.mono.wav`
sample to `mastering_samples/`.
- Wired into `master_album` as stage 5.5 (between verification and
post-QC); codec preview never blocks, mono fold short-circuits on FAIL.
- New standalone MCP tools `render_codec_preview` and `mono_fold_check`
run the same checks independently.
- Configurable via new `genre-presets.yaml` defaults
(`codec_preview_*`, `mono_fold_*`); user overrides supported.
- `reset_mastering` now accepts `mastering_samples` alongside
`mastered` / `polished`.
Tests: 30 new unit tests covering the analysis, renderer, report writer,
MCP wrappers, and pipeline stage (in-phase PASS + phase-inversion
short-circuit). All 3116 unit + 245 plugin tests pass.
Closes #296
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: CI lint and merged-in test regression
- mypy: annotate `worst` as `dict[str, Any]` so its entries can accept
`None | float` without inference narrowing; coerce `fold_to_mono` mean
return to `np.ndarray`; replace conditional expression in stage 5.5
config lookup with explicit if/else so mypy infers `dict[str, Any]`.
- test: `test_pre_qc_skips_truepeak_and_clicks` (landed on develop in
#297/#298 after I branched) still wrote empty bytes for the mastered
output; swap to `_write_tiny_stereo_wav` so stage 5.5 can read it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add setup skill to SKILL_INDEX.md alphabetical table (#292)
Co-authored-by: bitwize-music <bitwize@bitwizemusic.com>
* docs: add @alijahak to README contributors (#301)
Recognizes @alijahak's contribution via PR #292 (docs: add /setup skill
to index tables).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: realistic audio fixtures and integration tests (#300) (#302)
Add make_clicks_and_pops, make_silent_gaps, and make_phase_partial
generators for QC paths a sine wave can't trigger (injected DC pops,
internal silent gaps, partial phase cancellation). Move audio fixtures
from the unscanned tests/fixtures/audio/conftest.py into tests/conftest.py
so all tests auto-discover them.
Add tests/unit/mastering/test_integration_realistic_audio.py covering
sibilance/tinniness, partial-phase mono fold FAIL, click detection,
internal-gap silence FAIL, and LUFS spread across dynamic content.
Document the generator catalog and authoring conventions in
tests/fixtures/README.md.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: mastering foundation — 24/96 delivery + archival + config block (#290 phase 1a) (#304)
* docs: spec pointer for album-coherence mastering pipeline
Durable design reference pointing to issue #290 (canonical spec) and
companion issue #303 (full-fidelity metadata embedding).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: plan 1a — mastering foundation implementation plan
Adds docs/superpowers/plans/2026-04-13-mastering-foundation-plan-1a.md:
9-task TDD plan for the mastering foundation (24/96 delivery, archival,
config block, prune_archival) per issue #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(config): add mastering: config block with 24/96 defaults
Documents streaming-grade delivery defaults for the mastering pipeline
(24-bit WAV at 96 kHz, -14 LUFS, -1 dBTP) and the 96 kHz upsampling
caveat. Adds optional artist.copyright_holder and artist.label keys for
downstream metadata embedding.
All keys are optional; existing configs continue to work unchanged.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): add load_mastering_config + resolve_mastering_targets
Dedicated config loader for the mastering: YAML block plus a resolver
that combines config values, genre-preset values, and explicit handler
arguments into the effective per-run targets.
Precedence: explicit arg > genre preset > config > default.
Used by master_audio / master_album to honor streaming-grade delivery
defaults (24/96 WAV, -14 LUFS, -1 dBTP) without requiring per-call args.
15 unit tests covering: default behavior, override precedence, type
coercion, malformed input, and source-rate comparison.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): wire master_audio to mastering config for delivery targets
master_audio now consumes load_mastering_config() and passes the
resolved output bit depth + sample rate through to master_track via
preset. Mastered output defaults to 24-bit / 96 kHz (streaming-grade),
overridable via the mastering: config block or legacy genre presets.
Response settings dict now reports output_bits and output_sample_rate
alongside target_lufs and ceiling_db.
Existing tests pass unchanged; 3 new integration tests cover the 24/96
default, the 16-bit legacy override path, and the 44.1 kHz match-source
path.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): wire master_album + upsampling notice + 96 kHz QC support
Tasks 4 + 5 of Plan 1a:
master_album now consumes load_mastering_config() + resolve_mastering_targets()
identically to master_audio. Effective delivery targets (output_bits,
output_sample_rate) flow through the per-track _master_track call via
the preset dict. Auto-recovery path honors effective_ceiling and
output_bits for consistency.
Response settings block now reports: target_lufs, ceiling_db, output_bits,
output_sample_rate, source_sample_rate, upsampled_from_source,
archival_enabled, adm_aac_encoder, plus legacy cut_highmid/cut_highs/genre.
New 'notices' array in the response: emits a one-line caveat when the
effective delivery_sample_rate exceeds the measured source rate,
surfacing the 96 kHz honesty note at runtime.
Side effects:
- qc_tracks.py format check now accepts 44.1/48/88.2/96/176.4/192 kHz
(was limited to 44.1/48 — would FAIL on the new 96 kHz default).
- master_album initializes album_status before the state-update branch
(fixes latent UnboundLocalError when album not in state cache).
- Two existing tests updated to accept EQ propagation via preset dict
instead of only the legacy eq_settings tuple list.
Added:
- tests/unit/mastering/test_master_album_config_wiring.py — 2 integration
tests covering 24/96 default output, upsampling notice emission, and
matched-rate no-notice behavior.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): opt-in archival output + prune_archival MCP tool
Tasks 6 + 7 of Plan 1a:
Archival output (opt-in via mastering.archival_enabled = true):
- master_album writes 32-bit float copies of each mastered track to
{audio_dir}/archival/ as a new stage between post_qc and status_update.
- Default OFF; archival is intended for re-mastering without re-polishing
stems (e.g. spec changes, re-release with different targets).
- Cost: ~1-2 GB per 12-track album.
- 3 tests covering disabled-by-default, enabled-writes-float, stage-recorded.
prune_archival MCP tool:
- Explicit user-action cleanup. keep=N retains the N newest files by
mtime and removes the rest; keep=0 removes everything; negative keep
treated as 0; missing directory is a no-op with a note field.
- 5 tests covering all edge cases.
- Registered alongside existing master/analyze/qc tools.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): re-export prune_archival from handlers.processing + server.py
Completes the wiring so server.py's re-export completeness check passes
and the prune_archival tool is actually discoverable by MCP clients.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mastering): genre presets must not force 16-bit output
Bug: all shipped genre presets inherited output_bits=16 and
output_sample_rate=0 from tools/mastering/genre-presets.yaml defaults.
resolve_mastering_targets() then saw preset_bits=16 and took the "legacy
16-bit" branch, silently overriding the mastering.delivery_bit_depth=24
config default whenever a genre argument was passed.
Any call to master_album(genre=...) or master_audio(genre=...) would
downgrade to 16-bit output. Only no-genre calls honored the 24/96
streaming delivery target.
Fix: promote genre-presets.yaml defaults to output_bits=24 and
output_sample_rate=96000 so the built-in genres align with the new
streaming-grade foundation. User-supplied overrides in
{overrides}/mastering-presets.yaml can still force 16-bit legacy output
explicitly.
Tests:
- New regression unit test verifying all core genres
(electronic/hip-hop/metal/folk/pop) resolve to 24/96 with default
config.
- New integration test exercising master_album(genre="electronic") end
to end — verifies mastered output is actually 24-bit at 96 kHz.
Part of #290 phase 1a.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(mastering): address code review on PR #304
Applies 6 should-fix items from the code-reviewer pass:
1. tools/mastering/config.py — collapse dead-code branches in output_bits
resolution. The elif preset_bits == 16 branch was tautological after
commit 338b24c promoted YAML defaults to 24; both branches produced
output_bits = preset_bits. Replace with a single non-zero check.
2. config/config.example.yaml — remove misleading "use -1.5 for opus-safe
genres" comment on true_peak_ceiling. Per-genre opus-safe overrides
are reserved for a later phase; the field is global for now.
3. CHANGELOG.md — add [Unreleased] entry for the 24/96 foundation in
Added and document the default-format change in Changed so users
upgrading see the format shift explicitly.
4. migrations/0.90.0.md — add migration note so the plugin upgrade flow
surfaces the format change, disk-usage impact, and legacy-config
snippet at session start after upgrade.
5. audio.py::master_audio — remove dead eq_settings tuple list. EQ now
flows entirely through preset.cut_highmid / preset.cut_highs, which
master_track rebuilds internally.
6. audio.py::master_album — add logger.debug breadcrumb when the
source-sample-rate probe fails so silent fallback leaves a trail.
Plus two test hygiene improvements:
- test_eq_settings_propagated + test_genre_preset_sets_effective_settings:
drop the legacy-path else branch. Now asserts strictly on the preset
path, since that's the only path the production code takes after this
PR. Stronger coverage.
Part of #290 phase 1a. Review findings in PR #304 discussion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: multi-metric signature foundation (#290 phase 1b) (#305)
* docs: phase 1b multi-metric signature design
Specifies the analyze_track() extension that feeds Phase 2 anchor
selection and coherence checking: STL-95, low-RMS (STL-95-windowed,
20-200 Hz), vocal-RMS (polished stem when present, 1-4 kHz band
fallback), plus a signature_meta provenance dict. Additive return
schema; stem path auto-resolve covers album-root, polished/, and
mastered/ input layouts.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: phase 1b implementation plan + spec clarifications
Plan breaks Phase 1b into 8 TDD-structured tasks: STL-95, low-RMS,
vocal-RMS stem branch, vocal-RMS band fallback, signature_meta
provenance, stem auto-resolve, E2 disk-usage note, and full
regression run. Each task is self-contained with failing-test-first
cycles, exact code, and individual commit messages.
Spec clarifications: corrected handler-kwarg scoping (analyze_audio
batches per-album, so no handler change needed; kwarg lives only on
analyze_track for Phase 2 callers), tightened top-K and tie-break
semantics for STL-95.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): add STL-95 signature metric to analyze_track
Collects short-term LUFS values from the existing 3s/1s loop,
computes the 95th percentile when at least 20 windows exist, and
retains the top-5% window indices for downstream low-RMS windowing.
Returns None for tracks too short or silent to produce a meaningful
percentile.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): add low-RMS signature metric (20-200 Hz, windowed)
Bandpasses the mono mixdown at 20-200 Hz (4th-order Butterworth SOS,
zero-phase via sosfiltfilt) and takes the median RMS across the
top-5% STL windows identified by STL-95. Whole-track low-RMS would
false-alarm on arrangements with sparse verses and wall-of-bass
choruses; windowing keeps the metric faithful to what listeners
perceive as the track's low-end signature.
Returns None when STL-95 is None (track too short or silent).
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): add vocal-RMS stem branch with explicit stem kwarg
Adds optional vocal_stem_path kwarg to analyze_track(). When the
stem resolves and reads, measures whole-stem RMS in dB on a mono
mixdown, resampling to the mix rate when needed. Unreadable stem
files log a warning and fall through (band fallback lands in Task 4).
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): add 1-4 kHz band fallback for vocal-RMS
When no vocal stem resolves (or the stem is unreadable), falls back
to measuring whole-track RMS on the 1-4 kHz bandpassed mono mixdown
of the full mix. Covers the today-default path (polish does not yet
preserve per-stem WAVs); stem measurement activates automatically
once the per-stem artifact lands.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): expose signature_meta provenance dict
Records stl_window_count, stl_top_5pct_count, and vocal_rms_source
alongside the scalar signature fields. Downstream consumers (anchor
selector, coherence check) branch on vocal_rms_source to explain
their decisions ('stem' / 'band_fallback' / 'unavailable') without
re-parsing paths.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(mastering): auto-resolve vocal stem path for analyze_track
When the caller omits vocal_stem_path, analyze_track walks two
layouts looking for <input_stem>/vocals.wav under a sibling
polished/ directory — first the input's directory, then one level
up. Covers album-root, polished/, and mastered/ input callsites
without touching the wider filesystem.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(config): add disk-usage note to delivery_sample_rate comment
Expands the 24/96 default documentation with a concrete disk-usage
estimate (~33 MB/min at 24/96 vs ~10 MB/min at 44.1; ~1.5 GB per
12-track album before archival). Surfaces the tradeoff users should
weigh when enabling archival output.
Carried forward from #304 review item E2. Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(mastering): extend test_returns_expected_keys for phase 1b fields
The schema assertion used strict set equality on analyze_track's
return keys. Adds stl_95, low_rms, vocal_rms, and signature_meta to
the expected set so the phase 1b additions pass schema validation
alongside the existing fields.
Part of #290 phase 1b.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: anchor selector for album mastering (#290 phase 2) (#306)
* docs: phase 2 implementation plan — anchor selector (#290)
Covers composite scoring + README frontmatter override + deterministic
tie-breaker, plus the D1 carried-forward refactor to consolidate
build_effective_preset between master_audio and master_album.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract build_effective_preset helper (#290 phase 2)
Consolidates the ~30 lines of preset construction duplicated between
master_audio and master_album handlers (D1 review item from #304).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: clarify local-import rationale + cover source_sample_rate plumbing (#290 phase 2)
Addresses review nits from commit 153616b:
- The local `load_genre_presets` import isn't about a circular dependency
(there isn't one); it's about keeping config.py's startup import path
free of master_tracks.py's YAML-loading side effect.
- Adds three assertions to test_pop_genre_happy_path verifying
source_sample_rate and upsampled_from_source thread through to both
`targets` and `settings` — this plumbing is load-bearing for the
master_album upsampling-notice emitted in Task 2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: route master_audio+master_album through build_effective_preset (#290 phase 2)
Eliminates the last copy of the preset construction block. Both MCP
handlers now call the shared helper from tools.mastering.config,
completing the D1 refactor from the #304 review.
The consolidation removes the top-level load_mastering_config /
resolve_mastering_targets / load_genre_presets imports from the
handler — those names now live only inside build_effective_preset.
Three wiring tests that used to patch
audio_mod.load_mastering_config are re-pointed at
tools.mastering.config.load_mastering_config, where the helper
resolves the symbol, so behavior coverage is preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: drop dead assignments + tidy bundle unpacking (#290 phase 2)
Addresses review nits M1–M3 from commit e22fde0:
- Drop `preset_dict = bundle["preset_dict"]` at both sites (never read
after the refactor — the helper threads it into resolve_mastering_targets
internally now).
- Drop `genre_applied = bundle["genre_applied"]` in master_album (dead;
master_album reads the value via settings["genre"]).
- Pull `settings` and `effective_preset` once at the top of each handler
so downstream reads use simple indexing instead of bundle[..][..].
- Drop the redundant loop-local `effective_preset = bundle["effective_preset"]`
in both track loops — same binding is already in scope.
Behavior unchanged; test patch targets unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: surface anchor_track frontmatter field (#290 phase 2)
parse_album_readme now extracts the optional anchor_track override so
it flows through the state cache to the mastering pipeline without
mastering code ever reading README files directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add genre_ideal_lra + spectral_reference_energy defaults (#290 phase 2)
New preset fields consumed by the album-mastering anchor selector.
Pop-balanced defaults; per-genre overrides can land later as reference
albums are measured with measure_album_signature.
load_genre_presets() does not inherit nested-dict defaults into
per-genre presets; the anchor selector (Task 8) falls back to
hard-coded defaults at runtime when the per-genre preset omits
spectral_reference_energy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: anchor selector — spectral match score (#290 phase 2)
First scoring component for the album-mastering anchor selector:
Euclidean distance between track band_energy and genre reference
curve, mapped to (0, 1].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: anchor selector — mix_quality, representativeness, ceiling_penalty (#290 phase 2)
Implements the three per-track component scores from the issue #290
composite formula. Next task wires them together + override path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: select_anchor composite scoring + override + tie-breaker (#290 phase 2)
Wires mix_quality, representativeness, and ceiling_penalty into the
composite formula from issue #290. Supports README frontmatter
override and deterministic tie-breaker on ties within 0.05.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: harden anchor_selector inputs before handler integration (#290 phase 2)
Addresses review findings from the Tasks 5–7 combined pass:
- I1: reject non-int override_index (float, bool, str) at module
boundary with a clear override_reason; previously floats smuggled
through as selected_index, strings raised TypeError on `> 0`.
- I2: _is_eligible now validates band_energy has all 7 bands (not
just that it's truthy); select_anchor raises ValueError when the
genre preset's spectral_reference_energy is missing bands — that's
a config bug, not a per-track eligibility issue.
- I4: _album_medians uses t.get(key) everywhere (was t[key] on the
value-extraction line), eliminating a latent KeyError when a track
dict lacks a signature key entirely.
Adds 7 tests covering malformed override, malformed band_energy,
malformed preset, empty track list, and missing-key-entirely edge case.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: integrate anchor selector into master_album pipeline (#290 phase 2)
After Stage 2 (Analysis), master_album now runs the anchor selector
and records its choice in stages["anchor_selection"]. Honors the
optional anchor_track README frontmatter override (surfaced via the
state cache by parse_album_readme).
The mastering loop itself is unchanged — this phase ships the selector
as metadata. Coherence correction (which will use the anchor) lands
in the next phase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: propagate anchor_track through indexer + drop dead fallback (#290 phase 2)
Addresses review findings C1 and I1 from the Task 8 integration:
- C1: tools/state/parsers.py surfaces anchor_track in its output dict,
but tools/state/indexer.py never copied the field into the state
cache's albums[slug] entry. The README override chain was broken
end-to-end. Adds the propagation at both indexer write sites (full
scan + incremental re-parse) plus a state-schema test.
- I1: The handler's spectral-defaults fallback used
load_genre_presets().get("defaults", {}), which always returns {}
because load_genre_presets returns {genre: preset}, not a dict with
a top-level "defaults" key. The fallback did nothing — select_anchor's
internal pop-balanced defaults are what kept things working. Drop
the dead code and document where the real defaults live.
Adds an end-to-end override integration test covering the full chain:
frontmatter → parser → indexer → state cache → handler → select_anchor.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: extend _mock_analyze with Phase 1b signature fields (#290 phase 2)
After Task 8 landed the anchor_selection stage, the test_server_qc
mock for analyze_track no longer produced enough signal for the
selector to mark tracks eligible — every mocked track surfaced as
no_eligible_tracks / warn. test_full_pipeline_success asserts every
stage is "pass", so it regressed.
Add stl_95, short_term_range, low_rms, vocal_rms, and signature_meta
to _mock_analyze so the selector has something to score. All existing
tests in the file share this helper; none of them assert on the
anchor_selection stage specifically, so extending the mock is the
minimal fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: measure_album_signature MCP tool (#290 phase 3a) (#307)
* feat: add build_signature pure-Python aggregator (#290 phase 3a)
Introduces tools/mastering/album_signature.py with build_signature(),
which aggregates a list of analyze_track results into per-track +
album-level signature summaries (median/p95/min/max/range). Pure
Python — no I/O, no MCP coupling. Happy-path unit test covers a
three-track album.
compute_anchor_deltas() is also in the module but not yet covered;
subsequent commits add tests for None handling, aggregate edge cases,
and delta computation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: cover build_signature edge cases (#290 phase 3a)
Adds coverage for:
- None stl_95 values excluded from medians
- all-None metric returns None across aggregates
- non-finite lufs (inf, nan) excluded
- empty album → empty tracks + None aggregates
- single-track album → zero range
- 5-value p95 uses numpy's linear interpolation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: cover compute_anchor_deltas (#290 phase 3a)
Tests the pure-Python delta computation:
- deltas follow track - anchor convention
- None in track or anchor yields None delta
- None anchor propagates to every row's metric
- empty list + out-of-range index raise ValueError
- every AGGREGATE_KEYS metric has a delta_ column
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add measure_album_signature handler (#290 phase 3a)
New MCP handler in handlers/processing/audio.py. Resolves the album's
subfolder (default 'mastered/'), runs analyze_track on every WAV,
calls build_signature to produce per-track + album-level aggregates,
and optionally runs the anchor selector + compute_anchor_deltas when
genre or an explicit anchor_track is supplied.
Read-only — no files are written. Used for tuning genre tolerances
from reference albums and for feeding phase 3b coherence tools.
Guards against _shared.cache being None (test harness / early-init
callers) — state-cache anchor_track lookup is only attempted when
the cache is actually wired up.
Integration tests cover:
- no-anchor happy path (3 sine-wave tracks)
- missing subfolder → error
- subfolder escape blocked
- empty subfolder → error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: cover anchor paths for measure_album_signature (#290 phase 3a)
- genre argument → anchor block populated via select_anchor
- explicit anchor_track=2 → method=override, deltas[1].is_anchor=True
- unknown genre → error JSON with available_genres catalogue
- out-of-range anchor_track → override_reason set, falls through to
composite scoring
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: register measure_album_signature with MCP server (#290 phase 3a)
Adds mcp.tool()(measure_album_signature) to the audio-handler
register() so the tool is exposed via the bitwize-music MCP server
alongside analyze_audio / master_album / prune_archival.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: re-export measure_album_signature + CHANGELOG entry (#290 phase 3a)
- Add measure_album_signature to handlers.processing.__init__ and
server.py re-exports so the integrity test
TestReExportCompleteness::test_all_registered_tools_are_reexported
passes.
- Add an [Unreleased] CHANGELOG entry describing the new tool,
matching the style of the phase 1a entry (#304).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add phase 3a implementation plan (#290)
Plan document guiding the implementation of the measure_album_signature
MCP tool — phase 3a of issue #290. Captures the file structure,
design decisions (signature shape, aggregation rules, delta convention,
anchor precedence), and the TDD task breakdown used to land this PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: patch _shared.cache to None for no-anchor test (#290 phase 3a)
CI exposed a flake: the no-anchor test asserted "anchor" not in
result, but the handler was reading anchor_track from the persistent
state cache (populated by another test in the suite with anchor_track:
2 on a different test-album). Locally the cache started empty so the
test passed; in CI it didn't.
Patch _shared.cache to None for that specific test so the handler
treats the cache as unavailable — matching the test's intent of
"no anchor + no override". The other tests already pass anchor_track
explicitly so they're insulated from this leak.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: album coherence check + correct (#290 phase 3b) (#309)
* feat: add coherence tolerance fields to genre-presets defaults (#290 phase 3b)
Four new fields in the defaults block:
- coherence_stl_95_lu: 0.5 (±LU around anchor's STL-95)
- coherence_lra_floor_lu: 1.0 (minimum short_term_range allowed)
- coherence_low_rms_db: 2.0 (±dB around anchor's low_rms)
- coherence_vocal_rms_db: 2.0 (±dB around anchor's vocal_rms)
Also extends _PRESET_DEFAULTS in tools/mastering/master_tracks.py
so the fields flow through load_genre_presets() into per-genre
merged presets — load_genre_presets filters YAML keys through
_PRESET_DEFAULTS, so YAML-only additions are silently dropped
otherwise. (Phase 2's genre_ideal_lra_lu / spectral_reference_energy
work around this by falling back to hardcoded defaults in
anchor_selector — phase 3b prefers the cleaner flow-through path
so load_tolerances sees real preset values.)
Consumed by the upcoming album_coherence_check / album_coherence_correct
MCP tools to classify outlier tracks relative to the anchor. Default
pop-balanced values ship in defaults; per-genre overrides can be added
incrementally as reference-album tuning reveals needs.
Header-comment block extended with per-field docstrings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add tools/mastering/coherence.py with load_tolerances (#290 phase 3b)
Introduces a new pure-Python module that will own tolerance-band
merging, outlier classification, and correction planning for the
upcoming album_coherence_check / album_coherence_correct handlers.
This commit ships the DEFAULTS constant + load_tolerances():
- DEFAULTS maps the four coherence_* preset fields to their
documented defaults plus lufs_tolerance_lu (hardcoded 0.5 LU
matching master_album Stage 5).
- load_tolerances(preset) merges a partial preset on top of
defaults key-by-key; lufs_tolerance_lu is non-overridable.
classify_outliers + build_correction_plan are stubbed with
NotImplementedError — next commits fill them in with TDD.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: classify_outliers tolerance classifier (#290 phase 3b)
classify_outliers consumes the phase-3a compute_anchor_deltas output
plus per-genre tolerance bands and produces a per-track violation
list. Five metrics are checked per track:
- lufs: ±0.5 LU delta (correctable in MVP)
- stl_95: ±0.5 LU delta (reported, not correctable)
- lra_floor: absolute floor of 1.0 LU on short_term_range
- low_rms: ±2.0 dB delta (reported, not correctable)
- vocal_rms: ±2.0 dB delta (reported, not correctable)
Missing metrics (e.g., None vocal_rms) produce severity="missing"
which does NOT flag the track as an outlier — can't classify what
the analyzer couldn't compute.
10 new unit tests covering every metric, severity state, and the
anchor / no-anchor paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: build_correction_plan for LUFS-only correction (#290 phase 3b)
build_correction_plan consumes classify_outliers output and produces
a per-track plan:
- Anchor track → skipped with reason="is_anchor"
- Clean tracks → skipped with reason="no_violations"
- LUFS-outlier tracks → correctable, corrected_target_lufs = anchor's
measured LUFS (ground truth, not the preset target — guarantees
convergence because we chase real output)
- Non-LUFS-only outliers → non-correctable with a clear reason string
explaining the MVP scope limit
The anchor's measured LUFS becomes the correction target so we match
the actual mastered output rather than an idealized preset target that
the first mastering pass may have missed by a few tenths of a dB.
4 new unit tests covering every branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add album_coherence_check handler (#290 phase 3b)
Read-only MCP tool that classifies each mastered track against the
selected anchor using five per-genre tolerance bands:
- lufs (±0.5 LU, hardcoded, correctable)
- stl_95 (±coherence_stl_95_lu, reported)
- lra_floor (absolute floor, reported)
- low_rms (±coherence_low_rms_db, reported)
- vocal_rms (±coherence_vocal_rms_db, reported)
Requires either genre= (preferred) or anchor_track= (falls back to
default tolerances with a warning in the response). Response includes
a summary block with outlier counts broken down by metric so callers
can triage at a glance.
Three integration tests:
- LUFS outlier detected (amplitude-varied sine tracks)
- Errors without genre + anchor
- Falls back to default tolerances when anchor_track is given without genre
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add album_coherence_correct handler (#290 phase 3b)
Re-masters LUFS-outlier tracks from polished/ into mastered/ using
the anchor's measured LUFS as the per-track target. Staging pattern
mirrors master_album:
- .coherence_staging/ receives master_track output
- atomic replace into mastered/ on full success
- staging cleaned up on any failure (no partial writes)
Pre-flight validates:
- polished/ and mastered/ both exist
- every track in mastered/ has a matching file in polished/
- anchor is selectable (otherwise no correction reference)
- genre is set (tolerances + preset base require it)
Scope limits (MVP — documented in docstring):
- Corrects LUFS outliers only
- Non-LUFS outliers reported but not auto-corrected
- Single-pass (no iteration budget)
dry_run=True returns the plan without writing anything.
4 new integration tests covering dry-run, missing-polished,
polished/mastered file-set mismatch, and end-to-end correction
convergence (amplitude-varied sine outlier pulled within 1 dB of
anchor).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: register album_coherence_{check,correct} with MCP server (#290 phase 3b)
Adds the two new handlers to:
- handlers.processing.audio.register() (MCP tool registration)
- handlers.processing.__init__ (package re-export)
- server.py (top-level re-export)
Required for the integrity test TestReExportCompleteness to pass and
for the tools to be callable from the Claude Code client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: CHANGELOG entry + plan doc for album coherence (#290 phase 3b)
- CHANGELOG entry under [Unreleased] documenting both new MCP tools,
the four new coherence_* preset fields, and the MVP scope limit
(LUFS-only correction).
- Plan document captures the architecture, design decisions
(tolerance semantics, correction policy, why re-master from
polished/ not mastered/, scope limits), and the 8-task TDD
breakdown used to land this PR.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace lambda with functools.partial for mypy (#290 phase 3b)
mypy 'Cannot infer type of lambda' on the executor invocation in
album_coherence_correct. functools.partial preserves master_track's
typing while binding the per-iteration src/staged/modified_preset
values — same default-arg-binding behavior as the lambda but mypy
can resolve the call type cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: album signature persistence + frozen mode (#290 phase 4) (#310…1 parent 7306bca commit b4b70db
175 files changed
Lines changed: 31052 additions & 1441 deletions
File tree
- .claude-plugin
- .github
- workflows
- config
- overrides.example
- docs/images
- migrations
- reference
- mastering
- suno
- servers/bitwize-music-server
- handlers
- processing
- skills
- about
- album-conceptualizer
- health-check
- help
- lyric-refiner
- lyric-reviewer
- lyric-writer
- mastering-engineer
- promote-idea
- researchers-legal
- researchers-verifier
- session-start
- suno-engineer
- templates
- tests
- fixtures
- adm
- albums
- coherence
- audio
- plugin
- unit
- handlers
- mastering
- mixing
- state
- tools
- mastering
- mixing
- promotion
- state
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
35 | 39 | | |
36 | 40 | | |
37 | 41 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
23 | 25 | | |
24 | | - | |
25 | 26 | | |
26 | 27 | | |
27 | 28 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
198 | 203 | | |
199 | 204 | | |
200 | 205 | | |
| |||
206 | 211 | | |
207 | 212 | | |
208 | 213 | | |
209 | | - | |
| 214 | + | |
210 | 215 | | |
211 | 216 | | |
212 | 217 | | |
| |||
238 | 243 | | |
239 | 244 | | |
240 | 245 | | |
241 | | - | |
| 246 | + | |
242 | 247 | | |
243 | 248 | | |
244 | 249 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
82 | 85 | | |
83 | 86 | | |
84 | 87 | | |
| |||
97 | 100 | | |
98 | 101 | | |
99 | 102 | | |
100 | | - | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
101 | 106 | | |
102 | 107 | | |
103 | 108 | | |
| |||
139 | 144 | | |
140 | 145 | | |
141 | 146 | | |
| 147 | + | |
142 | 148 | | |
143 | 149 | | |
144 | 150 | | |
145 | 151 | | |
146 | 152 | | |
147 | 153 | | |
148 | | - | |
| 154 | + | |
149 | 155 | | |
150 | 156 | | |
151 | 157 | | |
| |||
269 | 275 | | |
270 | 276 | | |
271 | 277 | | |
272 | | - | |
| 278 | + | |
273 | 279 | | |
274 | 280 | | |
275 | 281 | | |
276 | 282 | | |
277 | 283 | | |
278 | 284 | | |
279 | 285 | | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
280 | 290 | | |
281 | 291 | | |
282 | 292 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
0 commit comments