perf: stream propagation working sets
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user