Commit:
91350ceParent:
ebff7d5Refactor BirdNET worker as app-lifetime singleton
src/lib/birdnet.svelte.ts
+40
-0
diff --git a/src/lib/birdnet.svelte.ts b/src/lib/birdnet.svelte.ts
new file mode 100644
index 0000000..70d24bb
@@ -0,0 +1,40 @@
import BirdNetWorker from "./birdnet.worker.ts?worker";
import type { Prediction, WorkerInMessage, WorkerOutMessage } from "./types";
import { addDetection } from "./history";
class BirdNetService {
workerReady = $state(false);
predictions = $state<Prediction[]>([]);
readonly #worker: Worker;
constructor() {
this.#worker = new BirdNetWorker();
this.#worker.addEventListener(
"message",
(e: MessageEvent<WorkerOutMessage>) => {
const msg = e.data;
if (msg.type === "ready") {
this.workerReady = true;
} else if (msg.type === "results") {
this.predictions = msg.predictions;
for (const p of msg.predictions) addDetection(p);
}
},
);
this.#worker.postMessage({ type: "init" } satisfies WorkerInMessage);
}
analyze(samples: Float32Array): void {
this.#worker.postMessage(
{ type: "analyze", samples } satisfies WorkerInMessage,
[samples.buffer],
);
}
clearPredictions(): void {
this.predictions = [];
}
}
export const birdnet = new BirdNetService();
src/routes/+page.svelte
+9
-33
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 62ace8d..781005d 100644
@@ -1,42 +1,19 @@
<script lang="ts">
import BirdNetWorker from "$lib/birdnet.worker.ts?worker";
import type { Prediction, WorkerOutMessage } from "$lib/types";
import { addDetection, endSession, startSession } from "$lib/history";
import { endSession, startSession } from "$lib/history";
import { labelsStore } from "$lib/labels";
import PatchBanner from "$lib/components/PatchBanner.svelte";
import { birdnet } from "$lib/birdnet.svelte";
const WINDOW_SAMPLES = 144000;
let listening = $state(false);
let workerReady = $state(false);
let predictions = $state<Prediction[]>([]);
let worker: Worker | null = null;
let audioCtx: AudioContext | null = null;
let workletNode: AudioWorkletNode | null = null;
let sourceNode: MediaStreamAudioSourceNode | null = null;
let stream: MediaStream | null = null;
let sampleBuffer: Float32Array = new Float32Array(0);
$effect(() => {
worker = new BirdNetWorker();
worker.addEventListener("message", (e: MessageEvent<WorkerOutMessage>) => {
const msg = e.data;
if (msg.type === "ready") {
workerReady = true;
} else if (msg.type === "results") {
predictions = msg.predictions;
for (const p of msg.predictions) addDetection(p);
}
});
worker.postMessage({ type: "init" });
return () => {
worker?.terminate();
worker = null;
};
});
async function startListening() {
startSession();
audioCtx = new AudioContext({ sampleRate: 48000 });
@@ -64,12 +41,10 @@
merged.set(chunk, sampleBuffer.length);
sampleBuffer = merged;
if (sampleBuffer.length >= WINDOW_SAMPLES && worker && workerReady) {
const window = sampleBuffer.slice(0, WINDOW_SAMPLES);
if (sampleBuffer.length >= WINDOW_SAMPLES && birdnet.workerReady) {
const samples = sampleBuffer.slice(0, WINDOW_SAMPLES);
sampleBuffer = sampleBuffer.slice(WINDOW_SAMPLES);
worker.postMessage({ type: "analyze", samples: window }, [
window.buffer,
]);
birdnet.analyze(samples);
}
},
);
@@ -89,6 +64,7 @@
stream = null;
audioCtx = null;
sampleBuffer = new Float32Array(0);
birdnet.clearPredictions();
endSession();
}
@@ -110,9 +86,9 @@
<PatchBanner text="BirdGO" />
{#if predictions.length > 0}
{#if birdnet.predictions.length > 0}
<ol>
{#each predictions as p (p.labelIndex)}
{#each birdnet.predictions as p (p.labelIndex)}
<li style:--pct={p.confidence * 100}>
<div class="info">
<span class="name">{$labelsStore[p.labelIndex]?.commonName ?? "Unknown"}</span>
@@ -126,7 +102,7 @@
<button
class:listening
disabled={!workerReady}
disabled={!birdnet.workerReady}
onclick={toggleListening}
aria-label={listening ? "Stop listening" : "Start listening"}
aria-pressed={listening}