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 기준 배열
- `assets/array-geometries/fermat-spiral-192.array.json`: 0.45 m 원형 aperture의 192채널 Fermat spiral
- **채널 수 비교:** 같은 0.45 m aperture에서 4·12·48·192·384채널 rectangular 배열
- **물리 크기 비교:** 같은 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)를 참고하세요.
## 테스트와 benchmark
@@ -241,7 +244,7 @@ test.cmd
```text
apps/ Dataset Builder와 Acoustic Camera UI
assets/ 192채널 배열과 synthetic Scene/Experiment fixture
assets/ 마이크 배열과 synthetic Scene/Experiment fixture
benchmarks/ 현재 브라우저에서 실행하는 WebGPU scaling 측정
docs/ 사용자 안내, 데이터 계약, 실행 계획과 기준 실측
packages/ 음향 수치 모델, 계약 검증, UI, WebGPU kernel
+3 -2
View File
@@ -113,8 +113,9 @@ curve는 합성 microphone channel에 항상 적용됩니다. **마이크 주파
- **Ground truth**를 켜면 source 위치와 검출 peak를 비교할 수 있습니다.
- **빔 분포**는 peak 주변 half-power lobe의 power 가중 공분산을 1× RMS 점선과 2× RMS 영역으로
표시합니다. 위치 확률, Gaussian 신뢰구간 또는 측정 불확실성 보정값은 아닙니다.
- rectangular와 Fermat spiral preset을 같은 Scene/Experiment에서 연속 실행하면 **배열 비교 기록**에
backend, timing과 평가 요약이 남습니다.
- **배열 선택**에는 채널 수(4·12·48·192·384), aperture 크기(0.12·0.45·1.20 m), rectangular와
Fermat spiral 배치를 분리 비교하는 8개 preset이 있습니다. 같은 Scene/Experiment에서 연속 실행하면
**배열 비교 기록**에 backend, timing과 평가 요약이 남습니다.
- 입력을 바꾼 뒤 화면에 남은 결과는 이전 실행 snapshot이며 UI가 이전 설정 결과임을 표시합니다.
- Camera에서 거리·음속·seed를 바꾼 Scene과 scan·STFT·band를 바꾼 Experiment는 우측의 각
**JSON 저장** 버튼으로 보존할 수 있습니다.
+23 -2
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<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>
<link rel="stylesheet" href="/packages/ui/app-shell.css" />
<link rel="stylesheet" href="/apps/acoustic-camera/styles.css" />
@@ -52,7 +52,28 @@
<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>
<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">
<summary>배열 미리보기와 좌표 정보</summary>
<canvas id="array-canvas" aria-label="마이크 배열 평면도"></canvas>
+6
View File
@@ -44,6 +44,12 @@ const MOVING_EXAMPLE_URLS = {
const ARRAY_PRESET_URLS = new Map([
["rectangular-16x12", "/assets/array-geometries/rectangular-16x12.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 MAX_HISTORY_ITEMS = 24;
+30 -24
View File
@@ -1,40 +1,46 @@
# Array Geometries
Camera에서 즉시 사용하고 테스트에서 재현할 수 있는 계약 버전 `1.0.0`의 192채널 Array JSON
fixture입니다. 좌표 단위는 m이고 모든 microphone은 배열 평면 `z = 0`에 있습니다. `+x`는 오른쪽,
`+y`는 위, `+z`는 scene 방향입니다.
Acoustic Camera에서 즉시 선택하고 같은 Scene/Experiment로 비교할 수 있는 Array JSON 예제입니다.
모든 파일은 계약 버전 `1.0.0`을 따르며 좌표 단위는 m입니다. microphone은 배열 평면
`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
- 폭과 높이가 각각 0.45 m인 정사각 aperture
- x/y 양 끝을 포함해 균일한 간격으로 배치
- 기본 single-tone experiment와 CPU/WebGPU 기준 fixture가 참조하는 baseline
Camera의 **배열 선택**은 비교 목적에 맞춰 preset을 그룹으로 나눕니다.
### `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
- 반지름 0.225 m, 지름 0.45 m인 원형 aperture
- golden angle과 면적 균등 반지름으로 만든 Fermat spiral
- rectangular baseline과 sidelobe·집중도·위치 오차를 비교하기 위한 preset
Acoustic Camera의 **비교 배열**에서 두 배열을 선택해 같은 Scene/Experiment 결과를 세션 기록에
나란히 추가할 수 있습니다.
이 preset들은 정확도를 보장하는 제품 사양이 아니라 trade-off를 관찰하기 위한 입력입니다. 채널 간격이
넓은 저채널·대형 배열에서 고주파 spatial aliasing과 grating lobe가 나타나거나, compact 배열에서
main lobe가 넓어지는 것은 예상 가능한 결과입니다. 384채널 배열은 같은 grid에서 계산량과 메모리 사용량도
가장 큽니다. 빠른 동작 확인은 4채널이나 12채널부터 시작하세요.
## fixture 다시 생성하기
저장소 최상위에서 다음 명령을 실행하면 두 JSON을 결정론적으로 다시 생성합니다.
저장소 최상위에서 다음 명령을 실행하면 위 8개 JSON을 결정론적으로 다시 생성합니다.
```powershell
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\generate-array-fixture.ps1
```
이 스크립트는 기존 `rectangular-16x12.array.json`과 `fermat-spiral-192.array.json`을 덮어씁니다.
`ExecutionPolicy Bypass`는 이 PowerShell 프로세스에만 적용됩니다. 생성 뒤
<http://127.0.0.1:4173/tests/>에서 schema와 192채널 배치 검증을 실행하세요.
`ExecutionPolicy Bypass`는 해당 PowerShell 프로세스에만 적용됩니다. 생성 뒤
`test.cmd`로 schema, 채널 수, 좌표 중복, 평면성, grid 행·열 수와 aperture 경계를 검증할 수
있습니다.
사용자 배열은 [`../../schemas/array.schema.json`](../../schemas/array.schema.json) 구조를 따르고
고유한 `id`, aperture와 microphone별 `[x, y, z]`를 가져야 합니다. 선택적인 `sensitivityDb`와
`phaseOffsetDeg`는 합성 channel 편차와 beamforming 역 calibration에 사용됩니다.
사용자 배열은 [array schema](../../schemas/array.schema.json) 구조를 따르고 고유한 `id`,
aperture와 microphone별 `[x, y, z]`를 가져야 합니다. 선택적인 `sensitivityDb`와
`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'
$projectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$outputPath = Join-Path $projectRoot 'assets/array-geometries/rectangular-16x12.array.json'
$microphones = [System.Collections.Generic.List[object]]::new()
$columns = 16
$rows = 12
$width = 0.45
$height = 0.45
$utf8WithoutBom = [System.Text.UTF8Encoding]::new($false)
for ($row = 0; $row -lt $rows; $row++) {
for ($column = 0; $column -lt $columns; $column++) {
$index = $row * $columns + $column
$x = -$width / 2 + $column * $width / ($columns - 1)
$y = -$height / 2 + $row * $height / ($rows - 1)
function New-CoordinateSystem {
return [ordered]@{
handedness = 'right'
xAxis = 'right'
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]@{
id = 'mic-{0:D3}' -f ($index + 1)
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'
kind = 'micarrsim.array'
id = 'rectangular-16x12'
name = '16x12 rectangular baseline'
description = '192-channel uniform rectangular layout on a 0.45 m square aperture'
id = $Id
name = $Name
description = $Description
units = 'm'
coordinateSystem = [ordered]@{
handedness = 'right'
xAxis = 'right'
yAxis = 'up'
zAxis = 'toward-scene'
}
aperture = [ordered]@{ width = $width; height = $height }
coordinateSystem = New-CoordinateSystem
aperture = [ordered]@{ width = $Width; height = $Height }
microphones = $microphones
}
}
$json = $arrayDefinition | ConvertTo-Json -Depth 8
[System.IO.File]::WriteAllText($outputPath, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
Write-Host "Generated $($microphones.Count) microphones: $outputPath"
function Write-ArrayFixture {
param([Parameter(Mandatory = $true)]$Definition)
$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()
$microphoneCount = 192
$radius = 0.225
@@ -64,20 +150,9 @@ $spiralDefinition = [ordered]@{
name = '192-channel Fermat spiral'
description = 'Equal-area Fermat spiral on a 0.45 m circular aperture for comparison with the rectangular baseline'
units = 'm'
coordinateSystem = [ordered]@{
handedness = 'right'
xAxis = 'right'
yAxis = 'up'
zAxis = 'toward-scene'
}
coordinateSystem = New-CoordinateSystem
aperture = [ordered]@{ width = 0.45; height = 0.45 }
microphones = $spiralMicrophones
}
$spiralJson = $spiralDefinition | ConvertTo-Json -Depth 8
[System.IO.File]::WriteAllText(
$spiralOutputPath,
$spiralJson + [Environment]::NewLine,
[System.Text.UTF8Encoding]::new($false)
)
Write-Host "Generated $($spiralMicrophones.Count) microphones: $spiralOutputPath"
Write-ArrayFixture $spiralDefinition
+25
View File
@@ -196,6 +196,31 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
!documentObject.querySelector("#run-button").disabled,
"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(
windowObject,
documentObject.querySelector("#array-preset"),
+96
View File
@@ -11,6 +11,16 @@ async function loadJson(path) {
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 () => {
const [arrayDefinition, scene, experiment] = await Promise.all([
loadJson("/assets/array-geometries/rectangular-16x12.array.json"),
@@ -36,6 +46,92 @@ test("192채널 기준 배열이 계약을 통과한다", async () => {
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 () => {
const arrayDefinition = await loadJson("/assets/array-geometries/fermat-spiral-192.array.json");
assertEqual(arrayDefinition.microphones.length, 192, "spiral microphone count");