본문 바로가기

Programming/DirectX

[DirectSound] 2. Wave 파일을 읽기

반응형

DirectSound를 통해서 Wave(wav)를 재생하기 위해서 먼저 파일을 읽어야 합니다.

Wave 파일의 포맷은 다음 링크를 통해서 확인할 수 있습니다.

2015/06/13 - [Programming/DirectX] - [DirectSound] 1. Wave 파일의 구조

포맷을 알면 일반적인 파일 I/O를 통해서 읽는 것도 가능합니다.

윈도우에서는 좀 더 효율적인 방법으로 읽을 수 있도록 Multimedia API를 제공합니다.

Multimedia API가 제공하는 기능 중의 하나가 Wave 파일을 읽고 재생하는 기능입니다.

이것을 사용하기 위해서는 먼저 다음과 같이 헤더를 추가해야 합니다.

#include <mmSystem.h>

그리고 프로젝트 속성의 Linker 메뉴에서 Additional Dependencies(추가 종속성)에 Winmm.lib를 추가합니다.

파일을 열기 위해서 mmioOpen() 함수를 사용합니다.

그리고 mmioDescend()와 mmioAscend()라는 함수를 사용해서 Chunk 단위로 값을 읽을 수 있습니다.

Chunk에 대한 자세한 설명은 Wave 파일의 구조 링크를 통해서 확인 가능합니다.

Visual Studio를 실행하고 Console Application을 생성합니다.

전체 소스 코드는 다음과 같습니다.

#include <memory>
#include <iostream>
#include <Windows.h>
#include <mmsystem.h>

#pragma comment(lib, "WinMM")

int ReadWaveFile(LPTSTR szFileName)
{
	if (nullptr == szFileName)
		return -1;

	// Wave File Open
	HMMIO hmmio = nullptr;
	hmmio = mmioOpen(szFileName, nullptr, MMIO_READ | MMIO_ALLOCBUF);
	if (nullptr == hmmio)
		return -2;

	// Find RIFF Chunk
	MMCKINFO mmckParent;
	memset(&mmckParent, 0x00, sizeof(mmckParent));
	mmckParent.fccType = mmioFOURCC('W', 'A', 'V', 'E');

	MMRESULT mmRes = mmioDescend(hmmio, &mmckParent, nullptr, MMIO_FINDRIFF);
	if (MMSYSERR_NOERROR != mmRes)
	{
		mmioClose(hmmio, 0);
		return -3;
	}		

	// Find Format Chunk
	MMCKINFO mmckChild;
	memset(&mmckChild, 0x00, sizeof(mmckChild));
	mmckChild.fccType = mmioFOURCC('f', 'm', 't', ' ');

	mmRes = mmioDescend(hmmio, &mmckChild, &mmckParent, MMIO_FINDCHUNK);
	if (MMSYSERR_NOERROR != mmRes)
	{
		mmioClose(hmmio, 0);
		return -4;
	}

	WAVEFORMATEX wfex;
	memset(&wfex, 0x00, sizeof(wfex));
	mmioRead(hmmio, (HPSTR)&wfex, mmckChild.cksize);

	std::cout << "wFormatTag      : " << wfex.wFormatTag << std::endl;
	std::cout << "nChannels       : " << wfex.nChannels << std::endl;
	std::cout << "nSamplesPerSec  : " << wfex.nSamplesPerSec << std::endl;
	std::cout << "wBitsPerSample  : " << wfex.wBitsPerSample << std::endl;
	std::cout << "nBlockAlign     : " << wfex.nBlockAlign << std::endl;
	std::cout << "nAvgBytesPerSec : " << wfex.nAvgBytesPerSec << std::endl;

	// Find Data Chunk
	mmioAscend(hmmio, &mmckChild, 0);

	mmckChild.ckid = mmioFOURCC('d', 'a', 't', 'a');

	mmRes = mmioDescend(hmmio, &mmckChild, &mmckParent, MMIO_FINDCHUNK);
	if (MMSYSERR_NOERROR != mmRes)
	{
		mmioClose(hmmio, 0);
		return -5;
	}

	DWORD dwDataSize = mmckChild.cksize;
	std::cout << "Data Size       : " << dwDataSize << std::endl;

	// Read Wave Data
// 	char* pData = nullptr;
// 	try
// 	{
// 		pData = new char[dwDataSize];
// 	}
// 	catch (std::bad_alloc e)
// 	{
// 		mmioClose(hmmio, 0);
// 		return -6;
// 	}
// 	mmioRead(hmmio, (HPSTR)pData, dwDataSize);
// 	delete[] pData;	

	mmioClose(hmmio, 0);

	return 0;
}

각 부분이 시작하는 구간에 대해서 주석을 추가했습니다.

가장 먼저 mmioOpen()을 호출해서 Wave 파일을 여는 작업을 진행합니다.

성공할 경우에 핸들이 리턴됩니다.

파일이 정상적으로 열리면 Chunk 단위로 작업을 진행합니다.

mmioDescend는 Chunk의 내부로 이동하고 mmioAscend는 해당 Chunk를 빠져 나옵니다.

RIFF Chunk의 'WAVE'를 찾는 작업을 진행해서 먼저 WAVE 파일인지를 확인합니다.

그리고 그 이후에 Format Chunk를 찾습니다.

찾을 때는 'fmt '를 사용해서 찾게 되며 공백이 포함되는 것을 주의해야 합니다.

Format Chunk를 정상적으로 찾아서 내용을 읽으면 Wave 파일의 PCM 포맷을 알 수 있습니다.

그리고 마지막은 Data Chunk에서 실제 PCM 데이터를 가져오는 작업입니다.

먼저 mmioAscend를 호출하는 것을 확인할 수 있습니다.

부모 Chunk -> 서브 Chunk로 이동했다가 서브 Chunk를 빠져 나가기 위해서 사용했습니다.

실제 데이터를 읽는 부분은 주석 처리를 했는데 적절하게 변환해서 데이터를 얻으면 됩니다.

참고로 mmioRead는 실제 읽은 바이트의 수를 리턴하며 -1일 경우에는 에러입니다.

Wave 포맷과 함께 보면 좀 더 쉽게 이해가 가능할 것 같습니다.

위의 코드는 일반적은 PCM 형식의 Mono(1채널), Stereo(2채널)에 유효한 코드입니다.

5.1 채널 등에 사용할 때는 WAVEFORMATEXTENSIBLE을 사용하는 등의 수정이 필요합니다.



반응형