--- 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(); 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/)