PCM은 사진 RAW 같은 인코딩 방식이고 WAV는 컨테이너. Linear PCM vs µ-law 차이, fmt chunk 읽는 법, sample rate·channel·bit depth 틀리면 깨지는 이유, 브라우저 PCM 재생(AudioContext), WAV→MP3 ffmpeg 변환까지.
PCM과 WAV. 이름은 항상 나오는데 정확히 뭐가 다른지 물어보면 막힙니다. 핵심은 단순합니다: PCM은 소리를 숫자로 저장한 것이고, WAV는 그 숫자에 "설명서"를 붙인 파일입니다. 하지만 "44바이트 헤더", "둘 다 비압축" 같은 오해가 많아서, 시니어 엔지니어 시선으로 하나씩 검증합니다.
PCM — 소리를 숫자로 저장한 것
**PCM (Pulse Code Modulation)**은 아날로그 소리를 디지털 숫자로 변환하는 인코딩 방식입니다. 파일 포맷이 아닙니다.
마이크 → ADC → [숫자, 숫자, 숫자, ...] ← 이게 PCM
시간 0ms → +0.5 → 16384
시간 1ms → +1.0 → 32767
시간 2ms → +0.3 → 9830
시간 3ms → -0.7 → -22937
...
그냥 숫자의 나열. 헤더도 없고 메타정보도 없음.
"이 숫자가 몇 Hz로 찍은 건지" 아무 정보도 없음.
비유하면 사진의 RAW 파일과 같습니다. 가공 없이 센서 데이터 그대로. 용량은 크지만 정확합니다.
PCM의 종류 — "PCM이면 다 비압축이다?" 아닙니다
Linear PCM (LPCM) — 비압축
샘플 값을 그대로 저장. 가공 없음.
CD 음질 = 44100Hz, 16bit, 스테레오 LPCM
1초 = 44100 × 2바이트 × 2채널 = 176,400 바이트 (≈ 172KB)
[1, 2, 3, 4, 5, 6, 7, 8] ← 숫자 그대로 저장
µ-law PCM — 로그 압축
µ-law는 PCM을 "압축"한 것입니다. 근데 압축 방식이 특이합니다.
왜 압축하냐?
PCM 원본은 용량이 큼.
특히 옛날 전화 시스템에서:
대역폭 좁음
데이터 빨리 보내야 함
→ "PCM 너무 크다 → 줄이자"
→ 근데 그냥 줄이면 품질 떨어짐
→ 그래서 "똑똑하게" 줄임
사람 귀의 특징:
작은 소리 → 민감하게 구분 (속삭임 차이를 잘 느낌)
큰 소리 → 둔감 (공사장 소음에서 약간의 차이를 못 느낌)
µ-law는 이걸 이용합니다:
작은 소리 → 정밀하게 저장 (비트를 많이 할당)
큰 소리 → 대충 저장 (어차피 못 느끼니까)
Linear PCM: [1, 2, 3, 4, 5, 6, 7, 8] ← 균일한 간격
µ-law: [1, 2, 3, 4, 8, 12, 20, 30] ← 작은 값은 촘촘, 큰 값은 듬성
중요한 부분은 살리고, 덜 중요한 건 줄임.
결과:
16bit → 8bit로 줄임 (용량 절반)
품질 일부 손실 (큰 소리 쪽에서)
전화망(PSTN)에서 사용
핵심: **"소리를 로그 스케일로 압축"**한 것. PCM 계열이지만 비압축이 아닙니다.
PCM → µ-law, 왜 굳이 바꾸냐?
핵심 이유 3개:
1. 네트워크 대역폭 절약
PCM (16bit, 8kHz, mono) → 128 kbps
µ-law (8bit, 8kHz, mono) → 64 kbps
→ 딱 절반으로 줄어듦
2. 실시간 통신 (전화, VoIP)
전화/콜센터는:
지연(latency) 중요
패킷 손실 있음
네트워크 불안정
여기서 중요한 건:
"완벽한 음질"이 아니라
"끊기지 않는 전달"
→ 가볍고 빠른 µ-law 사용
3. CPU 부담 거의 없음
MP3: 인코딩/디코딩 무거움
µ-law: 거의 연산 없음 (lookup table)
→ 서버/디바이스 부담 ↓
→ 실시간 처리에 최적
한 줄 비교:
µ-law = 빠르게 보내기용 (실시간, 전화)
MP3 = 작게 저장하기용 (다운로드, VOD)
디코딩 — 왜 다시 PCM으로 돌아와야 하나?
대부분의 시스템이 PCM 기준으로 돌아가기 때문입니다:
브라우저 AudioContext → PCM 기반
ffmpeg 처리 → PCM 기반
WAV 파일 → PCM 기반
믹싱, 필터, 볼륨 조절 → 다 PCM 기반
그래서:
µ-law (압축 상태)
↓ 디코딩 (복원)
PCM (처리 가능 상태)
"audio format이 1이 아니면 디코딩 필요" — 왜?
데이터가 "압축/인코딩된 상태"라서 그대로는 의미 있는 파형이 아니기 때문입니다.
✅ Linear PCM (audio format = 1)
실제 소리 값: [-1000, 2000, -500, ...]
→ 그대로 해석 가능 (decode 필요 없음)
❌ µ-law / MP3 / ADPCM (audio format ≠ 1)
압축된 값: [132, 255, 78, ...]
→ 이건 "소리 값"이 아니라 "압축된 표현"
→ 복원해야 진짜 파형이 됨
µ-law → PCM (디코딩)
MP3 → PCM (디코딩)
"Non-PCM 포맷은 압축된 표현이기 때문에, 후처리를 위해서는 반드시 Linear PCM으로 디코딩해야 합니다."
실제 흐름 — 소리는 항상 압축↔비압축을 반복한다
소리는 항상 이렇게 흐릅니다:
압축 ↔ 비압축 ↔ 압축 ↔ 비압축
저장/전송 → 압축 필요 (용량, 대역폭)
처리/재생 → 비압축 필요 (시스템이 PCM을 기대)
케이스별 흐름:
✅ 전화 시스템
PCM → µ-law → 네트워크 → µ-law → PCM → 재생
✅ TTS 서비스
PCM → WAV → MP3 → 업로드 → 재생
✅ 전화 음성 저장
µ-law → PCM → WAV → MP3
👉 PCM은 "기준 상태"
👉 코덱(Opus/µ-law/AAC)은 "목적에 따라 바꿔 쓰는 것"
WAV — PCM을 담는 박스
WAV는 컨테이너 포맷입니다. 박스라고 생각하면 됩니다.
PCM = 내용물 (소리 데이터)
WAV = 박스 (내용물 + 설명서)
WAV 파일
├── 헤더 (설명서)
│ ├── 샘플레이트
│ ├── 채널 수
│ ├── 비트 깊이
│ └── 코덱 (audio format)
└── 데이터 (오디오 샘플)
WAV는 그냥 "포장지".
안에 뭐가 들었는지는 헤더를 열어봐야 안다.
WAV 내부 구조 — RIFF Chunk 기반
RIFF
└── fmt ← 메타정보 (어떤 포맷인지)
└── data ← 실제 오디오 샘플
┌─────────────────────────────────────┐
│ RIFF Header (12B) │
│ "RIFF" + 파일크기 + "WAVE" │
├─────────────────────────────────────┤
│ fmt Chunk (24B~) │
│ audio format (코덱) │
│ 채널 수 │
│ 샘플레이트 │
│ 바이트레이트 │
│ 비트 깊이 │
├─────────────────────────────────────┤
│ [fact Chunk] (12B, non-PCM 필수) │
├─────────────────────────────────────┤
│ [LIST, bext 등 옵셔널 Chunk...] │
├─────────────────────────────────────┤
│ data Chunk Header (8B) │
│ "data" + 데이터 크기 │
├─────────────────────────────────────┤
│ 오디오 샘플 데이터 │
└─────────────────────────────────────┘
fmt Chunk — 겁먹을 필요 없음
fmt chunk는 WAV 헤더 안에 있는 정보 블록입니다. 가장 중요한 필드는 audio format:
fmt Chunk 핵심 필드:
┌──────────────┬──────────────────────────┐
│ 필드 │ 의미 │
├──────────────┼──────────────────────────┤
│ audio format │ 코덱 (아래 표 참고) │
│ channels │ mono(1) / stereo(2) │
│ sample rate │ 1초에 몇 개 숫자 (Hz) │
│ bit depth │ 숫자 하나가 몇 비트 │
└──────────────┴──────────────────────────┘
audio format 값:
┌────────┬─────────────────┬──────────┐
│ 값 │ 의미 │ 압축 │
├────────┼─────────────────┼──────────┤
│ 1 │ Linear PCM │ 비압축 │
│ 3 │ IEEE Float │ 비압축 │
│ 6 │ A-law │ 압축 │
│ 7 │ µ-law │ 압축 │
└────────┴─────────────────┴──────────┘
WAV 파일 받았을 때:
fmt chunk의 audio format부터 확인해야 합니다.
1이면 Linear PCM, 7이면 µ-law. 처리 방법이 다릅니다.
"44바이트 헤더" — 맞기도 하고 틀리기도 함
가장 기본적인 PCM WAV의 경우:
RIFF 헤더: 12바이트
fmt Chunk: 24바이트 (8B 청크헤더 + 16B 포맷정보)
data Chunk 헤더: 8바이트
─────────────────────────
합계: 44바이트 ← 여기서 나온 숫자
그래서 "오프셋 44부터 PCM 데이터 시작"이라는 공식이 퍼졌습니다.
하지만 실제로는:
non-PCM (float, µ-law 등):
→ fmt Chunk가 18~40바이트로 커짐
→ fact Chunk 12바이트 추가 필수
→ 최소 48~68바이트
메타데이터 포함 (녹음 소프트웨어가 추가):
→ LIST, bext, JUNK 등 옵셔널 Chunk
→ 수백 바이트까지 가능
⚠️ 오프셋 44 하드코딩은 위험합니다.
→ 반드시 Chunk ID를 파싱해서 "data" 청크 위치를 찾아야 함.
Raw PCM의 3가지 지뢰 — 하나라도 틀리면 깨짐
Raw PCM 파일에는 설명서가 없습니다. 샘플레이트, 채널, 비트 깊이를 외부에서 정확히 알려줘야 합니다. 하나라도 틀리면 소리가 깨집니다.
지뢰 1: Sample Rate — 1초에 숫자 몇 개?
같은 PCM 데이터를 다른 샘플레이트로 해석하면:
16kHz로 녹음한 데이터를 48kHz로 재생하면
→ 3배 빠르게 재생됨
→ 목소리가 다람쥐처럼 높아짐
48kHz로 녹음한 데이터를 16kHz로 재생하면
→ 3배 느리게 재생됨
→ 목소리가 느릿느릿 저음으로
속도와 음정이 동시에 깨집니다.
지뢰 2: Mono vs Stereo — 채널 해석 오류
Mono (1채널):
[1][2][3][4]
→ 샘플 하나가 한 시점의 소리
Stereo (2채널, Interleaved):
[L][R][L][R][L][R]
→ 좌/우가 번갈아 들어감
Stereo 데이터를 Mono로 착각하면:
[L][R][L][R] → [샘플1][샘플2][샘플3][샘플4]
좌/우 데이터가 뒤섞여서:
→ 음성 깨짐
→ STT 인식률 급락
→ 원인을 찾기 어려움 (소리가 "이상하게" 들리는 수준)
지뢰 3: Bit Depth — 숫자 하나가 몇 바이트?
16bit PCM: 숫자 하나 = 2바이트
[00 FF] [10 02] [A3 01]
→ 2바이트씩 묶어서 읽어야 함
16bit 데이터를 8bit로 읽으면:
[00][FF][10][02][A3][01]
→ 완전히 다른 숫자들
→ 결과: "지지직" 노이즈
비유:
원본 데이터: 01001100 01001101
16bit로 해석: [0100110001001101] → 하나의 숫자
8bit로 해석: [01001100][01001101] → 두 개의 다른 숫자
→ 결과 완전 다름
PCM vs WAV — 정확한 비교
┌───────────────┬──────────────────────────────┬──────────────────────────────┐
│ │ PCM (Raw) │ WAV │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 정체 │ 인코딩 방식 (코덱) │ 컨테이너 포맷 (파일) │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 비유 │ 사진 RAW 파일 │ RAW + EXIF 메타데이터 │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 헤더 │ 없음 │ 있음 (가변, PCM 기본 44B) │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 재생 │ 샘플레이트/비트깊이/채널을 │ fmt chunk에 메타 내장 │
│ │ 외부에서 알려줘야 재생 가능 │ → 바로 재생 가능 │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 압축 │ LPCM은 비압축 │ 담긴 코덱에 따라 다름 │
│ │ µ-law/A-law은 로그 압축 │ (PCM이면 비압축) │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 용도 │ 스트리밍, SDK 내부 버퍼, │ 파일 저장, 편집 소프트웨어, │
│ │ 하드웨어 인터페이스 │ 배포 │
├───────────────┼──────────────────────────────┼──────────────────────────────┤
│ 관계 │ WAV 안에 담기는 데이터 │ PCM을 담는 그릇 중 하나 │
│ │ │ (AIFF, AU, RF64도 가능) │
└───────────────┴──────────────────────────────┴──────────────────────────────┘
오디오 PCM 데이터는 단순한 숫자 배열이기 때문에, sample rate, channel 수, bit depth, byte order, signed 여부를 정확히 맞춰 해석하지 않으면 왜곡이나 노이즈가 발생합니다. WAV 같은 포맷이 존재하는 이유도 바로 이 "해석 방식을 헤더에 명시"하기 위해서입니다.
# 예: numpy로 raw PCM 읽을 때 dtype을 잘못 지정하면 바로 터짐import numpy as np
audio = np.frombuffer(raw_bytes, dtype=np.int16) # ✅ 16bit signed PCM
audio = np.frombuffer(raw_bytes, dtype=np.uint8) # ❌ 완전히 다른 데이터로 해석됨
브라우저에서의 차이 — PCM vs MP3
Raw PCM — 브라우저가 직접 재생 못 함
PCM (raw) = "소리 데이터만 있음. 설명서 없음."
샘플레이트? 모름
채널 수? 모름
비트 깊이? 모름
→ ❌ new Audio(url).play() 불가능
→ ⭕ AudioContext로 직접 디코딩해야 함
MP3 — 브라우저가 바로 재생
MP3 = "압축된 오디오 파일"
용량: WAV 대비 약 1/10
품질: 조금 손실 (사람이 못 느끼는 범위)
브라우저: 완벽 지원
→ ⭕ new Audio(url).play() 가능
AudioContext로 Raw PCM 재생하는 법
브라우저는 raw PCM을 new Audio()로 못 틉니다. PCM에 샘플레이트, 채널 수, 비트 깊이 정보가 없어서 브라우저가 이 바이트들을 어떻게 읽어야 하는지 모릅니다. 직접 알려줘야 합니다.
Google Speech-to-Text API:
WAV 파일 전송 시:
→ API가 fmt chunk에서 자동으로 샘플레이트/채널 파악
→ config에 별도 지정 불필요
Raw PCM 전송 시:
→ 반드시 config에 명시해야 함
config: {
encoding: 'LINEAR16',
sampleRateHertz: 16000,
audioChannelCount: 1
}
→ 하나라도 틀리면 인식률 0%
핵심 정리
1. PCM은 포맷이 아니라 인코딩 방식
→ 소리를 숫자로 바꾸는 규칙 (사진 RAW와 같음)
2. PCM이 다 비압축은 아님
→ Linear PCM = 비압축
→ µ-law = 로그 압축 (작은 소리 정밀, 큰 소리 대충)
3. WAV는 컨테이너 (박스)
→ fmt chunk의 audio format으로 안에 뭐가 담겼는지 확인
→ PCM 외에 float, µ-law, MP3도 담을 수 있음
4. "44바이트 헤더"는 PCM WAV의 최솟값
→ non-PCM이면 더 커짐. 오프셋 하드코딩 금지.
5. Raw PCM 쓸 때는 3가지 반드시 명시
→ sample rate, channels, bit depth
→ 하나라도 틀리면 소리 깨짐, STT 실패
6. 브라우저에서 PCM 직접 재생 불가
→ AudioContext로 수동 디코딩 필요
→ MP3/WAV로 변환하면 new Audio()로 바로 재생