feat: synthesize trajectory analysis windows
This commit is contained in:
@@ -9,9 +9,11 @@ import { dbToLinear, distance3 } from "/packages/acoustics-core/math.js";
|
||||
import { renderSceneMix } from "/packages/acoustics-core/mixdown.js";
|
||||
import { whiteNoiseAt } from "/packages/acoustics-core/noise.js";
|
||||
import { sphericalAttenuation, synthesizeMicrophoneSignals } from "/packages/acoustics-core/propagation.js";
|
||||
import { renderSignal } from "/packages/acoustics-core/signal.js";
|
||||
import { getFrameCount } from "/packages/acoustics-core/stft.js";
|
||||
import {
|
||||
createQuasiStaticWindows,
|
||||
sampleQuasiStaticWindow,
|
||||
sceneAtTime,
|
||||
sourcePositionAtTime,
|
||||
} from "/packages/acoustics-core/trajectory.js";
|
||||
@@ -441,3 +443,98 @@ test("PCM16 WAV가 표준 mono header를 쓰고 범위 밖 sample을 clamp한다
|
||||
assertEqual(actualSamples[index], expected, `PCM sample ${index}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("quasi-static synthesis window aligns samples, clips activity, and samples midpoint positions", () => {
|
||||
const scene = {
|
||||
sampleRate: 10,
|
||||
duration: 1,
|
||||
sources: [
|
||||
{ id: "ended", position: [0, 0, 1], startTime: 0, duration: 0.2, signal: { type: "tone", frequency: 1 } },
|
||||
{
|
||||
id: "active", position: [0, 0, 1], startTime: 0.15, duration: 0.5,
|
||||
trajectory: [
|
||||
{ time: 0, position: [0, 0, 1] },
|
||||
{ time: 1, position: [1, 2, 3] },
|
||||
],
|
||||
signal: { type: "tone", frequency: 1 },
|
||||
},
|
||||
{ id: "starts-inside", position: [0, 0, 1], startTime: 0.45, duration: 0.2, signal: { type: "tone", frequency: 1 } },
|
||||
{ id: "later", position: [0, 0, 1], startTime: 0.8, duration: 0.1, signal: { type: "tone", frequency: 1 } },
|
||||
],
|
||||
ambient: [{ id: "room", signal: { type: "noise", color: "white" } }],
|
||||
};
|
||||
|
||||
const window = sampleQuasiStaticWindow(scene, 0.21, 0.69);
|
||||
assertEqual(window.startSample, 3, "aligned start sample");
|
||||
assertEqual(window.endSample, 7, "aligned end sample");
|
||||
assertEqual(window.sampleCount, 4, "window sample count");
|
||||
assertNear(window.scene.duration, 0.4, 1e-12, "short scene duration");
|
||||
assertEqual(window.scene.sources.map(({ id }) => id).join(","), "active,starts-inside", "active sources");
|
||||
assertNear(window.scene.sources[0].position[0], 0.5, 1e-12, "midpoint x");
|
||||
assertNear(window.scene.sources[0].position[1], 1, 1e-12, "midpoint y");
|
||||
assertNear(window.scene.sources[0].startTime, 0, 1e-12, "active local start");
|
||||
assertNear(window.scene.sources[0].duration, 0.4, 1e-12, "active clipped duration");
|
||||
assertNear(window.scene.sources[1].startTime, 0.2, 1e-12, "inside local start");
|
||||
assertNear(window.scene.sources[1].duration, 0.2, 1e-12, "inside clipped duration");
|
||||
assertEqual(window.signalOffsets.sceneSampleOffset, 3, "absolute output origin");
|
||||
assertNear(window.signalOffsets.sources[0].signalStartTime, 0.15, 1e-12, "signal time origin");
|
||||
assertEqual(window.signalOffsets.sources[0].signalStartSample, 2, "signal sample origin");
|
||||
assertEqual(window.signalOffsets.ambient.length, 1, "ambient offset count");
|
||||
assertEqual(scene.sources.length, 4, "input scene is unchanged");
|
||||
});
|
||||
|
||||
test("absolute scene windows preserve tone, noise, asset, ambient, and synthesis continuity", () => {
|
||||
const scene = {
|
||||
sampleRate: 16,
|
||||
duration: 1,
|
||||
speedOfSound: 343,
|
||||
seed: 23,
|
||||
mix: { normalization: "none", targetPeak: 0.95 },
|
||||
sources: [
|
||||
{ id: "tone", position: [0, 0, 0], gainDb: -3, startTime: 1 / 16, duration: 14 / 16, signal: { type: "tone", frequency: 3, phaseDeg: 27 } },
|
||||
{ id: "noise", position: [0, 0, 0], gainDb: -6, startTime: 2 / 16, duration: 13 / 16, signal: { type: "noise", color: "pink", seedOffset: 9 } },
|
||||
{ id: "asset-source", position: [0, 0, 0], gainDb: 0, startTime: 3 / 16, duration: 12 / 16, signal: { type: "asset", assetId: "clip", channel: 0, loop: true } },
|
||||
],
|
||||
ambient: [{ id: "ambient", gainDb: -9, signal: { type: "noise", color: "white", seedOffset: 4 } }],
|
||||
};
|
||||
const assetBuffers = new Map([["clip", {
|
||||
sampleRate: 16,
|
||||
channels: [Float32Array.from({ length: 7 }, (_, index) => (index - 3) / 4)],
|
||||
}]]);
|
||||
const window = sampleQuasiStaticWindow(scene, 4 / 16, 12 / 16);
|
||||
|
||||
scene.sources.forEach((source, index) => {
|
||||
const whole = renderSignal(source, scene, assetBuffers);
|
||||
const partial = renderSignal(window.scene.sources[index], window.scene, assetBuffers, 0, {
|
||||
sceneSampleOffset: window.signalOffsets.sceneSampleOffset,
|
||||
...window.signalOffsets.sources[index],
|
||||
});
|
||||
partial.forEach((value, sampleIndex) => {
|
||||
assertEqual(value, whole[window.startSample + sampleIndex], `${source.id} sample ${sampleIndex}`);
|
||||
});
|
||||
});
|
||||
|
||||
const wholeAmbient = renderSignal(scene.ambient[0], scene, assetBuffers, 77);
|
||||
const partialAmbient = renderSignal(window.scene.ambient[0], window.scene, assetBuffers, 77, {
|
||||
sceneSampleOffset: window.signalOffsets.sceneSampleOffset,
|
||||
...window.signalOffsets.ambient[0],
|
||||
});
|
||||
partialAmbient.forEach((value, sampleIndex) => {
|
||||
assertEqual(value, wholeAmbient[window.startSample + sampleIndex], `ambient sample ${sampleIndex}`);
|
||||
});
|
||||
|
||||
const arrayDefinition = { microphones: [{ id: "mic", position: [0, 0, 0] }] };
|
||||
const wholeSynthesis = synthesizeMicrophoneSignals({ scene, arrayDefinition, assetBuffers, referenceDistance: 1, minimumDistance: 1 });
|
||||
const partialSynthesis = synthesizeMicrophoneSignals({
|
||||
scene: window.scene,
|
||||
arrayDefinition,
|
||||
assetBuffers,
|
||||
signalOffsets: window.signalOffsets,
|
||||
referenceDistance: 1,
|
||||
minimumDistance: 1,
|
||||
});
|
||||
assertEqual(partialSynthesis.channels[0].length, window.sampleCount, "short synthesis length");
|
||||
partialSynthesis.channels[0].forEach((value, sampleIndex) => {
|
||||
assertNear(value, wholeSynthesis.channels[0][window.startSample + sampleIndex], 2e-5, `mic sample ${sampleIndex}`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user