Skip to content

Commit 57b511f

Browse files
wthollidayclaude
andcommitted
Add Sallen-Key LPF to the benchmark suite
Standalone Lyte, Lua, and C implementations of a Sallen-Key low-pass filter processing 2M samples with per-sample coefficient recomputation (exp + tan per sample). fc is modulated slightly by the input signal to prevent the C optimizer from hoisting the coefficient computation out of the loop. Wired into benchmark/run.sh as the fourth benchmark row (SK LPF). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e7c7211 commit 57b511f

5 files changed

Lines changed: 209 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ benchmark/fft_c_o2
3030
benchmark/fft_c_o3
3131
benchmark/sort_c_o2
3232
benchmark/sort_c_o3
33+
benchmark/sk_lpf_c_o2
34+
benchmark/sk_lpf_c_o3
3335
benchmark/freeverb_bench
3436

3537
vscode-lyte/node_modules/

benchmark/run.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ cc -O2 -o benchmark/sort_c_o2 benchmark/sort.c -lm
3535
cc -O3 -o benchmark/sort_c_o3 benchmark/sort.c -lm
3636
cc -O2 -o benchmark/fft_c_o2 benchmark/fft.c -lm
3737
cc -O3 -o benchmark/fft_c_o3 benchmark/fft.c -lm
38+
cc -O2 -o benchmark/sk_lpf_c_o2 benchmark/sk_lpf.c -lm
39+
cc -O3 -o benchmark/sk_lpf_c_o3 benchmark/sk_lpf.c -lm
3840

3941
if ! command -v lua &>/dev/null; then
4042
echo "Error: lua not found. Install with: brew install lua" >&2
@@ -160,6 +162,13 @@ run_benchmark "FFT" \
160162
"benchmark/fft.lua" \
161163
"1024-point FFT x 2000 iterations"
162164

165+
run_benchmark "SK LPF" \
166+
"benchmark/sk_lpf.lyte" \
167+
"benchmark/sk_lpf_c_o2" \
168+
"benchmark/sk_lpf_c_o3" \
169+
"benchmark/sk_lpf.lua" \
170+
"Sallen-Key LPF, 2M samples with per-sample coefficients"
171+
163172
if [ "$FAIL" = "1" ]; then
164173
echo ""
165174
echo "!! One or more benchmarks failed — see errors above" >&2

benchmark/sk_lpf.c

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Sallen-Key low-pass filter benchmark
2+
// Process 2 million samples with per-sample coefficient recomputation
3+
// Based on Andrew Simper's nodal filter equations (SkfLinearTrapOptimised2.pdf)
4+
5+
#include <math.h>
6+
#include <stdio.h>
7+
#include <time.h>
8+
9+
int main(void) {
10+
int n = 2000000;
11+
double inv_sr = 1.0 / 44100.0;
12+
double f_min = log(50.0);
13+
double f_max = log(16000.0);
14+
double f_range = f_max - f_min;
15+
16+
double ic1eq = 0.0;
17+
double ic2eq = 0.0;
18+
double last_y = 0.0;
19+
20+
double phase = 0.0;
21+
double freq = 440.0 / 44100.0;
22+
double two_pi = 2.0 * M_PI;
23+
24+
double fc_val = 0.5;
25+
double res_val = 0.3;
26+
27+
struct timespec t0, t1;
28+
clock_gettime(CLOCK_MONOTONIC, &t0);
29+
30+
for (int i = 0; i < n; i++) {
31+
double x = sin(phase * two_pi);
32+
phase += freq;
33+
if (phase > 1.0) phase -= 1.0;
34+
35+
double fc_norm = fc_val + 0.001 * x;
36+
if (fc_norm < 0.0) fc_norm = 0.0;
37+
if (fc_norm > 1.0) fc_norm = 1.0;
38+
double res_c = res_val;
39+
if (res_c < 0.0) res_c = 0.0;
40+
if (res_c > 1.0) res_c = 1.0;
41+
42+
double cutoff = exp(fc_norm * f_range + f_min);
43+
double g = tan(M_PI * cutoff * inv_sr);
44+
double k = 2.0 * res_c;
45+
double g1 = 1.0 + g;
46+
double a0 = 1.0 / (g1 * g1 - g * k);
47+
double a1 = k * a0;
48+
double a2 = g1 * a0;
49+
double a3 = g * a2;
50+
double a4 = 1.0 / g1;
51+
double a5 = g * a4;
52+
53+
double v1 = a1 * ic2eq + a2 * ic1eq + a3 * x;
54+
double v2 = a4 * ic2eq + a5 * v1;
55+
56+
ic1eq = 2.0 * (v1 - k * v2) - ic1eq;
57+
ic2eq = 2.0 * v2 - ic2eq;
58+
59+
last_y = v2;
60+
}
61+
62+
clock_gettime(CLOCK_MONOTONIC, &t1);
63+
double elapsed = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) * 1e-9;
64+
fprintf(stderr, "exec: %.3fs\n", elapsed);
65+
printf("%d samples, last_y=%f\n", n, last_y);
66+
return 0;
67+
}

benchmark/sk_lpf.lua

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
-- Sallen-Key low-pass filter benchmark
2+
-- Process 2 million samples with per-sample coefficient recomputation
3+
-- Based on Andrew Simper's nodal filter equations (SkfLinearTrapOptimised2.pdf)
4+
5+
local sin = math.sin
6+
local exp = math.exp
7+
local tan = math.tan
8+
local log = math.log
9+
local pi = math.pi
10+
11+
local function run()
12+
local n = 2000000
13+
local inv_sr = 1.0 / 44100.0
14+
local f_min = log(50.0)
15+
local f_max = log(16000.0)
16+
local f_range = f_max - f_min
17+
18+
local ic1eq = 0.0
19+
local ic2eq = 0.0
20+
local last_y = 0.0
21+
22+
local phase = 0.0
23+
local freq = 440.0 / 44100.0
24+
local two_pi = 2.0 * pi
25+
26+
local fc_val = 0.5
27+
local res_val = 0.3
28+
29+
for i = 1, n do
30+
-- generate input sample
31+
local x = sin(phase * two_pi)
32+
phase = phase + freq
33+
if phase > 1.0 then phase = phase - 1.0 end
34+
35+
-- clamp controls (modulate fc slightly so coefficients vary per sample)
36+
local fc_norm = fc_val + 0.001 * x
37+
if fc_norm < 0.0 then fc_norm = 0.0 end
38+
if fc_norm > 1.0 then fc_norm = 1.0 end
39+
local res_c = res_val
40+
if res_c < 0.0 then res_c = 0.0 end
41+
if res_c > 1.0 then res_c = 1.0 end
42+
43+
-- map normalised fc to log frequency and compute coefficients
44+
local cutoff = exp(fc_norm * f_range + f_min)
45+
local g = tan(pi * cutoff * inv_sr)
46+
local k = 2.0 * res_c
47+
local g1 = 1.0 + g
48+
local a0 = 1.0 / (g1 * g1 - g * k)
49+
local a1 = k * a0
50+
local a2 = g1 * a0
51+
local a3 = g * a2
52+
local a4 = 1.0 / g1
53+
local a5 = g * a4
54+
55+
-- trapezoidal integrators
56+
local v1 = a1 * ic2eq + a2 * ic1eq + a3 * x
57+
local v2 = a4 * ic2eq + a5 * v1
58+
59+
ic1eq = 2.0 * (v1 - k * v2) - ic1eq
60+
ic2eq = 2.0 * v2 - ic2eq
61+
62+
last_y = v2
63+
end
64+
end
65+
66+
local t0 = os.clock()
67+
run()
68+
local elapsed = os.clock() - t0
69+
io.stderr:write(string.format("exec: %.3fs\n", elapsed))

benchmark/sk_lpf.lyte

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Sallen-Key low-pass filter benchmark
2+
// Process 2 million samples with per-sample coefficient recomputation
3+
// Based on Andrew Simper's nodal filter equations (SkfLinearTrapOptimised2.pdf)
4+
5+
main {
6+
var n = 2000000
7+
var inv_sr = 1.0 / 44100.0
8+
var f_min = ln(50.0)
9+
var f_max = ln(16000.0)
10+
var f_range = f_max - f_min
11+
12+
var ic1eq = 0.0
13+
var ic2eq = 0.0
14+
var last_y = 0.0
15+
16+
var phase = 0.0
17+
var freq = 440.0 / 44100.0
18+
var two_pi = 2.0 * 3.14159265
19+
20+
var fc_val = 0.5
21+
var res_val = 0.3
22+
23+
for i in 0 .. n {
24+
// generate input sample
25+
var x = sin(phase * two_pi)
26+
phase = phase + freq
27+
if phase > 1.0 { phase = phase - 1.0 }
28+
29+
// clamp controls (modulate fc slightly so coefficients vary per sample)
30+
var fc_norm = fc_val + 0.001 * x
31+
if fc_norm < 0.0 { fc_norm = 0.0 }
32+
if fc_norm > 1.0 { fc_norm = 1.0 }
33+
var res_c = res_val
34+
if res_c < 0.0 { res_c = 0.0 }
35+
if res_c > 1.0 { res_c = 1.0 }
36+
37+
// map normalised fc to log frequency and compute coefficients
38+
var cutoff = exp(fc_norm * f_range + f_min)
39+
var g = tan(3.14159265 * cutoff * inv_sr)
40+
var k = 2.0 * res_c
41+
var g1 = 1.0 + g
42+
var a0 = 1.0 / (g1 * g1 - g * k)
43+
var a1 = k * a0
44+
var a2 = g1 * a0
45+
var a3 = g * a2
46+
var a4 = 1.0 / g1
47+
var a5 = g * a4
48+
49+
// trapezoidal integrators
50+
var v1 = a1 * ic2eq + a2 * ic1eq + a3 * x
51+
var v2 = a4 * ic2eq + a5 * v1
52+
53+
ic1eq = 2.0 * (v1 - k * v2) - ic1eq
54+
ic2eq = 2.0 * v2 - ic2eq
55+
56+
last_y = v2
57+
}
58+
59+
print(last_y as i32)
60+
print(n)
61+
println(" samples processed")
62+
}

0 commit comments

Comments
 (0)