feat: add varied microphone array presets

This commit is contained in:
2026-08-28 19:43:46 +09:00
parent 85ad056a73
commit 7fe846fbf9
14 changed files with 7086 additions and 77 deletions
+7 -4
View File
@@ -201,10 +201,13 @@ noise source는 스펙트럼을 미리 알 수 없으므로 모든 band의 groun
## 배열 예제 ## 배열 예제
- `assets/array-geometries/rectangular-16x12.array.json`: 0.45 m 정사각 aperture의 16×12 기준 배열 - **채널 수 비교:** 같은 0.45 m aperture에서 4·12·48·192·384채널 rectangular 배열
- `assets/array-geometries/fermat-spiral-192.array.json`: 0.45 m 원형 aperture의 192채널 Fermat spiral - **물리 크기 비교:** 같은 192채널 16×12 topology에서 0.12·0.45·1.20 m 배열
- **배치 비교:** 같은 192채널 규모의 rectangular baseline과 0.45 m Fermat spiral
Camera의 **비교 배열** 선택으로 두 fixture를 같은 scene과 experiment에서 연속 실행할 수 있습니다. Camera의 **배열 선택**에서 8개 fixture를 바로 불러와 같은 Scene/Experiment로 연속 실행할 수 있습니다.
저채널·대형 배열의 grating lobe나 compact 배열의 넓은 main lobe는 배치 trade-off를 관찰하기 위한
의도된 예제 특성입니다.
상세한 생성법은 [`assets/array-geometries/README.md`](assets/array-geometries/README.md)를 참고하세요. 상세한 생성법은 [`assets/array-geometries/README.md`](assets/array-geometries/README.md)를 참고하세요.
## 테스트와 benchmark ## 테스트와 benchmark
@@ -241,7 +244,7 @@ test.cmd
```text ```text
apps/ Dataset Builder와 Acoustic Camera UI apps/ Dataset Builder와 Acoustic Camera UI
assets/ 192채널 배열과 synthetic Scene/Experiment fixture assets/ 마이크 배열과 synthetic Scene/Experiment fixture
benchmarks/ 현재 브라우저에서 실행하는 WebGPU scaling 측정 benchmarks/ 현재 브라우저에서 실행하는 WebGPU scaling 측정
docs/ 사용자 안내, 데이터 계약, 실행 계획과 기준 실측 docs/ 사용자 안내, 데이터 계약, 실행 계획과 기준 실측
packages/ 음향 수치 모델, 계약 검증, UI, WebGPU kernel packages/ 음향 수치 모델, 계약 검증, UI, WebGPU kernel
+3 -2
View File
@@ -113,8 +113,9 @@ curve는 합성 microphone channel에 항상 적용됩니다. **마이크 주파
- **Ground truth**를 켜면 source 위치와 검출 peak를 비교할 수 있습니다. - **Ground truth**를 켜면 source 위치와 검출 peak를 비교할 수 있습니다.
- **빔 분포**는 peak 주변 half-power lobe의 power 가중 공분산을 1× RMS 점선과 2× RMS 영역으로 - **빔 분포**는 peak 주변 half-power lobe의 power 가중 공분산을 1× RMS 점선과 2× RMS 영역으로
표시합니다. 위치 확률, Gaussian 신뢰구간 또는 측정 불확실성 보정값은 아닙니다. 표시합니다. 위치 확률, Gaussian 신뢰구간 또는 측정 불확실성 보정값은 아닙니다.
- rectangular와 Fermat spiral preset을 같은 Scene/Experiment에서 연속 실행하면 **배열 비교 기록**에 - **배열 선택**에는 채널 수(4·12·48·192·384), aperture 크기(0.12·0.45·1.20 m), rectangular와
backend, timing과 평가 요약이 남습니다. Fermat spiral 배치를 분리 비교하는 8개 preset이 있습니다. 같은 Scene/Experiment에서 연속 실행하면
**배열 비교 기록**에 backend, timing과 평가 요약이 남습니다.
- 입력을 바꾼 뒤 화면에 남은 결과는 이전 실행 snapshot이며 UI가 이전 설정 결과임을 표시합니다. - 입력을 바꾼 뒤 화면에 남은 결과는 이전 실행 snapshot이며 UI가 이전 설정 결과임을 표시합니다.
- Camera에서 거리·음속·seed를 바꾼 Scene과 scan·STFT·band를 바꾼 Experiment는 우측의 각 - Camera에서 거리·음속·seed를 바꾼 Scene과 scan·STFT·band를 바꾼 Experiment는 우측의 각
**JSON 저장** 버튼으로 보존할 수 있습니다. **JSON 저장** 버튼으로 보존할 수 있습니다.
+23 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="192채널 WebGPU acoustic camera simulator" /> <meta name="description" content="다중 채널 CPU·WebGPU acoustic camera simulator" />
<title>Acoustic Camera · micarrsim</title> <title>Acoustic Camera · micarrsim</title>
<link rel="stylesheet" href="/packages/ui/app-shell.css" /> <link rel="stylesheet" href="/packages/ui/app-shell.css" />
<link rel="stylesheet" href="/apps/acoustic-camera/styles.css" /> <link rel="stylesheet" href="/apps/acoustic-camera/styles.css" />
@@ -52,7 +52,28 @@
<div class="array-card"> <div class="array-card">
<div class="array-heading"><div><span class="compact-label">MIC ARRAY</span><h3 id="array-title">배열</h3></div><strong id="microphone-count">— ch</strong></div> <div class="array-heading"><div><span class="compact-label">MIC ARRAY</span><h3 id="array-title">배열</h3></div><strong id="microphone-count">— ch</strong></div>
<label class="field"><span>배열 선택</span><select id="array-preset"><option value="rectangular-16x12">16×12 rectangular · 기준</option><option value="fermat-spiral-192">Fermat spiral · 비교</option><option value="custom">불러온 Array JSON</option></select></label> <label class="field">
<span>배열 선택</span>
<select id="array-preset">
<optgroup label="기준 배열">
<option value="rectangular-16x12">16×12 rectangular · 192 ch / 0.45 m</option>
<option value="fermat-spiral-192">Fermat spiral · 192 ch / Ø 0.45 m</option>
</optgroup>
<optgroup label="채널 수 비교 · 0.45 m">
<option value="rectangular-2x2-450mm">2×2 sparse · 4 ch</option>
<option value="rectangular-4x3-450mm">4×3 sparse · 12 ch</option>
<option value="rectangular-8x6-450mm">8×6 · 48 ch</option>
<option value="rectangular-24x16-450mm">24×16 dense · 384 ch</option>
</optgroup>
<optgroup label="물리 크기 비교 · 192 ch">
<option value="rectangular-16x12-compact">16×12 compact · 0.12 m</option>
<option value="rectangular-16x12-large">16×12 large · 1.20 m</option>
</optgroup>
<optgroup label="사용자 배열">
<option value="custom">불러온 Array JSON</option>
</optgroup>
</select>
</label>
<details class="inline-details" id="array-details"> <details class="inline-details" id="array-details">
<summary>배열 미리보기와 좌표 정보</summary> <summary>배열 미리보기와 좌표 정보</summary>
<canvas id="array-canvas" aria-label="마이크 배열 평면도"></canvas> <canvas id="array-canvas" aria-label="마이크 배열 평면도"></canvas>
+6
View File
@@ -44,6 +44,12 @@ const MOVING_EXAMPLE_URLS = {
const ARRAY_PRESET_URLS = new Map([ const ARRAY_PRESET_URLS = new Map([
["rectangular-16x12", "/assets/array-geometries/rectangular-16x12.array.json"], ["rectangular-16x12", "/assets/array-geometries/rectangular-16x12.array.json"],
["fermat-spiral-192", "/assets/array-geometries/fermat-spiral-192.array.json"], ["fermat-spiral-192", "/assets/array-geometries/fermat-spiral-192.array.json"],
["rectangular-2x2-450mm", "/assets/array-geometries/rectangular-2x2-450mm.array.json"],
["rectangular-4x3-450mm", "/assets/array-geometries/rectangular-4x3-450mm.array.json"],
["rectangular-8x6-450mm", "/assets/array-geometries/rectangular-8x6-450mm.array.json"],
["rectangular-24x16-450mm", "/assets/array-geometries/rectangular-24x16-450mm.array.json"],
["rectangular-16x12-compact", "/assets/array-geometries/rectangular-16x12-compact.array.json"],
["rectangular-16x12-large", "/assets/array-geometries/rectangular-16x12-large.array.json"],
]); ]);
const BAND_COLORS = ["#65f2bf", "#58c7ff", "#ff7f8f", "#ffc56e", "#b294ff", "#8fe388", "#ff9ee5", "#83a8ff"]; const BAND_COLORS = ["#65f2bf", "#58c7ff", "#ff7f8f", "#ffc56e", "#b294ff", "#8fe388", "#ff9ee5", "#83a8ff"];
const MAX_HISTORY_ITEMS = 24; const MAX_HISTORY_ITEMS = 24;
+30 -24
View File
@@ -1,40 +1,46 @@
# Array Geometries # Array Geometries
Camera에서 즉시 사용하고 테스트에서 재현할 수 있는 계약 버전 `1.0.0`의 192채널 Array JSON Acoustic Camera에서 즉시 선택하고 같은 Scene/Experiment로 비교할 수 있는 Array JSON 예제입니다.
fixture입니다. 좌표 단위는 m이고 모든 microphone은 배열 평면 `z = 0`에 있습니다. `+x`는 오른쪽, 모든 파일은 계약 버전 `1.0.0`을 따르며 좌표 단위는 m입니다. microphone은 배열 평면
`+y`는 위, `+z`는 scene 방향입니다. `z = 0`에 있고, `+x`는 오른쪽, `+y`는 위, `+z`는 scene 방향입니다.
## 포함된 배열 ## 비교용 preset
### `rectangular-16x12.array.json` | 비교 축 | 파일 | 채널 | aperture | x × y pitch |
| --- | --- | ---: | ---: | ---: |
| 채널 수 · 최소 | `rectangular-2x2-450mm.array.json` | 4 | 0.45 × 0.45 m | 450 × 450 mm |
| 채널 수 · 적음 | `rectangular-4x3-450mm.array.json` | 12 | 0.45 × 0.45 m | 150 × 225 mm |
| 채널 수 · 중간 | `rectangular-8x6-450mm.array.json` | 48 | 0.45 × 0.45 m | 64.286 × 90 mm |
| 채널 수·크기 기준 | `rectangular-16x12.array.json` | 192 | 0.45 × 0.45 m | 30 × 40.909 mm |
| 채널 수 · 많음 | `rectangular-24x16-450mm.array.json` | 384 | 0.45 × 0.45 m | 19.565 × 30 mm |
| 물리 크기 · 소형 | `rectangular-16x12-compact.array.json` | 192 | 0.12 × 0.12 m | 8 × 10.909 mm |
| 물리 크기 · 대형 | `rectangular-16x12-large.array.json` | 192 | 1.20 × 1.20 m | 80 × 109.091 mm |
| 배치 패턴 | `fermat-spiral-192.array.json` | 192 | Ø 0.45 m | 비균일 |
- 16 columns × 12 rows = 192 microphones Camera의 **배열 선택**은 비교 목적에 맞춰 preset을 그룹으로 나눕니다.
- 폭과 높이가 각각 0.45 m인 정사각 aperture
- x/y 양 끝을 포함해 균일한 간격으로 배치
- 기본 single-tone experiment와 CPU/WebGPU 기준 fixture가 참조하는 baseline
### `fermat-spiral-192.array.json` - 채널 수 효과는 aperture가 모두 0.45 m인 4·12·48·192·384채널 rectangular 배열끼리 비교합니다.
- 물리 크기 효과는 topology와 채널 수가 같은 192채널 compact·baseline·large 배열끼리 비교합니다.
- 배치 패턴 효과는 192채널 0.45 m rectangular baseline과 Fermat spiral을 비교합니다.
- 192 microphones 이 preset들은 정확도를 보장하는 제품 사양이 아니라 trade-off를 관찰하기 위한 입력입니다. 채널 간격이
- 반지름 0.225 m, 지름 0.45 m인 원형 aperture 넓은 저채널·대형 배열에서 고주파 spatial aliasing과 grating lobe가 나타나거나, compact 배열에서
- golden angle과 면적 균등 반지름으로 만든 Fermat spiral main lobe가 넓어지는 것은 예상 가능한 결과입니다. 384채널 배열은 같은 grid에서 계산량과 메모리 사용량도
- rectangular baseline과 sidelobe·집중도·위치 오차를 비교하기 위한 preset 가장 큽니다. 빠른 동작 확인은 4채널이나 12채널부터 시작하세요.
Acoustic Camera의 **비교 배열**에서 두 배열을 선택해 같은 Scene/Experiment 결과를 세션 기록에
나란히 추가할 수 있습니다.
## fixture 다시 생성하기 ## fixture 다시 생성하기
저장소 최상위에서 다음 명령을 실행하면 두 JSON을 결정론적으로 다시 생성합니다. 저장소 최상위에서 다음 명령을 실행하면 위 8개 JSON을 결정론적으로 다시 생성합니다.
```powershell ```powershell
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\generate-array-fixture.ps1 powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\generate-array-fixture.ps1
``` ```
이 스크립트는 기존 `rectangular-16x12.array.json`과 `fermat-spiral-192.array.json`을 덮어씁니다. `ExecutionPolicy Bypass`는 해당 PowerShell 프로세스에만 적용됩니다. 생성 뒤
`ExecutionPolicy Bypass`는 이 PowerShell 프로세스에만 적용됩니다. 생성 뒤 `test.cmd`로 schema, 채널 수, 좌표 중복, 평면성, grid 행·열 수와 aperture 경계를 검증할 수
<http://127.0.0.1:4173/tests/>에서 schema와 192채널 배치 검증을 실행하세요. 있습니다.
사용자 배열은 [`../../schemas/array.schema.json`](../../schemas/array.schema.json) 구조를 따르고 사용자 배열은 [array schema](../../schemas/array.schema.json) 구조를 따르고 고유한 `id`,
고유한 `id`, aperture와 microphone별 `[x, y, z]`를 가져야 합니다. 선택적인 `sensitivityDb`와 aperture와 microphone별 `[x, y, z]`를 가져야 합니다. 선택적인 `sensitivityDb`와
`phaseOffsetDeg`는 합성 channel 편차와 beamforming 역 calibration에 사용됩니다. `phaseOffsetDeg`는 합성 channel 편차와 beamforming 역 calibration에 사용됩니다. geometry
효과와 calibration 효과를 분리하기 위해 기본 preset에는 이 값을 넣지 않았습니다.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
{
"schemaVersion": "1.0.0",
"kind": "micarrsim.array",
"id": "rectangular-2x2-450mm",
"name": "2x2 sparse rectangular",
"description": "4-channel uniform rectangular layout on the 0.45 m reference aperture",
"units": "m",
"coordinateSystem": {
"handedness": "right",
"xAxis": "right",
"yAxis": "up",
"zAxis": "toward-scene"
},
"aperture": {
"width": 0.45,
"height": 0.45
},
"microphones": [
{
"id": "mic-001",
"position": [
-0.225,
-0.225,
0
]
},
{
"id": "mic-002",
"position": [
0.225,
-0.225,
0
]
},
{
"id": "mic-003",
"position": [
-0.225,
0.225,
0
]
},
{
"id": "mic-004",
"position": [
0.225,
0.225,
0
]
}
]
}
@@ -0,0 +1,116 @@
{
"schemaVersion": "1.0.0",
"kind": "micarrsim.array",
"id": "rectangular-4x3-450mm",
"name": "4x3 sparse rectangular",
"description": "12-channel uniform rectangular layout on the 0.45 m reference aperture",
"units": "m",
"coordinateSystem": {
"handedness": "right",
"xAxis": "right",
"yAxis": "up",
"zAxis": "toward-scene"
},
"aperture": {
"width": 0.45,
"height": 0.45
},
"microphones": [
{
"id": "mic-001",
"position": [
-0.225,
-0.225,
0
]
},
{
"id": "mic-002",
"position": [
-0.075,
-0.225,
0
]
},
{
"id": "mic-003",
"position": [
0.075,
-0.225,
0
]
},
{
"id": "mic-004",
"position": [
0.225,
-0.225,
0
]
},
{
"id": "mic-005",
"position": [
-0.225,
0,
0
]
},
{
"id": "mic-006",
"position": [
-0.075,
0,
0
]
},
{
"id": "mic-007",
"position": [
0.075,
0,
0
]
},
{
"id": "mic-008",
"position": [
0.225,
0,
0
]
},
{
"id": "mic-009",
"position": [
-0.225,
0.225,
0
]
},
{
"id": "mic-010",
"position": [
-0.075,
0.225,
0
]
},
{
"id": "mic-011",
"position": [
0.075,
0.225,
0
]
},
{
"id": "mic-012",
"position": [
0.225,
0.225,
0
]
}
]
}
@@ -0,0 +1,404 @@
{
"schemaVersion": "1.0.0",
"kind": "micarrsim.array",
"id": "rectangular-8x6-450mm",
"name": "8x6 rectangular",
"description": "48-channel uniform rectangular layout on the 0.45 m reference aperture",
"units": "m",
"coordinateSystem": {
"handedness": "right",
"xAxis": "right",
"yAxis": "up",
"zAxis": "toward-scene"
},
"aperture": {
"width": 0.45,
"height": 0.45
},
"microphones": [
{
"id": "mic-001",
"position": [
-0.225,
-0.225,
0
]
},
{
"id": "mic-002",
"position": [
-0.160714,
-0.225,
0
]
},
{
"id": "mic-003",
"position": [
-0.096429,
-0.225,
0
]
},
{
"id": "mic-004",
"position": [
-0.032143,
-0.225,
0
]
},
{
"id": "mic-005",
"position": [
0.032143,
-0.225,
0
]
},
{
"id": "mic-006",
"position": [
0.096429,
-0.225,
0
]
},
{
"id": "mic-007",
"position": [
0.160714,
-0.225,
0
]
},
{
"id": "mic-008",
"position": [
0.225,
-0.225,
0
]
},
{
"id": "mic-009",
"position": [
-0.225,
-0.135,
0
]
},
{
"id": "mic-010",
"position": [
-0.160714,
-0.135,
0
]
},
{
"id": "mic-011",
"position": [
-0.096429,
-0.135,
0
]
},
{
"id": "mic-012",
"position": [
-0.032143,
-0.135,
0
]
},
{
"id": "mic-013",
"position": [
0.032143,
-0.135,
0
]
},
{
"id": "mic-014",
"position": [
0.096429,
-0.135,
0
]
},
{
"id": "mic-015",
"position": [
0.160714,
-0.135,
0
]
},
{
"id": "mic-016",
"position": [
0.225,
-0.135,
0
]
},
{
"id": "mic-017",
"position": [
-0.225,
-0.045,
0
]
},
{
"id": "mic-018",
"position": [
-0.160714,
-0.045,
0
]
},
{
"id": "mic-019",
"position": [
-0.096429,
-0.045,
0
]
},
{
"id": "mic-020",
"position": [
-0.032143,
-0.045,
0
]
},
{
"id": "mic-021",
"position": [
0.032143,
-0.045,
0
]
},
{
"id": "mic-022",
"position": [
0.096429,
-0.045,
0
]
},
{
"id": "mic-023",
"position": [
0.160714,
-0.045,
0
]
},
{
"id": "mic-024",
"position": [
0.225,
-0.045,
0
]
},
{
"id": "mic-025",
"position": [
-0.225,
0.045,
0
]
},
{
"id": "mic-026",
"position": [
-0.160714,
0.045,
0
]
},
{
"id": "mic-027",
"position": [
-0.096429,
0.045,
0
]
},
{
"id": "mic-028",
"position": [
-0.032143,
0.045,
0
]
},
{
"id": "mic-029",
"position": [
0.032143,
0.045,
0
]
},
{
"id": "mic-030",
"position": [
0.096429,
0.045,
0
]
},
{
"id": "mic-031",
"position": [
0.160714,
0.045,
0
]
},
{
"id": "mic-032",
"position": [
0.225,
0.045,
0
]
},
{
"id": "mic-033",
"position": [
-0.225,
0.135,
0
]
},
{
"id": "mic-034",
"position": [
-0.160714,
0.135,
0
]
},
{
"id": "mic-035",
"position": [
-0.096429,
0.135,
0
]
},
{
"id": "mic-036",
"position": [
-0.032143,
0.135,
0
]
},
{
"id": "mic-037",
"position": [
0.032143,
0.135,
0
]
},
{
"id": "mic-038",
"position": [
0.096429,
0.135,
0
]
},
{
"id": "mic-039",
"position": [
0.160714,
0.135,
0
]
},
{
"id": "mic-040",
"position": [
0.225,
0.135,
0
]
},
{
"id": "mic-041",
"position": [
-0.225,
0.225,
0
]
},
{
"id": "mic-042",
"position": [
-0.160714,
0.225,
0
]
},
{
"id": "mic-043",
"position": [
-0.096429,
0.225,
0
]
},
{
"id": "mic-044",
"position": [
-0.032143,
0.225,
0
]
},
{
"id": "mic-045",
"position": [
0.032143,
0.225,
0
]
},
{
"id": "mic-046",
"position": [
0.096429,
0.225,
0
]
},
{
"id": "mic-047",
"position": [
0.160714,
0.225,
0
]
},
{
"id": "mic-048",
"position": [
0.225,
0.225,
0
]
}
]
}
+114 -39
View File
@@ -1,17 +1,33 @@
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$projectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) $projectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$outputPath = Join-Path $projectRoot 'assets/array-geometries/rectangular-16x12.array.json' $utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
$microphones = [System.Collections.Generic.List[object]]::new()
$columns = 16
$rows = 12
$width = 0.45
$height = 0.45
for ($row = 0; $row -lt $rows; $row++) { function New-CoordinateSystem {
for ($column = 0; $column -lt $columns; $column++) { return [ordered]@{
$index = $row * $columns + $column handedness = 'right'
$x = -$width / 2 + $column * $width / ($columns - 1) xAxis = 'right'
$y = -$height / 2 + $row * $height / ($rows - 1) yAxis = 'up'
zAxis = 'toward-scene'
}
}
function New-RectangularArray {
param(
[Parameter(Mandatory = $true)][string]$Id,
[Parameter(Mandatory = $true)][string]$Name,
[Parameter(Mandatory = $true)][string]$Description,
[Parameter(Mandatory = $true)][int]$Columns,
[Parameter(Mandatory = $true)][int]$Rows,
[Parameter(Mandatory = $true)][double]$Width,
[Parameter(Mandatory = $true)][double]$Height
)
$microphones = [System.Collections.Generic.List[object]]::new()
for ($row = 0; $row -lt $Rows; $row++) {
for ($column = 0; $column -lt $Columns; $column++) {
$index = $row * $Columns + $column
$x = if ($Columns -eq 1) { 0 } else { -$Width / 2 + $column * $Width / ($Columns - 1) }
$y = if ($Rows -eq 1) { 0 } else { -$Height / 2 + $row * $Height / ($Rows - 1) }
$microphones.Add([ordered]@{ $microphones.Add([ordered]@{
id = 'mic-{0:D3}' -f ($index + 1) id = 'mic-{0:D3}' -f ($index + 1)
position = @([Math]::Round($x, 6), [Math]::Round($y, 6), 0) position = @([Math]::Round($x, 6), [Math]::Round($y, 6), 0)
@@ -19,28 +35,98 @@ for ($row = 0; $row -lt $rows; $row++) {
} }
} }
$arrayDefinition = [ordered]@{ return [ordered]@{
schemaVersion = '1.0.0' schemaVersion = '1.0.0'
kind = 'micarrsim.array' kind = 'micarrsim.array'
id = 'rectangular-16x12' id = $Id
name = '16x12 rectangular baseline' name = $Name
description = '192-channel uniform rectangular layout on a 0.45 m square aperture' description = $Description
units = 'm' units = 'm'
coordinateSystem = [ordered]@{ coordinateSystem = New-CoordinateSystem
handedness = 'right' aperture = [ordered]@{ width = $Width; height = $Height }
xAxis = 'right'
yAxis = 'up'
zAxis = 'toward-scene'
}
aperture = [ordered]@{ width = $width; height = $height }
microphones = $microphones microphones = $microphones
} }
}
$json = $arrayDefinition | ConvertTo-Json -Depth 8 function Write-ArrayFixture {
[System.IO.File]::WriteAllText($outputPath, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false)) param([Parameter(Mandatory = $true)]$Definition)
Write-Host "Generated $($microphones.Count) microphones: $outputPath"
$outputPath = Join-Path $projectRoot "assets/array-geometries/$($Definition.id).array.json"
$json = $Definition | ConvertTo-Json -Depth 8
[System.IO.File]::WriteAllText($outputPath, $json + [Environment]::NewLine, $utf8WithoutBom)
Write-Host "Generated $($Definition.microphones.Count) microphones: $outputPath"
}
$rectangularFixtures = @(
@{
Id = 'rectangular-2x2-450mm'
Name = '2x2 sparse rectangular'
Description = '4-channel uniform rectangular layout on the 0.45 m reference aperture'
Columns = 2
Rows = 2
Width = 0.45
Height = 0.45
},
@{
Id = 'rectangular-4x3-450mm'
Name = '4x3 sparse rectangular'
Description = '12-channel uniform rectangular layout on the 0.45 m reference aperture'
Columns = 4
Rows = 3
Width = 0.45
Height = 0.45
},
@{
Id = 'rectangular-8x6-450mm'
Name = '8x6 rectangular'
Description = '48-channel uniform rectangular layout on the 0.45 m reference aperture'
Columns = 8
Rows = 6
Width = 0.45
Height = 0.45
},
@{
Id = 'rectangular-16x12'
Name = '16x12 rectangular baseline'
Description = '192-channel uniform rectangular layout on a 0.45 m square aperture'
Columns = 16
Rows = 12
Width = 0.45
Height = 0.45
},
@{
Id = 'rectangular-24x16-450mm'
Name = '24x16 dense rectangular'
Description = '384-channel uniform rectangular layout on the 0.45 m reference aperture'
Columns = 24
Rows = 16
Width = 0.45
Height = 0.45
},
@{
Id = 'rectangular-16x12-compact'
Name = '16x12 compact rectangular'
Description = '192-channel uniform rectangular layout on a 0.12 m square aperture'
Columns = 16
Rows = 12
Width = 0.12
Height = 0.12
},
@{
Id = 'rectangular-16x12-large'
Name = '16x12 large rectangular'
Description = '192-channel uniform rectangular layout on a 1.20 m square aperture for spatial-aliasing comparison'
Columns = 16
Rows = 12
Width = 1.2
Height = 1.2
}
)
foreach ($fixture in $rectangularFixtures) {
Write-ArrayFixture (New-RectangularArray @fixture)
}
$spiralOutputPath = Join-Path $projectRoot 'assets/array-geometries/fermat-spiral-192.array.json'
$spiralMicrophones = [System.Collections.Generic.List[object]]::new() $spiralMicrophones = [System.Collections.Generic.List[object]]::new()
$microphoneCount = 192 $microphoneCount = 192
$radius = 0.225 $radius = 0.225
@@ -64,20 +150,9 @@ $spiralDefinition = [ordered]@{
name = '192-channel Fermat spiral' name = '192-channel Fermat spiral'
description = 'Equal-area Fermat spiral on a 0.45 m circular aperture for comparison with the rectangular baseline' description = 'Equal-area Fermat spiral on a 0.45 m circular aperture for comparison with the rectangular baseline'
units = 'm' units = 'm'
coordinateSystem = [ordered]@{ coordinateSystem = New-CoordinateSystem
handedness = 'right'
xAxis = 'right'
yAxis = 'up'
zAxis = 'toward-scene'
}
aperture = [ordered]@{ width = 0.45; height = 0.45 } aperture = [ordered]@{ width = 0.45; height = 0.45 }
microphones = $spiralMicrophones microphones = $spiralMicrophones
} }
$spiralJson = $spiralDefinition | ConvertTo-Json -Depth 8 Write-ArrayFixture $spiralDefinition
[System.IO.File]::WriteAllText(
$spiralOutputPath,
$spiralJson + [Environment]::NewLine,
[System.Text.UTF8Encoding]::new($false)
)
Write-Host "Generated $($spiralMicrophones.Count) microphones: $spiralOutputPath"
+25
View File
@@ -196,6 +196,31 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
!documentObject.querySelector("#run-button").disabled, !documentObject.querySelector("#run-button").disabled,
"original comparison configuration restore", "original comparison configuration restore",
); );
const comparisonPresets = [
["rectangular-2x2-450mm", "4 ch", "0.450 × 0.450 m"],
["rectangular-4x3-450mm", "12 ch", "0.450 × 0.450 m"],
["rectangular-8x6-450mm", "48 ch", "0.450 × 0.450 m"],
["rectangular-24x16-450mm", "384 ch", "0.450 × 0.450 m"],
["rectangular-16x12-compact", "192 ch", "0.120 × 0.120 m"],
["rectangular-16x12-large", "192 ch", "1.200 × 1.200 m"],
];
assertEqual(
documentObject.querySelectorAll("#array-preset option").length,
9,
"all built-in and custom array choices are visible",
);
for (const [presetId, microphoneCount, aperture] of comparisonPresets) {
dispatchValue(windowObject, documentObject.querySelector("#array-preset"), presetId);
await waitFor(
() => documentObject.querySelector("#array-preset").value === presetId &&
documentObject.querySelector("#microphone-count").textContent === microphoneCount &&
documentObject.querySelector("#array-meta").textContent.includes(aperture) &&
documentObject.documentElement.dataset.cameraValid === "true",
`${presetId} preset`,
);
}
dispatchValue( dispatchValue(
windowObject, windowObject,
documentObject.querySelector("#array-preset"), documentObject.querySelector("#array-preset"),
+96
View File
@@ -11,6 +11,16 @@ async function loadJson(path) {
return response.json(); return response.json();
} }
const RECTANGULAR_ARRAY_EXAMPLES = [
{ id: "rectangular-2x2-450mm", microphoneCount: 4, columns: 2, rows: 2, width: 0.45, height: 0.45 },
{ id: "rectangular-4x3-450mm", microphoneCount: 12, columns: 4, rows: 3, width: 0.45, height: 0.45 },
{ id: "rectangular-8x6-450mm", microphoneCount: 48, columns: 8, rows: 6, width: 0.45, height: 0.45 },
{ id: "rectangular-16x12", microphoneCount: 192, columns: 16, rows: 12, width: 0.45, height: 0.45 },
{ id: "rectangular-24x16-450mm", microphoneCount: 384, columns: 24, rows: 16, width: 0.45, height: 0.45 },
{ id: "rectangular-16x12-compact", microphoneCount: 192, columns: 16, rows: 12, width: 0.12, height: 0.12 },
{ id: "rectangular-16x12-large", microphoneCount: 192, columns: 16, rows: 12, width: 1.2, height: 1.2 },
];
test("moving-tone sample Scene and Experiment satisfy their linked contracts", async () => { test("moving-tone sample Scene and Experiment satisfy their linked contracts", async () => {
const [arrayDefinition, scene, experiment] = await Promise.all([ const [arrayDefinition, scene, experiment] = await Promise.all([
loadJson("/assets/array-geometries/rectangular-16x12.array.json"), loadJson("/assets/array-geometries/rectangular-16x12.array.json"),
@@ -36,6 +46,92 @@ test("192채널 기준 배열이 계약을 통과한다", async () => {
assertEqual((await validateArrayDefinition(arrayDefinition)).length, 0, "array issues"); assertEqual((await validateArrayDefinition(arrayDefinition)).length, 0, "array issues");
}); });
test("rectangular array examples cover low-to-high channel counts and compact-to-wide apertures", async () => {
const arrays = await Promise.all(RECTANGULAR_ARRAY_EXAMPLES.map(({ id }) =>
loadJson(`/assets/array-geometries/${id}.array.json`)));
for (let index = 0; index < arrays.length; index += 1) {
const arrayDefinition = arrays[index];
const expected = RECTANGULAR_ARRAY_EXAMPLES[index];
assertEqual(arrayDefinition.id, expected.id, `${expected.id} fixture id`);
assertEqual(arrayDefinition.microphones.length, expected.microphoneCount, `${expected.id} channel count`);
assertEqual(arrayDefinition.aperture.width, expected.width, `${expected.id} aperture width`);
assertEqual(arrayDefinition.aperture.height, expected.height, `${expected.id} aperture height`);
assertEqual(
(await validateArrayDefinition(arrayDefinition)).length,
0,
`${expected.id} contract issues`,
);
assertEqual(
new Set(arrayDefinition.microphones.map(({ id }) => id)).size,
expected.microphoneCount,
`${expected.id} microphone ids are unique`,
);
assertEqual(
new Set(arrayDefinition.microphones.map(({ position }) => position.join(","))).size,
expected.microphoneCount,
`${expected.id} positions are unique`,
);
const xCoordinates = arrayDefinition.microphones.map(({ position }) => position[0]);
const yCoordinates = arrayDefinition.microphones.map(({ position }) => position[1]);
assert(
arrayDefinition.microphones.every(({ position }) => position[2] === 0),
`${expected.id} microphones stay on the z=0 plane`,
);
assertEqual(new Set(xCoordinates).size, expected.columns, `${expected.id} column count`);
assertEqual(new Set(yCoordinates).size, expected.rows, `${expected.id} row count`);
const uniqueX = [...new Set(xCoordinates)].sort((left, right) => left - right);
const uniqueY = [...new Set(yCoordinates)].sort((left, right) => left - right);
const expectedXPitch = expected.width / (expected.columns - 1);
const expectedYPitch = expected.height / (expected.rows - 1);
assert(
uniqueX.slice(1).every((value, position) =>
Math.abs(value - uniqueX[position] - expectedXPitch) < 1.1e-6),
`${expected.id} uniform x pitch`,
);
assert(
uniqueY.slice(1).every((value, position) =>
Math.abs(value - uniqueY[position] - expectedYPitch) < 1.1e-6),
`${expected.id} uniform y pitch`,
);
assert(Math.abs(Math.min(...xCoordinates) + expected.width / 2) < 1e-6, `${expected.id} x minimum`);
assert(Math.abs(Math.max(...xCoordinates) - expected.width / 2) < 1e-6, `${expected.id} x maximum`);
assert(Math.abs(Math.min(...yCoordinates) + expected.height / 2) < 1e-6, `${expected.id} y minimum`);
assert(Math.abs(Math.max(...yCoordinates) - expected.height / 2) < 1e-6, `${expected.id} y maximum`);
}
const channelSeries = arrays.slice(0, 5);
assert(
channelSeries.every(({ aperture }) => aperture.width === 0.45 && aperture.height === 0.45),
"channel-count series keeps the 0.45 m aperture fixed",
);
assertEqual(
channelSeries.map(({ microphones }) => microphones.length).join(","),
"4,12,48,192,384",
"channel-count series",
);
const apertureSeries = [
arrays.find(({ id }) => id === "rectangular-16x12-compact"),
arrays.find(({ id }) => id === "rectangular-16x12"),
arrays.find(({ id }) => id === "rectangular-16x12-large"),
];
assert(
apertureSeries.every(({ microphones }) => microphones.length === 192),
"aperture series keeps 192 channels fixed",
);
assertEqual(
apertureSeries.map(({ aperture }) => aperture.width).join(","),
"0.12,0.45,1.2",
"compact-to-large aperture series",
);
assertEqual(
Math.max(...arrays.map(({ microphones }) => microphones.length)),
384,
"high-count example",
);
});
test("192채널 Fermat spiral 비교 배열이 계약과 원형 aperture를 만족한다", async () => { test("192채널 Fermat spiral 비교 배열이 계약과 원형 aperture를 만족한다", async () => {
const arrayDefinition = await loadJson("/assets/array-geometries/fermat-spiral-192.array.json"); const arrayDefinition = await loadJson("/assets/array-geometries/fermat-spiral-192.array.json");
assertEqual(arrayDefinition.microphones.length, 192, "spiral microphone count"); assertEqual(arrayDefinition.microphones.length, 192, "spiral microphone count");