From audio-production
Iteratively A/B tune an EQ preset to the user's taste. Each round renders two 15-second variants of the user's mic sample (with side-by-side spectrograms), the user picks A or B (or describes what they want changed), and the skill perturbs the preset accordingly until the user is happy. Then saves the winner. Use after suggest-eq when the heuristic preset doesn't quite land.
npx claudepluginhub danielrosehill/claude-code-plugins --plugin audio-productionThis skill is limited to using the following tools:
Iteratively narrow in on an EQ + dynamics preset by listening to short A/B clips and either picking a side or describing what's still wrong.
Conducts multi-round deep research on GitHub repos via API and web searches, generating markdown reports with executive summaries, timelines, metrics, and Mermaid diagrams.
Share bugs, ideas, or general feedback.
Iteratively narrow in on an EQ + dynamics preset by listening to short A/B clips and either picking a side or describing what's still wrong.
PLUGIN_DATA_DIR="${CLAUDE_USER_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/claude-plugins}/audio-production"
MICS_DIR="$PLUGIN_DATA_DIR/mics"
PRESETS_DIR="$PLUGIN_DATA_DIR/presets"
TUNE_DIR="$PLUGIN_DATA_DIR/tuning"
$ARGUMENTS:
--from=<preset> — starting preset. Default: <use-case>--<default_mic_id> if --use-case is given, else ask.--use-case=<podcast|vocals|spoken-word|broadcast> — used both as the seed if --from is absent and as the loudness/dynamics target.--mic=<mic-id> — defaults to default_mic_id. Determines which sample is auditioned.--name=<final-preset-name> — name to save the winner as. Default: <base-name>--tuned.--clip-duration=<seconds> — default 15.--clip-start=<auto|HH:MM:SS|<seconds>> — default auto (loudest 15s window in the mic's sample).Each round produces a self-contained directory under <TUNE_DIR>/<session-id>/round-<n>/ containing:
round-<n>/
base.json # preset state at the start of the round
variant-a.json
variant-b.json
before.wav # 15s reference clip (no processing)
variant-a.wav # 15s with variant A applied
variant-b.wav # 15s with variant B applied
compare.wav # ["Sample 1"] + variant-a + ["Sample 2"] + variant-b — single-file A/B
spectrogram-a.png # 0–8 kHz log-magnitude spectrogram
spectrogram-b.png
diff.txt # which parameters differ between A and B, in plain English
Load the base preset:
--from is given, copy that preset JSON into the new session as round-0/base.json.--use-case is given, copy <PRESETS_DIR>/<use-case>--<mic-id>.json./audio-production:list-presets).Cut a 15s before.wav from <MICS_DIR>/<mic-id>/sample.wav:
--clip-start=auto, pick the loudest 15s window using ffmpeg astats (same logic as /audio-production:extract-sample).Tell the user what was loaded and what the loudest 15s window contains.
Pick the axis that most likely shifts the user's perceived sound:
mud-cut — render variant A with current bands[freq=200..500].gain_db and variant B with that gain ±2 dB.presence — perturb the 2.5–4 kHz band's gain ±1.5 dB.de-ess — perturb deesser.threshold_db ±3 dB.compression — perturb compressor.ratio (e.g. 2.5 ↔ 3.5).If the user's previous-round feedback names a specific issue ("too sibilant", "muddy", "honky", "compressed-sounding", "thin", "boomy"), map it directly:
| User says | Axis | Direction |
|---|---|---|
| muddy / boxy / boomy | mud cut | more cut (−1 to −2 dB) |
| thin / hollow | mud cut | less cut |
| harsh / sibilant / sharp | de-ess | lower threshold (−3 dB) or +1 dB cut at sibilance peak |
| dull / dark / closed | presence | +1–2 dB at 3 kHz |
| nasal / honky | resonance band | −2 dB at the strongest 800–1500 Hz peak |
| compressed / squashed | compressor ratio | lower (e.g. 3:1 → 2:1) |
| dynamic / wild | compressor ratio | higher |
| pumping | release | longer (e.g. 80 → 150 ms) |
| rumble / boom | HPF | raise to 90–100 Hz |
So variant A = current, variant B = current + perturbation. (Or current ± perturbation if the previous-round feedback was just "neither — try a different direction".)
Write variant-a.json and variant-b.json with the appropriate perturbation, including a tuning_metadata block that records what was perturbed and by how much:
{
...preset fields...,
"tuning_metadata": {
"session_id": "<id>",
"round": <n>,
"axis": "mud-cut",
"delta": "+2 dB at 211 Hz",
"parent": "round-<n-1>/<chosen-variant>.json"
}
}
For each variant, build the ffmpeg filter chain (same translation as /audio-production:apply-preset) and render a 15s output:
ffmpeg -y -i "round-<n>/before.wav" -af "<chain>" -c:a pcm_s16le "round-<n>/variant-a.wav"
ffmpeg -y -i "round-<n>/before.wav" -af "<chain>" -c:a pcm_s16le "round-<n>/variant-b.wav"
Always run on the round's before.wav (not the source) so the audition window is byte-identical between variants.
Stitch the pre-generated TTS cues from <PLUGIN_DATA_DIR>/tts/ around the variants so the user hears one self-explanatory file:
ffmpeg -y \
-i "<TTS_DIR>/sample-1.wav" \
-i "round-<n>/variant-a.wav" \
-i "<TTS_DIR>/sample-2.wav" \
-i "round-<n>/variant-b.wav" \
-filter_complex "[0:a][1:a][2:a][3:a]concat=n=4:v=0:a=1[out]" \
-map "[out]" -c:a pcm_s16le "round-<n>/compare.wav"
If the cue files don't exist (<TTS_DIR>/sample-1.wav missing), surface a one-line message — Run /audio-production:generate-cues to create the announcement clips, or skip; variants are still in variant-a.wav / variant-b.wav — and continue without compare.wav.
Inline Python via heredoc, using librosa.display.specshow + matplotlib:
variant-a.wav and variant-b.wav.librosa.stft(n_fft=2048, hop_length=512), magnitude in dB.librosa.display.specshow(..., y_axis='log', x_axis='time', sr=sr), vmin=-80, vmax=0, frequency range 60–8000 Hz.If matplotlib isn't available, skip silently and note the omission in the round report.
diff.txtPlain-English comparison:
Round <n> — axis: mud-cut
Variant A (current):
Mud cut: -4 dB @ 211 Hz, Q 1.0
Variant B (perturbation):
Mud cut: -6 dB @ 211 Hz, Q 1.0
Everything else identical.
Print:
mpv "<round-dir>/compare.wav" # single-file A/B with announcements
# or audition the variants individually:
mpv "<round-dir>/variant-a.wav"
mpv "<round-dir>/variant-b.wav"
Ask: "A, B, both fine, neither, or describe what to change?"
The chosen variant becomes round-<n+1>/base.json. Increment round number.
When the user signals satisfaction:
Copy the current base preset to <PRESETS_DIR>/<final-name>.json.
Strip the tuning_metadata block (it was per-session bookkeeping).
Update derived_from to reference the analysis path of the bound mic.
Add a top-level tuned_via field pointing at the session directory for traceability:
"tuned_via": "tuning/<session-id>/round-<final-n>/"
Run /audio-production:audition-preset <final-name> to emit a fresh full-length audition with the saved preset.
Report the final preset path, the audition path, and how many rounds it took.
<TUNE_DIR>/<session-id>/ in place; they can resume by re-invoking with --from=<round-n-base>.--from. The final save is always to a new file unless the user explicitly opts to overwrite (--overwrite-from)./audio-production:install-deps.--clip-duration for longer auditions.before.wav is reused unless they explicitly pass a new --clip-start.