[C++] wav파일을 재생하는 방법(MCI - 미디어 컨트롤 인터페이스)


Development note/C , C++ , MFC  2020. 4. 30. 17:52

안녕하세요. 명월입니다.


이 글은 C++에서 wav파일을 재생하는 방법(MCI - 미디어 컨트롤 인터페이스)에 대한 글입니다.


이전에 제가 WinApi의 MCI를 이용해서 마이크를 통해 음성을 녹음하고 파일로 저장하는 방법에 대해서 글을 작성한 적이 있습니다.

링크 - [C++] 녹음기 프로그램 작성하는 방법(MCI - 미디어 컨트롤 인터페이스)


이번에는 그 반대입니다. 파일을 읽어와서 스피커로 파일을 재생하는 방법입니다.

소스는 녹음하는 것과 정반대라고 생각하면 됩니다.

#include <iostream>
#include <Windows.h>
#include <fstream>
using namespace std;
// wav에 접근하기 위해서는 필요합니다.
#pragma comment(lib, "winmm.lib")
// 함수 예약 - 녹음된 데이터를 파일로 작성하기 위한 함수
int OpenWavFile(const char*, WAVEFORMATEX*, PWAVEHDR);
// 메모리 구조체
typedef struct BufferNode
{
  int length;
  void* buffer;
};
// 시작 함수
int main()
{
  // 음성 포멧 지정
  WAVEFORMATEX pFormat;
  // 음성 데이터 구조체
  WAVEHDR WaveOutHdr;
  // 파일로부터 음성 포멧과 데이터를 취득한다.
  // 총 음성 길이를 받는다.
  int length = OpenWavFile("c:\\work\\temp.wav", &pFormat, &WaveOutHdr);
  // WAVE_FORMAT_PCM에서는 무시되는 값
  pFormat.cbSize = 0;
  // WAVE_FORMAT_PCM이라면 무압축이기 때문에 nSamplesPerSec와 같을 것이다.
  pFormat.nAvgBytesPerSec = pFormat.nSamplesPerSec;
  // 라이브러리에서 실제 녹음된 사이즈를 구하는 함수(사용자가 사용하는 값이 아님)
  WaveOutHdr.dwBytesRecorded = 0;
  // 라이브러리에서 callback 함수 사용시 사용되는 status flag
  WaveOutHdr.dwFlags = 0;
  // 반복 재생시 사용됨 (사용하지 않는다.)
  WaveOutHdr.dwLoops = 0;
  // 예약 재생시 사용됨 (사용하지 않는다.)
  WaveOutHdr.reserved = 0;

  // 음성을 출력하는 장치 구조체
  HWAVEOUT hWaveOut;
  cout << "Playing..." << endl;
  // waveOutOpen는 위 구조체로 장치를 Open하는 함수.
  // 파라미터는 HWAVEOUT, 두번째는 장치 선택인데 보통은 WAVE_MAPPER를 넣어도 됩니다.
  // 저는 특이하게 스피커가 여러개 있어서(노트북 기본 스피커, 해드셋 스피커) 가장 나중에 접속한 단자를 선택했습니다.
  // 세번째 파라미터는 음성 포멧을 넣습니다.
  // 마지막 파라미터는 WAVE_FORMAT_DIRECT를 설정해서 동기화된 소스를 작성합니다.
  if (waveOutOpen(&hWaveOut, waveInGetNumDevs() - 1 /*WAVE_MAPPER*/, &pFormat, 0, 0, WAVE_FORMAT_DIRECT))
  {
    // 에러 콘솔 출력
    cout << "Failed to open waveform output device." << endl;
    // 접속 실패
    return 1;
  }
  // 장치에 출력 준비를 알리는 함수
  if (waveOutPrepareHeader(hWaveOut, &WaveOutHdr, sizeof(WAVEHDR))) 
  {
    // 에러 콘솔 출력
    cout << "waveOutPrepareHeader error" << endl;
    // 장치 닫기
    waveOutClose(hWaveOut);
    return 1;
  }
  // 출력 시작
  if (waveOutWrite(hWaveOut, &WaveOutHdr, sizeof(WAVEHDR)))
  {
    // 에러 콘솔 출력
    cout << "waveOutWrite error" << endl;
    // 장치 닫기
    waveOutClose(hWaveOut);
    return 1;
  }
  // WAVE_FORMAT_DIRECT를 선택했기 때문에 출력이 끝날때까지 기다려야 합니다.
  // length로 음성의 길이를 알기 때문에 1초단위로 콘솔에 표시합니다.
  for (int i = 0; i <= length; i++)
  {
    cout << "\r";
    // 플레이 초 표시
    cout << "Sec - " << i;
    Sleep(1000);
  }
  // 장치 닫기
  waveOutClose(hWaveOut);
  return 0;
}
int OpenWavFile(const char* filename, WAVEFORMATEX* format, PWAVEHDR WaveHeader)
{
  ifstream istream;
  istream.open(filename, fstream::binary);
  // wav파일 구조체대로 작성한다.
  istream.seekg(0, ios::beg);
  // chunk id
  char riff[5];
  memset(riff, 0x00, 5);
  istream.read(riff, 4);
  cout << "chunk id - " << riff << endl;
  int chunksize;
  // chunk size (36 + SubChunk2Size))
  istream.read((char*)&chunksize, 4);
  cout << "chunksize " << chunksize << endl;
  // format
  char wave[5];
  memset(wave, 0x00, 5);
  istream.read(wave, 4);
  cout << "format - " << wave << endl;
  // subchunk1ID - fmt
  char subchunk1ID[5];
  memset(subchunk1ID, 0x00, 5);
  istream.read(subchunk1ID, 4);
  cout << "subchunk1ID - " << subchunk1ID << endl;
  // subchunk1size (무압축 PCM이면 16 고정)
  int subchunk1size;
  istream.read((char*)&subchunk1size, 4);
  cout << "subchunk1size (fixed - 16) - " << subchunk1size << endl;
  // AudioFormat (무압축 PCM이면 1 고정)
  istream.read((char*)&format->wFormatTag, 2);
  cout << "format->wFormatTag (fixed - 1) - " << format->wFormatTag << endl;
  // NumChannels
  istream.read((char*)&format->nChannels, 2);
  cout << "format->nChannels - " << format->nChannels << endl;
  // sample rate  
  istream.read((char*)&format->nSamplesPerSec, 4);
  cout << "format->nSamplesPerSec - " << format->nSamplesPerSec << endl;
  // byte rate (SampleRate * block align)
  int byteRate;
  istream.read((char*)&byteRate, 4);
  // block align
  istream.read((char*)&format->nBlockAlign, 2);
  cout << "byteRate - " << byteRate << " =  format->nSamplesPerSec * format->nBlockAlign - " << format->nSamplesPerSec * format->nBlockAlign << endl;
  // bits per sample
  istream.read((char*)&format->wBitsPerSample, 2);
  cout << "format->wBitsPerSample - " << format->wBitsPerSample << endl;
  // subchunk2ID
  char data[5];
  memset(data, 0x00, 5);
  istream.read(data, 4);
  cout << "data - " << data << endl;
  // subchunk2size (NumSamples * nBlockAlign)
  int subchunk2size;
  istream.read((char*)&subchunk2size, 4);
  cout << "chunksize - " << chunksize << " = 36 + chunksize = subchunk2size - " << subchunk2size << endl;
  // 실제 음악 데이터 읽어오기
  WaveHeader->dwBufferLength = subchunk2size / format->nChannels;
  WaveHeader->lpData = (char*)malloc(WaveHeader->dwBufferLength);
  istream.read(WaveHeader->lpData, WaveHeader->dwBufferLength);
  // 파일 닫기
  istream.close();

  // byteRate는 1초의 데이터 길이
  // WaveHeader->dwBufferLength는 데이터의 전체 길이
  // WaveHeader->dwBufferLength / byteRate는 wav의 음악 길이가 나온다.
  return WaveHeader->dwBufferLength / byteRate;
}

녹음을 할 때는 waveInOpen의 설정을 CALLBACK_FUNCTION로 하여 Callback 함수에서 데이터를 받는 것으로 설정했었습니다.

녹음은 우리가 시간을 정해서 녹음하는 것이 아니라, 그 길이를 예측할 수 없기 때문에 1초 단위로 데이터를 받아서 realloc으로 데이터를 만드는 작업을 했었습니다.


그러나 재생은 파일에 이미 총 데이터 길이가 정해져 있기 때문에 Callback으로 1초단위로 재생할 필요없이 한번에 Single thread에서 데이터를 읽어도 문제가 없기 때문에 WAVE_FORMAT_DIRECT로 설정했습니다.

그러나 내부적으로는 Thread로 재생이 되는 듯하니 Single thread가 재생이 끝날 때까지 기다려야 할 필요는 있습니다.


참조 - https://wiki.fileformat.com/audio/wav/


여기까지 C++에서 wav파일을 재생하는 방법(MCI - 미디어 컨트롤 인터페이스)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.