AI 발화 "지지직" — STT 캡처 WAV 클리핑(과레벨) 분석 분석

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

작성일: 2026-06-15 상태: 🟡 원인=과레벨 클리핑 확정 · 감지 경로(id-008) 실측 검증 완료 · 예방(재생단 리미터)·게인 출처 미해결 관련 영역: 아동(guest) AI 오디오 · 외부 STT 캡처 · aiAudioStream

증상 VOC

핑퐁이(AI) 발화 시 "지지직" 거리는 잡음/찌그러짐. 분석 대상은 STT 검증용으로 캡처된 AI 발화 오디오 파일 ai_stt_req19 (1).wav (무손실 PCM).
분석 방법: ffmpeg 디코드 → 프로젝트 노이즈 가드(use-ai-audio-noise-guard.ts)가 쓰는 음향 지표(RMS dB · ZCR · CV · FFT)를 오프라인 재현 + 클리핑/스펙트로그램 분석.

측정 결과 — 객관 지표

항목해석
포맷16kHz mono PCM, 3.33sAI STT 캡처본, 무손실
peak0.0 dBFS풀스케일 도달
클리핑 샘플941개 (1.8%), 0.9s~3.3s 전반하드 클리핑(flat-top)
큰 구간 RMS-3 ~ -5 dBFS정상 음성(-18~-12)보다 훨씬 과도
스펙트로그램전대역 에너지 번짐클리핑 고조파(왜곡) 전형
CV<0.35 기계음 후보17프레임 (1.25~1.5s, 1.95~2.3s 등)클리핑으로 파형이 납작해진 구간과 일치
프레임별로 0.90s부터 거의 모든 발화 프레임이 프레임당 17~44샘플씩 풀스케일에서 잘려나감(flat-top). 이 잘림이 곧 지지직/찌그러짐이다.

코드 메커니즘 — 어디서 잘리는가

AI STT 캡처 경로(use-guest-page-session.ts:847~854)의 Float32 → Int16 변환:

const toInt16 = (float32) => {
  for (...) {
    const s = Math.max(-1, Math.min(1, float32[i]));  // ±1.0 초과를 하드 클램프
    int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
  }
};
핵심: Math.min(1, ...) 클램프가 ±1.0을 넘는 샘플을 풀스케일로 깎아낸다. 측정된 flat-top 클리핑이 정확히 이것 → 즉 aiAudioStream의 float 신호가 이미 ±1.0을 초과한 과레벨 상태로 들어오고 있다.

캡처는 aiAudioStream에 붙인 ScriptProcessorNode(16kHz)로 떠가며, AI 발화 종료 시 requestAIAudioSTT()가 트리거(use-guest-page-session.ts:790~791).

의미 — 아동이 들은 지지직과 같은가

다른 "지지직"(PLC)과의 구분

LogRocket 로그 분석에서 본 또 다른 지지직 경로와 혼동 주의. "AI 지지직"은 두 가지 별개 경로가 있다.

경로원인로그/지표양상
① 네트워크 PLCguest↔OpenAI WebRTC 패킷 손실 → 디코더 보정 아티팩트AI_NOISE_GUARD "suppressed during PLC-active window" · concealedSampleRatio간헐적
과레벨 클리핑 (본 문서)aiAudioStream float > ±1.0 → toInt16 하드 클램프WAV peak 0dBFS · 941 클리핑 샘플 · RMS -3~-5dBFS발화 전반 지속
이 WAV는 명백히 ②(클리핑)이며, 발화 전반에 지속되므로 체감상 더 거슬렸을 것이다. ①의 노이즈 가드 PLC 판정 지표(concealedSamples/packetsLost/jitter)는 WebRTC inbound-rtp 통계로, WAV(최종 파형)에는 들어있지 않다 → 파일만으론 ①/② 구분이 어렵고, 로그 타임라인 교차검증이 필요.

한계 (정직하게)

권장 조치

감지 로직 실측 검증 검증

커밋 id-008 (feat: AI 발화 하드 클리핑 고속 감지 경로 추가)가 실제 WAV에서 동작하는지, 정상 발화 오탐은 없는지 독립 재현·검증.

검증 방법

커밋의 클리핑 판정 파라미터(CLIP_CREST_THRESHOLD_DB=6, CLIP_SAMPLE_LEVEL=0.98, CLIP_SAMPLE_RATIO_THRESHOLD=0.01, CLIP_RING_SIZE=4, CLIP_FRAME_THRESHOLD=3, grace 500ms, silence floor -50dB)를 그대로 재현. 라이브 AnalyserNode와 동일하게 48kHz·2048샘플 윈도우·50ms 틱으로 시뮬레이션(16kHz 캡처본은 48kHz 리샘플).

결과

파일결과비고
아동330_4회기_지지직 (문제)🔴 감지 t=1.15s첫 클립프레임 1.00s → 3프레임 누적 후 확정
ai_stt_req18 (정상)🟢 미감지클립프레임 0 — 오탐 없음
ai_stt_req19 (정상)🟢 미감지클립프레임 0 — 오탐 없음

잘 동작하는 점

한계

① reactive — 초반 지지직은 이미 들림: 확정 t=1.15s. 클리핑이 ~0.9s부터 재생됐으므로 확정 전 ~250ms는 아동이 이미 들은 뒤. "안 들리게 예방"이 아니라 "빨리 잡아 끊기".
결론: 이 커밋은 탐지 + 자동 중단 측면에서 효과 있고, 정상 발화를 오탐 없이 통과시키는 것까지 실측 확인. 단 본질이 reactive라 사용자가 듣는 초반 ~250ms 지지직은 못 막는다. 예방이 목표면 재생단 리미터가 별도로 필요하며 둘은 보완 관계(리미터=예방, 이 커밋=탐지·중단·알림).

재생 전 레벨 제어 — 예방 경로 (Realtime)

"안 들리게 막기"는 감지가 아니라 재생 직전 단계에서 레벨을 잡아야 한다. 현재 재생은 <audio>.srcObject = MediaStream 직결(use-ai-session.ts:1683/1706)이라 그래프가 없다.

리미터가 지지직을 없애줄지는 조건부: 원인이 "우리 출력단 과레벨 클리핑(가)"이면 막힘 ✅. "OpenAI가 이미 깨진 오디오 전송(나)" 또는 PLC면 무용 ❌. 클램프된 WAV로는 (가)/(나) 구분 불가 → 라이브 원격 트랙 over-unity(>1.0) 측정이 선행 필요. (정상 출력은 peak -1.4dBFS로 일정 → 이 케이스만 0dBFS로 튄 이상치)

관련 문서