skills/catalog/audio/audio-dsp/SKILL.md
Use when implementing real-time DSP algorithms, audio thread architecture, or offline spectral analysis in C++/Rust. Covers filters, STFT pipelines, lock-free concurrency, and RT safety. Not for JUCE plugin lifecycle wiring or ffmpeg.
npx skillsauth add erikstmartin/dotfiles audio-dspInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Requires: audio-engineering-principles
Before writing any code, classify each component:
| Component | Thread | Allocation allowed? | |-----------|--------|---------------------| | DSP (filter, STFT) | Audio | No — preallocate in init | | Parameter smoothing | Audio | No | | SPSC push (events out) | Audio | No — fixed buffer | | Analysis, logging | Worker | Yes | | Result publishing | Worker | Yes |
RT constraints checklist (audio thread must pass ALL):
new/malloc/std::vector::push_backstd::lock_guardprintf/std::coutprepare()/prepareToPlay()Biquad filter (Direct Form II Transposed) — RT-safe:
struct Biquad {
float b0, b1, b2, a1, a2;
float z1 = 0, z2 = 0;
float process(float in) {
float out = b0 * in + z1;
z1 = b1 * in - a1 * out + z2;
z2 = b2 * in - a2 * out;
return out;
}
void setLowpass(float freq, float q, float sr) {
float w0 = 2.0f * M_PI * freq / sr;
float alpha = sinf(w0) / (2.0f * q);
float cosw0 = cosf(w0);
float a0 = 1.0f + alpha;
b0 = ((1.0f - cosw0) / 2.0f) / a0;
b1 = (1.0f - cosw0) / a0;
b2 = b0;
a1 = (-2.0f * cosw0) / a0;
a2 = (1.0f - alpha) / a0;
}
};
⚠️ Pitfall: Call setLowpass() only on the non-RT thread or in prepare(). Calling it mid-block without smoothing causes audible zipper noise.
STFT pipeline (offline/hybrid — allocation OK):
void analyzeSpectrum(const float* input, size_t numSamples, float sampleRate) {
constexpr size_t fftSize = 2048; // default; Hann window
constexpr size_t hopSize = 512;
std::vector<float> window(fftSize);
hannWindow(window.data(), fftSize);
for (size_t pos = 0; pos + fftSize <= numSamples; pos += hopSize) {
std::vector<float> windowed(input + pos, input + pos + fftSize);
applyWindow(windowed.data(), window.data(), fftSize);
auto magnitudes = computeFFT(windowed.data(), fftSize);
float centroid = spectralCentroid(magnitudes.data(), fftSize, sampleRate);
// store or process centroid per frame
}
}
STFT defaults (use unless specified):
Parameter smoothing (required for any modulated RT param):
struct SmoothedParam {
float current, target, coeff;
void prepare(float sr, float smoothMs) {
coeff = expf(-1.0f / (sr * smoothMs * 0.001f));
}
float next() {
current = current * coeff + target * (1.0f - coeff);
return current;
}
};
Use SPSC queue to pass data from audio thread → worker thread. Never share mutable state without atomics.
template<typename T, size_t Size>
struct SPSCQueue {
std::array<T, Size> buffer;
std::atomic<size_t> head{0}, tail{0};
bool push(const T& item) {
size_t h = head.load(std::memory_order_relaxed);
size_t next = (h + 1) % Size;
if (next == tail.load(std::memory_order_acquire)) return false; // full
buffer[h] = item;
head.store(next, std::memory_order_release);
return true;
}
bool pop(T& item) {
size_t t = tail.load(std::memory_order_relaxed);
if (t == head.load(std::memory_order_acquire)) return false; // empty
item = buffer[t];
tail.store((t + 1) % Size, std::memory_order_release);
return true;
}
};
Pattern: Audio thread pushes events/samples; worker thread pops and processes. Publish results via atomic pointer swap or immutable snapshots — never reverse the direction through a lock.
⚠️ Pitfall: push returning false (queue full) must be silently dropped on the audio thread — never block or retry.
setLowpass design.push returns false; drain fully, verify pop returns false.// Impulse response test example
void testLowpassImpulse() {
Biquad f;
f.setLowpass(1000.0f, 0.707f, 48000.0f);
std::vector<float> ir(512, 0.0f);
ir[0] = 1.0f;
for (auto& s : ir) s = f.process(s);
// Verify: DC gain ≈ 1.0, gain at Nyquist ≈ 0
assert(std::abs(ir[0]) < 2.0f); // not clipping
}
std::vector construction in the inner loop.-O2 -ffast-math).| Pitfall | Symptom | Fix |
|---------|---------|-----|
| Denormals | CPU spikes on silence | Add juce::ScopedNoDenormals or FTZ/DAZ flags; add DC offset (1e-25f) to filter state |
| DC offset accumulation | Low-frequency drift | Apply 1-pole highpass after filter chain; y = x - x_prev + 0.995f * y_prev |
| Allocation on audio thread | Dropouts under load | Profile with -fsanitize=thread; audit every new/container use in callback |
| Coefficient change without smoothing | Zipper noise on parameter change | Use SmoothedParam above for all user-visible knobs |
| SPSC overflow dropped silently | Missing events | Size queue for worst-case burst; monitor drop rate in worker |
| STFT phase discontinuity | Spectral smearing | Apply window before FFT; use 75% overlap for reconstruction |
testing
Use when creating new skills, editing existing skills, or verifying skills work before deployment
development
Use when you have a spec or requirements for a multi-step task, before touching code
data-ai
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
tools
Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions