this repo has no description
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}