perf: harden realtime localization pipeline

This commit is contained in:
Codex
2026-08-28 18:58:48 +09:00
parent 35c33e360e
commit 256558e601
13 changed files with 765 additions and 84 deletions
+43 -10
View File
@@ -1,4 +1,5 @@
import { fftInPlace } from "./fft.js";
import { cooperativeYield } from "./cooperative-yield.js";
function throwIfAborted(signal) {
if (signal?.aborted) {
@@ -8,10 +9,6 @@ function throwIfAborted(signal) {
}
}
function yieldToEventLoop() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
export function hannWindow(size) {
if (!Number.isInteger(size) || size < 2) throw new Error("Hann window 크기는 2 이상이어야 합니다.");
const window = new Float64Array(size);
@@ -21,6 +18,32 @@ export function hannWindow(size) {
return window;
}
const STFT_WORKSPACE_KIND = "micarrsim.stft-workspace/1";
export function createStftWorkspace(fftSize) {
return {
kind: STFT_WORKSPACE_KIND,
fftSize,
window: hannWindow(fftSize),
real: new Float64Array(fftSize),
imaginary: new Float64Array(fftSize),
};
}
function resolveStftWorkspace(fftSize, workspace) {
if (workspace === undefined) return createStftWorkspace(fftSize);
if (
workspace?.kind !== STFT_WORKSPACE_KIND ||
workspace.fftSize !== fftSize ||
workspace.window?.length !== fftSize ||
workspace.real?.length !== fftSize ||
workspace.imaginary?.length !== fftSize
) {
throw new RangeError("STFT workspace must match fftSize.");
}
return workspace;
}
export function getFrameCount(sampleCount, fftSize, hopSize) {
if (!Number.isInteger(hopSize) || hopSize < 1 || hopSize > fftSize) {
throw new Error("hopSize는 1 이상 fftSize 이하여야 합니다.");
@@ -29,7 +52,15 @@ export function getFrameCount(sampleCount, fftSize, hopSize) {
}
export async function extractStftBins(channels, options) {
const { fftSize, hopSize, bins, signal, onProgress, transformBatchSize = 32 } = options;
const {
fftSize,
hopSize,
bins,
signal,
onProgress,
transformBatchSize = 32,
workspace: providedWorkspace,
} = options;
if (!Array.isArray(channels) || channels.length === 0) throw new Error("마이크 채널이 필요합니다.");
const sampleCount = channels[0].length;
if (channels.some((channel) => channel.length !== sampleCount)) throw new Error("마이크 채널 길이가 다릅니다.");
@@ -51,9 +82,8 @@ export async function extractStftBins(channels, options) {
});
}
const window = hannWindow(fftSize);
const real = new Float64Array(fftSize);
const imaginary = new Float64Array(fftSize);
const workspace = resolveStftWorkspace(fftSize, providedWorkspace);
const { window, real, imaginary } = workspace;
const totalTransforms = frameCount * microphoneCount;
let completedTransforms = 0;
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
@@ -73,9 +103,12 @@ export async function extractStftBins(channels, options) {
spectrum.imaginary[spectrumIndex] = imaginary[bin];
}
completedTransforms += 1;
if (completedTransforms % transformBatchSize === 0) {
if (
completedTransforms % transformBatchSize === 0 &&
completedTransforms < totalTransforms
) {
onProgress?.({ completedTransforms, totalTransforms });
await yieldToEventLoop();
await cooperativeYield();
throwIfAborted(signal);
}
}