src/lib/birdnet.worker.ts
+45
-50
diff --git a/src/lib/birdnet.worker.ts b/src/lib/birdnet.worker.ts
index 7fe1611..9a55e1c 100644
@@ -84,25 +84,22 @@ class MelSpecLayerSimple extends tf.layers.Layer {
// ── Custom STFT WebGL kernel ───────────────────────────────────────────────
// Ported from https://github.com/birdnet-team/real-time-pwa (MIT)
function registerStftKernel() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const kernelFunc: tf.KernelFunc = (params: any) => {
const { backend, inputs } = params as {
backend: unknown;
inputs: { signal: unknown; frameLength: number; frameStep: number };
};
const b = backend as Record<string, (...args: unknown[]) => unknown>;
const { signal, frameLength, frameStep } = inputs;
const fl = frameLength as number;
const fs = frameStep as number;
const innerDim = fl / 2;
const log2Inner = Math.log2(innerDim);
const kernelFunc: tf.KernelFunc = ({ backend, inputs }) => {
const b = backend as Record<string, (...args: unknown[]) => unknown>;
const { signal, frameLength, frameStep } = inputs;
const fl = frameLength as unknown as number;
const fs = frameStep as unknown as number;
const innerDim = fl / 2;
const log2Inner = Math.log2(innerDim);
const batch =
((signal as unknown as { size: number }).size - fl + fs) / fs | 0;
// Stage 1: windowing + bit-reversal
let cur = b.runWebGLProgram(
{
variableNames: ["x"],
outputShape: [(signal as { size: number }).size],
userCode: `void main(){
// Stage 1: windowing + bit-reversal
let cur = b.runWebGLProgram(
{
variableNames: ["x"],
outputShape: [batch, fl],
userCode: `void main(){
ivec2 c=getOutputCoords();
int p=c[1]%${innerDim};
int k=0;
@@ -117,19 +114,19 @@ function registerStftKernel() {
float mul=0.5-0.5*cos(cosArg);
setOutput(val*mul);
}`,
} as unknown,
[signal],
"float32",
) as unknown;
} as unknown,
[signal],
"float32",
) as unknown;
// Stage 2: FFT butterflies
for (let len = 1; len < innerDim; len *= 2) {
const prev = cur;
cur = b.runWebGLProgram(
{
variableNames: ["x"],
outputShape: [innerDim * 2],
userCode: `void main(){
// Stage 2: FFT butterflies
for (let len = 1; len < innerDim; len *= 2) {
const prev = cur;
cur = b.runWebGLProgram(
{
variableNames: ["x"],
outputShape: [batch, innerDim * 2],
userCode: `void main(){
ivec2 c=getOutputCoords();
int b=c[0];
int i=c[1];
@@ -150,21 +147,19 @@ function registerStftKernel() {
setOutput(evenK_im + (oddK_re*bsin + oddK_im*a)*float(highSign));
}
}`,
} as unknown,
[prev],
"float32",
) as unknown;
(
b.disposeIntermediateTensorInfo as (t: unknown) => void
)(prev);
}
} as unknown,
[prev],
"float32",
) as unknown;
b.disposeIntermediateTensorInfo(prev);
}
// Stage 3: real RFFT output
const real = b.runWebGLProgram(
{
variableNames: ["x"],
outputShape: [innerDim + 1],
userCode: `void main(){
// Stage 3: real RFFT output
const real = b.runWebGLProgram(
{
variableNames: ["x"],
outputShape: [batch, innerDim + 1],
userCode: `void main(){
ivec2 c=getOutputCoords();
int b=c[0];
int i=c[1];
@@ -180,12 +175,12 @@ function registerStftKernel() {
float result=(Zk0+Zk_conj0 + cos(t)*diff1 + sin(t)*diff0)*0.5;
setOutput(result);
}`,
} as unknown,
[cur],
"float32",
) as unknown;
(b.disposeIntermediateTensorInfo as (t: unknown) => void)(cur);
return real as tf.TensorInfo;
} as unknown,
[cur],
"float32",
) as unknown;
b.disposeIntermediateTensorInfo(cur);
return real as tf.TensorInfo;
};
tf.registerKernel({ kernelName: "STFT", backendName: "webgl", kernelFunc });
}
src/routes/+layout.svelte
+2
-1
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 640d140..193ce01 100644
@@ -1,4 +1,5 @@
<script lang="ts">
import { resolve } from "$app/paths";
import favicon from "$lib/assets/favicon.svg";
let { children } = $props();
</script>
@@ -14,7 +15,7 @@
<nav>
<ul>
<li>
<a href="/" aria-label="Home">
<a href={resolve("/")} aria-label="Home">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path
d="M21 8.5c-2.1-.7-4.5.1-6.1 1.8L12 12 9.1 10.3C7.5 8.6 5.1 7.8 3 8.5c.9 1.9 2.7 3.4 4.8 3.8l-2.3.8C6.8 15 9.2 15.4 11 14l1-.5 1 .5c1.8 1.4 4.2 1 5.5-.9l-2.3-.8C18.3 11.9 20.1 10.4 21 8.5z"
src/routes/+page.svelte
+26
-16
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 4a5c565..70b81cd 100644
@@ -50,19 +50,24 @@
sourceNode = audioCtx.createMediaStreamSource(stream);
workletNode = new AudioWorkletNode(audioCtx, "audio-processor");
workletNode.port.addEventListener("message", (e: MessageEvent<Float32Array>) => {
const chunk = e.data;
const merged = new Float32Array(sampleBuffer.length + chunk.length);
merged.set(sampleBuffer);
merged.set(chunk, sampleBuffer.length);
sampleBuffer = merged;
if (sampleBuffer.length >= WINDOW_SAMPLES && worker && workerReady) {
const window = sampleBuffer.slice(0, WINDOW_SAMPLES);
sampleBuffer = sampleBuffer.slice(WINDOW_SAMPLES);
worker.postMessage({ type: "analyze", samples: window }, [window.buffer]);
}
});
workletNode.port.addEventListener(
"message",
(e: MessageEvent<Float32Array>) => {
const chunk = e.data;
const merged = new Float32Array(sampleBuffer.length + chunk.length);
merged.set(sampleBuffer);
merged.set(chunk, sampleBuffer.length);
sampleBuffer = merged;
if (sampleBuffer.length >= WINDOW_SAMPLES && worker && workerReady) {
const window = sampleBuffer.slice(0, WINDOW_SAMPLES);
sampleBuffer = sampleBuffer.slice(WINDOW_SAMPLES);
worker.postMessage({ type: "analyze", samples: window }, [
window.buffer,
]);
}
},
);
workletNode.port.start();
sourceNode.connect(workletNode);
@@ -127,7 +132,9 @@
list-style: none;
padding: 1rem;
padding-top: calc(4rem);
padding-bottom: calc(var(--nav-height) + env(safe-area-inset-bottom) + 6rem);
padding-bottom: calc(
var(--nav-height) + env(safe-area-inset-bottom) + 6rem
);
display: flex;
flex-direction: column;
gap: 0.5rem;
@@ -140,7 +147,8 @@
border-radius: 0.5rem;
background: linear-gradient(
to right,
color-mix(in srgb, var(--accent) 25%, var(--nav-bg)) calc(var(--pct) * 1%),
color-mix(in srgb, var(--accent) 25%, var(--nav-bg))
calc(var(--pct) * 1%),
var(--nav-bg) calc(var(--pct) * 1%)
);
@@ -170,7 +178,9 @@
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, transform 0.1s;
transition:
background 0.2s,
transform 0.1s;
svg {
width: 2rem;