perf: stream propagation working sets

This commit is contained in:
2026-08-29 10:56:51 +09:00
parent de0053eae3
commit 044be3cf92
13 changed files with 1532 additions and 267 deletions
+93 -21
View File
@@ -1,5 +1,10 @@
import { maximumAcousticDelaySeconds } from "./environment.js";
import {
DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS,
selectAcousticPathSummary,
summarizeAcousticPaths,
} from "./environment.js";
import { ceilSampleIndex } from "./math.js";
import { effectiveSignalDurationSeconds } from "./signal.js";
function copyPosition(position) {
if (!Array.isArray(position) || position.length !== 3 || !position.every(Number.isFinite)) {
@@ -102,16 +107,20 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime, options = {})
const alignedEndTime = endSample / sampleRate;
const sampleTime = (startSample + endSample) / (2 * sampleRate);
const sampledScene = sceneAtTime(scene, sampleTime);
const pathSummary = options.arrayDefinition
? summarizeAcousticPaths({
scene: sampledScene,
arrayDefinition: options.arrayDefinition,
referenceDistance: options.referenceDistance ?? 1,
minimumDistance: options.minimumDistance ?? 0.01,
maximumPathEvaluations:
options.maximumPathEvaluations ??
DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS,
})
: undefined;
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;
maximumPropagationDelaySeconds = pathSummary?.maximumDelaySeconds ?? 0;
}
if (
!Number.isFinite(maximumPropagationDelaySeconds) ||
@@ -119,42 +128,110 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime, options = {})
) {
throw new RangeError("maximumPropagationDelaySeconds must be finite and non-negative.");
}
const preRollSamples = ceilSampleIndex(maximumPropagationDelaySeconds, sampleRate);
const requestedPreRollSamples = ceilSampleIndex(
maximumPropagationDelaySeconds,
sampleRate,
);
const requiredPreRollSamples = pathSummary
? ceilSampleIndex(pathSummary.maximumDelaySeconds, sampleRate)
: 0;
if (pathSummary && requestedPreRollSamples < requiredPreRollSamples) {
throw new RangeError(
"maximumPropagationDelaySeconds is shorter than an active acoustic path delay.",
);
}
const renderMaximumPropagationDelaySeconds = pathSummary
? pathSummary.sources.reduce(
(maximum, sourceSummary, sourceIndex) =>
sampledScene.sources[sourceIndex].signal.type === "tone"
? maximum
: Math.max(maximum, sourceSummary.maximumDelaySeconds),
0,
)
: maximumPropagationDelaySeconds;
const preRollSamples = ceilSampleIndex(
renderMaximumPropagationDelaySeconds,
sampleRate,
);
const renderStartSample = Math.max(0, startSample - preRollSamples);
const renderDuration = (endSample - renderStartSample) / sampleRate;
const sources = [];
const sourceOffsets = [];
const sourceIndexes = [];
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 signalDuration = effectiveSignalDurationSeconds(
source,
scene,
options.assetBuffers,
);
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;
const sourceMaximumDelaySeconds =
pathSummary?.sources[sourceIndex]?.maximumDelaySeconds ??
maximumPropagationDelaySeconds;
const latestPossibleArrivalEndSample = ceilSampleIndex(
signalStartTime + signalDuration + sourceMaximumDelaySeconds,
sampleRate,
);
const analyticTailMayOverlap =
source.signal.type === "tone" &&
signalStartSample < endSample &&
latestPossibleArrivalEndSample > startSample;
if (activeEndSample <= activeStartSample && !analyticTailMayOverlap) continue;
const hasEmissionInRender = activeEndSample > activeStartSample;
sources.push({
...source,
position: sampledScene.sources[sourceIndex].position,
startTime: (activeStartSample - renderStartSample) / sampleRate,
duration: (activeEndSample - activeStartSample) / sampleRate,
startTime: hasEmissionInRender
? (activeStartSample - renderStartSample) / sampleRate
: 0,
duration: hasEmissionInRender
? (activeEndSample - activeStartSample) / sampleRate
: renderDuration,
});
sourceOffsets.push({
signalStartTime,
signalStartSample,
signalDuration,
});
sourceIndexes.push(sourceIndex);
}
const ambient = Array.isArray(scene.ambient) ? scene.ambient.slice() : scene.ambient;
const windowScene = {
...scene,
duration: renderDuration,
sources,
...(Array.isArray(scene.ambient) ? { ambient } : {}),
};
const selectedPathSummary = pathSummary
? selectAcousticPathSummary({
summary: pathSummary,
sourceIndexes,
sourceScene: sampledScene,
scene: windowScene,
arrayDefinition: options.arrayDefinition,
referenceDistance: options.referenceDistance ?? 1,
minimumDistance: options.minimumDistance ?? 0.01,
})
: undefined;
const signalOffsets = {
sceneSampleOffset: renderStartSample,
cropStartSample: startSample - renderStartSample,
cropSampleCount: endSample - startSample,
maximumPropagationDelaySeconds,
renderMaximumPropagationDelaySeconds,
sourcePositionSampleTime: sampleTime,
windowingModel: "quasi-static-midpoint-propagation-preroll",
...(selectedPathSummary
? { acousticPathSummary: selectedPathSummary }
: {}),
sources: sourceOffsets,
ambient: (scene.ambient ?? []).map(() => ({
signalStartTime: 0,
@@ -163,7 +240,6 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime, options = {})
})),
};
const duration = (endSample - startSample) / sampleRate;
const renderDuration = (endSample - renderStartSample) / sampleRate;
return {
startTime: alignedStartTime,
endTime: alignedEndTime,
@@ -176,13 +252,9 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime, options = {})
renderDuration,
preRollSamples: startSample - renderStartSample,
maximumPropagationDelaySeconds,
renderMaximumPropagationDelaySeconds,
signalOffsets,
scene: {
...scene,
duration: renderDuration,
sources,
...(Array.isArray(scene.ambient) ? { ambient } : {}),
},
scene: windowScene,
};
}