오늘은 오디오 파일 포맷에 대해서 알아보려고 한다.
평소 우리가 흔하게 접하는 오디오파일의 확장자는
mp3, mp4, wav.. 등등 있다.
그 중에서 가장 흔하게 사용되는 wav 에 대해서 자세히 알아보도록 하자
목차
- WAV 란?
- WAV의 구조
- 실습 (Header의 활용)
1. WAV 란?
웨이브 폼 오디오 포맷 (WaveForm audio file format)의 준말로 개인용 컴퓨터에서 오디오를 재생하는 마이크로소프트와 IBM 오디오 파일 포맷 표준
사실 우리가 개발자로써 알아야 할 중요한 부분은
- WAV 는 비압축 오디오 포맷 (손실 압축 없이 그대로 저장해둔 파일)
이라는 것이다.
자 그럼, WAV가 무엇인지 알았으니 자세하게 파헤쳐보자
2. WAV의 구조

위 사진을 통해 WAV 파일의 구조를 간략히 설명한다면 아래와 같다.
- Wav 파일의 header는 위의 그림과 같이 크게 3개의 파트로 나뉘어진다.
- 최상단 보라색 부분은 파일의 형식을 나타내는 RIFF Chunk 이다.
- 다음 초록색 부분은 음성 관련 메타데이터 정보를 담고 있는 FMT Chunk 이다.
- 마지막으로 주황색 부분은 실제 사운드 데이터를 담고 있는 Data 이다.
- 총 44바이트로 구성된다.
큼직하게 구성을 살펴보았다.
그럼 다음에는 각 Chunk 별로 상세히 살펴보겠다.
1. RIFF Chunk

2. FMT Sub Chunk 1

> ‘fmt + (space)’라는 문자가 ASCII 값으로 들어간다.
> FMT Chunk 전체 사이즈에서, 위에 ‘Sub Chunk 1 ID’와 자기 자신인 ‘Sub Chunk 1 Size’를 제외한 값 → (24byte – 8byte = 6byte _ 고정값)
> PCM의 경우 ‘1’ 을 사용
> 음성 파일의 채널 수
> mono → 1 | stereo → 2 ... etc
> Hz 정보
> 1초 동안 소리를 내는데 필요한 byte 수
> SampleRate * NumChannels * BitsPerSample/8
> Sample Frame 의 크기
> mono 면 ‘sample 크기 * 1’, stereo면 ‘sample 크기 * 2’
> NumChannels * BitsPerSample/8
> Sample 한 개를 몇 개의 bit로 나타낼 지?
3. Data Chunk

> ‘data’라는 문자가 ASCII 값으로 들어간다. → 고정값
> 뒤이어 나올 Data 의 size
> 실제 소리 정보
실제로 WAV 파일을 메모장 으로 열어보면 아래와 같이 구조가 되어있는 것을 확인이 가능하다!

line 1 에서 RIFF fmt data 와 같이 chunk 에 대한 언급이 되어 있다!
3. 실습 (Header 활용)
지금까지의 내용을 종합해보면
WAVE 파일은 메타 데이터를 담는 Header 와
실제 음원 데이터가 나열되어 있는 Body 부분으로 나누어진다는 사실을 알 수 있다.
백문이 불여일견
Data 만 존재하는 PCM Raw Format의 파일을 WAVE 형태로 변환하는 간단한 프로그램을 만들어보자.
(반대로, WAVE 를 PCM 으로 변환하는 것도 해보겠다!)
필자는 c#/Winform 으로 실습을 진행해보겠다.
3.1 설계
※ 먼저 PCM > WAV 이다.

우선, PCM Row Format에 없는 Header 를 만들기 위해선, 반드시 Bit, SampleRate, Channels 정보가 필요하다.
사용자가 수동적으로 입력해주도록 한다.
Bit, SampleRate, Channels 를 이용해서 RIFF Chunk , FMT Chunk , DATA Chunk 를 구성하여 Header 를 만들어주고,
기존 Body 부분의 위치하는 소리데이터인 Data와 결합하여 WAV 형태의 음원을 새롭게 만든다.
※ 다음은 반대인 WAV > PCM Raw 이다.

Header 부분을 지우기 전에 Bit, SampleRate, Channel 과 같은 정보들을 추출한 후, PCM Row 파일을 생성하도록 구현한다.
3.2 핵심 함수 구현
// ======================================================================================
//================================ PCM to WAV ===========================================
// ======================================================================================
/// <summary>
/// WAV 파일의 헤더를 생성
/// </summary>
/// <param name="dataLength"></param>
/// <param name="sampleRate"></param>
/// <param name="bitsPerSample"></param>
/// <param name="channel"></param>
/// <returns></returns>
private byte[] GetWavHeader(long dataLength, int sampleRate, int bitsPerSample, int channel)
{
// 파일 크기 = 오디오 데이터 크기 + ( 헤더 크기(44바이트) - 'RIFF'와 파일 크기 필드의 크기(8바이트) )
long fileSize = dataLength + 36;
byte[] header = new byte[44];
using (var memoryStream = new MemoryStream(header))
using (var writer = new BinaryWriter(memoryStream))
{
// RIFF 헤더
writer.Write(Encoding.UTF8.GetBytes("RIFF"));
writer.Write((uint)fileSize);
writer.Write(Encoding.UTF8.GetBytes("WAVE"));
// fmt 청크
writer.Write(Encoding.UTF8.GetBytes("fmt "));
writer.Write(16); // fmt 청크의 크기
writer.Write((short)1); // 오디오 포맷 (1은 PCM)
writer.Write((short)channel);
writer.Write(sampleRate);
writer.Write(sampleRate * channel * bitsPerSample / 8); // 바이트 레이트
writer.Write((short)(channel * bitsPerSample / 8)); // 블록 정렬
writer.Write((short)bitsPerSample);
// data 청크
writer.Write(Encoding.UTF8.GetBytes("data"));
writer.Write((uint)dataLength);
}
return header;
}
/// <summary>
/// PCM 에서 WAV로 변환
/// </summary>
/// <param name="pcmFilePath"></param>
/// <param name="wavFilePath"></param>
public void ConvertPcmToWav(string pcmFilePath, string wavFilePath, string SamplingRate, string Bit, int channel)
{
log.Debug("PCM Row to WAV 변환 시작");
using (var pcmStream = new FileStream(pcmFilePath, FileMode.Open, FileAccess.Read))
using (var wavStream = new FileStream(wavFilePath, FileMode.Create, FileAccess.Write))
{
var dataLength = pcmStream.Length;
log.Debug("Header 정보 생성 시작");
var header = GetWavHeader(dataLength, Convert.ToInt32(SamplingRate), Convert.ToInt32(Bit), channel);
log.Debug("Header 정보 생성 완료");
log.Debug("생성한 헤더 정보 추가 시작");
wavStream.Write(header, 0, header.Length);
log.Debug("생성한 헤더 정보 추가 완료");
log.Debug("파일 복제 시작");
pcmStream.CopyTo(wavStream);
log.Debug("파일 복제 완료");
log.Debug("PCM Row to WAV 변환 종료");
}
}
// ======================================================================================
//================================ Wav to PCM ===========================================
// ======================================================================================
/// <summary>
/// WAV 파일의 헤더 정보를 읽어옵니다.
/// </summary>
/// <param name="wavStream">WAV 파일의 스트림</param>
private void GetHeaderInfo(Stream wavStream)
{
using (var reader = new BinaryReader(wavStream, Encoding.UTF8, true))
{
// RIFF 헤더 넘기기
reader.ReadBytes(12);
// fmt 청크 찾기
while (reader.BaseStream.Position < reader.BaseStream.Length)
{
var chunkId = new string(reader.ReadChars(4));
var chunkSize = reader.ReadUInt32();
if (chunkId == "fmt ")
{
reader.ReadUInt16(); // 오디오 포맷
_channel = reader.ReadUInt16(); // 채널 수
_sampleRate = reader.ReadUInt32(); // 샘플 레이트
reader.ReadUInt32(); // 바이트 레이트
reader.ReadUInt16(); // 블록 정렬
_bitsPerSample = reader.ReadUInt16(); // 비트 깊이
break;
}
else
{
reader.BaseStream.Seek(chunkSize, SeekOrigin.Current);
}
}
}
}
/// <summary>
/// WAV에서 PCM으로 변환
/// </summary>
/// <param name="wavFilePath">WAV 파일 경로</param>
/// <param name="pcmFilePath">변환될 PCM 파일 경로</param>
public void ConvertWavToPCMRow(string wavFilePath, string pcmFilePath)
{
log.Debug("WAV to PCM 변환 시작"); using (var wavStream = new FileStream(wavFilePath, FileMode.Open, FileAccess.Read))
using (var pcmStream = new FileStream(pcmFilePath, FileMode.Create, FileAccess.Write))
{
// WAV 파일에서 헤더 정보 읽기
log.Debug("WAV 파일에서 헤더 정보 읽기 시작");
GetHeaderInfo(wavStream);
log.Debug("WAV 파일에서 헤더 정보 읽기 종료");
// 헤더 정보를 제외한 PCM 데이터만 복사
log.Debug("헤더 정보 제외");
long pcmDataSize = wavStream.Length - wavStream.Position;
byte[] buffer = new byte[pcmDataSize];
wavStream.Read(buffer, 0, buffer.Length);
log.Debug("헤더 정보 미포함 파일 복제");
pcmStream.Write(buffer, 0, buffer.Length);
log.Debug("WAV to PCM 변환 종료");
}
}
3.3 코드 설명
GetWavHeader()
- Bit, SampleRate, Channel 을 인수로 받아 Header를 생성하는 함수이다.
- WAVE 파일의 Header를 생성하기 위해 고정 값(Chunk ID, Audio Format..etc) 등을 제외한 File Size, Channel, SampleRate, Byte Rate, Block Align 과 같은 정보들을 생성한다.
ConvertPcmToWav()
- 우측의 코드는 Winform 으로 부터 pcm 음원 파일의 경로, 생성한 wav를 추출할 경로, header를 생성하기 위해 필요한 bit, sampleRate, channel의 정보를 받아와 PCM 형식의 오디오 파일을 WAV 형식으로 변환하는 함수이다.
- PCM 파일을 열고(FileMode.Open), 새 WAV 파일을 생성한다(FileMode.Create). 생성한 WAV 파일 헤더를 WAV 파일에 작성한다. 그 다음, PCM 파일의 데이터를 WAV 파일에 복사한다. 이로써, Header를 포함한 WAV 파일을 생성된다
GetHeaderInfo()
- Wav 음성 파일을 읽어 Bit, Channel, SampleRate를 추출하는 함수이다.
- FMT Chunk 까지만 읽는다. ( Data Chunk에는 필요한 정보가 없기 때문)
ConvertWavToPCMRow()
- Winform 으로 부터 pcm 음원 파일의 경로, 생성한 wav를 추출할 경로를 받아 Wav 형태의 음원을 PCM Row 형태로 변환하는 함수이다.
- 파일의 경로를 Open 한 후 WAV 헤더의 정보를 읽어 전역 변수에 저장한다.
- 이후 PCM 의 경로에 헤더 정보를 제외한 Data 부분만을 저장한다.
3.4 테스트

테스트는 아래 순서로 진행한다.
- Alarm01.wav 를 PCM 으로 변환한다. (MediaInfo에서 확인된 내용과, 추출한 내용이 일치한 지 확인)
- 변환된 Alarm01.pcm 을 추출한 Bit, SampleRate, Channel 정보를 바탕으로 다시 wav로 변환한다.
- 정상적으로 재생이 되는 지 확인한다.
1. Alarm01.wav 를 PCM 으로 변환한다. (MediaInfo에서 확인된 내용과, 추출한 내용이 일치한 지 확인)


MediaInfo 에서 확인된 Bit, SampleRate, Channel 정보와 직접 Byte를 읽어 추출한 정보가 일치한 것이 확인되었다!
2. 변환된 Alarm01.pcm 을 추출한 Bit, SampleRate, Channel 정보를 바탕으로 다시 wav로 변환한다.

PCM 파일을 다시 WAV로 정상적으로 변환됨을 확인했다!
3. 정상적으로 재생이 되는 지 확인한다.

윈도우 미디어 플레이어 에서도 정상적으로 재생이 되는 것을 확인했다!
'Knowledge > CS 지식' 카테고리의 다른 글
| [Codec 분석] Signature Code (0) | 2025.03.04 |
|---|