본문 바로가기

Application Programming Interface/Windows API

waveOut 함수 사용 예제 #1

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

 waveOut 함수는 Windows API에서 사운드 파일을 직접 읽어서 사운드 드라이버에 전달하는 저수준 출력 함수입니다. 저수준 함수인만큼 mp3, flac 등의 압축된 형식을 직접 읽을수는 없고 PCM 방식으로 저장된 파일(일명 wav 파일)을 읽습니다. 이를 이해하기 위해서는 먼저 웨이브 파일의 구조를 이해해야 합니다. (자세한 내용은 http://crystalcube.co.kr/123참조.) 웨이브 파일의 구조를 C 스타일의 구조체로 표현하면 다음과 같이 3개의 구조체로 표현가능합니다.

/* WAVE 파일을 구성하는 3개의 구조체 */
// 파일에서 PCM WAVE가 기록된 가장 첫 부분에 해당하는 내용입니다.
// 보통은 파일의 가장 첫 부분에 이 구조체에 들어맞는 내용이 옵니다.
#pragma pack(push, 1) // 1바이트 단위로 구조체 할당
typedef struct
{
	CHAR szChunkID[4]; // 항상 'R', 'I', 'F', 'F'라는 4개의 ASCII 문자가 옵니다.
	DWORD dwChunkSize; // 이 필드 바로 다음부터 파일의 맨 끝까지의 크기를 저장합니다.
	CHAR dwFormat[4]; // 이 필드 다음에 오는 형식의 종류를 지정합니다. 여기에서는 'W', 'A', 'V', 'E'의 4개의 ASCII 문자가 옵니다.
} RIFF_HEADER;

// WAVE 포맷을 나타냅니다.
typedef struct
{
	CHAR szChunkID[4]; // 항상 'f', 'm', 't', ' '라는 4개의 ASCII 문자가 옵니다. 맨 마지막 글자는 공백(0x20)입니다.
	DWORD dwChunkSize; // 이 필드 바로 다음부터 파일의 맨 끝까지의 크기를 저장합니다.
	WORD wFormatTag; // 데이터의 종류를 지정합니다. PCM 형식은 1입니다.
	WORD nChannels; // 채널의 수를 지정합니다.
	DWORD nSamplesPerSec; // 초당 샘플링 회수를 지정합니다.
	DWORD nAvgBytesPerSec; // 1초당 소리 크기를 바이트 수로 지정합니다.
	WORD nBlockAlign; // 채널의 총 수를 고려하여 sample 1회당 바이트 수를 나타낸 것입니다.
	WORD wBitsPerSample; // 1개 채널당 sample 1회당 바이트 수를 나타낸 것입니다.
} RIFF_FORMAT;

// 실제 데이터가 시작됨을 나타냅니다.
typedef struct
{
	CHAR szChunkID[4]; // 항상 'd', 'a', 't', 'a'라는 4개의 ASCII 문자가 옵니다.
	DWORD dwChunkSize; // 이 필드 바로 다음부터 파일의 맨 끝까지의 크기를 저장합니다. 헤더 정보를 뺀 순수 데이터의 크기입니다.
} RIFF_DATA;
#pragma pack(pop)

 Low-Level로 웨이브 파일을 직접 읽어 출력하는 방법은 크게,
1. 파일읽기
2. 헤더분석
3. 데이터찾기
4. 소리 출력 하드웨어 찾아 열기
5. 드라이버를 통해 하드웨어에 소리출력을 알림
6. 드라이버를 통해 하드웨어에 소리 데이터 기록
7. 드라이버를 통해 하드웨어에 소리출력의 종료를 알림 8. 열었던 소리 출력 하드웨어 닫기
위의 8단계로 구성됩니다. 아래 예시 소스를 보겠습니다. RIFF_HEADER, RIFF_FORMAT, RIFF_DATA 구조체는 위에서 선언했으므로 생략합니다.

 /* waveout.c */
#pragma comment(lib, "winmm.lib") // waveOutXXX 함수는 winmm.lib 라이브러리를 사용합니다. 이를 직접 import합니다.

#define _CRT_SECURE_NO_WARNINGS // Visual Studio는 strcpy 함수 대신 strcpy_s함수 사용을 권장합니다. 이를 무시하도록 매크로 선언합니다.

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <locale.h>
#include <assert.h>
#include <conio.h>
#include <tchar.h>

void * memmem(const void * haystack, size_t haystacklen, const void * needle, size_t needlelen)
{
	const char * p = haystack;
	const char * q = needle;
	size_t i, j;

	for (i = 0; i < haystacklen - needlelen + 1; i++)
	{
		if (*p == *q)
		{
			for (j = 1; j < needlelen; j++)
			{
				if (p[j] != q[j])
				{
					break;
				}
			}
			if (j == needlelen)
			{
				return (void *)p;
			}
		}

		p++;
	}

	return NULL;
}

int _tmain(int argc, TCHAR * argv[])
{
	TCHAR szFile[MAX_PATH]; // 파일 경로를 저장하는 버퍼입니다.
	HANDLE hFile = NULL; // 파일 핸들로 쓸 변수입니다.
	DWORD dwGlobal = 0; // 파일의 크기입니다.
	HGLOBAL hGlobal = NULL; // 파일의 크기만큼 할당된 메모리를 나타내는 핸들입니다.
	LPBYTE lpGlobal = NULL;

	DWORD numberOfBytesToRead = 0; // 파일을 읽을 크기입니다.
	DWORD numberOfBytesRead = 0; // 읽어진 파일의 바이트 수입니다.

	RIFF_HEADER riffHeader; char riffID[] = "RIFF";
	RIFF_FORMAT riffFormat; char fmtID[] = "fmt ";
	RIFF_DATA riffData; char dataID[] = "data";

	HWAVEOUT hWaveOut = NULL; // 출력장치 핸들입니다.
	WAVEFORMATEX waveFormatEx; // 웨이브 파일에 기록된 헤더를 WinAPI가 받아들이는 형태로 구성한 구조체입니다.
	WAVEHDR waveHeader; // 드라이버를 통해 하드웨어로 전달될 소리 정보입니다.

	DWORD dwDelay = 0; // 소리가 출력될 동안 프로그램의 종료를 지연시킵니다.

	setlocale(LC_ALL, ""); // _tprintf가 제대로 출력되도록 로케일 설정

	_tcscpy(szFile, TEXT("wave.wav")); // wav 파일의 이름 설정

	// 평범한 설정으로 파일을 열고 핸들을 만듭니다.
	hFile = CreateFile(szFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); assert(hFile);
	// 파일의 크기를 구합니다.
	dwGlobal = GetFileSize(hFile, NULL); assert(dwGlobal > 0);
	// 파일의 크기만큼 메모리를 할당받습니다.
	hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwGlobal); assert(hGlobal);
	// 할당받은 메모리를 운영체제에서 임의로 이동시키지 못하도록 잠시 고정합니다.
	lpGlobal = GlobalLock(hGlobal); assert(lpGlobal);

	// 파일의 시작 부분으로 포인터를 맞춥니다.
	SetFilePointer(hFile, 0, NULL, SEEK_SET);
	// dwGlobal만큼 파일을 읽습니다.
	numberOfBytesToRead = dwGlobal;
	ReadFile(hFile, lpGlobal, numberOfBytesToRead, &numberOfBytesRead, NULL);
	assert(numberOfBytesToRead == numberOfBytesRead); // 읽어야할 파일의 크기와 읽은 크기가 같음을 검증합니다.
	// 파일을 닫습니다.
	CloseHandle(hFile);

	// 'R', 'I', 'F', 'F'로 시작하는 부분을 찾아 이 부분을 riffHeader 구조체로 복사함
	lpGlobal = memmem(lpGlobal, dwGlobal, riffID, 4); assert(lpGlobal);
	CopyMemory(&riffHeader, lpGlobal, sizeof(RIFF_HEADER));
	// 'f', 'm', 't', ' '로 시작하는 부분을 찾아 이 부분을 riffFormat 구조체로 복사함
	lpGlobal = memmem(lpGlobal, dwGlobal, fmtID, 4); assert(lpGlobal);
	CopyMemory(&riffFormat, lpGlobal, sizeof(RIFF_FORMAT));
	// 'd', 'a', 't', 'a'로 시작하는 부분을 찾아 이 부분을 riffData 구조체로 복사함
	lpGlobal = memmem(lpGlobal, dwGlobal, dataID, 4); assert(lpGlobal);
	CopyMemory(&riffData, lpGlobal, sizeof(RIFF_DATA));

	lpGlobal += sizeof(RIFF_DATA);

	// 읽은 데이터 중 일부를 WAVEFORMAT 구조체로 옮김
	waveFormatEx.nAvgBytesPerSec = riffFormat.nAvgBytesPerSec;
	waveFormatEx.nBlockAlign = riffFormat.nBlockAlign;
	waveFormatEx.nChannels = riffFormat.nChannels;
	waveFormatEx.nSamplesPerSec = riffFormat.nSamplesPerSec;
	waveFormatEx.wFormatTag = riffFormat.wFormatTag;
	waveFormatEx.wBitsPerSample = riffFormat.wBitsPerSample;
	waveFormatEx.cbSize = 0;
	// 소리 출력 장치를 열기
	assert(waveOutOpen(&hWaveOut, WAVE_MAPPER, &waveFormatEx, NULL, NULL, CALLBACK_NULL) == MMSYSERR_NOERROR);

	// 프로그램 종료 지연시간을 구함
	dwDelay = (riffFormat.wBitsPerSample / 8) * riffFormat.nSamplesPerSec * riffFormat.nChannels;
	_tprintf(TEXT("bytesPerSec = %d\n"), dwDelay);
	dwDelay = dwGlobal / dwDelay;
	_tprintf(TEXT("TotalSec = %d\n"), dwDelay);

	// 드라이버로 전송할 데이터 정보 구성
	waveHeader.lpData = lpGlobal; // 소리 데이터가 있는 위치
	waveHeader.dwBufferLength = riffData.dwChunkSize; // 소리 데이터의 크기
	waveHeader.dwBytesRecorded = 0;
	waveHeader.dwFlags = 0L;
	waveHeader.dwLoops = 0L;
	waveHeader.dwUser = 0;
	waveHeader.lpNext = NULL;
	waveHeader.reserved = NULL;
	// 드라이버로 소리출력 알림
	assert(waveOutPrepareHeader(hWaveOut, &waveHeader, sizeof(WAVEHDR)) == MMSYSERR_NOERROR);
	// 소리출력
	assert(waveOutWrite(hWaveOut, &waveHeader, sizeof(WAVEHDR)) == MMSYSERR_NOERROR);
	// 소리가 완전히 끝날때까지 프로그램 종료를 지연
	Sleep(dwDelay * 1000 + 5000);
	// 드라이버로 소리출력 끝을 알림
	assert(waveOutUnprepareHeader(hWaveOut, &waveHeader, sizeof(WAVEHDR)) == MMSYSERR_NOERROR);
	// 소리 출력 끝
	assert(waveOutClose(hWaveOut) == MMSYSERR_NOERROR);

	// 메모리 할당 해제
	GlobalUnlock(hGlobal);
	GlobalFree(hGlobal);

	_getch();

	return 0;
}