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
+422 -175
View File
@@ -13,8 +13,16 @@ import {
nextPowerOfTwo,
prepareDelaySpectrum,
} from "./fractional-delay.js";
import { buildAcousticPaths } from "./environment.js";
import { renderSignal } from "./signal.js";
import {
buildAcousticPaths,
DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS,
isAcousticPathSummaryCompatible,
summarizeAcousticPaths,
} from "./environment.js";
import {
effectiveSignalDurationSeconds,
renderSignal,
} from "./signal.js";
import { describeFrequencyAliasing } from "./sampling.js";
export function propagationDelaySeconds(sourcePosition, receiverPosition, speedOfSound) {
@@ -68,16 +76,28 @@ function addSampledAnalogTonePaths(options) {
microphone,
paths,
timeMapping = {},
outputSceneSampleOffset,
outputRenderSampleOffset = 0,
} = options;
const signal = entry.signal;
const frequencyHz = signal.frequency;
const sampling = describeFrequencyAliasing(frequencyHz, scene.sampleRate);
const sceneSampleOffset = timeMapping.sceneSampleOffset ?? 0;
const outputSampleOffset = outputSceneSampleOffset ?? sceneSampleOffset;
const signalStartTime = timeMapping.signalStartTime ?? entry.startTime ?? 0;
const signalDuration = timeMapping.signalDuration ?? entry.duration ?? scene.duration;
if (!Number.isSafeInteger(sceneSampleOffset) || sceneSampleOffset < 0) {
throw new RangeError("sceneSampleOffset must be a non-negative safe integer.");
}
if (!Number.isSafeInteger(outputSampleOffset) || outputSampleOffset < 0) {
throw new RangeError("outputSceneSampleOffset must be a non-negative safe integer.");
}
if (
!Number.isSafeInteger(outputRenderSampleOffset) ||
outputRenderSampleOffset < 0
) {
throw new RangeError("outputRenderSampleOffset must be a non-negative safe integer.");
}
if (!Number.isFinite(signalStartTime)) {
throw new TypeError("signalStartTime must be finite.");
}
@@ -102,14 +122,14 @@ function addSampledAnalogTonePaths(options) {
const firstSample = Math.max(
0,
ceilSampleIndex(signalStartTime + path.delaySeconds, scene.sampleRate) -
sceneSampleOffset,
outputSampleOffset,
);
const lastSample = Math.min(
output.length,
ceilSampleIndex(
signalStartTime + signalDuration + path.delaySeconds,
scene.sampleRate,
) - sceneSampleOffset,
) - outputSampleOffset,
);
if (firstSample >= lastSample) continue;
const amplitude = baseGain * path.gain;
@@ -132,7 +152,7 @@ function addSampledAnalogTonePaths(options) {
const initialCarrier = carrierAtSample(
sampling.signedAdcFrequencyHz,
scene.sampleRate,
sceneSampleOffset + firstSample,
outputSampleOffset + firstSample,
);
let carrierCosine = initialCarrier.cosine;
let carrierSine = initialCarrier.sine;
@@ -157,7 +177,7 @@ function addSampledAnalogTonePaths(options) {
const carrier = carrierAtSample(
sampling.signedAdcFrequencyHz,
scene.sampleRate,
sceneSampleOffset + sampleIndex,
outputSampleOffset + sampleIndex,
);
carrierCosine = carrier.cosine;
carrierSine = carrier.sine;
@@ -167,11 +187,13 @@ function addSampledAnalogTonePaths(options) {
carrierSine * sineCoefficient + carrierCosine * cosineCoefficient;
const nextSample = sampleIndex + 1;
if (nextSample % phaseReanchorInterval === 0) {
if (
(outputRenderSampleOffset + nextSample) % phaseReanchorInterval === 0
) {
const carrier = carrierAtSample(
sampling.signedAdcFrequencyHz,
scene.sampleRate,
sceneSampleOffset + nextSample,
outputSampleOffset + nextSample,
);
carrierCosine = carrier.cosine;
carrierSine = carrier.sine;
@@ -186,16 +208,9 @@ function addSampledAnalogTonePaths(options) {
}
}
export function synthesizeMicrophoneSignals(options) {
const {
scene,
arrayDefinition,
assetBuffers = new Map(),
referenceDistance = 1,
minimumDistance = 0.01,
maximumBufferBytes = 512 * 1024 * 1024,
signalOffsets,
} = options;
export const DEFAULT_MAXIMUM_SYNTHESIS_BUFFER_BYTES = 512 * 1024 * 1024;
function resolveSynthesisDimensions(scene, signalOffsets) {
const sampleCount = ceilSampleIndex(scene.duration, scene.sampleRate);
const sceneSampleOffset = signalOffsets?.sceneSampleOffset ?? 0;
const cropStartSample = signalOffsets?.cropStartSample ?? 0;
@@ -212,12 +227,322 @@ export function synthesizeMicrophoneSignals(options) {
) {
throw new RangeError("signal output crop must be a non-empty range inside the synthesis buffer.");
}
const cropEndSample = cropStartSample + cropSampleCount;
const needsOutputCrop = cropStartSample !== 0 || cropSampleCount !== sampleCount;
const sourcePathSets = scene.sources.map((definition) => ({
const outputStartSample = sceneSampleOffset + cropStartSample;
const outputEndSample = outputStartSample + cropSampleCount;
if (
!Number.isSafeInteger(outputStartSample) ||
!Number.isSafeInteger(outputEndSample)
) {
throw new RangeError("signal output absolute sample range exceeds the safe integer range.");
}
return {
sampleCount,
sceneSampleOffset,
cropStartSample,
cropSampleCount,
outputStartSample,
};
}
function sampledSignalTransient(sampleCount, maximumDelaySeconds, sampleRate) {
const maximumDelaySamples = ceilSamplePosition(maximumDelaySeconds * sampleRate);
const fftSize = nextPowerOfTwo(sampleCount + maximumDelaySamples + 2);
const sampleBytes = sampleCount * Float32Array.BYTES_PER_ELEMENT;
const spectrumAndWorkspaceBytes =
fftSize * Float64Array.BYTES_PER_ELEMENT * 4;
return {
maximumDelaySamples,
fftSize,
sampleBytes,
spectrumAndWorkspaceBytes,
totalBytes: sampleBytes + spectrumAndWorkspaceBytes,
};
}
/**
* Estimate the maximum simultaneously live typed-array storage used by the
* source-streaming synthesizer. Paths are represented separately by bounded
* short-lived JS objects and are not included in this typed working-set cap.
*/
export function estimateSynthesisWorkingSet(options) {
const {
scene,
arrayDefinition,
referenceDistance = 1,
minimumDistance = 0.01,
signalOffsets,
maximumPathEvaluations = DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS,
} = options;
if (
!Number.isSafeInteger(maximumPathEvaluations) ||
maximumPathEvaluations <= 0
) {
throw new RangeError(
"maximumPathEvaluations must be a positive safe integer.",
);
}
const dimensions = resolveSynthesisDimensions(scene, signalOffsets);
const summaryOptions = {
scene,
arrayDefinition,
referenceDistance,
minimumDistance,
};
const candidateSummary =
options.acousticPathSummary ?? signalOffsets?.acousticPathSummary;
const reusedPathSummary = isAcousticPathSummaryCompatible(
candidateSummary,
summaryOptions,
);
const acousticPathSummary = reusedPathSummary
? candidateSummary
: summarizeAcousticPaths({
...summaryOptions,
maximumPathEvaluations,
});
if (acousticPathSummary.pathCount > maximumPathEvaluations) {
throw new RangeError(
`acoustic path count ${acousticPathSummary.pathCount} exceeds ` +
`maximumPathEvaluations ${maximumPathEvaluations}.`,
);
}
const activePathMaximumDelaySamples = ceilSamplePosition(
acousticPathSummary.maximumDelaySeconds * scene.sampleRate,
);
const channelBytes =
arrayDefinition.microphones.length *
dimensions.cropSampleCount *
Float32Array.BYTES_PER_ELEMENT;
let maximumTransientBytes = 0;
let maximumTransient = {
scope: "none",
id: null,
maximumDelaySamples: 0,
fftSize: 0,
sampleBytes: 0,
spectrumAndWorkspaceBytes: 0,
totalBytes: 0,
};
const sampledSources = [];
scene.sources.forEach((source, sourceIndex) => {
if (source.signal.type === "tone") return;
const transient = sampledSignalTransient(
dimensions.sampleCount,
acousticPathSummary.sources[sourceIndex].maximumDelaySeconds,
scene.sampleRate,
);
const entry = { scope: "source", id: source.id, ...transient };
sampledSources.push(entry);
if (entry.totalBytes > maximumTransientBytes) {
maximumTransientBytes = entry.totalBytes;
maximumTransient = entry;
}
});
const sampledAmbient = [];
for (const ambient of scene.ambient ?? []) {
if (ambient.signal.type === "tone") continue;
const transient = sampledSignalTransient(
dimensions.sampleCount,
0,
scene.sampleRate,
);
const entry = { scope: "ambient", id: ambient.id, ...transient };
sampledAmbient.push(entry);
if (entry.totalBytes > maximumTransientBytes) {
maximumTransientBytes = entry.totalBytes;
maximumTransient = entry;
}
}
const estimatedBufferBytes = channelBytes + maximumTransientBytes;
if (!Number.isSafeInteger(estimatedBufferBytes)) {
throw new RangeError("estimated synthesis typed working set exceeds the safe integer range.");
}
return {
...dimensions,
estimatedBufferBytes,
channelBytes,
maximumTransientBytes,
maximumTransient,
sampledSources,
sampledAmbient,
acousticPathSummary,
activePathMaximumDelaySamples,
reusedPathSummary,
model: "crop-channels-plus-one-sampled-signal-spectrum-and-workspace",
};
}
function assertWorkingSetWithinLimit(estimatedBytes, maximumBufferBytes) {
if (
!(maximumBufferBytes > 0) ||
!Number.isFinite(maximumBufferBytes)
) {
throw new RangeError("maximumBufferBytes must be finite and positive.");
}
if (estimatedBytes > maximumBufferBytes) {
throw new Error(
`예상 signal buffer ${(estimatedBytes / 1024 ** 2).toFixed(1)} MiB가 ` +
`현재 한도 ${(maximumBufferBytes / 1024 ** 2).toFixed(1)} MiB를 넘습니다. duration 또는 sample rate를 줄이세요.`,
);
}
}
function createSourceArrivalState(options) {
const {
definition,
microphonePaths: arrayDefinition.microphones.map((microphone) =>
buildAcousticPaths({
scene,
assetBuffers,
timeMapping,
} = options;
const emissionStartTime =
timeMapping.signalStartTime ?? definition.startTime ?? 0;
const declaredDuration =
timeMapping.signalDuration ?? definition.duration ?? scene.duration;
const emissionDuration = effectiveSignalDurationSeconds(
{ ...definition, duration: declaredDuration },
scene,
assetBuffers,
);
return {
sourceId: definition.id,
emissionStartTime,
emissionDuration,
earliestArrivalSample: Infinity,
latestArrivalEndSample: -Infinity,
overlapsOutput: false,
overlappingPathKinds: new Set(),
};
}
function observeSourceArrivalPaths(
state,
paths,
sampleRate,
outputStartSample,
outputEndSample,
) {
for (const path of paths) {
const arrivalStartSample = ceilSampleIndex(
state.emissionStartTime + path.delaySeconds,
sampleRate,
);
const arrivalEndSample = ceilSampleIndex(
state.emissionStartTime + state.emissionDuration + path.delaySeconds,
sampleRate,
);
state.earliestArrivalSample = Math.min(
state.earliestArrivalSample,
arrivalStartSample,
);
state.latestArrivalEndSample = Math.max(
state.latestArrivalEndSample,
arrivalEndSample,
);
if (
arrivalStartSample < outputEndSample &&
arrivalEndSample > outputStartSample
) {
state.overlapsOutput = true;
state.overlappingPathKinds.add(path.kind);
}
}
}
function finalizeSourceArrival(state, sampleRate) {
return {
sourceId: state.sourceId,
emissionStartSample: ceilSampleIndex(state.emissionStartTime, sampleRate),
emissionEndSample: ceilSampleIndex(
state.emissionStartTime + state.emissionDuration,
sampleRate,
),
earliestArrivalSample: Number.isFinite(state.earliestArrivalSample)
? state.earliestArrivalSample
: null,
latestArrivalEndSample: Number.isFinite(state.latestArrivalEndSample)
? state.latestArrivalEndSample
: null,
overlapsOutput: state.overlapsOutput,
overlappingPathKinds: [...state.overlappingPathKinds].sort(),
};
}
export function synthesizeMicrophoneSignals(options) {
const {
scene,
arrayDefinition,
assetBuffers = new Map(),
referenceDistance = 1,
minimumDistance = 0.01,
maximumBufferBytes = DEFAULT_MAXIMUM_SYNTHESIS_BUFFER_BYTES,
maximumPathEvaluations = DEFAULT_MAXIMUM_ACOUSTIC_PATH_EVALUATIONS,
signalOffsets,
} = options;
const workingSet = estimateSynthesisWorkingSet({
scene,
arrayDefinition,
referenceDistance,
minimumDistance,
signalOffsets,
acousticPathSummary: options.acousticPathSummary,
maximumPathEvaluations,
});
assertWorkingSetWithinLimit(
workingSet.estimatedBufferBytes,
maximumBufferBytes,
);
const {
sampleCount,
sceneSampleOffset,
cropStartSample,
cropSampleCount,
outputStartSample,
acousticPathSummary,
} = workingSet;
const outputEndSample = outputStartSample + cropSampleCount;
const needsOutputCrop =
cropStartSample !== 0 || cropSampleCount !== sampleCount;
const synthesisChannels = arrayDefinition.microphones.map(
() => new Float32Array(cropSampleCount),
);
const sourceArrivals = [];
scene.sources.forEach((definition, sourceIndex) => {
const timeMapping = {
sceneSampleOffset,
...(signalOffsets?.sources?.[sourceIndex] ?? {}),
};
const arrivalState = createSourceArrivalState({
definition,
scene,
assetBuffers,
timeMapping,
});
const sourceWorkingSet = workingSet.sampledSources.find(
({ id }) => id === definition.id,
);
if (sourceWorkingSet) {
assertWorkingSetWithinLimit(
workingSet.channelBytes + sourceWorkingSet.totalBytes,
maximumBufferBytes,
);
}
const samples = definition.signal.type === "tone"
? undefined
: renderSignal(definition, scene, assetBuffers, 0, timeMapping);
const spectrum = samples
? prepareDelaySpectrum(
samples,
sourceWorkingSet.maximumDelaySamples,
)
: undefined;
const workspace = spectrum
? createDelayWorkspace(spectrum.fftSize)
: undefined;
arrayDefinition.microphones.forEach((microphone, microphoneIndex) => {
const paths = buildAcousticPaths({
scene,
sourcePosition: definition.position,
receiverPosition: microphone.position,
@@ -225,136 +550,14 @@ export function synthesizeMicrophoneSignals(options) {
receiverId: microphone.id,
referenceDistance,
minimumDistance,
}),
),
}));
const acousticPathCounts = {};
let acousticPathCount = 0;
let maximumDelaySamples = 0;
for (const { microphonePaths } of sourcePathSets) {
for (const paths of microphonePaths) {
for (const path of paths) {
acousticPathCount += 1;
acousticPathCounts[path.kind] = (acousticPathCounts[path.kind] ?? 0) + 1;
maximumDelaySamples = Math.max(
maximumDelaySamples,
path.delaySeconds * scene.sampleRate,
);
}
}
}
const sampledSourceCount = scene.sources
.filter((source) => source.signal.type !== "tone")
.length;
const hasSampledAmbient = (scene.ambient ?? [])
.some((ambient) => ambient.signal.type !== "tone");
const retainedBufferCount =
arrayDefinition.microphones.length +
sampledSourceCount +
(hasSampledAmbient ? 1 : 0);
const delayFftSize = nextPowerOfTwo(
sampleCount + ceilSamplePosition(maximumDelaySamples) + 2,
);
const delayWorkingBytes =
sampledSourceCount > 0 || hasSampledAmbient
? delayFftSize * Float64Array.BYTES_PER_ELEMENT * 4
: 0;
const outputCropBytes = needsOutputCrop
? arrayDefinition.microphones.length * cropSampleCount * Float32Array.BYTES_PER_ELEMENT
: 0;
const estimatedBufferBytes =
retainedBufferCount * sampleCount * Float32Array.BYTES_PER_ELEMENT +
delayWorkingBytes +
outputCropBytes;
if (estimatedBufferBytes > maximumBufferBytes) {
throw new Error(
`예상 signal buffer ${(estimatedBufferBytes / 1024 ** 2).toFixed(1)} MiB가 ` +
`현재 한도 ${(maximumBufferBytes / 1024 ** 2).toFixed(1)} MiB를 넘습니다. duration 또는 sample rate를 줄이세요.`,
);
}
const synthesisChannels = arrayDefinition.microphones.map(
() => new Float32Array(sampleCount),
);
const renderedSources = sourcePathSets.map(({ definition, microphonePaths }, sourceIndex) => ({
definition,
microphonePaths,
timeMapping: {
sceneSampleOffset,
...(signalOffsets?.sources?.[sourceIndex] ?? {}),
},
...(definition.signal.type === "tone"
? {}
: {
samples: renderSignal(definition, scene, assetBuffers, 0, {
sceneSampleOffset,
...(signalOffsets?.sources?.[sourceIndex] ?? {}),
}),
}),
}));
const outputStartSample = sceneSampleOffset + cropStartSample;
const outputEndSample = outputStartSample + cropSampleCount;
const sourceArrivals = renderedSources.map(({ definition, microphonePaths, timeMapping }) => {
const emissionStartTime = timeMapping.signalStartTime ?? definition.startTime ?? 0;
const emissionDuration = timeMapping.signalDuration ?? definition.duration ?? scene.duration;
let earliestArrivalSample = Infinity;
let latestArrivalEndSample = -Infinity;
let overlapsOutput = false;
const overlappingPathKinds = new Set();
for (const paths of microphonePaths) {
for (const path of paths) {
const arrivalStartSample = ceilSampleIndex(
emissionStartTime + path.delaySeconds,
scene.sampleRate,
);
const arrivalEndSample = ceilSampleIndex(
emissionStartTime + emissionDuration + path.delaySeconds,
scene.sampleRate,
);
earliestArrivalSample = Math.min(earliestArrivalSample, arrivalStartSample);
latestArrivalEndSample = Math.max(latestArrivalEndSample, arrivalEndSample);
if (arrivalStartSample < outputEndSample && arrivalEndSample > outputStartSample) {
overlapsOutput = true;
overlappingPathKinds.add(path.kind);
}
}
}
return {
sourceId: definition.id,
emissionStartSample: ceilSampleIndex(emissionStartTime, scene.sampleRate),
emissionEndSample: ceilSampleIndex(
emissionStartTime + emissionDuration,
});
observeSourceArrivalPaths(
arrivalState,
paths,
scene.sampleRate,
),
earliestArrivalSample: Number.isFinite(earliestArrivalSample)
? earliestArrivalSample
: null,
latestArrivalEndSample: Number.isFinite(latestArrivalEndSample)
? latestArrivalEndSample
: null,
overlapsOutput,
overlappingPathKinds: [...overlappingPathKinds].sort(),
};
});
renderedSources.forEach(({ definition, microphonePaths, samples, timeMapping }) => {
let sourceMaximumDelaySamples = 0;
for (const paths of microphonePaths) {
for (const path of paths) {
sourceMaximumDelaySamples = Math.max(
sourceMaximumDelaySamples,
path.delaySeconds * scene.sampleRate,
);
}
}
const spectrum = samples
? prepareDelaySpectrum(samples, sourceMaximumDelaySamples)
: undefined;
const workspace = spectrum
? createDelayWorkspace(spectrum.fftSize)
: undefined;
arrayDefinition.microphones.forEach((microphone, microphoneIndex) => {
const paths = microphonePaths[microphoneIndex];
const sensitivity = dbToLinear(microphone.sensitivityDb ?? 0);
outputStartSample,
outputEndSample,
);
if (definition.signal.type === "tone") {
addSampledAnalogTonePaths({
output: synthesisChannels[microphoneIndex],
@@ -363,33 +566,47 @@ export function synthesizeMicrophoneSignals(options) {
microphone,
paths,
timeMapping,
outputSceneSampleOffset: outputStartSample,
outputRenderSampleOffset: cropStartSample,
});
} else {
addFrequencyDomainPaths({
output: synthesisChannels[microphoneIndex],
spectrum,
paths: paths.map((path) => ({
delaySamples: path.delaySeconds * scene.sampleRate,
gain: path.gain,
})),
gain: sensitivity,
paths,
pathDelaySampleRate: scene.sampleRate,
gain: dbToLinear(microphone.sensitivityDb ?? 0),
phaseOffsetDeg: microphone.phaseOffsetDeg ?? 0,
frequencyResponse: microphone.frequencyResponse,
sampleRate: scene.sampleRate,
workspace,
outputOffsetSamples: cropStartSample,
});
}
});
sourceArrivals.push(finalizeSourceArrival(arrivalState, scene.sampleRate));
});
if (Array.isArray(scene.ambient)) {
scene.ambient.forEach((ambient, ambientIndex) => {
const ambientTimeMapping = {
sceneSampleOffset,
...(signalOffsets?.ambient?.[ambientIndex] ?? {}),
};
const ambientWorkingSet = workingSet.sampledAmbient.find(
({ id }) => id === ambient.id,
);
if (ambientWorkingSet) {
assertWorkingSetWithinLimit(
workingSet.channelBytes + ambientWorkingSet.totalBytes,
maximumBufferBytes,
);
}
const ambientWorkspace = ambientWorkingSet
? createDelayWorkspace(ambientWorkingSet.fftSize)
: undefined;
synthesisChannels.forEach((channel, microphoneIndex) => {
const microphone = arrayDefinition.microphones[microphoneIndex];
const ambientTimeMapping = {
sceneSampleOffset,
...(signalOffsets?.ambient?.[ambientIndex] ?? {}),
};
if (ambient.signal.type === "tone") {
addSampledAnalogTonePaths({
output: channel,
@@ -398,13 +615,21 @@ export function synthesizeMicrophoneSignals(options) {
microphone,
paths: [{ delaySeconds: 0, gain: 1 }],
timeMapping: ambientTimeMapping,
outputSceneSampleOffset: outputStartSample,
outputRenderSampleOffset: cropStartSample,
});
return;
}
const seedSalt = Math.imul(ambientIndex + 1, 0x45d9f3b) ^ Math.imul(microphoneIndex + 1, 0x27d4eb2d);
const ambientSamples = renderSignal(ambient, scene, assetBuffers, seedSalt, {
...ambientTimeMapping,
});
const seedSalt =
Math.imul(ambientIndex + 1, 0x45d9f3b) ^
Math.imul(microphoneIndex + 1, 0x27d4eb2d);
const ambientSamples = renderSignal(
ambient,
scene,
assetBuffers,
seedSalt,
ambientTimeMapping,
);
const spectrum = prepareDelaySpectrum(ambientSamples, 0);
addFrequencyDomainDelay({
output: channel,
@@ -414,15 +639,14 @@ export function synthesizeMicrophoneSignals(options) {
phaseOffsetDeg: microphone.phaseOffsetDeg ?? 0,
frequencyResponse: microphone.frequencyResponse,
sampleRate: scene.sampleRate,
workspace: createDelayWorkspace(spectrum.fftSize),
workspace: ambientWorkspace,
outputOffsetSamples: cropStartSample,
});
});
});
}
const channels = needsOutputCrop
? synthesisChannels.map((channel) => channel.slice(cropStartSample, cropEndSample))
: synthesisChannels;
const channels = synthesisChannels;
let preNormalizationPeak = 0;
for (const channel of channels) {
@@ -451,19 +675,40 @@ export function synthesizeMicrophoneSignals(options) {
preNormalizationPeak,
appliedScale,
clippedSampleCount,
estimatedBufferBytes,
estimatedBufferBytes: workingSet.estimatedBufferBytes,
workingSet: {
model: workingSet.model,
channelBytes: workingSet.channelBytes,
maximumTransientBytes: workingSet.maximumTransientBytes,
maximumTransientScope: workingSet.maximumTransient.scope,
maximumTransientId: workingSet.maximumTransient.id,
pathSummary: workingSet.reusedPathSummary
? "reused-authenticated-geometry-summary"
: "computed-for-synthesis",
summaryPathPairEvaluations: workingSet.reusedPathSummary
? 0
: acousticPathSummary.sourceReceiverPairCount,
synthesisPathBuildPairEvaluations:
acousticPathSummary.sourceReceiverPairCount,
retainedPathObjects: "one-source-receiver-pair",
remainingNonToneTransformModel: "one-ifft-per-source-and-microphone",
},
acousticModel: scene.acoustics?.model ?? "free-field",
acousticPathCount,
acousticPathCounts,
acousticPathCount: acousticPathSummary.pathCount,
acousticPathCounts: { ...acousticPathSummary.pathCounts },
windowing: {
model: signalOffsets?.windowingModel ?? "whole-scene",
renderStartSample: sceneSampleOffset,
outputStartSample,
outputSampleCount: cropSampleCount,
preRollSamples: cropStartSample,
activePathMaximumDelaySeconds: maximumDelaySamples / scene.sampleRate,
renderMaximumPropagationDelaySeconds:
signalOffsets?.renderMaximumPropagationDelaySeconds ??
acousticPathSummary.maximumDelaySeconds,
activePathMaximumDelaySeconds: acousticPathSummary.maximumDelaySeconds,
requestedMaximumPropagationDelaySeconds:
signalOffsets?.maximumPropagationDelaySeconds ?? maximumDelaySamples / scene.sampleRate,
signalOffsets?.maximumPropagationDelaySeconds ??
acousticPathSummary.maximumDelaySeconds,
geometryModel: Number.isFinite(signalOffsets?.sourcePositionSampleTime)
? "source-position-fixed-at-window-midpoint"
: "static-source-position",
@@ -471,7 +716,9 @@ export function synthesizeMicrophoneSignals(options) {
dopplerMode: "not-modeled",
delayedTailPolicy: needsOutputCrop
? "propagation-preroll-then-output-crop"
: "whole-render-buffer",
: signalOffsets?.windowingModel
? "live-request-window-direct-output-with-analytic-path-gating"
: "whole-render-buffer",
arrivalOverlapModel: "path-gated-half-open-sample-interval",
arrivingSourceIds: sourceArrivals
.filter(({ overlapsOutput }) => overlapsOutput)