feat: define moving source trajectories
This commit is contained in:
@@ -10,6 +10,11 @@ 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 { getFrameCount } from "/packages/acoustics-core/stft.js";
|
||||
import {
|
||||
createQuasiStaticWindows,
|
||||
sceneAtTime,
|
||||
sourcePositionAtTime,
|
||||
} from "/packages/acoustics-core/trajectory.js";
|
||||
import { encodeMonoPcm16Wav } from "/packages/acoustics-core/wav.js";
|
||||
|
||||
function assertNear(actual, expected, tolerance, label) {
|
||||
@@ -39,6 +44,73 @@ function createArray(columns, rows, width, height) {
|
||||
return { id: "test-array", aperture: { width, height }, microphones };
|
||||
}
|
||||
|
||||
test("trajectory 위치를 선형 보간하고 시간창 중앙에서 준정적으로 샘플링한다", () => {
|
||||
const source = {
|
||||
position: [9, 8, 7],
|
||||
trajectory: [
|
||||
{ time: 0.2, position: [-1, 0, 2] },
|
||||
{ time: 0.6, position: [1, 2, 4] },
|
||||
{ time: 1, position: [3, 0, 2] },
|
||||
],
|
||||
};
|
||||
assertEqual(sourcePositionAtTime({ position: [4, 5, 6] }, 0.5).join(","), "4,5,6", "fixed fallback");
|
||||
assertEqual(sourcePositionAtTime(source, 0).join(","), "-1,0,2", "before first keyframe");
|
||||
assertEqual(sourcePositionAtTime(source, 1.5).join(","), "3,0,2", "after last keyframe");
|
||||
const interpolated = sourcePositionAtTime(source, 0.4);
|
||||
assertNear(interpolated[0], 0, 1e-12, "interpolated x");
|
||||
assertNear(interpolated[1], 1, 1e-12, "interpolated y");
|
||||
assertNear(interpolated[2], 3, 1e-12, "interpolated z");
|
||||
|
||||
const scene = { duration: 1, sources: [source] };
|
||||
const sampled = sceneAtTime(scene, 0.4);
|
||||
assert(sampled !== scene && sampled.sources[0] !== source, "sceneAtTime is immutable");
|
||||
assertEqual(source.position.join(","), "9,8,7", "original fallback position is unchanged");
|
||||
assertEqual(sampled.sources[0].trajectory, source.trajectory, "trajectory is preserved");
|
||||
assertNear(sampled.sources[0].position[0], 0, 1e-12, "sampled scene x");
|
||||
assertNear(sampled.sources[0].position[1], 1, 1e-12, "sampled scene y");
|
||||
assertNear(sampled.sources[0].position[2], 3, 1e-12, "sampled scene z");
|
||||
|
||||
const windows = createQuasiStaticWindows(scene, { windowDuration: 0.4, hopDuration: 0.4 });
|
||||
assertEqual(windows.length, 3, "partial final window");
|
||||
assertNear(windows[0].sampleTime, 0.2, 1e-12, "first midpoint");
|
||||
assertNear(windows[1].sampleTime, 0.6, 1e-12, "second midpoint");
|
||||
assertNear(windows[2].sampleTime, 0.9, 1e-12, "partial midpoint");
|
||||
assertNear(windows[2].duration, 0.2, 1e-12, "partial duration");
|
||||
assertNear(windows[1].scene.sources[0].position[0], 1, 1e-12, "window x");
|
||||
assertNear(windows[1].scene.sources[0].position[1], 2, 1e-12, "window y");
|
||||
assertNear(windows[1].scene.sources[0].position[2], 4, 1e-12, "window z");
|
||||
});
|
||||
|
||||
test("trajectory가 없는 sceneAtTime snapshot은 기존 합성 결과와 sample 단위로 동일하다", () => {
|
||||
const scene = {
|
||||
sampleRate: 8000,
|
||||
duration: 0.01,
|
||||
speedOfSound: 343,
|
||||
seed: 17,
|
||||
mix: { normalization: "none", targetPeak: 0.95 },
|
||||
sources: [{
|
||||
id: "fixed", name: "fixed", position: [0.1, -0.2, 1.2], gainDb: -12, startTime: 0, duration: 0.01,
|
||||
signal: { type: "tone", frequency: 750, phaseDeg: 35 },
|
||||
}],
|
||||
ambient: [],
|
||||
};
|
||||
const arrayDefinition = {
|
||||
microphones: [
|
||||
{ id: "left", position: [-0.1, 0, 0] },
|
||||
{ id: "right", position: [0.1, 0, 0], sensitivityDb: -1, phaseOffsetDeg: 3 },
|
||||
],
|
||||
};
|
||||
const baseline = synthesizeMicrophoneSignals({ scene, arrayDefinition });
|
||||
const sampled = synthesizeMicrophoneSignals({ scene: sceneAtTime(scene, 0.007), arrayDefinition });
|
||||
assertEqual(sampled.appliedScale, baseline.appliedScale, "applied scale");
|
||||
assertEqual(sampled.preNormalizationPeak, baseline.preNormalizationPeak, "pre-normalization peak");
|
||||
baseline.channels.forEach((channel, channelIndex) => {
|
||||
channel.forEach((value, sampleIndex) => {
|
||||
assertEqual(sampled.channels[channelIndex][sampleIndex], value, `channel ${channelIndex} sample ${sampleIndex}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("거리, dB와 구면 감쇠 기본식을 계산한다", () => {
|
||||
assertNear(distance3([0, 0, 0], [1, 2, 2]), 3, 1e-12, "distance");
|
||||
assertNear(dbToLinear(-6.020599913), 0.5, 1e-9, "dB amplitude");
|
||||
|
||||
@@ -43,6 +43,28 @@ test("단일 tone 장면과 실험이 관계 검증을 통과한다", async () =
|
||||
assertEqual((await validateExperiment(experiment, { arrayDefinition, scene })).length, 0, "experiment issues");
|
||||
});
|
||||
|
||||
test("이동 음원 trajectory의 keyframe 순서와 장면 시간 범위를 검증한다", async () => {
|
||||
const scene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
|
||||
scene.sources[0].trajectory = [
|
||||
{ time: 0, position: [-0.2, 0, 2] },
|
||||
{ time: 0.125, position: [0, 0.1, 2] },
|
||||
{ time: 0.25, position: [0.2, 0, 2] },
|
||||
];
|
||||
assertEqual((await validateScene(scene)).length, 0, "valid trajectory issues");
|
||||
|
||||
scene.sources[0].trajectory[2].time = 0.125;
|
||||
let issues = await validateScene(scene);
|
||||
assert(issues.some(({ path }) => path === "$.sources[0].trajectory[2].time"), "strictly ordered times");
|
||||
|
||||
scene.sources[0].trajectory[2].time = 0.3;
|
||||
issues = await validateScene(scene);
|
||||
assert(issues.some(({ path }) => path === "$.sources[0].trajectory[2].time"), "scene duration bound");
|
||||
|
||||
scene.sources[0].trajectory = [{ time: 0, position: [0, 0, 2] }];
|
||||
issues = await validateScene(scene);
|
||||
assert(issues.some(({ path }) => path === "$.sources[0].trajectory"), "two-keyframe minimum");
|
||||
});
|
||||
|
||||
test("중복 마이크 id를 거부한다", async () => {
|
||||
const arrayDefinition = {
|
||||
schemaVersion: "1.0.0", kind: "micarrsim.array", id: "duplicate-test", name: "duplicate test", units: "m",
|
||||
|
||||
Reference in New Issue
Block a user