오디오 처리 체인 (Gate→EQ→Comp→Limiter) — 코드레벨 동작 흐름 P1코드레벨

마지막 업데이트 2026-07-22

작성일: 2026-06-14 대상: 개발자 — 마이크 입력 DSP 파이프라인 파악 핵심 파일: hooks/use-audio-processing-chain.ts

개요 · 범위

아동 마이크 입력을 OpenAI로 보내기 전에 가공하는 Web Audio 기반 DSP 체인. 노이즈 게이트로 무음 구간을 닫고, EQ로 음성 대역을 다듬고, 컴프레서/리미터로 레벨을 고르게 만든다. 파라미터는 사운드 엔지니어 자문값으로 코드에 하드코딩되어 있다.

호출 위치: 세션 매니저(#1)의 createMediaStream에서 항상 적용된다. getUserMedia(또는 증폭기 #17 출력) → audioProcessingChain.processMediaStream(stream) → 반환된 stream이 pc.addTrack으로 OpenAI에 송출. 세션 시작 후 startProcessing()으로 게이트 모니터링 루프 가동.

노드 그래프 processMediaStream L266–280

source(MediaStreamSource) → gateGain(GainNode) // 1. Gate(Expander) — 게인으로 개폐 → eqHighPass(155Hz) // 2. EQ → eqPeaking210(210Hz -4.5dB) → eqLowPass(13kHz) → compressor // 3. Compressor → limiter // 4. Limiter → outputGain(0.891=-1dB) // 출력 ceiling → destination(MediaStreamDestination) → processedStream source → analyser(병렬, terminal) // 게이트 모니터링용 RMS 측정 탭

destination.stream이 반환값. analyser는 source에서 별도 분기(terminal)라 재생 경로에 영향 없음 — RMS 측정 전용.

① 노이즈 게이트(Expander) startProcessing L92–143

DynamicsCompressor가 아니라 GainNode + JS 모니터링 루프로 직접 구현한 히스테리시스 게이트. 20ms마다 analyser의 time-domain RMS(dBFS)를 재서 게인을 토글한다.

// 20ms 간격(MONITOR_INTERVAL_MS)
analyser.getFloatTimeDomainData(buffer);
levelDb = calculateRMSDb(buffer);
if (levelDb > -40)        // OPEN 임계 초과 → 게이트 열기
   gateGain.gain.setTargetAtTime(1.0, t, 0.0035);   // 3.5ms attack
else if (levelDb < -55)  // CLOSE 임계 미만 → 게이트 닫기
   gateGain.gain.setTargetAtTime(0.001, t, 0.25);  // 250ms release, noise floor
  • 히스테리시스: 열기 -40dBFS / 닫기 -55dBFS (15dB 갭) — 임계 근처에서 깜빡임(chatter) 방지.
  • 닫힘 게인은 0이 아닌 0.001(거의 무음) — 완전 차단이 아니라 noise floor로 낮춤.
  • 상태(gateClosed, lastLevelDb)는 onStatusChange 콜백으로 외부에 통지(UI/로그).

② EQ (3밴드 BiquadFilter) L207–228

밴드type설정목적
High-passhighpass155Hz, Q 0.5저역 험/진동 제거
Peakingpeaking210Hz, -4.5dB, Q 1.30저중역 부밍 감쇠
Low-passlowpass13kHz, Q 0.67고역 노이즈/치찰음 제거

③ 컴프레서 · ④ 리미터 L230–254

노드thresholdratioattack/releaseknee
Compressor-14dB8:19ms / 100ms0 (hard)
Limiter-12dB20:11ms / 10ms0 (hard)
  • 둘 다 DynamicsCompressorNode. 리미터는 높은 ratio(20:1) + 빠른 attack(1ms)로 사실상 천장 역할.
  • 출력 ceiling: outputGain = 0.891 (= -1dB). 리미터 뒤에서 최종 피크를 -1dB로 억제.

라이프사이클

메서드역할
processMediaStream(stream)기존 컨텍스트 정리 → 새 AudioContext + 노드 그래프 구성 → processedStream 반환. 실패 시 원본 stream 그대로 반환(graceful)
startProcessing()20ms 게이트 모니터링 루프 시작(중복 시작 방지)
stopProcessing()모니터링 루프 중지
cleanup()루프 중지 + AudioContext close + 모든 ref null
getStatus / setOnStatusChange게이트 상태 조회/콜백 등록

함정 · 주의

  • 게이트는 코드 게이트(JS 루프): Web Audio 표준 노드가 아니라 GainNode를 20ms 루프로 직접 여닫는다. 루프가 안 돌면(startProcessing 누락) 게이트가 항상 열린 상태(gain 1.0)로 남는다.
  • 항상 적용: 증폭기(#17)와 달리 이 체인은 디바이스 무관 항상 적용된다. 초기화가 startProcessing의 전제(analyser/gateGain 필요).
  • 파라미터는 엔지니어 자문 하드코딩: 임의 변경 시 음질·VAD 민감도에 직접 영향. 변경 전 사운드 이슈 종합 분석 참고.
  • 실패 시 원본 반환: 그래프 구성 실패해도 throw하지 않고 원본 stream을 돌려준다 → "DSP가 안 먹는다"가 조용히 발생 가능(로그 확인).
  • 출력 ceiling 이중: limiter + outputGain(-1dB) 두 단계로 천장을 잡는다. 한쪽만 바꾸면 의도와 달라짐.

파일 · 라인 레퍼런스

파일역할
hooks/use-audio-processing-chain.tsDSP 체인 본체(그래프·게이트 루프·라이프사이클)
hooks/use-session-manager.ts:996–1034createMediaStream에서 호출(항상 적용)
hooks/use-volume-amplifier.ts이 체인 앞단(내장 마이크 한정, #17)