Scope & privacy
현재 모델의 범위
- micarrsim 1.0은 점 음원, 구면 감쇠·분수 지연을 쓰는 자유음장 모델과 conventional
- DAS를 제공합니다. 실내 반사·잔향, 실제 마이크 위치 오차와 비선형성, DAMAS,
- CLEAN-SC, DCASE/MIMII 자동 adapter와 실제 192채널 capture stream은 현재 범위에 포함되지 않습니다. 결과는 실제
- 장비의 절대 성능 보증이 아니라 같은 조건에서 배열을 비교하는 재현 가능한 기준입니다.
+ micarrsim 1.0은 점 음원의 자유음장과 계산량을 제한한 shoebox 희소 경로, 마이크
+ 주파수 응답과 conventional DAS를 제공합니다. 파동 방정식·고차 반사·재질별 흡음,
+ 복잡한 mesh, sensor self-noise·비선형, ADC·clock, DAMAS/CLEAN-SC와 자동 장치
+ capture adapter는 포함하지 않습니다. 결과는 장비의 절대 성능 보증이 아니라 같은
+ 조건에서 배열과 알고리즘을 비교하는 재현 가능한 기준입니다.
원본 asset은 브라우저 메모리에서만 읽고 서버나 외부 네트워크로 보내지 않습니다.
@@ -226,6 +249,8 @@
전체 README설치·실행·구조·제약
Builder 설명서source·ambient·Preview WAV
Camera 설명서backend·결과·비교
+ 음향 모델마이크 응답·반사·산란·회절
+ 실측 포팅block·streaming 위치추정 API
데이터 계약JSON Schema와 재현성
실행 계획구현 상태와 향후 단계
브라우저 테스트단위·통합 테스트 실행
diff --git a/docs/realtime-porting.md b/docs/realtime-porting.md
new file mode 100644
index 0000000..e992b1a
--- /dev/null
+++ b/docs/realtime-porting.md
@@ -0,0 +1,332 @@
+# 실측 다채널 위치추정 코어 포팅
+
+`packages/acoustics-core/realtime-localizer.js`는 Scene 생성이나 음향 합성 없이 이미 sampling된
+동기식 마이크 채널을 conventional CPU DAS에 직접 넣는다. 브라우저 DOM과 외부 package에
+의존하지 않는 ES module이므로, 실제 장치 adapter 뒤에 붙이거나 다른 프로젝트로 분리할 수 있다.
+
+이 API는 ADC와 transport를 제어하지 않는다. 입력 channel을 획득·정렬·동기화한 다음 위치 map과
+대역별 local peak 후보를 계산하는 경계부터 담당한다.
+
+## 최소 복사 단위
+
+직접 경로로 `realtime-localizer.js`를 import한다면 다음 여덟 파일과 상대 경로를 함께
+복사하면 된다.
+
+```text
+acoustics-core/
+ realtime-localizer.js
+ das.js
+ stft.js
+ fft.js
+ math.js
+ frequency-response.js
+ cooperative-yield.js
+ evaluation.js
+```
+
+`index.js`는 전체 core의 편의 export이므로 최소 포팅에는 필요하지 않다. 현재 portable API는
+CPU 기준 구현이며 Camera의 WebGPU worker나 Scene·propagation 모듈을 요구하지 않는다.
+
+## 입력 계약
+
+`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 상한에 의해 잘리지 않는다.
+
+## 공통 계획 만들기
+
+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 },
+ 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 < sampleRate / 2`
+- scan의 `columns`와 `rows`는 양의 정수
+- `speedOfSound`는 양수
+- `normalization`은 `peak` 또는 `none`
+- `maxCompensationDb`는 0–120 dB
+- `peakDetection.thresholdRatio`는 0–1,
+ `minimumPeakDistanceM`는 0 이상,
+ `maximumDetections`는 1–128의 정수
+
+계획은 거리 행렬, bin calibration, 임시 map과 허용되는 경우 steering matrix를 유지하는
+stateful 객체다. 같은 계획으로 두 분석을 동시에 실행하지 말고, worker마다 별도 계획을 만든다.
+Array·scan·band·STFT를 바꾸면 새 계획을 만들어야 한다.
+
+portable 계획의 기본 compute batch는 scan point 256개와 STFT transform 128개 단위로 event
+loop에 양보한다. `computeOptions.pointBatchSize`와
+`computeOptions.stftTransformBatchSize`로 조정할 수 있지만, 큰 batch는 throughput과
+취소·UI 응답성의 trade-off가 있으므로 장치에서 함께 측정한다.
+
+## 한 블록 분석
+
+짧은 녹음이나 adapter가 이미 만든 고정 길이 frame은 `localizeMultichannel`로 처리한다.
+
+```js
+const controller = new AbortController();
+
+const result = await localizeMultichannel({
+ plan,
+ channels, // Array
+ signal: controller.signal,
+ onProgress(progress) {
+ updateProgress(progress);
+ },
+});
+
+for (const location of result.locations) {
+ console.log(location.bandId, location.position, location.rawPower);
+}
+```
+
+`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`로 묶어 해석한다.
+
+`peakDetection`은 후처리 위치 목록만 바꾸며 `result.bands[n].rawMap`과
+`displayMap`은 그대로 보존한다. 같은 대역의 두 음원이 main lobe보다 가까우면 local
+peak가 하나로 합쳐질 수 있고, threshold를 낮추면 sidelobe 후보도 늘 수 있다.
+`sampleCount`와 `duration`도 결과에 함께 반환된다.
+
+Block 결과의 `processingTimeMs`는 DAS 계산과 local-peak 검출에 걸린 monotonic wall
+time이다. capture, adapter 변환과 결과 rendering 시간은 포함하지 않으므로 end-to-end latency는
+호출자가 별도로 측정한다.
+
+계획을 전달하지 않고 `localizeMultichannel({ channels, ...config })` 형태로도 호출할 수
+있지만, 매번 geometry cache를 다시 만들므로 반복·실시간 처리에는 권장하지 않는다.
+
+## 연속 chunk 처리
+
+`StreamingDasLocalizer` 또는 같은 factory인 `createStreamingLocalizer`는 고정 용량
+ring buffer와 분석 workspace를 갖는다.
+
+```js
+import { createStreamingLocalizer } from "./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 },
+ 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`를 반환한다.
+
+## 지연과 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`이며 할당 전에 검사한다.
+계획은 거리 행렬, bin calibration, Hann window, Float64 FFT workspace와 임시 map을 재사용한다.
+기본 retained data가 `maximumPlanBytes`를 넘으면 계획 생성을 거절한다. 전체 steering
+matrix가 64 MiB 또는 계획 전체 한도를 넘으면 선택 bin 앞쪽부터 예산에 들어가는 만큼만 cache하고
+나머지는 고정 microphone workspace에서 필요할 때 계산한다.
+
+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
+수가 들어가므로 설정별 최적화 효과를 계측할 수 있다.
+
+실시간 목표를 맞추는 조정 순서는 보통 다음과 같다.
+
+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 또는 다른 구성이 실시간이라는
+보장이 아니다. 마이크 수, scan point, 선택 bin, STFT frame에 따른 주연산은 여전히 대략
+`O(P × M × F × T)`다. `deadlineMisses` 또는 `droppedSamples`가 누적되면
+grid·band bin·FFT/분석창 또는 update rate를 줄여야 한다. deadline은 위치추정 처리량 판정이며
+capture와 출력까지 포함하는 시스템 deadline은 adapter에서 별도로 검증한다.
+
+## 취소와 오류 복구
+
+`localizeMultichannel`과 `analyzeAvailable`에 `AbortSignal`을 전달하면 STFT와
+DAS 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
+DAS map에서 threshold와 거리 억제를 통과한 grid local peak 후보들이다.