From pulp
Reproduce audio plugin bugs that only appear in DAWs (Logic, Live) by driving headless Processor scenes and a standalone AudioUnit host probe.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pulp:audio-headless-debugThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When a plugin bug reproduces in a host (Logic, Live, REAPER) but every unit
When a plugin bug reproduces in a host (Logic, Live, REAPER) but every unit test is green, you are almost always testing the wrong layer. This skill is the playbook that took a multi-day "audio cuts out when I touch a parameter" ghost down to a deterministic, no-DAW reproduction in one session.
Pulp has two offline harnesses, and they exercise different code paths:
| Harness | What it drives | Misses |
|---|---|---|
HeadlessHost (pulp/format/headless.hpp) | the Processor directly (DSP) | the entire format adapter, the host↔store sync, the render thread, Globals() parameter store |
Standalone AU host probe (AudioComponentInstanceNew + AudioUnitRender) | the real .component through CoreAudio | nothing — it is the real adapter + render path |
A bug that only shows up in a DAW is, by definition, in something HeadlessHost
skips — the adapter, the parameter-sync, threading, or a dynamic DSP path the
static scene tests never hit. Reproduce at the adapter layer first, then
bisect downward into the Processor once you can trigger it.
These were built deliberately for exactly this. Reaching for a hand-written
rms_db/peak loop instead is how a −39 dB ghost ships past a lenient threshold.
Before writing any audio reproduction or assertion, use one of these:
| Need | Use (already built) | Where |
|---|---|---|
| Metrics from a rendered buffer (peak/RMS/dBFS, clip, silence, NaN/Inf) | pulp::audio::analysis::analyze() → BufferMetrics | pulp/audio/analysis/audio_metrics.hpp (link pulp::audio-analysis) |
| Pass/fail gates in a test | assert_not_silent, assert_no_nan_inf, assert_rms_between, assert_peak_between, assert_frequency_near, assert_null_near, assert_channels_independent | pulp/audio/analysis/audio_assertions.hpp |
| Fundamental-frequency check | estimate_frequency() / assert_frequency_near() | audio_metrics.hpp / audio_assertions.hpp |
| Human-readable buffer report | summarize(metrics) | audio_metrics.hpp |
| Glitch/artifact + "audio doctor" detection | audio_artifacts.hpp, audio_doctor_artifacts.hpp | tools/audio/analysis/ |
| Command-line: render/inspect/compare a WAV without writing C++ | pulp audio validate <summarize|doctor|compare|assert> (assert checks: no_nan_inf, not_silent, silent, peak_below, frequency_near) | tools/cli/cmd_audio_validate.cpp |
| Live per-callback metering / MIDI log / buffer-underrun capture | pulp::inspect::AudioInspector | inspect/include/pulp/inspect/audio_inspector.hpp |
| Interactive inspection from an agent session | the pulp_inspect_audio MCP tool (the Audio Inspector) | MCP |
Rule of thumb: if you typed std::sqrt, * x[i], or 20.0 * log10 in a test,
stop — analyze() already did it correctly. The only thing you author is the
stimulus (what signal, what parameter motion) and the gate
(assert_*(...).passed).
Fast, deterministic, no Apple frameworks. Use HeadlessHost + the offline audio
analysis helpers. Pattern (see tests/test_scenes.cpp in a plugin repo):
#include <pulp/format/headless.hpp>
#include <pulp/audio/analysis/audio_metrics.hpp>
#include <pulp/audio/analysis/audio_assertions.hpp>
pulp::format::HeadlessHost host(create_my_processor);
host.prepare(48000.0, 512);
// render blocks, optionally mutating params per block via host.state().set_value(...)
auto m = pulp::audio::analysis::analyze(out_view, 48000.0);
REQUIRE(pulp::audio::analysis::assert_not_silent(m).passed); // the gate that matters
Always use pulp/audio/analysis — never hand-roll rms_db. analyze() →
BufferMetrics; assert with assert_not_silent, assert_no_nan_inf,
assert_rms_between, assert_frequency_near, assert_null_near,
assert_channels_independent. These encode the right thresholds (e.g. the
silence floor) so a test can't "pass" on a −39 dB ghost the way a lenient
hand-written > -50 dB check did.
When Tier 1 is green but the DAW still fails, load the real component and
drive it like a host. The reference implementation is
bendr-pulp/tools/au_host_probe.cpp — copy and adapt it. It:
AudioComponentFindNext by {type, subtype, manufacturer} (read these from
the built bundle: PlistBuddy -c "Print :AudioComponents:0:...").AudioComponentInstanceNew → AudioUnitInitialize with a stream format +
MaximumFramesPerSlice + an input render callback that supplies a tone.AudioUnitRender while the main thread mutates a parameter — either via
AudioUnitSetParameter (host path) or, to faithfully mimic the plugin's own
editor, by fetching the StateStore through the editor-context property
(kPulpEditorContextProperty = 'PuEd', struct {Processor*, StateStore*})
and running a real pulp::state::ParameterEdit gesture.Build/run (the probe does offline render to memory — no speakers, so no audio-etiquette concern):
cp -R build/AU/MyPlug.component ~/Library/Audio/Plug-Ins/Components/
rm -rf ~/Library/Caches/AudioUnitCache; killall -9 AudioComponentRegistrar
./build/au-host-probe # exits non-zero if the bug reproduces
This is the tool to reach for whenever someone says "can we automate this instead of me testing in Logic." It works on any Pulp AU plugin.
In Logic, multiple AUs share one AUHostingServiceXPC process. If touching
plugin A reliably kills plugin B's audio (e.g. the upstream software
instrument dies until reopened), suspect a crash of the shared process,
not a stall — and the guilty binary can be a different plugin than the one
being touched. Before bisecting anything:
ls -t ~/Library/Logs/DiagnosticReports/ | head # AUHostingServiceXPC_*.ips
Parse the .ips: the asiBacktraces field holds the original throw-site
backtrace with the guilty image name. Two gotchas: macOS throttles duplicate
crash reports (repeat wedges may not write new files), and an uncaught
ObjC exception in drawRect: is fatal to the whole process (AppKit
_crashOnException) — one plugin's paint bug silences every plugin in the
process. A multi-day "wedge" hunt across params/DSP/GPU was solved in minutes
once the crash log was read. (SDK hardening from that incident: UTF-8-safe
text_x_for_byte, ns_string_never_nil in the CG canvas, and a @try/@catch
guard around the plugin-view paint.)
Once reproduced, the single most localizing fact for a cutout is: is the host still calling render, and is input vs output silent?
os_log/runtime::log does not surface from Logic's sandboxed
AUHostingService process. To watch the live render path in-host, write a
throttled trace to a file (/tmp/...) from ProcessBufferLists and
tail -f it over SSH. Better: reproduce in the probe and read state directly.
The single highest-leverage diagnostic when "audio breaks when I touch a control": render at a sweep of STATIC values across the parameter's full range, measure the output LEVEL at each, and toggle each related mode on/off. The differential pattern localizes the broken stage in one table:
for (bool mode_on : {true, false})
for (float v : {0.f, 3.f, 6.f, 9.f, 12.f}) {
HeadlessHost host(create_my_processor); host.prepare(48000, 512);
host.state().set_value(kSomeMode, mode_on); host.state().set_value(kParam, v);
auto out = render(host, tone);
auto m = pulp::audio::analysis::analyze(out_view, 48000);
printf("mode=%d param=%.1f -> %.2f dBFS\n", mode_on, v, m.max_rms());
}
A real worked example: "audio cuts out when I change a parameter" was NOT a threading/adapter bug and NOT a rate-of-change latch (two wrong theories that cost hours). The level sweep showed instantly:
preserve=1 pitch= 0 st -> -9 dB preserve=0 pitch= 0 st -> -9 dB
preserve=1 pitch= 6 st -> -34 dB preserve=0 pitch= 6 st -> -9 dB
preserve=1 pitch=7.2 st -> -39 dB preserve=0 pitch=7.2 st -> -9 dB
→ a static level collapse confined to the formant-preserve path,
progressive with pitch. That pinned it to SpectralEnvelopeShifter (the
envelope correction wasn't energy-preserving, so a narrow-band tone was scaled
by the falling envelope above its peak). One sweep replaced days of guessing.
Measure LEVEL, never only frequency. The bug above rendered the correct
frequency the whole time — every existing scene checked peak_hz / frequency
and passed at −39 dB. assert_frequency_near alone is a trap; pair it with
assert_rms_between / assert_not_silent.
Beyond the static sweep, for every continuous parameter add a rapid-jump scene (change every 1–2 blocks, then settle and assert recovery) and a rapid-toggle scene for booleans — these catch genuine latching state (an edge handler or accumulator that self-sustains). Run the AU host probe in CI gating on output staying alive across scripted parameter automation; it exercises the adapter+render path no headless scene can.
If you discover a failure mode, add the scene to test_scenes.cpp AND extend
the probe so it can never regress.
SpectralEnvelopeShifter, found by the level sweep above,
fixed with energy-preserving normalization. Frequency-only tests missed it.GetParameter → store pull clobbered UI writes. Tier-2;
fixed by a host↔store reconcile. See au-param-host-store-clobber.AUEventListenerNotify stalling Logic's render thread on
every param change. Tier-2; never call it from ProcessBufferLists.npx claudepluginhub danielraffel/pulp --plugin pulpProvides deterministic audio signal generators, metrics, and assertions for proving and debugging what a Pulp Processor emits. Use for proving DSP contracts, measuring THD/frequency response, or investigating silence/distortion signal issues without a device or speakers.
Validates VST3 (.vst3) and AudioUnit (.component) plugins using pluginval at configurable strictness levels. Activates on 'run pluginval', 'validate plugin', or cross-format testing requests. macOS/Windows/Linux; AudioUnit macOS-only.
Pre-mix audio analysis and problem detection for audio engineering. Runs Phantom MCP diagnostic tools on stems, catalogs issues by severity (dealbreaker/significant/moderate/minor), identifies frequency masking between stems, and produces a structured mix brief. Use this skill whenever the user wants to analyze audio stems or files before mixing, diagnose audio problems (phase issues, clipping, noise, hum, mud, harshness), assess recording quality, prepare a mix session overview, check if a mix is ready for mastering, or investigate why something "sounds wrong." Also use when the user provides WAV file paths and asks for analysis, quality checks, or problem identification -- even if they don't explicitly mention "diagnostics."