feat: synthesize trajectory analysis windows
This commit is contained in:
@@ -23,6 +23,7 @@ export function synthesizeMicrophoneSignals(options) {
|
||||
referenceDistance = 1,
|
||||
minimumDistance = 0.01,
|
||||
maximumBufferBytes = 512 * 1024 * 1024,
|
||||
signalOffsets,
|
||||
} = options;
|
||||
const sampleCount = Math.ceil(scene.duration * scene.sampleRate);
|
||||
const retainedBufferCount = arrayDefinition.microphones.length + scene.sources.length + 1;
|
||||
@@ -46,9 +47,13 @@ export function synthesizeMicrophoneSignals(options) {
|
||||
);
|
||||
}
|
||||
const channels = arrayDefinition.microphones.map(() => new Float32Array(sampleCount));
|
||||
const renderedSources = scene.sources.map((source) => ({
|
||||
const sceneSampleOffset = signalOffsets?.sceneSampleOffset ?? 0;
|
||||
const renderedSources = scene.sources.map((source, sourceIndex) => ({
|
||||
definition: source,
|
||||
samples: renderSignal(source, scene, assetBuffers),
|
||||
samples: renderSignal(source, scene, assetBuffers, 0, {
|
||||
sceneSampleOffset,
|
||||
...(signalOffsets?.sources?.[sourceIndex] ?? {}),
|
||||
}),
|
||||
}));
|
||||
|
||||
renderedSources.forEach(({ definition, samples }) => {
|
||||
@@ -77,7 +82,10 @@ export function synthesizeMicrophoneSignals(options) {
|
||||
channels.forEach((channel, microphoneIndex) => {
|
||||
const microphone = arrayDefinition.microphones[microphoneIndex];
|
||||
const seedSalt = Math.imul(ambientIndex + 1, 0x45d9f3b) ^ Math.imul(microphoneIndex + 1, 0x27d4eb2d);
|
||||
const ambientSamples = renderSignal(ambient, scene, assetBuffers, seedSalt);
|
||||
const ambientSamples = renderSignal(ambient, scene, assetBuffers, seedSalt, {
|
||||
sceneSampleOffset,
|
||||
...(signalOffsets?.ambient?.[ambientIndex] ?? {}),
|
||||
});
|
||||
const spectrum = prepareDelaySpectrum(ambientSamples, 0);
|
||||
addFrequencyDomainDelay({
|
||||
output: channel,
|
||||
|
||||
@@ -15,7 +15,16 @@ function sampleAsset(signal, assetBuffers, time) {
|
||||
return channel[left] + (rightValue - channel[left]) * fraction;
|
||||
}
|
||||
|
||||
export function renderSignal(entry, scene, assetBuffers = new Map(), seedSalt = 0) {
|
||||
/**
|
||||
* Render one signal entry into the scene-sized output buffer.
|
||||
*
|
||||
* `timeMapping` lets a short, rebased scene window retain the original signal
|
||||
* timeline. `sceneSampleOffset` maps output sample zero to an absolute sample
|
||||
* in the original scene, while `signalStartTime`/`signalStartSample` preserve
|
||||
* the entry's original tone/asset time origin and deterministic noise index.
|
||||
* Omitting it keeps the historical whole-scene behaviour exactly unchanged.
|
||||
*/
|
||||
export function renderSignal(entry, scene, assetBuffers = new Map(), seedSalt = 0, timeMapping = {}) {
|
||||
const sampleCount = Math.ceil(scene.duration * scene.sampleRate);
|
||||
const output = new Float32Array(sampleCount);
|
||||
const startTime = entry.startTime ?? 0;
|
||||
@@ -23,12 +32,25 @@ export function renderSignal(entry, scene, assetBuffers = new Map(), seedSalt =
|
||||
const gain = dbToLinear(entry.gainDb ?? 0);
|
||||
const startSample = Math.max(0, Math.ceil(startTime * scene.sampleRate));
|
||||
const endSample = Math.min(sampleCount, Math.ceil((startTime + duration) * scene.sampleRate));
|
||||
const sceneSampleOffset = timeMapping.sceneSampleOffset ?? 0;
|
||||
const signalStartTime = timeMapping.signalStartTime ?? startTime;
|
||||
const signalStartSample = timeMapping.signalStartSample ?? Math.ceil(startTime * scene.sampleRate);
|
||||
if (!Number.isSafeInteger(sceneSampleOffset) || sceneSampleOffset < 0) {
|
||||
throw new RangeError("sceneSampleOffset must be a non-negative safe integer.");
|
||||
}
|
||||
if (!Number.isFinite(signalStartTime)) {
|
||||
throw new TypeError("signalStartTime must be finite.");
|
||||
}
|
||||
if (!Number.isSafeInteger(signalStartSample) || signalStartSample < 0) {
|
||||
throw new RangeError("signalStartSample must be a non-negative safe integer.");
|
||||
}
|
||||
const signal = entry.signal;
|
||||
const seed = (scene.seed + (signal.seedOffset ?? 0) + hashString(entry.id ?? "signal") + seedSalt) >>> 0;
|
||||
|
||||
for (let sampleIndex = startSample; sampleIndex < endSample; sampleIndex += 1) {
|
||||
const localIndex = sampleIndex - startSample;
|
||||
const localTime = sampleIndex / scene.sampleRate - startTime;
|
||||
const absoluteSampleIndex = sceneSampleOffset + sampleIndex;
|
||||
const localIndex = absoluteSampleIndex - signalStartSample;
|
||||
const localTime = absoluteSampleIndex / scene.sampleRate - signalStartTime;
|
||||
let value;
|
||||
if (signal.type === "tone") {
|
||||
value = Math.sin(TAU * signal.frequency * localTime + ((signal.phaseDeg ?? 0) * Math.PI) / 180);
|
||||
|
||||
@@ -74,13 +74,73 @@ export function sampleQuasiStaticWindow(scene, startTime, endTime) {
|
||||
if (Number.isFinite(scene?.duration) && endTime > scene.duration) {
|
||||
throw new RangeError("window endTime must not exceed scene.duration.");
|
||||
}
|
||||
const sampleTime = startTime + (endTime - startTime) / 2;
|
||||
const sampleRate = scene?.sampleRate;
|
||||
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
|
||||
const sampleTime = startTime + (endTime - startTime) / 2;
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
sampleTime,
|
||||
duration: endTime - startTime,
|
||||
scene: sceneAtTime(scene, sampleTime),
|
||||
};
|
||||
}
|
||||
|
||||
// Ceil selects the first sample whose timestamp is inside each half-open
|
||||
// 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);
|
||||
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 sources = [];
|
||||
const sourceOffsets = [];
|
||||
|
||||
for (const source of scene.sources) {
|
||||
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 activeEndSample = Math.min(endSample, signalEndSample);
|
||||
if (activeEndSample <= activeStartSample) continue;
|
||||
|
||||
sources.push({
|
||||
...source,
|
||||
position: sourcePositionAtTime(source, sampleTime),
|
||||
startTime: (activeStartSample - startSample) / sampleRate,
|
||||
duration: (activeEndSample - activeStartSample) / sampleRate,
|
||||
});
|
||||
sourceOffsets.push({ signalStartTime, signalStartSample });
|
||||
}
|
||||
|
||||
const ambient = Array.isArray(scene.ambient) ? scene.ambient.slice() : scene.ambient;
|
||||
const signalOffsets = {
|
||||
sceneSampleOffset: startSample,
|
||||
sources: sourceOffsets,
|
||||
ambient: (scene.ambient ?? []).map(() => ({ signalStartTime: 0, signalStartSample: 0 })),
|
||||
};
|
||||
const duration = (endSample - startSample) / sampleRate;
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
startTime: alignedStartTime,
|
||||
endTime: alignedEndTime,
|
||||
sampleTime,
|
||||
duration: endTime - startTime,
|
||||
scene: sceneAtTime(scene, sampleTime),
|
||||
startSample,
|
||||
endSample,
|
||||
sampleCount: endSample - startSample,
|
||||
duration,
|
||||
signalOffsets,
|
||||
scene: {
|
||||
...scene,
|
||||
duration,
|
||||
sources,
|
||||
...(Array.isArray(scene.ambient) ? { ambient } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user