본문 바로가기
Knowledge/CS 지식

오디오 포맷 : WAV

by 잘 까먹는 다람쥐 2025. 1. 29.

오늘은 오디오 파일 포맷에 대해서 알아보려고 한다.

 

평소 우리가 흔하게 접하는 오디오파일의 확장자는

 

mp3, mp4, wav.. 등등 있다.

 

그 중에서 가장 흔하게 사용되는 wav 에 대해서 자세히 알아보도록 하자


목차

  1. WAV 란?
  2. WAV의 구조
  3. 실습 (Header의 활용)

1. WAV 란?

웨이브 폼 오디오 포맷 (WaveForm audio file format)의 준말로 개인용 컴퓨터에서 오디오를 재생하는 마이크로소프트와 IBM 오디오 파일 포맷 표준

 

사실 우리가 개발자로써 알아야 할 중요한 부분은

  • WAV 는 비압축 오디오 포맷 (손실 압축 없이 그대로 저장해둔 파일)

이라는 것이다.

 

자 그럼, WAV가 무엇인지 알았으니 자세하게 파헤쳐보자


2. WAV의 구조

WAVE 파일 구조

 

위 사진을 통해 WAV 파일의 구조를 간략히 설명한다면 아래와 같다.

  • Wav 파일의 header는 위의 그림과 같이 크게 3개의 파트로 나뉘어진다.
  • 최상단 보라색 부분은 파일의 형식을 나타내는 RIFF Chunk 이다.
  • 다음 초록색 부분은 음성 관련 메타데이터 정보를 담고 있는 FMT Chunk 이다.
  • 마지막으로 주황색 부분은 실제 사운드 데이터를 담고 있는 Data 이다.
  • 총 44바이트로 구성된다.

큼직하게 구성을 살펴보았다.

 

그럼 다음에는 각 Chunk 별로 상세히 살펴보겠다.

 

1. RIFF Chunk

 

 

Chunk ID (4 byte)
 > ‘RIFF’라는 문자가 ASCII 값으로 들어간다. → wave 파일 고정값
Chunk Size (4 byte)
 > 파일 전체 사이즈에서, 위에 ‘Chunk ID’와 자기 자신인 ‘Chunk Size’를 제외한 값 (전체 파일 크기 – 8byte)
Format (4 byte)
 > Wav 파일인 경우, ‘WAVE’ 라는 문자가 ASCII 값으로 들어간다. wave 파일 고정값
 

2. FMT Sub Chunk 1

 

Sub Chunk1 ID (4 byte)

 > ‘fmt + (space)’라는 문자가 ASCII 값으로 들어간다.

Sub Chunk1 Size (4 byte)

 > FMT Chunk 전체 사이즈에서, 위에 ‘Sub Chunk 1 ID’와 자기 자신인 ‘Sub Chunk 1 Size’를 제외한  (24byte 8byte = 6byte _ 고정값)

Audio Format (2 byte)

 > PCM의 경우 ‘1’ 을 사용

NumChannels (2 byte)

 > 음성 파일의 채널 수

 > mono 1 | stereo  2 ... etc

SampleRate (4 byte)

 > Hz 정보

ByteRate (4 byte)

 > 1초 동안 소리를 내는데 필요한 byte

 > SampleRate * NumChannels * BitsPerSample/8

BlockAlign

 > Sample Frame 의 크기

 > mono ‘sample 크기 * 1’, stereo‘sample 크기 * 2’

 > NumChannels * BitsPerSample/8

BitPerSample

 > Sample 한 개를 몇 개의 bit로 나타낼 지?

 

3. Data Chunk

 

Sub Chunk 2 ID (4 byte)

 > ‘data’라는 문자가 ASCII 값으로 들어간다. → 고정값

Sub Chunk 2 Size (4 byte)

 > 뒤이어 나올 Data size

Data (4 byte)

 > 실제 소리 정보

 

 

 

실제로 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 이다.

샘플 App

우선, 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 테스트

테스트 할 음원 파일이다.

테스트는 아래 순서로 진행한다.

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

 

 

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