feat: add realistic portable localization core

This commit is contained in:
Codex
2026-08-28 18:19:31 +09:00
parent bc63ada1cd
commit d002a2bd70
20 changed files with 2345 additions and 70 deletions
+50 -18
View File
@@ -1,10 +1,12 @@
import { dbToLinear, distance3 } from "./math.js";
import {
addFrequencyDomainDelay,
addFrequencyDomainPaths,
createDelayWorkspace,
nextPowerOfTwo,
prepareDelaySpectrum,
} from "./fractional-delay.js";
import { buildAcousticPaths, maximumAcousticDelaySeconds } from "./environment.js";
import { renderSignal } from "./signal.js";
export function propagationDelaySeconds(sourcePosition, receiverPosition, speedOfSound) {
@@ -27,15 +29,13 @@ export function synthesizeMicrophoneSignals(options) {
} = options;
const sampleCount = Math.ceil(scene.duration * scene.sampleRate);
const retainedBufferCount = arrayDefinition.microphones.length + scene.sources.length + 1;
let maximumDelaySamples = 0;
for (const source of scene.sources) {
for (const microphone of arrayDefinition.microphones) {
maximumDelaySamples = Math.max(
maximumDelaySamples,
(distance3(source.position, microphone.position) / scene.speedOfSound) * scene.sampleRate,
);
}
}
const maximumDelaySamples =
maximumAcousticDelaySeconds({
scene,
arrayDefinition,
referenceDistance,
minimumDistance,
}) * scene.sampleRate;
const delayFftSize = nextPowerOfTwo(sampleCount + Math.ceil(maximumDelaySamples) + 2);
const delayWorkingBytes = delayFftSize * Float64Array.BYTES_PER_ELEMENT * 4;
const estimatedBufferBytes =
@@ -56,22 +56,49 @@ export function synthesizeMicrophoneSignals(options) {
}),
}));
const acousticPathCounts = {};
let acousticPathCount = 0;
renderedSources.forEach(({ definition, samples }) => {
const distances = arrayDefinition.microphones.map((microphone) => distance3(definition.position, microphone.position));
const maximumDelaySamples = (Math.max(...distances) / scene.speedOfSound) * scene.sampleRate;
const spectrum = prepareDelaySpectrum(samples, maximumDelaySamples);
const microphonePaths = arrayDefinition.microphones.map((microphone) =>
buildAcousticPaths({
scene,
sourcePosition: definition.position,
receiverPosition: microphone.position,
sourceId: definition.id,
receiverId: microphone.id,
referenceDistance,
minimumDistance,
}),
);
let sourceMaximumDelaySamples = 0;
for (const paths of microphonePaths) {
for (const path of paths) {
sourceMaximumDelaySamples = Math.max(
sourceMaximumDelaySamples,
path.delaySeconds * scene.sampleRate,
);
}
}
const spectrum = prepareDelaySpectrum(samples, sourceMaximumDelaySamples);
const workspace = createDelayWorkspace(spectrum.fftSize);
arrayDefinition.microphones.forEach((microphone, microphoneIndex) => {
const distance = distances[microphoneIndex];
const delaySamples = (distance / scene.speedOfSound) * scene.sampleRate;
const paths = microphonePaths[microphoneIndex];
for (const path of paths) {
acousticPathCount += 1;
acousticPathCounts[path.kind] = (acousticPathCounts[path.kind] ?? 0) + 1;
}
const sensitivity = dbToLinear(microphone.sensitivityDb ?? 0);
const amplitude = sphericalAttenuation(distance, referenceDistance, minimumDistance) * sensitivity;
addFrequencyDomainDelay({
addFrequencyDomainPaths({
output: channels[microphoneIndex],
spectrum,
delaySamples,
gain: amplitude,
paths: paths.map((path) => ({
delaySamples: path.delaySeconds * scene.sampleRate,
gain: path.gain,
})),
gain: sensitivity,
phaseOffsetDeg: microphone.phaseOffsetDeg ?? 0,
frequencyResponse: microphone.frequencyResponse,
sampleRate: scene.sampleRate,
workspace,
});
});
@@ -93,6 +120,8 @@ export function synthesizeMicrophoneSignals(options) {
delaySamples: 0,
gain: dbToLinear(microphone.sensitivityDb ?? 0),
phaseOffsetDeg: microphone.phaseOffsetDeg ?? 0,
frequencyResponse: microphone.frequencyResponse,
sampleRate: scene.sampleRate,
workspace: createDelayWorkspace(spectrum.fftSize),
});
});
@@ -127,5 +156,8 @@ export function synthesizeMicrophoneSignals(options) {
appliedScale,
clippedSampleCount,
estimatedBufferBytes,
acousticModel: scene.acoustics?.model ?? "free-field",
acousticPathCount,
acousticPathCounts,
};
}