//! biquad filter - the fundamental building block for digital audio effects. //! //! a biquad is a second-order IIR filter defined by 5 coefficients. //! by choosing different coefficients, you get different filter types: //! lowpass, highpass, bandpass, notch, allpass. //! //! inspired by torvalds/AudioNoise const std = @import("std"); /// filter coefficients (determines the filter's frequency response) pub const Coefficients = struct { b0: f32, b1: f32, b2: f32, a1: f32, a2: f32, }; /// filter state (remembers previous samples) pub const State = struct { w1: f32 = 0, w2: f32 = 0, }; /// process one sample through the filter (direct form II transposed) pub fn step(c: Coefficients, s: *State, x0: f32) f32 { const w0 = x0 - c.a1 * s.w1 - c.a2 * s.w2; const y0 = c.b0 * w0 + c.b1 * s.w1 + c.b2 * s.w2; s.w2 = s.w1; s.w1 = w0; return y0; } /// lowpass filter - passes frequencies below cutoff, attenuates above pub fn lowpass(freq: f32, q: f32, sample_rate: f32) Coefficients { const w0 = 2.0 * std.math.pi * freq / sample_rate; const cos_w0 = @cos(w0); const sin_w0 = @sin(w0); const alpha = sin_w0 / (2.0 * q); const a0_inv = 1.0 / (1.0 + alpha); const b1 = (1.0 - cos_w0) * a0_inv; return .{ .b0 = b1 / 2.0, .b1 = b1, .b2 = b1 / 2.0, .a1 = -2.0 * cos_w0 * a0_inv, .a2 = (1.0 - alpha) * a0_inv, }; } /// highpass filter - passes frequencies above cutoff, attenuates below pub fn highpass(freq: f32, q: f32, sample_rate: f32) Coefficients { const w0 = 2.0 * std.math.pi * freq / sample_rate; const cos_w0 = @cos(w0); const sin_w0 = @sin(w0); const alpha = sin_w0 / (2.0 * q); const a0_inv = 1.0 / (1.0 + alpha); const b1 = (1.0 + cos_w0) * a0_inv; return .{ .b0 = b1 / 2.0, .b1 = -b1, .b2 = b1 / 2.0, .a1 = -2.0 * cos_w0 * a0_inv, .a2 = (1.0 - alpha) * a0_inv, }; } /// bandpass filter - passes frequencies around center, attenuates others pub fn bandpass(freq: f32, q: f32, sample_rate: f32) Coefficients { const w0 = 2.0 * std.math.pi * freq / sample_rate; const cos_w0 = @cos(w0); const sin_w0 = @sin(w0); const alpha = sin_w0 / (2.0 * q); const a0_inv = 1.0 / (1.0 + alpha); return .{ .b0 = alpha * a0_inv, .b1 = 0, .b2 = -alpha * a0_inv, .a1 = -2.0 * cos_w0 * a0_inv, .a2 = (1.0 - alpha) * a0_inv, }; } /// notch filter - attenuates frequencies around center, passes others pub fn notch(freq: f32, q: f32, sample_rate: f32) Coefficients { const w0 = 2.0 * std.math.pi * freq / sample_rate; const cos_w0 = @cos(w0); const sin_w0 = @sin(w0); const alpha = sin_w0 / (2.0 * q); const a0_inv = 1.0 / (1.0 + alpha); return .{ .b0 = 1.0 * a0_inv, .b1 = -2.0 * cos_w0 * a0_inv, .b2 = 1.0 * a0_inv, .a1 = -2.0 * cos_w0 * a0_inv, .a2 = (1.0 - alpha) * a0_inv, }; } /// allpass filter - passes all frequencies but shifts phase (used in phasers) pub fn allpass(freq: f32, q: f32, sample_rate: f32) Coefficients { const w0 = 2.0 * std.math.pi * freq / sample_rate; const cos_w0 = @cos(w0); const sin_w0 = @sin(w0); const alpha = sin_w0 / (2.0 * q); const a0_inv = 1.0 / (1.0 + alpha); return .{ .b0 = (1.0 - alpha) * a0_inv, .b1 = -2.0 * cos_w0 * a0_inv, .b2 = 1.0, // same as a0 .a1 = -2.0 * cos_w0 * a0_inv, .a2 = (1.0 - alpha) * a0_inv, }; } // tests test "lowpass attenuates high frequencies" { const lpf = lowpass(1000, 0.707, 48000); var state: State = .{}; // feed in a high frequency signal (10kHz sampled at 48kHz = ~5 samples per cycle) var max_output: f32 = 0; for (0..1000) |i| { const phase = @as(f32, @floatFromInt(i)) * 2.0 * std.math.pi * 10000.0 / 48000.0; const input = @sin(phase); const output = step(lpf, &state, input); max_output = @max(max_output, @abs(output)); } // high frequency should be heavily attenuated (well below 0.5) try std.testing.expect(max_output < 0.2); } test "lowpass passes low frequencies" { const lpf = lowpass(1000, 0.707, 48000); var state: State = .{}; // feed in a low frequency signal (100Hz) var max_output: f32 = 0; for (0..1000) |i| { const phase = @as(f32, @floatFromInt(i)) * 2.0 * std.math.pi * 100.0 / 48000.0; const input = @sin(phase); const output = step(lpf, &state, input); if (i > 100) { // skip transient max_output = @max(max_output, @abs(output)); } } // low frequency should pass through (close to 1.0) try std.testing.expect(max_output > 0.8); }