Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
142 lines (140 loc) · 4.19 KB
/
Copy pathindex.js
File metadata and controls
142 lines (140 loc) · 4.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// 256 channels, 512 Hz sampling, continuous 128-sample blocks with retained filter state.
import { WebGPUjs } from "../../../src/pipeline";
import { generate, biquad, display } from "./shaders.js";
// Normalized RBJ biquad coefficients: https://www.w3.org/TR/audio-eq-cookbook/
export function coefficients(type, frequency, q, sampleRate = 512) {
const omega = (2 * Math.PI * frequency) / sampleRate,
c = Math.cos(omega),
alpha = Math.sin(omega) / (2 * q),
a0 = 1 + alpha;
let b0 = 1,
b1 = -2 * c,
b2 = 1;
if (type === "lowpass") {
b0 = (1 - c) / 2;
b1 = 1 - c;
b2 = b0;
}
if (type === "highpass") {
b0 = (1 + c) / 2;
b1 = -(1 + c);
b2 = b0;
}
return {
b0: b0 / a0,
b1: b1 / a0,
b2: b2 / a0,
a1: (-2 * c) / a0,
a2: (1 - alpha) / a0,
};
}
export async function setup(canvas, device) {
const channels = 256,
sampleCount = 128,
sampleRate = 512,
length = channels * sampleCount;
const filters = [
coefficients("notch", 60, 30),
coefficients("highpass", 3, Math.SQRT1_2),
coefficients("lowpass", 45, Math.SQRT1_2),
];
const chain = await WebGPUjs.createComputeChain(
[
{ shader: generate, inputs: { sampleCount, sampleRate } },
...filters.map((inputs, index) => ({
shader: biquad,
resources: {
source: ["signal", "notched", "highpassed"][index],
result: ["notched", "highpassed", "filtered"][index],
state: "state-" + index,
},
inputs: { sampleCount, ...inputs },
options: { computePass: { workgroupsX: 4 } },
})),
],
{ device },
);
const allocations = () => ({
signal: new Float32Array(length),
notched: new Float32Array(length),
highpassed: new Float32Array(length),
filtered: new Float32Array(length),
"state-0": new Float32Array(channels * 2),
"state-1": new Float32Array(channels * 2),
"state-2": new Float32Array(channels * 2),
});
await chain.run({ ...allocations(), blockIndex: 0, hum: 0.8 });
const displayPipeline = await WebGPUjs.combineShaders(
display,
{
canvas,
resourceBindings: {
original: { resource: "signal" },
notched: { resource: "notched" },
filtered: { resource: "filtered" },
},
renderPass: { vbos: [], vertexCount: 3 },
},
chain.passes.at(-1),
);
let blockIndex = 0,
last = 0,
hum = 0.8,
drawKey = "",
restart = false;
async function frame(time = 0, controls = {}) {
const nextHum = controls.hum ?? 0.8;
if (restart) {
blockIndex = 0;
last = time;
await chain.run({ ...allocations(), blockIndex, hum: nextHum });
restart = false;
} else if (time - last >= 250 || nextHum !== hum) {
last = time;
blockIndex++;
await chain.run({ blockIndex, hum: nextHum });
}
hum = nextHum;
const channel = Math.max(
0,
Math.min(255, Math.round(controls.channel ?? 1) - 1),
),
key = blockIndex + ":" + channel;
if (key === drawKey) return;
drawKey = key;
await displayPipeline.render({ inputs: { sampleCount, channel } });
}
return {
pipelines: [...chain.passes, displayPipeline],
chain,
frame,
filters,
sampleCount,
channels,
reset() {
restart = true;
drawKey = "";
},
get resultInfo() {
return `256 independent channels × 128 samples · 512 Hz sampling · block ${blockIndex}.\nCyan: input. Gold: 60 Hz notch. Green: notch → 3 Hz high-pass → 45 Hz low-pass.\nEach channel retains two state values per biquad between blocks. These are causal IIR sections, not periodic FFT masks.`;
},
};
}
export default {
id: "streaming-biquad",
title: "Streaming multichannel IIR filters",
description:
"A 60 Hz notch and 3–45 Hz bandpass process 256 sampled channels with persistent per-channel state across blocks. One invocation owns each channel; channels run in parallel.",
category: "Signal processing",
algorithm: true,
controls: [
["channel", "Channel", 1, 256, 1, 1],
["hum", "60 Hz interference", 0, 1.5, 0.05, 0.8],
],
legend: [
["#4dd8ff", "Raw signal"],
["#ffb840", "60 Hz notch"],
["#4dffa5", "3–45 Hz bandpass"],
],
setup,
};