Files
micarrsim/docs/realtime-porting.md
T

14 KiB
Raw Blame History

실측 다채널 위치추정 코어 포팅

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한다면 다음 여덟 파일과 상대 경로를 함께 복사하면 된다.

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에는 최소한 다음 정보가 필요하다.

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 설정이 바뀌지 않는 동안 계획을 한 번 만들고 재사용한다.

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로 처리한다.

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.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를 갖는다.

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 후보들이다.