feat: implement free-field CPU beamforming

This commit is contained in:
Codex
2026-08-28 09:48:34 +09:00
parent 691a4b03ce
commit 6a1197d60e
15 changed files with 1148 additions and 4 deletions
@@ -0,0 +1,53 @@
import { fftInPlace } from "./fft.js";
import { TAU } from "./math.js";
export function nextPowerOfTwo(value) {
if (!Number.isFinite(value) || value < 1) throw new Error(`유효하지 않은 FFT 길이: ${value}`);
let result = 1;
while (result < value) result *= 2;
return result;
}
export function prepareDelaySpectrum(samples, maximumDelaySamples = 0) {
const fftSize = nextPowerOfTwo(samples.length + Math.ceil(maximumDelaySamples) + 2);
const real = new Float64Array(fftSize);
const imaginary = new Float64Array(fftSize);
real.set(samples);
fftInPlace(real, imaginary);
return { real, imaginary, fftSize };
}
export function createDelayWorkspace(fftSize) {
return { real: new Float64Array(fftSize), imaginary: new Float64Array(fftSize) };
}
export function addFrequencyDomainDelay(options) {
const { output, spectrum, delaySamples, gain = 1, phaseOffsetDeg = 0, workspace } = options;
if (!(delaySamples >= 0) || !Number.isFinite(delaySamples)) throw new Error(`유효하지 않은 delay: ${delaySamples}`);
if (workspace.real.length !== spectrum.fftSize || workspace.imaginary.length !== spectrum.fftSize) {
throw new Error("fractional-delay workspace 크기가 spectrum과 다릅니다.");
}
const size = spectrum.fftSize;
const calibrationPhase = (phaseOffsetDeg * Math.PI) / 180;
for (let bin = 0; bin < size; bin += 1) {
const signedBin = bin <= size / 2 ? bin : bin - size;
const phaseSign = signedBin === 0 ? 0 : signedBin > 0 ? 1 : -1;
const phase = (-TAU * signedBin * delaySamples) / size + phaseSign * calibrationPhase;
const phaseReal = Math.cos(phase);
const phaseImaginary = Math.sin(phase);
const inputReal = spectrum.real[bin];
const inputImaginary = spectrum.imaginary[bin];
workspace.real[bin] = inputReal * phaseReal - inputImaginary * phaseImaginary;
workspace.imaginary[bin] = inputReal * phaseImaginary + inputImaginary * phaseReal;
}
// A real sequence has a real-valued Nyquist coefficient. Projecting this single self-conjugate
// bin back to the real axis preserves Hermitian symmetry for fractional delays.
workspace.imaginary[size / 2] = 0;
fftInPlace(workspace.real, workspace.imaginary, true);
const outputLength = Math.min(output.length, workspace.real.length);
for (let sampleIndex = 0; sampleIndex < outputLength; sampleIndex += 1) {
output[sampleIndex] += workspace.real[sampleIndex] * gain;
}
}