259 lines
18 KiB
Markdown
259 lines
18 KiB
Markdown
# Acoustics Core
|
||
|
||
두 앱이 공유하는 무의존성 수치 기준 구현입니다.
|
||
|
||
- 결정적 tone/white/pink noise 및 오디오 asset resampling
|
||
- 자유음장 직접음과 선택적 저비용 shoebox multipath
|
||
- zero-padded FFT 전달함수 기반 fractional sample delay와 마이크 주파수 응답
|
||
- radix-2 FFT, Hann STFT와 선택 bin 추출
|
||
- scan grid 거리 cache 및 DAS·DAS-PHAT·GCC-PHAT 위치 추정
|
||
|
||
## Nyquist 초과 입력
|
||
|
||
`sampling.js`는 anti-alias filter 없는 이상적 real sampler의 signed alias와 band-to-FFT-bin
|
||
mapping을 제공한다. tone 전파는 먼저 원 analog 주파수에서 path delay와 microphone response를
|
||
적용하고 그 뒤 ADC 시각에서 sampling한다. 따라서 시간축에는 alias가 나타나지만 센서 간 위상은
|
||
원 파장의 물리를 유지한다.
|
||
|
||
Nyquist 위 band는 모든 등가 ADC FFT bin center로 접어 관찰한다. 연속 fold segment와 이산
|
||
bin-center membership은 별도 metadata다. DAS 계열은 양의 ADC bin 주파수로 steering하고
|
||
GCC-PHAT는 선택 bin의 lag correlation을 사용하며, 원 carrier를 몰래 추론하거나 peak를
|
||
고르지 않는다. 단일 channel 시간축에서 같은 sequence로 접혀도 다채널 공간 위상에는 원 파장의
|
||
증거가 남을 수 있다. 현재 localizer는 carrier hypothesis를 열거하지 않으므로 Result에
|
||
`aliasOrderResolution: not-attempted`로 남긴다. asset/noise 입력은 이미 sampling된 baseband로
|
||
취급한다.
|
||
|
||
Tone과 band의 검증 범위는 0–1 MHz다. 이는 Nyquist cutoff가 아니라 현재 IEEE-754 위상 계산,
|
||
마이크 응답과 희소 경로 모델의 명시적 정밀도 범위다. public sampling helper는 비정상적으로
|
||
작은 sample rate에서도 최대 513개 fold segment를 넘기 전에 거부한다.
|
||
|
||
## 위치 추정 알고리즘
|
||
|
||
`localization-algorithms.js`의 registry와 `localization.js`의 generic API가 다음 세 경로를
|
||
공통 interface로 제공합니다.
|
||
|
||
| `algorithm` | 계산 | raw scale | backend |
|
||
| --- | --- | --- | --- |
|
||
| `das` | conventional frequency-domain delay-and-sum | `power` | CPU, WebGPU |
|
||
| `das-phat` | 채널 spectrum을 phase-only로 만든 DAS | `score` | CPU, WebGPU |
|
||
| `gcc-phat` | 하나의 reference mic와 나머지 mic의 GCC-PHAT 지연 scan | `score` | CPU only |
|
||
|
||
Portable API에서는 top-level `algorithm`으로 선택합니다. 생략하면 기존과 같은 `das`이며, 낮은
|
||
수준의 generic API는 `experiment.beamforming.algorithm`을 사용합니다.
|
||
|
||
```js
|
||
import {
|
||
createLocalizationPlan,
|
||
localizeMultichannel,
|
||
} from "./packages/acoustics-core/realtime-localizer.js";
|
||
|
||
const plan = createLocalizationPlan({
|
||
...commonConfig, // sampleRate, array, scan, bands, stft
|
||
algorithm: "gcc-phat", // "das" | "das-phat" | "gcc-phat"
|
||
phatFloorRatio: 1e-6,
|
||
});
|
||
|
||
// 반복 분석에서도 같은 plan을 재사용합니다.
|
||
const result = await localizeMultichannel({ plan, channels });
|
||
```
|
||
|
||
`das-phat`의 `phatRelativeFloor`와 `gcc-phat`의 `phatFloorRatio`는 모두 0~1의 상대
|
||
크기 floor이며 기본값은 `1e-6`입니다. GCC-PHAT는 `phatRelativeFloor`도 호환 alias로
|
||
받지만 새 코드에서는 알고리즘별 이름을 권장합니다. PHAT 경로는 mic gain을 위치 score에
|
||
사용하지 않고 알려진 mic·frequency-response phase만 보정합니다.
|
||
|
||
GCC-PHAT는 단일 tone보다 여러 FFT bin에 에너지가 있는 broadband 입력을 권장합니다. 하나의
|
||
band에 선택된 bin이 8개 미만이면 코어는 계산을 허용하지만 주기적 모호성과 ghost peak를 직접
|
||
검증해야 합니다. 또한 reference pair의 scan 전체 최대 절대 기하 지연이 `fftSize / 2` sample을
|
||
넘으면 circular correlation alias를 피하기 위해 plan 생성을 거절합니다. 이때는 `fftSize`를
|
||
늘리거나 배열·scan 기하를 조정합니다.
|
||
|
||
각 band의 `rawScale`은 `power` 또는 `score`입니다. `rawValue`가 scale에 무관한 표준 peak
|
||
값이며, `rawPower`는 `power`, `rawScore`는 `score`일 때만 쓰는 편의 alias입니다. `score`는
|
||
확률·신뢰도·SPL이 아니고 동일 알고리즘·설정 안에서 상대 peak를 비교하는 비교정 값입니다.
|
||
`power`도 입력과 calibration이 절대 단위로 교정되지 않았다면 물리적 W 또는 Pa²로 해석하지 않습니다.
|
||
GCC-PHAT score는 pair 수, band coefficient 수와 IFFT 크기로 정규화한 0–1 양의 상관 응답이라
|
||
scan 범위와 마이크 수에 의한 단순 재척도화는 피하지만, 여전히 확률로 보정된 confidence는 아닙니다.
|
||
|
||
## 실측 다채널 스트림 위치 추정
|
||
|
||
`realtime-localizer.js`는 Scene·음원 합성 없이 이미 sampling된 동기식 마이크 채널을 직접
|
||
받습니다. 외부 package 의존성이 없는 ES module이며, 다른 프로젝트에는 `acoustics-core`
|
||
디렉터리를 그대로 복사해 사용할 수 있습니다. 채널 순서는 `array.microphones` 순서와 같아야 하고
|
||
각 chunk는 동일 길이의 `Float32Array` 목록이어야 합니다.
|
||
|
||
`StreamingLocalizer`는 알고리즘 중립 class 이름입니다. 기존 `StreamingDasLocalizer`와 같은 class의
|
||
export alias이므로 오래된 import도 그대로 동작하며, `createStreamingLocalizer()` factory도 세
|
||
알고리즘을 모두 지원합니다.
|
||
|
||
```js
|
||
import { createStreamingLocalizer } from "./packages/acoustics-core/realtime-localizer.js";
|
||
|
||
const localizer = createStreamingLocalizer({
|
||
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-phat" 또는 "gcc-phat"도 선택할 수 있습니다.
|
||
maxCompensationDb: 24,
|
||
peakDetection: { thresholdRatio: 0.5, maximumDetections: 8 },
|
||
analysisWindowSamples: 4_800,
|
||
analysisHopSamples: 4_800,
|
||
maximumBufferedSamples: 19_200,
|
||
maximumResultsPerCall: 1,
|
||
});
|
||
|
||
// Capture callback: 복사 후 즉시 반환하며, 오래 밀린 sample은 oldest-first로 버립니다.
|
||
localizer.append(channels);
|
||
|
||
// Analysis loop: 호출마다 설정된 상한 이하의 map만 반환합니다.
|
||
const results = await localizer.analyzeAvailable({ signal: abortController.signal });
|
||
for (const result of results) {
|
||
const { position, rawValue, rawScale } = result.locations[0];
|
||
drawMarker(position, rawValue, rawScale, result.centerTime);
|
||
if (!result.deadlineMet) reportOverload(result.processingTimeMs, result.deadlineMs);
|
||
}
|
||
```
|
||
|
||
`locations`는 band 순서, 같은 band 안에서는 `rawValue` 내림차순의 평평한 목록입니다. 각 항목은
|
||
`{ bandId, rank, index, position, rawValue, rawScale }`이며 같은 band의 여러 local peak를 포함할
|
||
수 있습니다. 기존 DAS 결과의 `rawPower` 별칭은 그대로 유지됩니다. PHAT score는 `rawValue`와
|
||
`rawScale`을 기준으로 읽고, score peak 객체에 포함된 경우 `rawScore`를 같은 값의 별칭으로 씁니다.
|
||
`peakDetection.thresholdRatio` 기본값은 최강 peak의 0.5배,
|
||
`minimumPeakDistanceM` 기본값은 scan cell 대각선, `maximumDetections` 기본값은 8입니다.
|
||
기존처럼 `locations[0]`만 읽으면 최강 후보를 그대로 얻습니다.
|
||
|
||
Streaming 계산이 중단되거나 실패하면 해당 분석창과 sequence를 commit하지 않고 다음 호출에서 같은
|
||
창을 재시도합니다. 성공한 뒤에만 hop·sequence·`completedAnalyses`를 commit하며, 그 뒤 호출되는
|
||
`onResult` callback의 예외는 `failedAnalyses`에 포함하지 않습니다. 계산 중 capture overflow가 나면
|
||
ring의 오래된 sample은 평소와 같이 drop telemetry에 기록됩니다.
|
||
|
||
한 블록 결과의 `processingTimeMs`는 선택한 위치 추정 알고리즘과 local-peak 검출에 걸린 실제 시간을
|
||
기록합니다. Streaming
|
||
결과는 `deadlineMs = analysisHopSamples / sampleRate × 1000`과 `deadlineMet`을 더하며,
|
||
`getState()`는 최근/최대 처리 시간과 `deadlineMisses` 누계를 제공합니다. `deadlineMisses` 또는
|
||
`droppedSamples`가 증가하면 grid·대역 bin·분석창·update rate를 장치 성능에 맞게 줄여야 합니다.
|
||
이 deadline 비교는 실시간 성능 보장이 아니라 완료된 창의 측정값입니다. 목표 장치에서 warm-up을 거친
|
||
지속 부하를 직접 측정하고, capture·adapter·출력을 포함한 end-to-end deadline은 별도로 검증해야 합니다.
|
||
|
||
깊은 응답 notch의 역필터가 sensor noise까지 폭증시키지 않도록 inverse response의 양의 boost는 기본
|
||
24 dB로 제한합니다. Experiment의 `beamforming.maxCompensationDb` 또는 portable plan의 top-level
|
||
`maxCompensationDb`를 0~120 dB로 조절할 수 있습니다. 이 값은 boost만 제한하며 attenuation과 phase
|
||
보정은 유지합니다. 선택 bin 전체의 gain/phase calibration byte도 plan 생성 전에 산정하므로 lazy
|
||
cache가 나중에 `maximumPlanBytes`를 넘지 않습니다.
|
||
|
||
한 블록만 처리할 때는 `createLocalizationPlan()`과 `localizeMultichannel()`을 사용합니다.
|
||
`getState()`은 absolute sample origin, buffered/dropped sample, 대기 window 수와 고정 input
|
||
buffer byte 수를 돌려주므로 capture adapter가 지연을 감시할 수 있습니다. 실제 장치의 clock,
|
||
channel framing과 transport는 하드웨어별 adapter 책임이며 이 모듈은 그 다음 단계부터 시작합니다.
|
||
|
||
반복 CPU 분석 계획은 공통 거리 행렬과 STFT workspace를 재사용합니다. DAS 계열은 bin calibration,
|
||
Hann window, 임시 bin map과 예산 내의 Float64 steering matrix를, GCC-PHAT는 reference 선택과
|
||
correlation workspace를 추가로 유지합니다. 같은 plan을 동시에 두 분석에 쓰지 말고 설정이나 알고리즘이
|
||
바뀌면 새 plan을 만듭니다. 예를 들어 DAS로 192채널·50×50·2 bins를 10회 분석하면
|
||
거리 계산은 4,800,000회에서 480,000회로, steering point/microphone pair는 9,600,000회에서
|
||
960,000회로 줄어듭니다(sin/cos 호출 17,280,000회 회피). 이 경우 약 16.5 MiB를 유지합니다.
|
||
|
||
기본 64 MiB보다 전체 wide-band steering matrix가 크더라도 cache 전체를 끄지 않습니다. 선택 bin
|
||
앞쪽부터 예산에 들어가는 만큼만 Float64로 유지하고 나머지는 고정 microphone workspace에서 계산합니다.
|
||
`analysisCache`는 알고리즘별 실제 작업을 제공합니다. DAS 계열은 cached bin capacity,
|
||
uncached steering pair, STFT transform·FFT butterfly·complex MAC을, GCC-PHAT는 STFT/IFFT,
|
||
2-pass cross-spectrum operation, scan lookup과 correlation workspace reuse를 기록합니다.
|
||
|
||
`P`를 scan point 수, `M`을 mic 수, `T`를 STFT frame 수, `F`를 선택한 고유 bin 수,
|
||
`B`를 band 수, `N`을 `fftSize`라 하면 DAS의 주 scan 연산은 대략
|
||
`P × M × T × F` complex MAC입니다. GCC-PHAT는 forward STFT에 더해 reference pair `M-1`개마다
|
||
band bin을 두 번 순회하는 cross spectrum, `(M-1) × B`회의 길이 `N` IFFT,
|
||
`P × (M-1) × B`회의 fractional correlation lookup을 수행합니다. band별 bin 수를 `F_b`라 하면
|
||
cross-spectrum 항목 수는 구현의 두 pass를 포함해 `2 × (M-1) × T × ΣF_b`입니다. 따라서 GCC가
|
||
scan MAC을 줄여도 작은 grid에서는 forward STFT와 pair별 IFFT가 전체 시간을 지배할 수 있으므로
|
||
두 알고리즘 모두 실제 목표 장치에서 측정해야 합니다.
|
||
|
||
## 이동 음원과 분석 시간창
|
||
|
||
`sourcePositionAtTime`은 scene 시작 기준의 절대 시각에서 trajectory keyframe 사이를 선형 보간하며,
|
||
경로 범위 밖에서는 가장 가까운 끝점 위치를 유지합니다. `sampleQuasiStaticWindow`는 요청 경계를
|
||
sample 경계로 올림 정렬하고, 시간창 중앙의 위치에서 이동 음원을 정지한 것으로 근사합니다.
|
||
|
||
시간창은 요청 출력 시작보다 앞선 신호를 현재 array·음향 환경의 최대 경로 지연만큼 pre-roll합니다.
|
||
따라서 창이 시작되기 전에 발음이 끝난 source라도 direct·reflection·scattering·diffraction·diffuse
|
||
경로가 창 안에 도착하면 제거하지 않습니다. `signalOffsets`는 원래 scene/source의 절대 sample
|
||
offset과 출력 crop을 함께 보존합니다. `synthesizeMicrophoneSignals`는 pre-roll 버퍼에 모든 경로를
|
||
적용한 뒤 요청 구간만 반환하므로 tone phase, seed 기반 noise index, audio asset 재생 위치와
|
||
ambient가 전체 timeline과 같은 절대 시간축을 사용합니다.
|
||
|
||
이 보장은 현재 희소 경로 모델 안에서의 **전파 지연 tail**에 관한 것입니다. quasi-static 창은 pre-roll
|
||
구간까지 포함해 source를 출력 창 중앙의 한 위치에 고정하며, retarded-time trajectory, 창 안의 연속
|
||
이동과 Doppler는 계산하지 않습니다. 합성 결과의 `windowing` metadata는 적용한 pre-roll, 출력 절대
|
||
sample, `source-position-fixed-at-window-midpoint` geometry와 `dopplerMode: not-modeled`를 기록합니다.
|
||
`sourceArrivals`는 source별 방사·도착 sample 경계와 출력에 겹친 path kind를,
|
||
`arrivingSourceIds`는 적어도 한 실제 path의 half-open 도착 구간이 출력과 겹친 source를 기록합니다.
|
||
이는 사후 ground-truth 평가에만 쓰이며 위치 추정 입력에는 전달하지 않습니다.
|
||
sampled asset/noise의 fractional delay와 microphone response는 기존 zero-padded block FFT 모델을
|
||
그대로 사용하므로 임의 길이 analog microphone impulse response의 streaming state를 재현하는 모델은
|
||
아닙니다.
|
||
|
||
## Beam spread 의미
|
||
|
||
Peak의 `beamSpread`는 raw map의 peak 주변 연결 성분을 가중한 공간 covariance(`covarianceM2`, m²)와
|
||
그 RMS 주축 ellipse로 요약한 폭입니다. `power` map은 half-power/power-weighted,
|
||
`score` map은 half-maximum/score-weighted 규칙을 사용합니다. 어느 쪽도 가우시안 확률 모델,
|
||
위치 확률 또는 신뢰구간으로 보정된 값이 아니며 `calibrated: false`를 유지합니다.
|
||
|
||
FFT는 `X[k] = Σ x[n] exp(-j2πkn/N)` 규약을 사용합니다. 마이크 신호의 전파 phase
|
||
`exp(-j2πfr/c)`를 정렬하기 위해 DAS steering은 `exp(+j2πfr/c)`를 곱합니다. 단일 tone의
|
||
ground-truth 위치에서 peak가 생기는 회귀 테스트로 이 부호를 고정합니다.
|
||
|
||
마이크 `sensitivityDb`와 `phaseOffsetDeg`는 수신 channel에 편차로 적용합니다. Conventional DAS는
|
||
알려진 calibration의 역 gain/phase를 모두 보정하고, DAS-PHAT와 GCC-PHAT는 amplitude gain을 무시한
|
||
채 알려진 phase만 보정합니다. raw `power`/`score` map과 표시용 normalized map은 서로 다른 buffer로
|
||
보존합니다. bin spectrum은 제한된 batch로 처리합니다. cooperative yield는 지원 환경에서
|
||
`scheduler.yield()`, 그 외에는 `MessageChannel`을 사용하고 timer task에도 주기적으로 기회를 주므로
|
||
진행 callback과 `AbortSignal` 취소를 유지하면서 연속 `setTimeout(0)` clamp를 피합니다. portable
|
||
plan의 기본 point batch는 DAS 계열 256개, GCC-PHAT 4,096개이고 STFT transform batch는
|
||
128개입니다. GCC-PHAT의 두 cross-spectrum pass도 16,384 operation마다 양보합니다.
|
||
`pointBatchSize`, `stftTransformBatchSize`, `crossSpectrumBatchSize`를
|
||
`computeOptions`로 조정할 수 있습니다.
|
||
|
||
마이크별 `frequencyResponse`는 2~64개의 `{ frequencyHz, gainDb, phaseDeg? }` 점으로 정의합니다.
|
||
`interpolateFrequencyResponse`는 dB와 wrapped phase를 구간 선형 보간하고 측정 범위 밖에서는 가장
|
||
가까운 끝점을 사용합니다. 합성 시 기존 sensitivity/phase 편차에 이 응답을 추가하며, CPU 위치 추정은
|
||
선택 FFT bin의 알고리즘별 역응답을 calibration에 포함합니다. WebGPU DAS 계열 경로는 GPU 전송 직전의
|
||
packed STFT에 같은 역응답을 적용합니다. `beamforming.compensateFrequencyResponse: false`는
|
||
sensitivity/phase 보정은 유지한 채 곡선 보정만 꺼 robustness 비교를 재현합니다.
|
||
|
||
응답 곡선이 없는 배열은 기존 계산 경로와 결과를 유지합니다. 곡선이 있으면 합성 FFT마다 bin당
|
||
보간·복소 곱이 추가되고, WebGPU 준비 단계에는 `선택 bin × 마이크` calibration과 STFT 복소 곱이
|
||
추가되지만 GPU buffer layout이나 dispatch 크기는 늘어나지 않습니다.
|
||
|
||
## 저비용 환경 근사
|
||
|
||
Scene의 `acoustics`를 생략하거나 `{ "model": "free-field" }`로 두면 기존 직접음
|
||
`delay = r/c`, `gain = 1/r`만 사용합니다. `model: "shoebox"`는 다음 경로를 선택적으로
|
||
추가합니다.
|
||
|
||
- 직육면체 `room`의 여섯 면에 대한 1차 image-source 반사
|
||
- 반사 에너지 일부를 deterministic 지연 경로로 나누는 scattering 근사
|
||
- 축 정렬 box `occluders`를 통과하는 직접음 감쇠와 가장 짧은 corner detour 회절 경로
|
||
- RT60·gain·tap 수로 제한한 deterministic diffuse-reverb tail
|
||
|
||
모든 경로의 위상과 gain은 마이크마다 하나의 frequency-domain 전달함수로 합친 뒤 IFFT를 한 번만
|
||
수행합니다. 경로 위상은 recurrence로 갱신하고 1,024 bin마다 재고정하므로 각 bin·path마다
|
||
삼각함수를 다시 호출하지 않습니다. UI preset도 수신점당 sparse path 상한을 표시합니다.
|
||
|
||
이 모델은 배열 배치와 위치추정 robustness를 빠르게 비교하기 위한 근사입니다. 파동 방정식,
|
||
고차 반사, 재질별 주파수 의존 흡음, 복잡한 mesh 회절, Doppler, 음원·마이크 지향성, 압력/SPL,
|
||
센서 self-noise, ADC quantization·clock jitter는 계산하지 않습니다. 그런 효과가 중요한
|
||
검증에서는 실측 다채널 데이터를 아래 streaming API에 넣어 결과를 비교해야 합니다.
|
||
|
||
snapshot 합성은 전체 signal과 가장 늦은 sparse path를 포함하는 delay FFT working set을
|
||
512 MiB로 제한합니다. 실측·장시간 입력은 위 streaming localizer의 bounded ring buffer로
|
||
전체 녹음을 메모리에 보관하지 않고 처리할 수 있습니다. CPU analysis plan도 기본 512 MiB
|
||
hard cap을 할당 전에 검사합니다. DAS steering cache는 별도의 64 MiB 안에서 가능한 bin만 유지하고,
|
||
GCC-PHAT는 거리·STFT·correlation workspace 전체를 plan 한도 안에서 검사합니다. Plan 없는 단발
|
||
GCC 분석도 거리 행렬을 만들기 전에 spectrum·calibration·output을 포함한 working set을 preflight합니다.
|
||
|
||
192채널·50×50 fixture는 기존 DAS 호환 경로를 실제 합성부터 peak 검출까지 고정하며, 별도의 결정적
|
||
회귀 fixture가 DAS-PHAT와 GCC-PHAT의 score scale·lag 부호·plan 재사용을 검사합니다.
|