feat: add realistic portable localization core
This commit is contained in:
@@ -4,6 +4,8 @@ export const SCHEMA_VERSION = "1.0.0";
|
||||
const MAX_MICROPHONES = 1024;
|
||||
const MAX_SOURCES = 32;
|
||||
const MAX_GRID_SIDE = 512;
|
||||
const MAX_FREQUENCY_RESPONSE_POINTS = 64;
|
||||
const MAX_FREQUENCY_RESPONSE_HZ = 1_000_000;
|
||||
|
||||
/** @typedef {{ path: string, message: string }} ValidationIssue */
|
||||
|
||||
@@ -65,6 +67,28 @@ function requirePosition(value, path, issues) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function requireBounds(value, path, issues) {
|
||||
if (!requireRecord(value, path, issues)) return false;
|
||||
const minimumValid = requirePosition(value.min, path + ".min", issues);
|
||||
const maximumValid = requirePosition(value.max, path + ".max", issues);
|
||||
if (!minimumValid || !maximumValid) return false;
|
||||
let ordered = true;
|
||||
for (let axis = 0; axis < 3; axis += 1) {
|
||||
if (value.min[axis] >= value.max[axis]) {
|
||||
addIssue(issues, path, "min 좌표는 모든 축에서 max 좌표보다 작아야 합니다.");
|
||||
ordered = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
function boundsContain(bounds, position) {
|
||||
return position.every(
|
||||
(coordinate, axis) => coordinate >= bounds.min[axis] && coordinate <= bounds.max[axis],
|
||||
);
|
||||
}
|
||||
|
||||
function validateHeader(value, expectedKind, issues) {
|
||||
if (!requireRecord(value, "$", issues)) return false;
|
||||
if (value.schemaVersion !== SCHEMA_VERSION) {
|
||||
@@ -131,6 +155,37 @@ function validateArrayRelationships(value) {
|
||||
if (microphone.sensitivityDb !== undefined) {
|
||||
requireFinite(microphone.sensitivityDb, `${path}.sensitivityDb`, issues, { min: -120, max: 120 });
|
||||
}
|
||||
if (microphone.frequencyResponse !== undefined) {
|
||||
const responsePath = `${path}.frequencyResponse`;
|
||||
if (!Array.isArray(microphone.frequencyResponse)) {
|
||||
addIssue(issues, responsePath, "주파수 응답점 배열이어야 합니다.");
|
||||
} else {
|
||||
if (microphone.frequencyResponse.length < 2 || microphone.frequencyResponse.length > MAX_FREQUENCY_RESPONSE_POINTS) {
|
||||
addIssue(issues, responsePath, `주파수 응답점은 2~${MAX_FREQUENCY_RESPONSE_POINTS}개여야 합니다.`);
|
||||
}
|
||||
let previousFrequency = -Infinity;
|
||||
microphone.frequencyResponse.forEach((point, pointIndex) => {
|
||||
const pointPath = `${responsePath}[${pointIndex}]`;
|
||||
if (!requireRecord(point, pointPath, issues)) return;
|
||||
const frequencyValid = requireFinite(point.frequencyHz, `${pointPath}.frequencyHz`, issues, {
|
||||
min: 0,
|
||||
max: MAX_FREQUENCY_RESPONSE_HZ,
|
||||
});
|
||||
requireFinite(point.gainDb, `${pointPath}.gainDb`, issues, { min: -120, max: 120 });
|
||||
if (point.phaseDeg !== undefined) {
|
||||
requireFinite(point.phaseDeg, `${pointPath}.phaseDeg`, issues, { min: -180, max: 180 });
|
||||
}
|
||||
if (frequencyValid && point.frequencyHz <= previousFrequency) {
|
||||
addIssue(
|
||||
issues,
|
||||
`${pointPath}.frequencyHz`,
|
||||
"주파수 응답점은 frequencyHz가 중복 없이 엄격히 증가하도록 정렬하세요.",
|
||||
);
|
||||
}
|
||||
if (frequencyValid) previousFrequency = point.frequencyHz;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const duplicate of findDuplicates(ids)) {
|
||||
addIssue(issues, "$.microphones", `중복 마이크 id: ${duplicate}`);
|
||||
@@ -178,6 +233,44 @@ function validateSceneRelationships(value) {
|
||||
requireFinite(value.speedOfSound, "$.speedOfSound", issues, { min: 250, max: 400 });
|
||||
requireInteger(value.seed, "$.seed", issues, { min: 0, max: 0xffffffff });
|
||||
|
||||
let acousticRoom = null;
|
||||
if (value.acoustics !== undefined && requireRecord(value.acoustics, "$.acoustics", issues)) {
|
||||
const acoustics = value.acoustics;
|
||||
if (acoustics.model === "free-field") {
|
||||
const unusedFields = ["room", "reflections", "diffuseReverb", "occluders"]
|
||||
.filter((key) => acoustics[key] !== undefined);
|
||||
if (unusedFields.length > 0) {
|
||||
addIssue(
|
||||
issues,
|
||||
"$.acoustics",
|
||||
"free-field 모델에서는 사용되지 않는 설정을 제거하세요: " + unusedFields.join(", "),
|
||||
);
|
||||
}
|
||||
} else if (acoustics.model === "shoebox") {
|
||||
if (acoustics.room === undefined) {
|
||||
addIssue(issues, "$.acoustics.room", "shoebox 모델에는 room 경계가 필요합니다.");
|
||||
} else if (requireBounds(acoustics.room, "$.acoustics.room", issues)) {
|
||||
acousticRoom = acoustics.room;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(acoustics.occluders)) {
|
||||
const occluderIds = [];
|
||||
acoustics.occluders.forEach((occluder, index) => {
|
||||
const path = "$.acoustics.occluders[" + index + "]";
|
||||
if (!requireRecord(occluder, path, issues)) return;
|
||||
if (requireString(occluder.id, path + ".id", issues)) occluderIds.push(occluder.id);
|
||||
if (requireBounds(occluder, path, issues) && acousticRoom) {
|
||||
if (!boundsContain(acousticRoom, occluder.min) || !boundsContain(acousticRoom, occluder.max)) {
|
||||
addIssue(issues, path, "occluder는 room 경계 안에 있어야 합니다.");
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const duplicate of findDuplicates(occluderIds)) {
|
||||
addIssue(issues, "$.acoustics.occluders", "중복 occluder id: " + duplicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assets = Array.isArray(value.assets) ? value.assets : [];
|
||||
const assetsById = new Map();
|
||||
const allIds = [];
|
||||
@@ -231,7 +324,10 @@ function validateSceneRelationships(value) {
|
||||
if (!requireRecord(source, path, issues)) return;
|
||||
if (requireString(source.id, `${path}.id`, issues)) sourceIds.push(source.id);
|
||||
requireString(source.name, `${path}.name`, issues);
|
||||
requirePosition(source.position, `${path}.position`, issues);
|
||||
const sourcePositionValid = requirePosition(source.position, `${path}.position`, issues);
|
||||
if (sourcePositionValid && acousticRoom && !boundsContain(acousticRoom, source.position)) {
|
||||
addIssue(issues, `${path}.position`, "shoebox room 경계 안에 있어야 합니다.");
|
||||
}
|
||||
if (Array.isArray(source.trajectory)) {
|
||||
let previousTime = -Infinity;
|
||||
source.trajectory.forEach((keyframe, keyframeIndex) => {
|
||||
@@ -241,7 +337,10 @@ function validateSceneRelationships(value) {
|
||||
min: 0,
|
||||
max: sceneDurationValid ? value.duration : 300,
|
||||
});
|
||||
requirePosition(keyframe.position, `${keyframePath}.position`, issues);
|
||||
const keyframePositionValid = requirePosition(keyframe.position, `${keyframePath}.position`, issues);
|
||||
if (keyframePositionValid && acousticRoom && !boundsContain(acousticRoom, keyframe.position)) {
|
||||
addIssue(issues, `${keyframePath}.position`, "shoebox room 경계 안에 있어야 합니다.");
|
||||
}
|
||||
if (timeValid && keyframe.time <= previousTime) {
|
||||
addIssue(issues, `${keyframePath}.time`, "trajectory time은 앞 keyframe보다 커야 합니다.");
|
||||
}
|
||||
@@ -346,6 +445,16 @@ function validateExperimentRelationships(value, context = {}) {
|
||||
max: 10,
|
||||
exclusiveMin: true,
|
||||
});
|
||||
if (
|
||||
value.beamforming.compensateFrequencyResponse !== undefined &&
|
||||
typeof value.beamforming.compensateFrequencyResponse !== "boolean"
|
||||
) {
|
||||
addIssue(
|
||||
issues,
|
||||
"$.beamforming.compensateFrequencyResponse",
|
||||
"주파수 응답 보정 사용 여부는 true 또는 false여야 합니다.",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (value.evaluation !== undefined && requireRecord(value.evaluation, "$.evaluation", issues)) {
|
||||
const threshold = value.evaluation.detectionThresholdRatio;
|
||||
|
||||
Reference in New Issue
Block a user