docs: explain localization choices and cost

This commit is contained in:
2026-08-28 20:58:38 +09:00
parent fe0f70ab11
commit a48791075e
11 changed files with 675 additions and 185 deletions
+175 -35
View File
@@ -1,7 +1,7 @@
# 실측 다채널 위치추정 코어 포팅
`packages/acoustics-core/realtime-localizer.js`는 Scene 생성이나 음향 합성 없이 이미 sampling된
동기식 마이크 채널을 conventional CPU DAS에 직접 넣는다. 브라우저 DOM과 외부 package에
동기식 마이크 채널을 CPU DAS·DAS-PHAT·GCC-PHAT 위치 추정에 직접 넣는다. 브라우저 DOM과 외부 package에
의존하지 않는 ES module이므로, 실제 장치 adapter 뒤에 붙이거나 다른 프로젝트로 분리할 수 있다.
이 API는 ADC와 transport를 제어하지 않는다. 입력 channel을 획득·정렬·동기화한 다음 위치 map과
@@ -9,13 +9,16 @@
## 최소 복사 단위
직접 경로로 `realtime-localizer.js`를 import한다면 다음 여덟 파일과 상대 경로를 함께
직접 경로로 `realtime-localizer.js`를 import한다면 다음 열한 파일과 상대 경로를 함께
복사하면 된다.
```text
acoustics-core/
realtime-localizer.js
localization.js
localization-algorithms.js
das.js
gcc-phat.js
stft.js
fft.js
math.js
@@ -24,8 +27,10 @@ acoustics-core/
evaluation.js
```
`index.js`는 전체 core의 편의 export이므로 최소 포팅에는 필요하지 않다. 현재 portable API는
CPU 기준 구현이며 Camera의 WebGPU worker나 Scene·propagation 모듈을 요구하지 않는다.
`index.js`는 전체 core의 편의 export이므로 최소 포팅에는 필요하지 않다. Portable API와
`localization.js`의 analysis plan은 CPU 기준 구현이며 Camera의 WebGPU worker나 Scene·propagation
모듈을 요구하지 않는다. Camera backend 통합은 DAS와 DAS-PHAT에 WebGPU를 제공하지만 GCC-PHAT는
CPU only다. GCC-PHAT에 명시적 WebGPU를 요청하면 오류가 나며 `auto`는 CPU를 선택한다.
## 입력 계약
@@ -68,6 +73,58 @@ const array = {
`beamforming.maxCompensationDb`로 제한하며 기본 24 dB, 허용 범위 0–120 dB다.
응답 peak를 줄이는 attenuation과 phase 보상은 이 boost 상한에 의해 잘리지 않는다.
## 알고리즘 선택과 결과 scale
Portable plan은 top-level `algorithm`으로 위치 추정기를 선택한다. 이 값을 생략하면 기존 동작과
결과를 유지하는 `das`다. 기존 구성 형태인 `beamforming.algorithm`도 허용한다.
| `algorithm` | 핵심 계산 | `rawScale` | portable backend |
| --- | --- | --- | --- |
| `das` | frequency-domain delay-and-sum | `power` | CPU |
| `das-phat` | phase-only spectrum을 steering하는 DAS | `score` | CPU |
| `gcc-phat` | 한 reference mic에 대한 `M-1`개 GCC-PHAT 지연 scan | `score` | CPU only |
같은 array·scan·band·STFT로 알고리즘을 비교할 때도 알고리즘마다 별도 plan을 만든다.
```js
const dasPlan = createLocalizationPlan({
...commonConfig,
algorithm: "das",
});
const dasPhatPlan = createLocalizationPlan({
...commonConfig,
algorithm: "das-phat",
phatRelativeFloor: 1e-6,
});
const gccPhatPlan = createLocalizationPlan({
...commonConfig,
algorithm: "gcc-phat",
phatFloorRatio: 1e-6,
});
```
`phatRelativeFloor`는 DAS-PHAT의 channel spectrum 정규화 floor이고, `phatFloorRatio`는
GCC-PHAT pair cross spectrum 정규화 floor다. 둘 다 해당 묶음의 최대 magnitude에 대한 0~1
비율이며 기본값은 `1e-6`이다. GCC-PHAT는 `phatRelativeFloor`도 호환 alias로 받지만 새 코드에서는
`phatFloorRatio`를 쓰는 편이 의미가 명확하다. 값을 키우면 낮은 magnitude 성분의 기여가 줄어들지만
너무 크게 두면 유효한 broadband 정보도 약해질 수 있으므로 현장 데이터로 조정한다.
각 band는 `rawScale`을 반환한다. `rawValue`는 scale에 관계없이 읽을 수 있는 canonical peak
값이다. `rawPower`는 `rawScale === "power"`, `rawScore`는 `rawScale === "score"`인 peak에만
사용하는 편의 alias이며 두 필드가 항상 함께 있다고 가정하면 안 된다. `score`는 음향 power,
확률 또는 신뢰도가 아니라 동일 알고리즘·설정 안에서 상대 peak를 비교하는 비보정 값이다.
`power`도 capture amplitude와 calibration이 절대 단위로 교정되지 않았다면 W 또는 Pa²가 아니다.
GCC-PHAT의 raw score는 pair 수·band coefficient 수·IFFT 크기로 정규화한 0–1 양의 상관 응답이라
scan 범위와 microphone 수 자체에 따른 척도 변화는 피하지만, 확률로 보정된 confidence는 아니다.
GCC-PHAT에는 여러 FFT bin에 에너지가 있는 broadband 입력을 권장한다. Band당 8 bin 미만도 hard
error는 아니지만 단일 tone이나 좁은 band에서는 반복 correlation peak 때문에 위치가 모호할 수 있다.
Plan 생성 시 reference pair와 scan point에서 가능한 최대 절대 기하 지연을 계산하며, 이 값이
`fftSize / 2` sample을 넘으면 circular alias를 막기 위해 거절한다. 더 큰 `fftSize`를 쓰거나
배열·scan 기하를 줄여야 하며, 결과의 `maximumLagSamples`로 실제 상한을 기록할 수 있다.
## 공통 계획 만들기
Array, scan grid, band와 STFT 설정이 바뀌지 않는 동안 계획을 한 번 만들고 재사용한다.
@@ -94,6 +151,7 @@ const plan = createLocalizationPlan({
{ id: "machine", minHz: 900, maxHz: 1100, color: "#65f2bf" },
],
stft: { fftSize: 512, hopSize: 256 },
algorithm: "das", // "das" | "das-phat" | "gcc-phat"
speedOfSound: 343,
normalization: "peak",
compensateFrequencyResponse: true,
@@ -113,19 +171,63 @@ const plan = createLocalizationPlan({
- scan의 `columns`와 `rows`는 양의 정수
- `speedOfSound`는 양수
- `normalization`은 `peak` 또는 `none`
- `algorithm`은 `das`, `das-phat`, `gcc-phat` 중 하나
- `phatRelativeFloor`와 `phatFloorRatio`는 지정한 경우 0–1
- `maxCompensationDb`는 0–120 dB
- `peakDetection.thresholdRatio`는 0–1,
`minimumPeakDistanceM`는 0 이상,
`maximumDetections`는 1–128의 정수
계획은 거리 행렬, bin calibration, 임시 map과 허용되는 경우 steering matrix를 유지하는
stateful 객체다. 같은 계획으로 두 분석을 동시에 실행하지 말고, worker마다 별도 계획을 만든다.
Array·scan·band·STFT를 바꾸면 새 계획을 만들어야 한다.
계획은 알고리즘별 거리 행렬과 workspace를 유지하는 stateful 객체다. DAS 계열은 bin calibration,
임시 map과 허용되는 경우 steering matrix를 유지하고, GCC-PHAT는 reference mic 선택,
STFT·correlation workspace를 유지한다. 같은 계획으로 두 분석을 동시에 실행하지 말고 worker마다
별도 계획을 만든다. Array·scan·band·STFT·알고리즘을 바꾸면 새 계획을 만들어야 한다.
Portable plan의 generic cache는 `plan.analysisPlan`이며 기존 호출자를 위해 DAS 계열에는
`plan.dasPlan`, GCC-PHAT에는 `plan.gccPhatPlan` alias도 제공한다.
portable 계획의 기본 compute batch는 scan point 256개와 STFT transform 128개 단위로 event
loop에 양보한다. `computeOptions.pointBatchSize`와
`computeOptions.stftTransformBatchSize`로 조정할 수 있지만, 큰 batch는 throughput과
취소·UI 응답성의 trade-off가 있으므로 장치에서 함께 측정한다.
portable 계획의 기본 scan point batch는 DAS 계열 256개, GCC-PHAT 4,096개이고 STFT transform
batch는 128개다. GCC-PHAT cross-spectrum은 16,384 operation 단위로도 양보한다. 각 batch 경계에서
event loop에 기회를 주며 `computeOptions.pointBatchSize`,
`computeOptions.stftTransformBatchSize`, `computeOptions.crossSpectrumBatchSize`로 조정할
수 있다. 큰 batch는 throughput과 취소·UI 응답성의 trade-off가 있으므로 장치에서 함께 측정한다.
낮은 수준에서 직접 사용할 때는 `localization.js`의 generic plan과 compute 함수를 짝지어 쓴다.
Plan compatibility는 값이 같은 새 객체가 아니라 생성할 때 넘긴 `arrayDefinition`, `scene`,
`experiment`의 정확히 같은 객체를 요구한다.
```js
import {
computeLocalizationMap,
createLocalizationAnalysisPlan,
} from "./acoustics-core/localization.js";
const experiment = {
stft: { fftSize: 512, hopSize: 256, window: "hann" },
scan,
frequencyBands: bands,
beamforming: { algorithm: "das-phat", normalization: "peak" },
};
const scene = { speedOfSound: 343 };
const analysisPlan = createLocalizationAnalysisPlan({
sampleRate: 48_000,
arrayDefinition: array,
scene,
experiment,
});
for (const channels of capturedBlocks) {
const result = await computeLocalizationMap({
channels,
sampleRate: 48_000,
arrayDefinition: array,
scene,
experiment,
analysisPlan,
phatRelativeFloor: 1e-6,
});
consume(result);
}
```
## 한 블록 분석
@@ -144,22 +246,28 @@ const result = await localizeMultichannel({
});
for (const location of result.locations) {
console.log(location.bandId, location.position, location.rawPower);
console.log(
location.bandId,
location.position,
location.rawScale,
location.rawValue,
);
}
```
`result.locations`는 입력 band별 raw map의 local peak를 flat array로 담는다. 각 band
안에서는 power가 강한 순서이며 항목에는 `bandId`, band 내 `rank`, scan
`index`, `position`과 `rawPower`가 있다. 기본 threshold는 해당 band 최강
peak의 50%, 최소 peak 간격은 scan cell 대각선, 최대 검출 수는 band당 8개다. 여러 대역을
사용한다면 `bandId`로 묶어 해석한다.
안에서는 `rawValue`가 큰 순서이며 항목에는 `bandId`, band 내 `rank`, scan `index`,
`position`, `rawValue`, `rawScale`이 있다. DAS의 기존 `rawPower` 필드는 호환 alias로 유지된다.
Score 결과에서는 `rawValue`를 공통 필드로 읽고, `rawScore`가 포함된 peak 객체에서는 같은 값의
score alias로 사용할 수 있다. 기본 threshold는 해당 band 최강 peak의 50%, 최소 peak 간격은
scan cell 대각선, 최대 검출 수는 band당 8개다. 여러 대역을 사용한다면 `bandId`로 묶어 해석한다.
`peakDetection`은 후처리 위치 목록만 바꾸며 `result.bands[n].rawMap`과
`displayMap`은 그대로 보존한다. 같은 대역의 두 음원이 main lobe보다 가까우면 local
peak가 하나로 합쳐질 수 있고, threshold를 낮추면 sidelobe 후보도 늘 수 있다.
`sampleCount`와 `duration`도 결과에 함께 반환된다.
Block 결과의 `processingTimeMs`는 DAS 계산과 local-peak 검출에 걸린 monotonic wall
Block 결과의 `processingTimeMs`는 선택한 위치 추정 계산과 local-peak 검출에 걸린 monotonic wall
time이다. capture, adapter 변환과 결과 rendering 시간은 포함하지 않으므로 end-to-end latency는
호출자가 별도로 측정한다.
@@ -168,13 +276,14 @@ time이다. capture, adapter 변환과 결과 rendering 시간은 포함하지
## 연속 chunk 처리
`StreamingDasLocalizer` 또는 같은 factory인 `createStreamingLocalizer`는 고정 용량
ring buffer와 분석 workspace를 갖는다.
`StreamingLocalizer`는 알고리즘 중립 이름이며 기존 `StreamingDasLocalizer` class의 export alias다.
오래된 이름은 호환을 위해 유지된다. 두 이름과 `createStreamingLocalizer` factory는 모두 선택한
`algorithm`에 맞는 고정 용량 ring buffer와 분석 workspace를 만든다.
```js
import { createStreamingLocalizer } from "./acoustics-core/realtime-localizer.js";
import { StreamingLocalizer } from "./acoustics-core/realtime-localizer.js";
const localizer = createStreamingLocalizer({
const localizer = new StreamingLocalizer({
sampleRate: 48_000,
array,
scan: {
@@ -190,6 +299,8 @@ const localizer = createStreamingLocalizer({
{ id: "machine", minHz: 900, maxHz: 1100, color: "#65f2bf" },
],
stft: { fftSize: 512, hopSize: 256 },
algorithm: "das-phat",
phatRelativeFloor: 1e-6,
analysisWindowSamples: 4_800,
analysisHopSamples: 2_400,
maximumBufferedSamples: 19_200,
@@ -224,6 +335,11 @@ async function analysisLoop(signal) {
`deadlineMs = analysisHopSamples / sampleRate × 1000`과
`deadlineMet = processingTimeMs <= deadlineMs`를 반환한다.
`deadlineMet`는 완료된 한 창의 위치 추정 시간이 hop 예산 안이었는지를 나타내는 측정값이지,
해당 구성이 실시간이라는 사전 보장이 아니다. JIT warm-up, GC, 다른 thread의 부하를 포함해 목표
장치에서 연속 실행을 측정해야 한다. 이 값은 capture·transport·rendering을 포함하지 않으므로 전체
시스템 deadline은 adapter 경계에서 따로 계측한다.
## 지연과 backpressure 조정
서로 다른 hop이 두 층에 있다.
@@ -268,18 +384,42 @@ async function analysisLoop(signal) {
입력 retained byte는
`채널 수 × (maximumBufferedSamples + analysisWindowSamples) × 4`이며 할당 전에 검사한다.
계획은 거리 행렬, bin calibration, Hann window, Float64 FFT workspace와 임시 map을 재사용한다.
기본 retained data가 `maximumPlanBytes`를 넘으면 계획 생성을 거절한다. 전체 steering
matrix가 64 MiB 또는 계획 전체 한도를 넘으면 선택 bin 앞쪽부터 예산에 들어가는 만큼만 cache하고
나머지는 고정 microphone workspace에서 필요할 때 계산한다.
모든 계획은 거리 행렬과 STFT workspace를 재사용한다. DAS 계열은 bin calibration, Hann window,
임시 map과 steering cache를, GCC-PHAT는 선택 bin metadata, reference mic와 correlation workspace를
재사용한다. 기본 retained data가 `maximumPlanBytes`를 넘으면 할당 전에 계획 생성을 거절한다.
Plan 없는 GCC 단발 분석도 거리 행렬 할당 전에 전체 working set을 `maximumWorkingBytes`와 비교한다.
DAS의 전체 steering matrix가 64 MiB 또는 계획 전체 한도를 넘으면 선택 bin 앞쪽부터 예산에 들어가는
만큼만 cache하고 나머지는 고정 microphone workspace에서 필요할 때 계산한다.
192채널, 50×50 scan, 선택 bin 2개를 같은 계획으로 10회 분석하는 기준에서 구현상 거리 계산은
기존 DAS 호환 경로에서 192채널, 50×50 scan, 선택 bin 2개를 같은 계획으로 10회 분석하면 거리 계산은
4,800,000회에서 480,000회, steering point–microphone pair는 9,600,000회에서 960,000회로
줄어든다. 17,280,000회의 `sin/cos` 호출을 피하고 약 16.5 MiB를 유지한다. 자동 테스트는
부분 cache와 cache 없음의 raw/display `Float32` map 모든 원소가 bit-exact임을 확인한다.
각 결과의 `analysisCache`에는 cache된 bin capacity와 실제 cached/uncached 사용,
거리·steering pair, STFT transform·FFT butterfly·DAS complex MAC, workspace allocation·reuse
수가 들어가므로 설정별 최적화 효과를 계측할 수 있다.
각 결과의 `analysisCache`에는 공통 거리·STFT 통계와 알고리즘별 작업량이 들어간다. DAS 계열은
cached/uncached steering pair, FFT butterfly와 complex MAC을, GCC-PHAT는 cross-spectrum
operation, pair별 IFFT, scan lookup과 correlation workspace reuse를 기록한다.
연산량을 비교할 때 다음 기호를 사용한다.
- `P`: scan point 수
- `M`: microphone 수
- `T`: 한 분석창의 STFT frame 수
- `F`: 선택한 고유 FFT bin 수
- `F_b`: band `b`의 bin 수, `B`: band 수
- `N`: `fftSize`
DAS와 DAS-PHAT의 지배적인 steering 누산은 대략 `P × M × T × F` complex MAC이다. PHAT
정규화가 추가되지만 scan 차수는 같다. GCC-PHAT는 모든 mic의 forward STFT를 한 번 수행한 뒤
reference pair `M-1`개에 대해 다음 작업을 수행한다.
- magnitude 최대값 pass와 PHAT 누산 pass:
`2 × (M-1) × T × ΣF_b` cross-spectrum operation
- 각 pair·band의 길이 `N` correlation IFFT: `(M-1) × B`회
- 각 scan point의 fractional lag lookup: `P × (M-1) × B`회
따라서 GCC-PHAT의 scan lookup은 DAS의 `P × M × T × F`보다 작을 수 있지만 공짜는 아니다.
Forward STFT는 그대로 필요하고, 작은 grid·큰 `N`·많은 band에서는 pair별 IFFT나 2-pass cross
spectrum이 지배할 수 있다. `workload`와 실제 `processingTimeMs`를 함께 비교한다.
실시간 목표를 맞추는 조정 순서는 보통 다음과 같다.
@@ -293,15 +433,15 @@ matrix가 64 MiB 또는 계획 전체 한도를 넘으면 선택 bin 앞쪽부
7. capture와 분석을 별도 Worker/thread에 두고 결과만 UI 또는 제어 loop로 전달한다.
캐시 최적화와 위 수치는 반복 계산 감소의 근거이지 192채널·50×50 또는 다른 구성이 실시간이라는
보장이 아니다. 마이크 수, scan point, 선택 bin, STFT frame에 따른 주연산은 여전히 대략
`O(P × M × F × T)`다. `deadlineMisses` 또는 `droppedSamples`가 누적되면
grid·band bin·FFT/분석창 또는 update rate를 줄여야 한다. deadline은 위치추정 처리량 판정이며
capture와 출력까지 포함하는 시스템 deadline은 adapter에서 별도로 검증한다.
보장이 아니다. `deadlineMisses` 또는 `droppedSamples`가 누적되면 grid·band bin·FFT/분석창 또는
update rate를 줄여야 한다. 평균 한 번만 재지 말고 목표 장치의 실제 입력으로 warm-up 뒤 충분히 긴
구간의 최대값과 상위 percentile을 측정한다. Capture와 출력까지 포함하는 시스템 deadline은
adapter에서 별도로 검증한다.
## 취소와 오류 복구
`localizeMultichannel`과 `analyzeAvailable`에 `AbortSignal`을 전달하면 STFT와
DAS batch 경계에서 `AbortError`를 발생시키고 계획 lock을 해제한다.
알고리즘별 scan/correlation batch 경계에서 `AbortError`를 발생시키고 계획 lock을 해제한다.
Streaming 분석이 취소되거나 실패하면 `failedAnalyses`가 증가하지만 해당 hop을
소비하지 않는다. 실패한 창은 별도 analysis workspace에 보존되며, 다음
@@ -329,4 +469,4 @@ hop을 commit하고 sequence를 증가시킨다.
코어 뒤에서는 `locations`를 제어에 바로 쓰기 전에 현장 데이터로 검출 임계값, false peak,
latency와 위치 오차를 검증해야 한다. 현재 `locations`는 통계적 신뢰구간이 아니라 각 band
DAS map에서 threshold와 거리 억제를 통과한 grid local peak 후보들이다.
위치 map에서 threshold와 거리 억제를 통과한 grid local peak 후보들이다.