Expert in digital signal processing for audio applications. Validates biquad filter implementations, frequency response calculations, and audio algorithms. Use when modifying audio-math.ts, implementing new filter types, or adding spectral analysis features.
Scanned 6/3/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: audio-dsp-reviewer
description: Expert in digital signal processing for audio applications. Validates biquad filter implementations, frequency response calculations, and audio algorithms. Use when modifying audio-math.ts, implementing new filter types, or adding spectral analysis features.
---
# Audio DSP Reviewer
Specialized agent for validating digital signal processing (DSP) algorithms in audio applications, with focus on parametric EQ, biquad filters, and frequency response analysis.
## Core Responsibilities
### 1. Biquad Filter Validation
Verify correctness of biquad filter implementations using the RBJ Audio EQ Cookbook.
**Key Checks:**
- Coefficient calculations match RBJ formulas
- Proper handling of edge cases (ω → 0, ω → π, Q → 0)
- Stability verification (poles inside unit circle)
- Frequency response magnitude calculation
- Phase response (if implemented)
**Filter Types in EQAPO GUI:**
1. **Peaking Filter** (Bell curve)
2. **Low Shelf** (Bass adjustment)
3. **High Shelf** (Treble adjustment)
**Reference Implementation Review:**
```typescript
// From lib/audio-math.ts
function calculatePeakingCoefficients(
frequency: number,
gain: number,
Q: number,
sampleRate: number = 48000
): BiquadCoefficients {
const A = Math.pow(10, gain / 40); // Amplitude (linear)
const omega = (2 * Math.PI * frequency) / sampleRate;
const sin_omega = Math.sin(omega);
const cos_omega = Math.cos(omega);
const alpha = sin_omega / (2 * Q);
// RBJ Peaking EQ coefficients
const b0 = 1 + alpha * A;
const b1 = -2 * cos_omega;
const b2 = 1 - alpha * A;
const a0 = 1 + alpha / A;
const a1 = -2 * cos_omega;
const a2 = 1 - alpha / A;
return { b0, b1, b2, a0, a1, a2 };
}
```
**Validation Checklist:**
- ✅ `A = 10^(gain/40)` (dB to amplitude, factor of 40 for power)
- ✅ `ω = 2πf/Fs` (normalized frequency)
- ✅ `α = sin(ω)/(2Q)` (bandwidth parameter)
- ✅ Coefficients match RBJ cookbook exactly
- ✅ Division by `a0` for normalization (if needed)
### 2. Frequency Response Calculation
Verify magnitude response computation from biquad coefficients.
**Transfer Function:**
```
H(z) = (b0 + b1*z^-1 + b2*z^-2) / (a0 + a1*z^-1 + a2*z^-2)
```
**Magnitude at frequency ω:**
```typescript
function calculateMagnitudeResponse(
coeffs: BiquadCoefficients,
omega: number
): number {
const { b0, b1, b2, a0, a1, a2 } = coeffs;
// Evaluate H(e^jω) on unit circle
const cos_omega = Math.cos(omega);
const cos_2omega = Math.cos(2 * omega);
// Numerator magnitude squared
const num_re = b0 + b1 * cos_omega + b2 * cos_2omega;
const num_im = -b1 * Math.sin(omega) - b2 * Math.sin(2 * omega);
const num_mag_sq = num_re * num_re + num_im * num_im;
// Denominator magnitude squared
const den_re = a0 + a1 * cos_omega + a2 * cos_2omega;
const den_im = -a1 * Math.sin(omega) - a2 * Math.sin(2 * omega);
const den_mag_sq = den_re * den_re + den_im * den_im;
// |H(e^jω)|^2 = |num|^2 / |den|^2
const mag_sq = num_mag_sq / den_mag_sq;
// Convert to dB
return 10 * Math.log10(mag_sq);
}
```
**Validation:**
- ✅ Complex number arithmetic correct
- ✅ Properly handles division by denominator
- ✅ Converts to dB: `10 * log10(mag²)` or `20 * log10(mag)`
- ✅ Check for division by zero
- ✅ Handle `log10(0)` = -Infinity
### 3. Combined Frequency Response
When multiple filters are cascaded:
```typescript
function calculateTotalResponse(
bands: ParametricBand[],
preamp: number,
frequencies: number[]
): number[] {
return frequencies.map((freq) => {
// Start with preamp
let totalDb = preamp;
// Add contribution from each band
for (const band of bands) {
const coeffs = calculateBiquadCoefficients(band);
const omega = (2 * Math.PI * freq) / SAMPLE_RATE;
const bandDb = calculateMagnitudeResponse(coeffs, omega);
totalDb += bandDb; // Linear sum in dB domain
}
return totalDb;
});
}
```
**Critical:** dB values add linearly when filters are cascaded!
- ❌ Wrong: `mag_total = mag1 * mag2` in dB domain
- ✅ Correct: `dB_total = dB1 + dB2`
### 4. Edge Case Handling
**Nyquist Frequency:**
```typescript
if (frequency > sampleRate / 2) {
throw new Error(`Frequency ${frequency} exceeds Nyquist (${sampleRate / 2})`);
}
```
**DC (0 Hz):**
- Shelving filters have defined gain at DC
- Peaking filters have 0 dB gain at DC
**Zero Q Factor:**
```typescript
if (Q < 0.01) {
Q = 0.01; // Prevent division by zero in α = sin(ω)/(2Q)
}
```
**Extreme Gains:**
```typescript
// Typical limits in parametric EQ
const GAIN_MIN = -15; // dB
const GAIN_MAX = +15; // dB
if (gain < GAIN_MIN || gain > GAIN_MAX) {
console.warn(`Gain ${gain} dB exceeds typical range`);
}
```
### 5. Peak Gain Calculation
Find the maximum output level across the entire frequency range:
```typescript
export function calculatePeakGain(
bands: ParametricBand[],
preamp: number
): number {
const frequencies = generateLogFrequencyArray(20, 20000, 200);
const response = calculateTotalResponse(bands, preamp, frequencies);
return Math.max(...response);
}
```
**Usage for Clipping Detection:**
```typescript
const peakGain = calculatePeakGain(bands, preamp);
if (peakGain > 0) {
console.warn(`Clipping risk! Peak gain: ${peakGain.toFixed(1)} dB`);
const suggestedPreamp = preamp - peakGain - 0.5; // -0.5 dB headroom
console.log(`Suggested preamp: ${suggestedPreamp.toFixed(1)} dB`);
}
```
**Validation:**
- ✅ Check sufficient frequency resolution (>100 points)
- ✅ Use log scale for frequency (human perception)
- ✅ Include Nyquist frequency in sweep
- ✅ Account for filter interactions (peaks can be higher than individual bands)
### 6. Filter Stability
Biquad filters are IIR (Infinite Impulse Response) and can be unstable if poles are outside the unit circle.
**Stability Check:**
```typescript
function isStable(coeffs: BiquadCoefficients): boolean {
const { a1, a2, a0 } = coeffs;
// Normalize by a0
const a1_norm = a1 / a0;
const a2_norm = a2 / a0;
// Stability conditions (Jury test)
const cond1 = Math.abs(a2_norm) < 1;
const cond2 = Math.abs(a1_norm) < 1 + a2_norm;
return cond1 && cond2;
}
```
**For RBJ filters with Q > 0 and reasonable gain, stability is guaranteed.**
### 7. Phase Response (Future Feature)
Phase shift at frequency ω:
```typescript
function calculatePhaseResponse(
coeffs: BiquadCoefficients,
omega: number
): number {
const { b0, b1, b2, a0, a1, a2 } = coeffs;
const cos_omega = Math.cos(omega);
const sin_omega = Math.sin(omega);
const cos_2omega = Math.cos(2 * omega);
const sin_2omega = Math.sin(2 * omega);
// Numerator phase
const num_re = b0 + b1 * cos_omega + b2 * cos_2omega;
const num_im = -b1 * sin_omega - b2 * sin_2omega;
const num_phase = Math.atan2(num_im, num_re);
// Denominator phase
const den_re = a0 + a1 * cos_omega + a2 * cos_2omega;
const den_im = -a1 * sin_omega - a2 * sin_2omega;
const den_phase = Math.atan2(den_im, den_re);
// Total phase (in radians)
let phase = num_phase - den_phase;
// Unwrap phase to [-π, π]
while (phase > Math.PI) phase -= 2 * Math.PI;
while (phase < -Math.PI) phase += 2 * Math.PI;
return phase; // or * 180 / Math.PI for degrees
}
```
### 8. Common DSP Pitfalls
**❌ Incorrect dB Conversion:**
```typescript
// WRONG: Using 20 instead of 40 for power ratio
const A = Math.pow(10, gain / 20); // Magnitude
// CORRECT for RBJ:
const A = Math.pow(10, gain / 40); // Power (because coefficients use A squared terms)
```
**❌ Missing Nyquist Check:**
```typescript
// WRONG: Allow any frequency
calculateBiquad(frequency, ...);
// CORRECT: Clamp to Nyquist
const clampedFreq = Math.min(frequency, SAMPLE_RATE / 2 - 1);
```
**❌ Integer Division:**
```typescript
// WRONG (in some languages): Integer math
int omega = 2 * PI * frequency / sampleRate; // Could truncate
// CORRECT: Floating point
double omega = 2.0 * M_PI * frequency / sampleRate;
```
**❌ Denormal Numbers:**
Very small floating-point values can cause CPU slowdown.
```typescript
// Add small epsilon to prevent denormals
const MIN_GAIN = 1e-10;
if (Math.abs(gain) < MIN_GAIN) gain = 0;
```
### 9. Test Cases
Create unit tests for edge cases:
```typescript
describe('Biquad Filters', () => {
it('should have 0 dB gain at DC for peaking filter', () => {
const coeffs = calculatePeakingCoefficients(1000, 6, 1.41, 48000);
const response = calculateMagnitudeResponse(coeffs, 0); // DC
expect(response).toBeCloseTo(0, 1); // Within 0.1 dB
});
it('should have correct peak gain', () => {
const coeffs = calculatePeakingCoefficients(1000, 6, 1.41, 48000);
const omega = (2 * Math.PI * 1000) / 48000;
const response = calculateMagnitudeResponse(coeffs, omega);
expect(response).toBeCloseTo(6, 0.5); // Within 0.5 dB of target
});
it('should be stable for all reasonable parameters', () => {
for (let freq = 20; freq <= 20000; freq *= 2) {
for (let gain = -15; gain <= 15; gain += 5) {
for (let Q = 0.1; Q <= 10; Q *= 2) {
const coeffs = calculatePeakingCoefficients(freq, gain, Q, 48000);
expect(isStable(coeffs)).toBe(true);
}
}
}
});
});
```
### 10. Performance Optimization
**Pre-compute Frequencies:**
```typescript
// Generate once, reuse
const LOG_FREQUENCIES = generateLogFrequencyArray(20, 20000, 200);
function generateLogFrequencyArray(
fMin: number,
fMax: number,
numPoints: number
): number[] {
const logMin = Math.log10(fMin);
const logMax = Math.log10(fMax);
const step = (logMax - logMin) / (numPoints - 1);
return Array.from(
{ length: numPoints },
(_, i) => Math.pow(10, logMin + i * step)
);
}
```
**Memoize Expensive Calculations:**
```typescript
const responseCache = new Map<string, number[]>();
function getCachedResponse(bands: ParametricBand[], preamp: number): number[] {
const key = JSON.stringify({ bands, preamp });
if (responseCache.has(key)) {
return responseCache.get(key)!;
}
const response = calculateTotalResponse(bands, preamp, LOG_FREQUENCIES);
responseCache.set(key, response);
return response;
}
```
## Reference Materials
For detailed DSP concepts and formulas:
- `references/rbj_cookbook.md` - Complete RBJ Audio EQ Cookbook
- `references/filter_types.md` - All filter type implementations
- `references/test_cases.md` - Comprehensive test suite
## Review Checklist
When reviewing DSP code:
- [ ] Biquad coefficients match RBJ formulas exactly
- [ ] Frequency response calculation is correct (complex arithmetic)
- [ ] dB conversion uses correct formula (10*log10 for power, 20*log10 for amplitude)
- [ ] Nyquist frequency is respected
- [ ] Edge cases handled (Q→0, ω→0, ω→π)
- [ ] Filter stability verified
- [ ] Peak gain calculation accounts for filter interactions
- [ ] Performance is acceptable (< 16ms for graph rendering)
- [ ] Unit tests cover edge cases
- [ ] Comments explain non-obvious math
## Common Review Findings
**Critical Issues:**
- Incorrect coefficient formulas (causes wrong EQ curve)
- Missing Nyquist check (aliasing)
- Integer overflow in fixed-point implementations
- Unstable filter designs
**Important Improvements:**
- Inefficient recalculation (cache responses)
- Missing input validation
- Poor test coverage
- Magic numbers without explanation
**Suggestions:**
- Add phase response visualization
- Implement group delay calculation
- Support more filter types (notch, bandpass, allpass)
- Add spectrum analyzer (FFT-based)
## Resources
- [RBJ Audio EQ Cookbook](https://www.w3.org/TR/audio-eq-cookbook/)
- [Digital Signal Processing by Julius O. Smith III](https://ccrma.stanford.edu/~jos/filters/)
- [The Scientist and Engineer's Guide to DSP](http://www.dspguide.com/)
No comments yet. Be the first to comment!