fix: expose physical aliasing and delayed arrivals

This commit is contained in:
2026-08-29 10:21:33 +09:00
parent e048ccbadc
commit de0053eae3
38 changed files with 3086 additions and 214 deletions
+56 -14
View File
@@ -1,3 +1,6 @@
import { maximumAcousticDelaySeconds } from "./environment.js";
import { ceilSampleIndex } from "./math.js";
function copyPosition(position) {
if (!Array.isArray(position) || position.length !== 3 || !position.every(Number.isFinite)) {
throw new TypeError("source position must be a finite [x, y, z] array.");
@@ -65,7 +68,7 @@ export function sceneAtTime(scene, time) {
* Approximate a moving scene as stationary during [startTime, endTime] by
* sampling every trajectory at the window midpoint.
*/
export function sampleQuasiStaticWindow(scene, startTime, endTime) {
export function sampleQuasiStaticWindow(scene, startTime, endTime, options = {}) {
requireFiniteTime(startTime, "startTime");
requireFiniteTime(endTime, "endTime");
if (startTime < 0 || endTime <= startTime) {
@@ -90,42 +93,77 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime) {
// boundary. Adjacent windows therefore share an exact boundary without a
// duplicated or omitted sample, even when their requested times are not
// integer multiples of the sample period.
const startSample = Math.ceil(startTime * sampleRate);
const endSample = Math.ceil(endTime * sampleRate);
const startSample = ceilSampleIndex(startTime, sampleRate);
const endSample = ceilSampleIndex(endTime, sampleRate);
if (endSample <= startSample) {
throw new RangeError("window must contain at least one sample.");
}
const alignedStartTime = startSample / sampleRate;
const alignedEndTime = endSample / sampleRate;
const sampleTime = (startSample + endSample) / (2 * sampleRate);
const sampledScene = sceneAtTime(scene, sampleTime);
let maximumPropagationDelaySeconds = options.maximumPropagationDelaySeconds;
if (maximumPropagationDelaySeconds === undefined) {
maximumPropagationDelaySeconds = options.arrayDefinition
? maximumAcousticDelaySeconds({
scene: sampledScene,
arrayDefinition: options.arrayDefinition,
referenceDistance: options.referenceDistance ?? 1,
minimumDistance: options.minimumDistance ?? 0.01,
})
: 0;
}
if (
!Number.isFinite(maximumPropagationDelaySeconds) ||
maximumPropagationDelaySeconds < 0
) {
throw new RangeError("maximumPropagationDelaySeconds must be finite and non-negative.");
}
const preRollSamples = ceilSampleIndex(maximumPropagationDelaySeconds, sampleRate);
const renderStartSample = Math.max(0, startSample - preRollSamples);
const sources = [];
const sourceOffsets = [];
for (const source of scene.sources) {
for (let sourceIndex = 0; sourceIndex < scene.sources.length; sourceIndex += 1) {
const source = scene.sources[sourceIndex];
const signalStartTime = source.startTime ?? 0;
const signalDuration = source.duration ?? scene.duration;
const signalStartSample = Math.ceil(signalStartTime * sampleRate);
const signalEndSample = Math.ceil((signalStartTime + signalDuration) * sampleRate);
const activeStartSample = Math.max(startSample, signalStartSample);
const signalStartSample = ceilSampleIndex(signalStartTime, sampleRate);
const signalEndSample = ceilSampleIndex(signalStartTime + signalDuration, sampleRate);
const activeStartSample = Math.max(renderStartSample, signalStartSample);
const activeEndSample = Math.min(endSample, signalEndSample);
if (activeEndSample <= activeStartSample) continue;
sources.push({
...source,
position: sourcePositionAtTime(source, sampleTime),
startTime: (activeStartSample - startSample) / sampleRate,
position: sampledScene.sources[sourceIndex].position,
startTime: (activeStartSample - renderStartSample) / sampleRate,
duration: (activeEndSample - activeStartSample) / sampleRate,
});
sourceOffsets.push({ signalStartTime, signalStartSample });
sourceOffsets.push({
signalStartTime,
signalStartSample,
signalDuration,
});
}
const ambient = Array.isArray(scene.ambient) ? scene.ambient.slice() : scene.ambient;
const signalOffsets = {
sceneSampleOffset: startSample,
sceneSampleOffset: renderStartSample,
cropStartSample: startSample - renderStartSample,
cropSampleCount: endSample - startSample,
maximumPropagationDelaySeconds,
sourcePositionSampleTime: sampleTime,
windowingModel: "quasi-static-midpoint-propagation-preroll",
sources: sourceOffsets,
ambient: (scene.ambient ?? []).map(() => ({ signalStartTime: 0, signalStartSample: 0 })),
ambient: (scene.ambient ?? []).map(() => ({
signalStartTime: 0,
signalStartSample: 0,
signalDuration: scene.duration,
})),
};
const duration = (endSample - startSample) / sampleRate;
const renderDuration = (endSample - renderStartSample) / sampleRate;
return {
startTime: alignedStartTime,
endTime: alignedEndTime,
@@ -134,10 +172,14 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime) {
endSample,
sampleCount: endSample - startSample,
duration,
renderStartSample,
renderDuration,
preRollSamples: startSample - renderStartSample,
maximumPropagationDelaySeconds,
signalOffsets,
scene: {
...scene,
duration,
duration: renderDuration,
sources,
...(Array.isArray(scene.ambient) ? { ambient } : {}),
},
@@ -170,7 +212,7 @@ export function createQuasiStaticWindows(scene, options = {}) {
const windows = [];
for (let windowStart = startTime; windowStart < endTime; windowStart += hopDuration) {
const windowEnd = Math.min(windowStart + windowDuration, endTime);
windows.push(sampleQuasiStaticWindow(scene, windowStart, windowEnd));
windows.push(sampleQuasiStaticWindow(scene, windowStart, windowEnd, options));
}
return windows;
}