this repo has no description
at main 158 lines 4.8 kB view raw
1//! biquad filter - the fundamental building block for digital audio effects. 2//! 3//! a biquad is a second-order IIR filter defined by 5 coefficients. 4//! by choosing different coefficients, you get different filter types: 5//! lowpass, highpass, bandpass, notch, allpass. 6//! 7//! inspired by torvalds/AudioNoise 8 9const std = @import("std"); 10 11/// filter coefficients (determines the filter's frequency response) 12pub const Coefficients = struct { 13 b0: f32, 14 b1: f32, 15 b2: f32, 16 a1: f32, 17 a2: f32, 18}; 19 20/// filter state (remembers previous samples) 21pub const State = struct { 22 w1: f32 = 0, 23 w2: f32 = 0, 24}; 25 26/// process one sample through the filter (direct form II transposed) 27pub fn step(c: Coefficients, s: *State, x0: f32) f32 { 28 const w0 = x0 - c.a1 * s.w1 - c.a2 * s.w2; 29 const y0 = c.b0 * w0 + c.b1 * s.w1 + c.b2 * s.w2; 30 s.w2 = s.w1; 31 s.w1 = w0; 32 return y0; 33} 34 35/// lowpass filter - passes frequencies below cutoff, attenuates above 36pub fn lowpass(freq: f32, q: f32, sample_rate: f32) Coefficients { 37 const w0 = 2.0 * std.math.pi * freq / sample_rate; 38 const cos_w0 = @cos(w0); 39 const sin_w0 = @sin(w0); 40 const alpha = sin_w0 / (2.0 * q); 41 const a0_inv = 1.0 / (1.0 + alpha); 42 const b1 = (1.0 - cos_w0) * a0_inv; 43 44 return .{ 45 .b0 = b1 / 2.0, 46 .b1 = b1, 47 .b2 = b1 / 2.0, 48 .a1 = -2.0 * cos_w0 * a0_inv, 49 .a2 = (1.0 - alpha) * a0_inv, 50 }; 51} 52 53/// highpass filter - passes frequencies above cutoff, attenuates below 54pub fn highpass(freq: f32, q: f32, sample_rate: f32) Coefficients { 55 const w0 = 2.0 * std.math.pi * freq / sample_rate; 56 const cos_w0 = @cos(w0); 57 const sin_w0 = @sin(w0); 58 const alpha = sin_w0 / (2.0 * q); 59 const a0_inv = 1.0 / (1.0 + alpha); 60 const b1 = (1.0 + cos_w0) * a0_inv; 61 62 return .{ 63 .b0 = b1 / 2.0, 64 .b1 = -b1, 65 .b2 = b1 / 2.0, 66 .a1 = -2.0 * cos_w0 * a0_inv, 67 .a2 = (1.0 - alpha) * a0_inv, 68 }; 69} 70 71/// bandpass filter - passes frequencies around center, attenuates others 72pub fn bandpass(freq: f32, q: f32, sample_rate: f32) Coefficients { 73 const w0 = 2.0 * std.math.pi * freq / sample_rate; 74 const cos_w0 = @cos(w0); 75 const sin_w0 = @sin(w0); 76 const alpha = sin_w0 / (2.0 * q); 77 const a0_inv = 1.0 / (1.0 + alpha); 78 79 return .{ 80 .b0 = alpha * a0_inv, 81 .b1 = 0, 82 .b2 = -alpha * a0_inv, 83 .a1 = -2.0 * cos_w0 * a0_inv, 84 .a2 = (1.0 - alpha) * a0_inv, 85 }; 86} 87 88/// notch filter - attenuates frequencies around center, passes others 89pub fn notch(freq: f32, q: f32, sample_rate: f32) Coefficients { 90 const w0 = 2.0 * std.math.pi * freq / sample_rate; 91 const cos_w0 = @cos(w0); 92 const sin_w0 = @sin(w0); 93 const alpha = sin_w0 / (2.0 * q); 94 const a0_inv = 1.0 / (1.0 + alpha); 95 96 return .{ 97 .b0 = 1.0 * a0_inv, 98 .b1 = -2.0 * cos_w0 * a0_inv, 99 .b2 = 1.0 * a0_inv, 100 .a1 = -2.0 * cos_w0 * a0_inv, 101 .a2 = (1.0 - alpha) * a0_inv, 102 }; 103} 104 105/// allpass filter - passes all frequencies but shifts phase (used in phasers) 106pub fn allpass(freq: f32, q: f32, sample_rate: f32) Coefficients { 107 const w0 = 2.0 * std.math.pi * freq / sample_rate; 108 const cos_w0 = @cos(w0); 109 const sin_w0 = @sin(w0); 110 const alpha = sin_w0 / (2.0 * q); 111 const a0_inv = 1.0 / (1.0 + alpha); 112 113 return .{ 114 .b0 = (1.0 - alpha) * a0_inv, 115 .b1 = -2.0 * cos_w0 * a0_inv, 116 .b2 = 1.0, // same as a0 117 .a1 = -2.0 * cos_w0 * a0_inv, 118 .a2 = (1.0 - alpha) * a0_inv, 119 }; 120} 121 122// tests 123 124test "lowpass attenuates high frequencies" { 125 const lpf = lowpass(1000, 0.707, 48000); 126 var state: State = .{}; 127 128 // feed in a high frequency signal (10kHz sampled at 48kHz = ~5 samples per cycle) 129 var max_output: f32 = 0; 130 for (0..1000) |i| { 131 const phase = @as(f32, @floatFromInt(i)) * 2.0 * std.math.pi * 10000.0 / 48000.0; 132 const input = @sin(phase); 133 const output = step(lpf, &state, input); 134 max_output = @max(max_output, @abs(output)); 135 } 136 137 // high frequency should be heavily attenuated (well below 0.5) 138 try std.testing.expect(max_output < 0.2); 139} 140 141test "lowpass passes low frequencies" { 142 const lpf = lowpass(1000, 0.707, 48000); 143 var state: State = .{}; 144 145 // feed in a low frequency signal (100Hz) 146 var max_output: f32 = 0; 147 for (0..1000) |i| { 148 const phase = @as(f32, @floatFromInt(i)) * 2.0 * std.math.pi * 100.0 / 48000.0; 149 const input = @sin(phase); 150 const output = step(lpf, &state, input); 151 if (i > 100) { // skip transient 152 max_output = @max(max_output, @abs(output)); 153 } 154 } 155 156 // low frequency should pass through (close to 1.0) 157 try std.testing.expect(max_output > 0.8); 158}