149 lines
10 KiB
Markdown
149 lines
10 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 및 conventional frequency-domain DAS
|
||
|
||
## 실측 다채널 스트림 위치 추정
|
||
|
||
`realtime-localizer.js`는 Scene·음원 합성 없이 이미 sampling된 동기식 마이크 채널을 직접
|
||
받습니다. 외부 package 의존성이 없는 ES module이며, 다른 프로젝트에는 `acoustics-core`
|
||
디렉터리를 그대로 복사해 사용할 수 있습니다. 채널 순서는 `array.microphones` 순서와 같아야 하고
|
||
각 chunk는 동일 길이의 `Float32Array` 목록이어야 합니다.
|
||
|
||
```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 },
|
||
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, rawPower } = result.locations[0];
|
||
drawMarker(position, rawPower, result.centerTime);
|
||
if (!result.deadlineMet) reportOverload(result.processingTimeMs, result.deadlineMs);
|
||
}
|
||
```
|
||
|
||
`locations`는 band 순서, 같은 band 안에서는 raw power 내림차순의 평평한 목록입니다. 각 항목은
|
||
기존 `{ bandId, index, position, rawPower }`에 `rank`를 더하며 같은 band의 여러 local peak를
|
||
포함할 수 있습니다. `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`는 DAS와 local-peak 검출에 걸린 실제 시간을 기록합니다. Streaming
|
||
결과는 `deadlineMs = analysisHopSamples / sampleRate × 1000`과 `deadlineMet`을 더하며,
|
||
`getState()`는 최근/최대 처리 시간과 `deadlineMisses` 누계를 제공합니다. `deadlineMisses` 또는
|
||
`droppedSamples`가 증가하면 grid·대역 bin·분석창·update rate를 장치 성능에 맞게 줄여야 합니다.
|
||
|
||
깊은 응답 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 분석 계획은 거리 행렬, bin calibration, Hann window, Float64 FFT workspace, 임시 bin map과
|
||
예산 내의 Float64 steering matrix를 재사용합니다. 예를 들어 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`는 cached bin capacity, uncached 사용 횟수·pair 수, STFT transform·FFT butterfly·DAS
|
||
complex MAC 수와 실제 workspace allocation/reuse 수를 제공합니다. 자동 테스트는 부분 cache와
|
||
cache 없음 사이 raw/display Float32 map의 전 요소 bit-exact 일치를 고정합니다.
|
||
|
||
## 이동 음원과 분석 시간창
|
||
|
||
`sourcePositionAtTime`은 scene 시작 기준의 절대 시각에서 trajectory keyframe 사이를 선형 보간하며,
|
||
경로 범위 밖에서는 가장 가까운 끝점 위치를 유지합니다. `sampleQuasiStaticWindow`는 요청 경계를
|
||
sample 경계로 올림 정렬하고, 시간창 중앙의 위치에서 이동 음원을 정지한 것으로 근사합니다.
|
||
|
||
시간창은 활성 source만 잘라 짧은 Scene으로 만들지만 `signalOffsets`에 원래 scene/source의 절대 sample
|
||
offset을 보존합니다. 이를 `synthesizeMicrophoneSignals`에 전달하면 각 창을 따로 합성해도 tone phase,
|
||
seed 기반 noise index, audio asset 재생 위치와 ambient가 전체 timeline 합성과 연속됩니다.
|
||
|
||
## Beam spread 의미
|
||
|
||
Peak의 `beamSpread`는 half-power 연결 성분을 power 가중한 공간 covariance(`covarianceM2`, m²)와 그
|
||
RMS 주축 ellipse로 요약한 빔 폭입니다. 가우시안 확률 모델, 위치 확률 또는 신뢰구간으로 보정된 값이
|
||
아니며 `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에 편차로 적용하고, DAS에서는 알려진
|
||
calibration 값의 역 gain/phase로 보정합니다. raw power map과 표시용 normalized map은 서로 다른
|
||
buffer로 보존합니다. bin spectrum은 제한된 batch로 처리합니다. cooperative yield는 지원 환경에서
|
||
`scheduler.yield()`, 그 외에는 `MessageChannel`을 사용하고 timer task에도 주기적으로 기회를 주므로
|
||
진행 callback과 `AbortSignal` 취소를 유지하면서 연속 `setTimeout(0)` clamp를 피합니다. portable
|
||
plan의 기본 batch는 point 256개·STFT transform 128개이며 `computeOptions`로 조정할 수 있습니다.
|
||
|
||
마이크별 `frequencyResponse`는 2~64개의 `{ frequencyHz, gainDb, phaseDeg? }` 점으로 정의합니다.
|
||
`interpolateFrequencyResponse`는 dB와 wrapped phase를 구간 선형 보간하고 측정 범위 밖에서는 가장
|
||
가까운 끝점을 사용합니다. 합성 시 기존 sensitivity/phase 편차에 이 응답을 추가하며, CPU DAS는
|
||
선택 FFT bin의 역응답을 calibration/steering cache에 포함합니다. WebGPU 경로는 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을 할당 전에 검사하며 steering cache는 별도의 64 MiB 안에서 가능한 bin만 유지합니다.
|
||
|
||
192채널·50×50 fixture는 브라우저 통합 테스트에서 실제 합성부터 DAS peak 검출까지 실행합니다.
|