507 lines
24 KiB
Markdown
507 lines
24 KiB
Markdown
# 실측 다채널 위치추정 코어 포팅
|
||
|
||
`packages/acoustics-core/realtime-localizer.js`는 Scene 생성이나 음향 합성 없이 이미 sampling된
|
||
동기식 마이크 채널을 CPU DAS·DAS-PHAT·GCC-PHAT 위치 추정에 직접 넣는다. 브라우저 DOM과 외부 package에
|
||
의존하지 않는 ES module이므로, 실제 장치 adapter 뒤에 붙이거나 다른 프로젝트로 분리할 수 있다.
|
||
|
||
이 API는 ADC와 transport를 제어하지 않는다. 입력 channel을 획득·정렬·동기화한 다음 위치 map과
|
||
대역별 local peak 후보를 계산하는 경계부터 담당한다.
|
||
|
||
## 최소 복사 단위
|
||
|
||
직접 경로로 `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
|
||
frequency-response.js
|
||
cooperative-yield.js
|
||
evaluation.js
|
||
```
|
||
|
||
`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를 선택한다.
|
||
|
||
## 입력 계약
|
||
|
||
`channels`는 다음 조건을 만족해야 한다.
|
||
|
||
- `array.microphones`와 같은 순서·개수의 JavaScript 배열
|
||
- 모든 원소가 `Float32Array`
|
||
- 한 호출 또는 chunk 안에서 모든 채널의 sample 수가 동일
|
||
- `sampleRate`가 모든 채널에서 동일하고 sample index가 같은 시각을 의미함
|
||
- 값은 선형 진폭 sample이며, 장치 integer PCM은 adapter에서 일관된 scale의 float로 변환
|
||
|
||
코어는 channel timestamp, interleaved frame, endian, packet loss 또는 sample clock을 추론하지
|
||
않는다. 잘못된 channel 순서나 채널 사이의 sample slip은 위치 오차로 그대로 나타난다.
|
||
|
||
Array에는 최소한 다음 정보가 필요하다.
|
||
|
||
```js
|
||
const array = {
|
||
microphones: [
|
||
{
|
||
id: "mic-0",
|
||
position: [-0.10, 0.10, 0],
|
||
sensitivityDb: 0,
|
||
phaseOffsetDeg: 0,
|
||
frequencyResponse: [
|
||
{ frequencyHz: 100, gainDb: -6, phaseDeg: -5 },
|
||
{ frequencyHz: 1000, gainDb: 0, phaseDeg: 0 },
|
||
{ frequencyHz: 10000, gainDb: -2, phaseDeg: 8 },
|
||
],
|
||
},
|
||
// 실제 capture channel 순서대로 계속
|
||
],
|
||
};
|
||
```
|
||
|
||
`frequencyResponse`는 선택 사항이다. 있으면 2–64개 점의 주파수가 엄격히 증가해야 한다.
|
||
`compensateFrequencyResponse`의 기본값은 `true`이며, 알려진 curve와
|
||
`sensitivityDb`/`phaseOffsetDeg`의 역보정을 steering에 포함한다. 응답 notch의
|
||
양의 보상 boost는 top-level `maxCompensationDb` 또는
|
||
`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 |
|
||
| `music` | 표준 좁은대역 Schmidt noise-subspace scan | `noise-projection` | 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,
|
||
});
|
||
|
||
const musicPlan = createLocalizationPlan({
|
||
...commonConfig,
|
||
algorithm: "music",
|
||
normalization: "none",
|
||
musicSourceCountMethod: "fixed",
|
||
musicSourceCount: 1,
|
||
});
|
||
```
|
||
|
||
`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 설정이 바뀌지 않는 동안 계획을 한 번 만들고 재사용한다.
|
||
|
||
```js
|
||
import {
|
||
createLocalizationPlan,
|
||
localizeMultichannel,
|
||
} from "./acoustics-core/realtime-localizer.js";
|
||
|
||
const plan = createLocalizationPlan({
|
||
sampleRate: 48_000,
|
||
array,
|
||
scan: {
|
||
xMin: -1,
|
||
xMax: 1,
|
||
yMin: -1,
|
||
yMax: 1,
|
||
z: 2,
|
||
columns: 50,
|
||
rows: 50,
|
||
},
|
||
bands: [
|
||
{ id: "machine", minHz: 900, maxHz: 1100, color: "#65f2bf" },
|
||
],
|
||
stft: { fftSize: 512, hopSize: 256 },
|
||
algorithm: "das", // "das" | "das-phat" | "gcc-phat" | "music"
|
||
speedOfSound: 343,
|
||
normalization: "peak",
|
||
compensateFrequencyResponse: true,
|
||
maxCompensationDb: 24,
|
||
peakDetection: {
|
||
thresholdRatio: 0.5,
|
||
// minimumPeakDistanceM을 생략하면 scan cell 대각선 길이
|
||
maximumDetections: 8,
|
||
},
|
||
});
|
||
```
|
||
|
||
주요 검증 조건은 다음과 같다.
|
||
|
||
- `fftSize`는 2 이상의 2의 거듭제곱이고 `hopSize <= fftSize`
|
||
- band는 하나 이상이며 `0 <= minHz < maxHz`. Nyquist 위 구간은 anti-alias filter 없는 ideal
|
||
real ADC의 등가 baseband bin으로 접힌다. 단일 channel 시간 sample은 원 alias order를
|
||
복원하지 못하지만 다채널 공간 위상에는 원 파장의 증거가 남을 수 있다. 현재 portable
|
||
localizer는 carrier hypothesis를 열거하지 않는다.
|
||
- scan의 `columns`와 `rows`는 양의 정수
|
||
- `speedOfSound`는 양수
|
||
- `normalization`은 `peak` 또는 `none`
|
||
- `algorithm`은 `das`, `das-phat`, `gcc-phat`, `music` 중 하나
|
||
- `phatRelativeFloor`와 `phatFloorRatio`는 지정한 경우 0–1
|
||
- `maxCompensationDb`는 0–120 dB
|
||
- `peakDetection.thresholdRatio`는 0–1,
|
||
`minimumPeakDistanceM`는 0 이상,
|
||
`maximumDetections`는 1–128의 정수
|
||
|
||
계획은 알고리즘별 거리 행렬과 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`, MUSIC에는 `plan.musicPlan` alias도 제공한다.
|
||
|
||
### MUSIC 포팅 경계
|
||
|
||
MUSIC raw `q = ||E_n^H a||²/(a^H a)`는 작은 값이 좋은 noise projection이다. 표시용
|
||
`P/Pmax = q_min/q`도 확률·신뢰도·Gaussian 신뢰구간이 아니며, raw q 보존을 위해
|
||
`normalization: "none"`만 허용한다.
|
||
|
||
band마다 사용자가 지정한 analog FFT-grid 가설 하나와 접힌 positive ADC bin 하나만 사용한다.
|
||
Scene carrier/위치 truth는 읽지 않는다. analog carrier `f_a`의 공간 위상을 alias orientation
|
||
`η`로 `exp[-j η 2π f_a Δr/c]`에 적용하며, inversion이면 calibration과 steering을 포함한
|
||
전체 complex manifold를 켤레화한다. 정확한 DC/Nyquist는 quadrature-degenerate라 거절한다.
|
||
|
||
Fixed K는 `1..M-1`이다. Wax–Kailath MDL/AIC는 complete non-overlapping frame, `T > M`,
|
||
strict-positive noise eigenvalue, IID complex Gaussian snapshot·spatially white noise·고정 K를
|
||
가정한다. non-overlap은 독립성의 증명이 아니다. 조건 불충족과 식별 불가능한 partition은
|
||
`MusicUnavailableError` (`code: "MUSIC_UNAVAILABLE"`)이며 fixed fallback은 없다.
|
||
diagonal loading, floor, spatial smoothing, multibin/wideband fusion도 없다. grating lobe,
|
||
동률과 coherent rank deficiency를 결과가 좋아 보이도록 제거하지 않는다.
|
||
|
||
계획은 allocation 전에 typed working bytes, FFT butterfly, covariance complex accumulation,
|
||
Jacobi pair update와 projection MAC 상한을 검사한다. 이 CPU cap은 메모리·응답성 보호 장치이지
|
||
realtime 보장이 아니므로 `processingTimeMs`와 streaming deadline 통계를 대상 장치에서 측정한다.
|
||
|
||
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);
|
||
}
|
||
```
|
||
|
||
## 한 블록 분석
|
||
|
||
짧은 녹음이나 adapter가 이미 만든 고정 길이 frame은 `localizeMultichannel`로 처리한다.
|
||
|
||
```js
|
||
const controller = new AbortController();
|
||
|
||
const result = await localizeMultichannel({
|
||
plan,
|
||
channels, // Array<Float32Array>
|
||
signal: controller.signal,
|
||
onProgress(progress) {
|
||
updateProgress(progress);
|
||
},
|
||
});
|
||
|
||
for (const location of result.locations) {
|
||
console.log(
|
||
location.bandId,
|
||
location.position,
|
||
location.rawScale,
|
||
location.rawValue,
|
||
);
|
||
}
|
||
```
|
||
|
||
`result.locations`는 입력 band별 raw map의 local peak를 flat array로 담는다. 각 band
|
||
안에서는 `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`는 선택한 위치 추정 계산과 local-peak 검출에 걸린 monotonic wall
|
||
time이다. capture, adapter 변환과 결과 rendering 시간은 포함하지 않으므로 end-to-end latency는
|
||
호출자가 별도로 측정한다.
|
||
|
||
계획을 전달하지 않고 `localizeMultichannel({ channels, ...config })` 형태로도 호출할 수
|
||
있지만, 매번 geometry cache를 다시 만들므로 반복·실시간 처리에는 권장하지 않는다.
|
||
|
||
## 연속 chunk 처리
|
||
|
||
`StreamingLocalizer`는 알고리즘 중립 이름이며 기존 `StreamingDasLocalizer` class의 export alias다.
|
||
오래된 이름은 호환을 위해 유지된다. 두 이름과 `createStreamingLocalizer` factory는 모두 선택한
|
||
`algorithm`에 맞는 고정 용량 ring buffer와 분석 workspace를 만든다.
|
||
|
||
```js
|
||
import { StreamingLocalizer } from "./acoustics-core/realtime-localizer.js";
|
||
|
||
const localizer = new StreamingLocalizer({
|
||
sampleRate: 48_000,
|
||
array,
|
||
scan: {
|
||
xMin: -1,
|
||
xMax: 1,
|
||
yMin: -1,
|
||
yMax: 1,
|
||
z: 2,
|
||
columns: 50,
|
||
rows: 50,
|
||
},
|
||
bands: [
|
||
{ 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,
|
||
maximumResultsPerCall: 1,
|
||
});
|
||
|
||
function onCaptureChunk(channels) {
|
||
// append()는 동기식으로 복사하고 즉시 상태를 반환한다.
|
||
const state = localizer.append(channels);
|
||
if (state.droppedSamples > 0) reportBackpressure(state);
|
||
}
|
||
|
||
async function analysisLoop(signal) {
|
||
const results = await localizer.analyzeAvailable({ signal });
|
||
for (const result of results) {
|
||
renderMap(result.bands[0].displayMap, result.centerTime);
|
||
if (!result.deadlineMet) {
|
||
reportOverload(result.processingTimeMs, result.deadlineMs);
|
||
}
|
||
}
|
||
const state = localizer.getState();
|
||
if (state.deadlineMisses > 0 || state.droppedSamples > 0) reportBackpressure(state);
|
||
}
|
||
```
|
||
|
||
`push(channels, options)`는 `append` 후 `analyzeAvailable`을 한 번 호출하는
|
||
편의 메서드다. 임의의 capture chunk 경계로 나누어 넣어도 보존된 분석창의 결과는 동일하다.
|
||
|
||
각 streaming 결과에는 `sequence`, `startSample`, `endSample`,
|
||
`startTime`, `endTime`과 `centerTime`이 추가된다. sample 위치는 localizer를
|
||
만든 뒤 받은 첫 sample을 0으로 하는 절대 index다. 또한
|
||
`deadlineMs = analysisHopSamples / sampleRate × 1000`과
|
||
`deadlineMet = processingTimeMs <= deadlineMs`를 반환한다.
|
||
|
||
`deadlineMet`는 완료된 한 창의 위치 추정 시간이 hop 예산 안이었는지를 나타내는 측정값이지,
|
||
해당 구성이 실시간이라는 사전 보장이 아니다. JIT warm-up, GC, 다른 thread의 부하를 포함해 목표
|
||
장치에서 연속 실행을 측정해야 한다. 이 값은 capture·transport·rendering을 포함하지 않으므로 전체
|
||
시스템 deadline은 adapter 경계에서 따로 계측한다.
|
||
|
||
## 지연과 backpressure 조정
|
||
|
||
서로 다른 hop이 두 층에 있다.
|
||
|
||
- `stft.hopSize`: 한 분석창 안에서 spectrum을 평균하는 STFT frame 간격
|
||
- `analysisHopSamples`: 다음 위치 map을 시작하는 간격
|
||
|
||
`analysisWindowSamples`는 `fftSize` 이상이어야 한다. 긴 창은 안정적인 spectrum
|
||
평균에 유리하지만 결과 지연과 연산량이 늘어난다. 주파수 bin 간격은 주로 `fftSize`로
|
||
결정된다.
|
||
`analysisHopSamples`는 창보다 클 수 없으며 작게 할수록 map update rate와 중첩 연산량이
|
||
늘어난다.
|
||
|
||
`maximumBufferedSamples`는 최소 한 분석창 이상이어야 한다. 새 chunk가 용량을 넘으면 가장
|
||
오래된 sample부터 버리고 `droppedSamples`, `bufferStartSample`을 갱신한다. 호출마다
|
||
반환할 결과 수는 기본 1개이고 구성 상한은 32개다. 한 번에 긴 backlog를 모두 계산하지 않도록
|
||
`maximumResultsPerCall`을 작게 유지하고 event loop나 worker scheduler가 다시 호출하게 한다.
|
||
|
||
`getState()`으로 다음 값을 감시할 수 있다.
|
||
|
||
- `bufferedSamples`, `availableAnalysisCount`, `bufferStartSample`
|
||
- `totalReceivedSamples`와 `droppedSamples`
|
||
- `analysisAttempts`, `completedAnalyses`, `failedAnalyses`
|
||
- `analysisDeadlineMs`, `lastProcessingTimeMs`,
|
||
`maximumProcessingTimeMs`와 `deadlineMisses`
|
||
- `busy`, `retainedInputBytes`, 다음 `sequence`
|
||
|
||
`deadlineMisses`는 완료됐지만 deadline을 넘긴 결과의 누계다. 취소·계산 오류는
|
||
`failedAnalyses`에서 별도로 확인한다.
|
||
|
||
`analyzeAvailable()`을 같은 instance에서 중복 호출하면 오류가 난다. 분석 중에도
|
||
`append()`는 안전하다. 분석기는 먼저 현재 창을 별도 workspace에 복사하며, 해당 hop은
|
||
위치추정이 성공한 뒤에만 ring에서 소비한다.
|
||
|
||
## 메모리와 연산 최적화
|
||
|
||
기본 한도는 다음과 같다.
|
||
|
||
- CPU 계획 전체 `maximumPlanBytes`: 512 MiB
|
||
- steering cache `maximumSteeringCacheBytes`: 64 MiB
|
||
- streaming 입력 `maximumInputBufferBytes`: 256 MiB
|
||
|
||
입력 retained byte는
|
||
`채널 수 × (maximumBufferedSamples + analysisWindowSamples) × 4`이며 할당 전에 검사한다.
|
||
모든 계획은 거리 행렬과 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에서 필요할 때 계산한다.
|
||
|
||
기존 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`에는 공통 거리·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`를 함께 비교한다.
|
||
|
||
실시간 목표를 맞추는 조정 순서는 보통 다음과 같다.
|
||
|
||
1. scan grid를 50×50 이하로 시작한다.
|
||
2. 실제 필요한 band만 좁게 두어 선택 FFT bin 수를 줄인다.
|
||
3. 필요한 시간 분해능에 맞춰 `analysisHopSamples`을 정한다.
|
||
4. 낮은 주파수 분해능이 필요하지 않다면 FFT와 분석창을 줄인다.
|
||
5. 같은 장치에서 각 결과의 `processingTimeMs <= deadlineMs`가 지속되는지 확인한다.
|
||
6. `getState()`의 `deadlineMisses`, backlog와 `droppedSamples`가
|
||
증가하지 않는지 확인한다.
|
||
7. capture와 분석을 별도 Worker/thread에 두고 결과만 UI 또는 제어 loop로 전달한다.
|
||
|
||
캐시 최적화와 위 수치는 반복 계산 감소의 근거이지 192채널·50×50 또는 다른 구성이 실시간이라는
|
||
보장이 아니다. `deadlineMisses` 또는 `droppedSamples`가 누적되면 grid·band bin·FFT/분석창 또는
|
||
update rate를 줄여야 한다. 평균 한 번만 재지 말고 목표 장치의 실제 입력으로 warm-up 뒤 충분히 긴
|
||
구간의 최대값과 상위 percentile을 측정한다. Capture와 출력까지 포함하는 시스템 deadline은
|
||
adapter에서 별도로 검증한다.
|
||
|
||
## 취소와 오류 복구
|
||
|
||
`localizeMultichannel`과 `analyzeAvailable`에 `AbortSignal`을 전달하면 STFT와
|
||
알고리즘별 scan/correlation batch 경계에서 `AbortError`를 발생시키고 계획 lock을 해제한다.
|
||
|
||
Streaming 분석이 취소되거나 실패하면 `failedAnalyses`가 증가하지만 해당 hop을
|
||
소비하지 않는다. 실패한 창은 별도 analysis workspace에 보존되며, 다음
|
||
`analyzeAvailable()` 호출이 같은 `startSample`과 sample을 먼저 재시도한다. 계산 중
|
||
capture overflow로 ring의 시작점이 전진해도 이 pending 창은 workspace에 남는다. 성공하면 그때
|
||
hop을 commit하고 sequence를 증가시킨다.
|
||
|
||
`onResult` callback은 성공 결과와 hop을 commit한 뒤 호출된다. consumer callback의 예외는
|
||
호출자에게 전달되지만 성공한 창을 `failedAnalyses`로 세거나 재시도 상태로 되돌리지 않는다.
|
||
|
||
`reset()`은 idle 상태에서 ring, 시간 원점과 통계를 초기화한다. 분석 중에는 호출할 수 없다.
|
||
|
||
## 장치 adapter가 책임질 부분
|
||
|
||
포팅 대상 시스템은 코어 앞에서 다음을 구현해야 한다.
|
||
|
||
- 장치 발견, sample rate·channel 수 설정과 capture lifecycle
|
||
- interleaved integer/float frame을 channel별 `Float32Array`로 변환
|
||
- `array.microphones`와 물리 channel 번호의 고정 mapping
|
||
- 공통 sample clock, channel framing, timestamp와 drift/slip 감시
|
||
- packet loss·overflow 정책과 `droppedSamples` telemetry 결합
|
||
- 필요 시 DC 제거, anti-alias/장치 filter와 일관된 amplitude scale
|
||
- 실측한 gain·phase·주파수 응답을 Array 정의에 연결
|
||
- worker scheduling, `AbortController`, UI/네트워크로의 결과 전달
|
||
|
||
코어 뒤에서는 `locations`를 제어에 바로 쓰기 전에 현장 데이터로 검출 임계값, false peak,
|
||
latency와 위치 오차를 검증해야 한다. 현재 `locations`는 통계적 신뢰구간이 아니라 각 band
|
||
위치 map에서 threshold와 거리 억제를 통과한 grid local peak 후보들이다.
|