마이크에 대고 말하면 상대방 스피커에서 소리가 나옵니다. 당연한 것 같지만, 그 사이에는 ADC, 오디오 전처리, 코덱 인코딩, RTP 패킷화, UDP 전송, Jitter Buffer, 디코딩, DAC까지 수많은 단계가 있습니다. 각 단계에서 왜 그 선택을 했는지, 시니어 엔지니어 시선으로 정리합니다.
1단계: 소리가 컴퓨터로 들어오는 과정
마이크 → ADC → PCM
마이크 (아날로그)
↓
ADC (Analog to Digital Converter)
↓
PCM (디지털 오디오 데이터)
마이크: 공기 진동 → 전기 신호 (연속적, 아날로그)
ADC: 전기 신호 → 숫자 (이산적, 디지털)
PCM: 그 숫자들의 나열
ADC는 OS/하드웨어 레벨에서 이미 처리됨.
우리가 제어하는 건 PCM 이후.
그래서 시스템 설계에서는 보통 이렇게 표현합니다:
Mic → PCM → processing → encoding
ADC는 "있다"는 것만 알면 충분합니다.
2단계: 오디오 전처리 — AEC, AGC, NS
PCM이 나왔다고 바로 보내지 않습니다. 전처리가 먼저입니다.
PCM (원본)
↓
AEC (Acoustic Echo Cancellation)
→ 스피커에서 나온 소리가 마이크에 다시 들어가는 것 방지
→ 화상회의에서 "에코" 현상 제거
↓
AGC (Automatic Gain Control)
→ 볼륨 자동 조절
→ 작게 말해도 적당히, 크게 말해도 적당히
↓
NS (Noise Suppression)
→ 배경 노이즈 제거
→ 키보드 소리, 에어컨 소리 등
↓
전처리된 PCM
WebRTC와 Agora SDK는 이 3가지를 내장하고 있습니다. 개발자가 직접 구현할 일은 거의 없지만, 존재를 아는 것이 트러블슈팅의 시작점입니다.
3단계: 코덱 인코딩 — 왜 PCM 그대로 안 보내나?
PCM 그대로 보내면:
PCM (16bit, 48kHz, mono) ≈ 768 kbps
Opus 인코딩 후 ≈ 16~64 kbps
→ 거의 10~40배 차이
PCM은:
✅ 용량 큼
✅ 품질 좋음
✅ 계산/처리는 쉬움
❌ 네트워크에 올리면 비쌈 + 느림
→ 그래서 압축(인코딩) 필요
코덱 선택 — 상황에 따라 다르다
┌─────────────────────┬─────────┬──────────────────────────────────┐
│ 상황 │ 코덱 │ 이유 │
├─────────────────────┼─────────┼──────────────────────────────────┤
│ WebRTC / 라이브 통신 │ Opus │ 저지연, adaptive, 패킷 손실 대응 │
├─────────────────────┼─────────┼──────────────────────────────────┤
│ 전화 / SIP / PSTN │ µ-law │ 초경량, CPU 거의 안 씀, 레거시 │
├─────────────────────┼─────────┼──────────────────────────────────┤
│ VOD / 저장 / HLS │ AAC │ 고음질, 압축 효율, 스트리밍 표준 │
├─────────────────────┼─────────┼──────────────────────────────────┤
│ 간단한 웹 다운로드 │ MP3 │ 호환성 최고, 구형 시스템 포함 │
└─────────────────────┴─────────┴──────────────────────────────────┘
각 코덱의 특징
Opus
- 실시간 최강
- adaptive bitrate (네트워크 상태에 따라 자동 조절)
- 패킷 손실에도 복구 가능
- latency 매우 낮음
→ 라이브용. WebRTC = 거의 무조건 Opus.
µ-law
- 매우 가벼움 (lookup table 수준)
- CPU 거의 안 씀
- 음질 낮음 (8bit, 8kHz)
→ 레거시 전화용. WebRTC에서는 거의 안 씀.
AAC
- 고음질
- 같은 bitrate에서 MP3보다 더 좋은 음질
- HLS, MP4 필수급 표준
→ 콘텐츠 배포용. 유튜브, 넷플릭스, HLS 전부 AAC.
MP3
- 호환성 최고
- 구형 시스템 포함 어디서든 재생
→ fallback + 범용.
TCP:
정확하지만 느림
패킷 유실 시 재전송 → 지연 발생
UDP:
빠름
일부 손실 허용
실시간 통신은:
정확성보다 "지연"이 더 중요
0.5초 전 소리를 정확히 듣는 것보다
지금 소리를 대충이라도 듣는 게 나음
→ UDP 선택
RTP (Real-time Transport Protocol)
오디오/영상 실시간 전송 프로토콜.
UDP 위에서 동작하면서 부족한 부분을 보완합니다:
sequence number → 패킷 순서 관리
timestamp → 타이밍 동기화
UDP는 순서/타이밍 보장이 없으므로
RTP가 이 정보를 헤더에 추가합니다.
인코딩된 오디오 데이터:
↓
RTP 패킷으로 포장 (헤더 + payload)
↓
UDP로 전송
Jitter Buffer — 패킷 도착 시간을 재생 타이밍으로 바꾸는 장치
먼저 지터(Jitter)가 뭔지 정확히 잡아야 합니다.
지터 = 패킷 도착 시간의 흔들림
정상: 10ms 20ms 30ms 40ms (일정한 간격)
지터: 10ms 35ms 15ms 50ms (들쭉날쭉)
순서 문제가 아니라 "시간 간격이 불규칙"한 것.
그대로 재생하면 → 끊김, 찢어짐, 로봇소리
지터 버퍼가 하는 일:
1. 패킷 도착 (들쭉날쭉)
2. 버퍼에 잠깐 쌓음
3. 일정 간격으로 꺼내서 재생
도착: 2 1 3
버퍼: [1 2 3] ← 순서 정렬
재생: → → → ← 일정 간격으로 출력
핵심:
"도착 기준"이 아니라
"재생 타이밍 기준"으로 바꾼다
TCP식 사고 vs Jitter Buffer — 중요한 차이
TCP식 사고 (정확성 우선):
순서 틀리면 기다림
빠진 거 재요청
→ 결과: 느림
Jitter Buffer (부드러움 우선):
조금만 기다림
늦으면 그냥 포기
대신 끊김 방지
한 줄: "완벽한 데이터"보다 "부드러운 재생"이 목표
Jitter Buffer 크기 = Latency vs 안정성 Trade-off
버퍼 작으면:
장점: 지연 낮음 (빠름)
단점: 끊김 많음
버퍼 크면:
장점: 안정적
단점: 지연 증가 (느림)
┌──────────────────┬───────────┐
│ 상황 │ 선택 │
├──────────────────┼───────────┤
│ 게임 / 통화 │ 작은 버퍼 │
│ 방송 / 스트리밍 │ 큰 버퍼 │
└──────────────────┴───────────┘
Adaptive Jitter Buffer — 요즘은 고정이 아님
네트워크 상태를 보고 자동 조절:
네트워크 안정 → 버퍼 줄임 → 지연 낮아짐
네트워크 불안 → 버퍼 늘림 → 안정성 확보
WebRTC, Agora 모두 adaptive jitter buffer를 사용합니다.
Packet Loss 대응 — SFU와 클라이언트의 협력
SFU는 직접 복구하지 않습니다. 클라이언트와 협력해서 처리합니다.
1. NACK (재전송 요청)
클라이언트: "sequence 123 없어!"
SFU: "그거 다시 보내줄게"
→ 빠른 재전송
2. FEC (Forward Error Correction)
미리 여분 데이터를 추가해서 전송
데이터 + 복구용 패킷
→ 일부 loss는 재전송 없이 클라이언트에서 복구
3. PLC (Packet Loss Concealment)
클라이언트에서:
이전 프레임 복사
보간(interpolation)
→ "티 안 나게 숨김"
4. Bitrate Adaptation
네트워크 안 좋으면
→ SFU가 낮은 bitrate stream 선택해서 전달
→ 끊기는 것보다 화질 낮추는 게 나음
SFU = 전달 + 힌트 제공자
복구 = 클라이언트 + 프로토콜(WebRTC)이 담당
클라이언트 수신 전체 흐름:
UDP packet 도착
→ jitter buffer 저장
→ 순서 정렬
→ missing 있으면 NACK 요청
→ 복구(FEC) or 숨김(PLC)
→ 디코딩
→ 재생
5단계: 수신 — 디코딩 → DAC → 스피커
UDP 수신
↓
RTP depacketize (헤더 제거, 오디오 데이터 추출)
↓
Jitter Buffer (흔들림 보정)
↓
Opus decode → PCM (압축 해제)
↓
DAC (Digital to Analog Converter)
↓
스피커 (전기 → 공기 진동 → 소리)
WebRTC / Agora 실제 흐름
WebRTC와 Agora는 내부 구조가 거의 동일합니다.
📡 송신 (보내는 쪽)
Mic
↓
ADC
↓
PCM
↓
Audio Processing (AEC, AGC, NS)
↓
Opus 인코딩 (압축)
↓
RTP packetize
↓
UDP 전송
📡 수신 (받는 쪽)
UDP 수신
↓
RTP depacketize
↓
Jitter Buffer
↓
Opus decode → PCM
↓
DAC
↓
Speaker
Agora의 차이점 — SD-RTN
일반 WebRTC:
Client A → STUN/TURN → Client B (P2P 또는 SFU)
Agora SD-RTN:
Client A → 가까운 Edge PoP → 최적 경로 선택 → Edge PoP → Client B
Edge ↔ Edge 사이:
전 세계 PoP들이 full mesh 형태로 연결
각 PoP가 다른 PoP로 가는 경로 성능을 계속 측정
실시간으로 더 좋은 경로를 자동 선택
사용자 패킷 수신
→ 가까운 Edge에 들어감
→ Edge가 loss / RTT / jitter / congestion 상태를 봄
→ "지금 제일 괜찮은 경로" 선택 (필요하면 우회)
→ 수신 측 Edge에서 클라이언트로 전달
녹화/스트리밍 — 인코딩, 트랜스코딩, 패키징
실시간 전송과 별개로, 녹화/VOD 배포에는 다른 파이프라인이 필요합니다.
인코딩 (Encoding)
원본을 압축 포맷으로 바꾸는 것.
raw video → H.264
raw audio (PCM) → AAC
트랜스코딩 (Transcoding)
이미 인코딩된 스트림을 다른 해상도/비트레이트/포맷으로 바꾸는 것.
1080p 6Mbps → 720p 3Mbps
1080p 6Mbps → 480p 1.2Mbps
→ 다양한 네트워크 환경에 대응 (Adaptive Bitrate)
패키징 (Packaging)
인코딩된 비디오/오디오를 HLS용 조각 파일로 만드는 것.
H.264 + AAC
→ segment-0001.ts
→ segment-0002.ts
→ index.m3u8
→ CDN에서 배포 가능한 형태
RTMP Push — CDN으로 스트림 밀어넣기
RTMP push는 인코딩된 H.264/AAC 스트림을
실시간으로 CDN ingest endpoint에 전달하는 과정입니다.
OBS / 인코더
↓ RTMP push (H.264 + AAC, TCP 기반)
CDN Ingest Endpoint
↓ 수신
HLS 패키징 (TS 세그먼트 + m3u8)
↓
Edge 캐싱
↓
사용자 재생
핵심:
RTMP = 단순히 "입력 프로토콜" (스트림을 밀어넣는 파이프)
CDN = 받은 스트림을 HLS로 패키징 -> edge에 캐싱 -> 배포
→ RTMP 자체는 시청자에게 직접 전달되지 않음
→ CDN이 HLS로 변환해서 배포하는 구조
RTMP → WebRTC 변환 — Media Gateway가 하는 일
RTMP 스트림을 WebRTC로 "변환"한다고 흔히 말하지만, 정확히는 프로토콜을 변환하는 게 아닙니다. 서버가 미디어를 꺼내서 다른 방식으로 다시 포장하는 것입니다.
전체 흐름:
OBS (RTMP push)
↓
Agora Media Gateway (RTMP ingest)
↓
디코딩 또는 트랜스코딩
↓
WebRTC용으로 재패키징 (RTP/UDP)
↓
클라이언트 (WebRTC)
단계별로 보면
1) OBS → RTMP (업로드)
Video (H.264) + Audio (AAC)
→ RTMP (TCP 기반)
→ Agora ingest 서버
이건 그냥 "방송 업로드". 여기까진 단순함.
2) Media Gateway (핵심)
여기서 진짜 일이 일어남. 서버가 두 가지를 합니다:
(A) 디코딩 (필요한 경우)
H.264 → raw frame
AAC → PCM
왜?
bitrate 조절이 필요할 때
해상도 변경이 필요할 때
여러 시청자 대응 (Adaptive Bitrate)
코덱이 같으면 디코딩 없이 passthrough도 가능.
(B) 재패키징 (항상 필요)
WebRTC는 RTMP를 못 먹습니다.
RTMP = TCP 기반 스트림
WebRTC = RTP/UDP 기반 패킷
그래서:
RTMP stream (TCP)
→ 미디어 데이터 추출
→ RTP packet으로 재포장 (WebRTC format)
→ UDP로 전송
"RTMP → WebRTC 프로토콜 변환" (X)
"미디어를 꺼내서 → WebRTC 방식으로 다시 싸서 보냄" (O)
3) 클라이언트 (WebRTC 수신)
RTP depacketize
→ Jitter Buffer
→ 디코딩 (H.264 → frame, Opus → PCM)
→ 렌더링/재생
여기서부터는 일반 WebRTC 수신과 동일.
RTMP → FLV tag 파싱이란?
RTMP 안에 들어있는 비디오/오디오 데이터 구조를 꺼내는 것입니다.
RTMP 스트림 내부 구조:
RTMP
→ FLV container
→ tag (video/audio)
각 FLV tag:
┌──────────────┐
│ type │ ← video / audio
│ timestamp │ ← 시점
│ data │ ← H.264 NAL units 또는 AAC frames
└──────────────┘
파싱 = tag를 읽어서 type에 따라 H.264 또는 AAC 데이터를 꺼내는 것
MTU — 왜 1200 바이트씩 쪼개냐?
MTU (Maximum Transmission Unit)
= 네트워크에서 한 번에 보낼 수 있는 최대 패킷 크기
Ethernet MTU ≈ 1500 bytes
IP header ≈ 20 bytes
UDP header ≈ 8 bytes
RTP header ≈ 12 bytes
─────────────────────────
RTP payload ≈ 1200 bytes (안전 마진 포함)
왜 중요하냐?
TCP: 자동으로 쪼개줌 (segmentation)
UDP: 자동 쪼개기 없음. 직접 쪼개야 함.
MTU를 넘기면?
→ IP 레벨에서 fragmentation 발생
→ 하나라도 조각이 유실되면 전체 패킷 손실
→ 실시간에서는 치명적
그래서 1200 바이트 기준으로 미리 쪼갬.
내부 동작 — 의사 코드로 이해하기
실제 구현은 훨씬 복잡하지만, 개념을 잡기 위한 추상적인 의사 코드입니다.
# 전체 파이프라인 (개념 이해용 의사 코드)whileTrue:
rtmp_packet = read_rtmp() # 1. RTMP 읽기
media = extract_payload(rtmp_packet) # 2. payload 추출
rtp_packets = packetize_to_rtp(media) # 3. RTP로 쪼개기
send_to_sfu(rtp_packets) # 4. SFU로 전달
# 1단계: RTMP 읽기defread_rtmp():
# TCP 소켓에서 데이터 읽음
data = socket.recv()
# RTMP -> FLV tag 파싱return parse_rtmp(data)
# 3단계: RTP 패킷으로 쪼개기 (핵심)defpacketize_to_rtp(media):
packets = []
for chunk in split(media, size=1200): # MTU 기준으로 분할
rtp = {
"sequence": next_seq(), # 순서 번호"timestamp": current_ts(), # 타이밍 정보"payload": chunk
}
packets.append(rtp)
return packets
# 실제로는:# H.264는 FU-A 같은 fragmentation 방식 사용# timestamp = 프레임 단위 기준
# 4단계: SFU로 전달defsend_to_sfu(rtp_packets):
for p in rtp_packets:
sfu_input_queue.put(p)
SFU — 디코딩 없이 복사해서 뿌림
SFU(Selective Forwarding Unit)는 Media Gateway와 역할이 다릅니다.
Media Gateway = 포맷 변환 (RTMP → RTP)
SFU = 분배 (RTP → 여러 클라이언트)
SFU는:
디코딩 안 함
인코딩 안 함
그냥 전달 + 선택
# SFU 내부 (개념 이해용 의사 코드)defsfu_loop():
whileTrue:
packet = input_queue.get()
for subscriber in subscribers:
send_udp(packet, subscriber)
# 확장: 실제 SFU는 네트워크 상태에 따라 선택적 전달defsfu_loop_adaptive():
whileTrue:
packet = input_queue.get()
for subscriber in subscribers:
if subscriber.network_bad:
send_low_bitrate_stream(packet, subscriber)
else:
send_high_bitrate_stream(packet, subscriber)
RTMP ingest 단계에서는 Media Gateway가 H.264/AAC payload를 추출해
RTP packet으로 재패키징하고, 이후 SFU가 이 RTP 스트림을 복제하여
다수의 WebRTC 클라이언트에 전달합니다.
포맷 변환과 분배는 별도의 컴포넌트로 분리되어 동작합니다.
┌──────────────┬──────────────────────┐
│ 단계 │ 역할 │
├──────────────┼──────────────────────┤
│ Gateway │ 포맷 변환 (RTMP→RTP) │
│ SFU │ 분배 (복제+선택 전달) │
└──────────────┴──────────────────────┘
오버레이 네트워크 — 경로를 직접 선택한다
Agora SD-RTN이 일반 인터넷과 다른 점은 오버레이 네트워크입니다. 패킷은 여전히 인터넷 위를 흐르지만, 경로 선택은 Agora가 합니다.
일반 인터넷 (Underlay):
서울 → 미국
= ISP가 아무 경로로 보냄
= 우리는 제어 불가
오버레이 네트워크:
서울 Edge → 도쿄 Edge → 미국 Edge
= "우리가 정한 길로 보냄"
= 경로 성능을 계속 측정하고 최적 선택
┌──────────────┬──────────────────┬──────────────────┐
│ │ Underlay (인터넷) │ Overlay (SD-RTN) │
├──────────────┼──────────────────┼──────────────────┤
│ 경로 결정 │ ISP │ 애플리케이션 │
│ 제어 │ 없음 │ 있음 │
│ 최적화 │ 제한적 │ 실시간 가능 │
└──────────────┴──────────────────┴──────────────────┘
왜 필요하냐?
latency 줄이려고
packet loss 피하려고
jitter 줄이려고
핵심 포인트:
1. depacketization
RTP 패킷에서 미디어 데이터 추출 (RTP 헤더 제거)
2. 디코딩 + 재인코딩 (트랜스코딩)
WebRTC 코덱(Opus, VP8)과 RTMP 코덱(AAC, H.264)이
다르기 때문에 디코딩 후 재인코딩 필요
Opus → PCM → AAC
VP8 → raw frame → H.264
3. 코덱이 이미 일치하면?
WebRTC에서 H.264를 쓰고 있다면
→ 디코딩/재인코딩 없이 리패키징만 수행 가능
→ CPU 부담 대폭 감소
H.264(RTP) → H.264(RTMP) 리패키징만
Opus(RTP) → AAC(RTMP) 트랜스코딩 필요
Voice AI 파이프라인에서 VAD는 "언제 말하는지 감지"하는 역할입니다. 없으면 어떻게 되는지 보면 왜 필요한지 바로 이해됩니다.
먼저 구분: 에코 vs VAD
마이크가 TTS 소리를 픽업하는 것
|
| 이건 물리적 현상
| (스피커 -> 공기 -> 마이크)
|
v
AEC가 처리해야 할 문제
VAD와는 직접 관련 없음
AEC(Acoustic Echo Cancellation)는 스피커 출력이 마이크로 다시 들어오는 물리적 에코를 제거합니다. VAD는 에코가 아니라 "사람이 말하고 있는지"를 판단합니다. 역할이 다릅니다.
VAD가 없으면 생기는 4가지 문제
1. 침묵도 STT로 전송 -- 비용 낭비
상황 VAD 있음 VAD 없음
-------------------------------------------------
침묵 구간 무시 STT로 계속 전송
말하는 구간 감지 후 전달 구분 불가
발화 시작/끝 정확히 자름 잘림 없이 흘러감
오디오 스트림이 2분 동안 연결되어 있으면, 침묵 구간의 무음 PCM 데이터도 "데이터"입니다. VAD 없이는 이 모든 것이 STT로 흘러갑니다.
타임라인:
00:00 ---------------------------------------- 02:00
| |
Mic 연결됨 Mic 여전히 연결
| |
v v
오디오 스트림이 계속 STT로 흘러들어감
(침묵 = 무음 PCM 데이터도 "데이터"임)
2. 발화 종료 시점 모름 -- LLM 호출 타이밍 불명확
Mic (연속) --> STT --> 끝없는 텍스트 조각들
--> LLM이 언제 응답해야 할지 모름
--> 중복/쓸모없는 추론 폭발
Mic ---------------------------------------------------------->
[침묵][말][침묵][말][침묵]
^-- VAD 없으면 이 모든 것이 STT로 전달됨
STT가 침묵, 잡음, 배경음까지 텍스트로 변환 시도
LLM이 발화 완료 시점을 알 수 없음 -> 응답 타이밍 불가
토큰 낭비 -> 비용 폭증
3. 발화 시작 시점 모름 -- STT 항상 켜져 있음
VAD가 있으면 발화 시작을 감지해서 STT를 활성화합니다. 없으면 STT가 항상 켜져 있어야 합니다. 상시 가동 = 상시 비용.
4. Barge-in 감지 불가 -- TTS 재생 중 유저가 말해도 모름
VAD 없음:
TTS가 재생되는 동안 유저가 말하기 시작
-> 시스템이 유저 발화 시작을 감지 못함
-> TTS를 끊지 못함 (interruption 불가)
-> 대화가 아니라 일방적 출력이 됨
VAD가 있으면 유저가 말하기 시작할 때 TTS 재생을 즉시 중단(barge-in)할 수 있습니다. "유저가 다시 말하기 시작했다"는 판단이 VAD의 역할입니다.
VAD 없음 전체 흐름:
Mic -> 침묵/잡음/말 구분 불가
-> STT <-- 쓰레기 입력 (Garbage In)
-> LLM <-- 언제 응답할지 모름
-> TTS <-- 엉뚱한 타이밍에 말함
-> User <-- 혼란, 끊김, barge-in 불가
STT Endpointing vs VAD — 타이밍 문제
Streaming STT는 자체적으로 "발화 끝"을 판단합니다. 문제는 그 방식이 침묵 timeout 기반이라 느리다는 것입니다.
STT 2가지 모드:
1. Streaming STT (실시간) <-- Voice AI에서 사용
2. Batch STT (파일 업로드)
문제 1: 타이밍이 늦다
유저: "주문하고 싶어요" [말 끝]
STT endpointing만 사용 (VAD 없음):
---------------------------------------------------
t=0.0s "주문하고 싶어요" 말하기 완료
t=0.0s 유저 입장: "이제 답해줘야지"
<-- 여기서 바로 LLM 호출했으면 좋겠음
t=0.5s STT 내부: "아직 말 더 할 수도 있으니 대기"
t=1.0s STT 내부: "아직 대기..."
t=1.5s STT 내부: "침묵 1.5초 지났네 -> final 출력!"
t=1.5s final: "주문하고 싶어요" -> LLM 호출
t=2.5s LLM 응답 생성
t=3.0s TTS 출력
---------------------------------------------------
유저 체감: 말 끝내고 -> 3초 후에 응답
VAD가 있으면?
---------------------------------------------------
t=0.0s "주문하고 싶어요" 말하기 완료
t=0.3s VAD: "발화 종료 감지!" -> 즉시 STT final 트리거
t=0.3s LLM 호출
t=1.3s LLM 응답
t=1.6s TTS 출력
---------------------------------------------------
유저 체감: 말 끝내고 -> 1.6초 후 응답
VAD의 end-of-speech 감지는 보통 200-500ms입니다. STT의 silence timeout 1-2초와 비교하면 체감 차이가 큽니다.
문제 2: 부정확하다 — 문장이 쪼개진다
케이스 A — 문장 중간 침묵
유저: "저는... [2초 침묵] ...치킨 주문할게요"
STT endpointing (VAD 없음):
---------------------------------------------------
t=0.5s interim: "저는"
t=2.5s 침묵 2초 경과 -> STT가 final 판단!
final: "저는" -> LLM 호출
LLM: "네, 무엇을 도와드릴까요?" (엉뚱한 응답)
t=3.0s 유저: "치킨 주문할게요" (계속 말하는 중)
STT: 또 final -> LLM 또 호출
---------------------------------------------------
결과:
-> 문장이 쪼개져서 LLM이 맥락 없이 응답
-> 유저는 말 중간에 끊김 경험
케이스 B — 생각하면서 말하기
유저: "음... 그러니까... 제가 원하는 건..."
STT endpointing (VAD 없음):
-> "음" -> final -> LLM 호출
-> "그러니까" -> final -> LLM 호출
-> "제가 원하는 건" -> final -> LLM 호출
LLM은 계속 엉뚱한 응답 생성 중...
VAD는 단순 침묵이 아니라 음성 에너지, 주파수 패턴을 분석해서 "진짜 말이 끝났는지" 판단합니다. "음...", "그러니까..." 같은 filler는 여전히 발화 중으로 판단할 수 있습니다.
VAD의 위치 — 파이프라인에서 어디에?
Mic -> PCM -> 전처리(AEC/AGC/NS)
|
v
+-----+-----+
| VAD | <-- 여기서 발화 감지
+-----+-----+
|
+---------+---------+
| |
[말하는 중] [침묵]
| |
STT로 전송 전송 안 함
|
STT interim/final
|
LLM 호출 (발화 종료 시)
|
TTS 응답
VAD는 전처리 직후, STT 직전에 위치합니다. 게이트키퍼 역할 — 말하고 있을 때만 STT에 오디오를 전달하고, 발화 종료 시 LLM 호출을 트리거합니다.