[C++] Wave 파일 믹싱(Mixing)


Development note/C , C++ , MFC  2020. 5. 19. 16:52

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


이 글은 C++에서 Wave 파일 믹싱(Mixing)에 대한 글입니다.


이전에 제가 MCI를 이용해서 레코딩과 재생하는 방법에 대해 설명한 적이 있습니다.

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

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


사실 제가 이 음성에 대한 사양을 정확하게 잘 모릅니다. 그래서 구글링으로 여러 자료를 참조해서 보기는 하는데.. 참 어렵네요...

많이 부족하지만 혹시 도움이 되는 분이 있을지 몰라서 정리했습니다.


먼저 wav 파일을 합성(mixing)하려면 예전에 파일을 재생하는 방법에서 데이터를 가져와서 객체(class)화하는 게 우선입니다.

#pragma once
#include <iostream>  
#include <Windows.h>
#include <fstream>
using namespace std;
// wav에 접근하기 위해서는 필요합니다.
#pragma comment(lib, "winmm.lib")
// 메모리 구조체
typedef struct BufferNode
{
  int length;
  void* buffer;
};
// Wave 객체
class Wave
{
private:
  // 음성 길이(초 단위)
  int m_length;
  // 음성 포멧 지정
  WAVEFORMATEX m_format;
  // 음성 데이터 구조체
  WAVEHDR m_waveHdr;
  // 음성을 출력하는 장치 구조체
  HWAVEOUT m_hWaveOut;
public:
  // 생성자
  Wave();
  // Wav파일을 객체화 하는 함수
  void OpenWavFile(const char*);
  // 음성 실행 함수
  void Play();
  // 바이너리 단위 샘플링 길이
  DWORD GetSize() { return m_waveHdr.dwBufferLength; }
  // sample rate
  DWORD GetSampleRate() { return m_format.nSamplesPerSec; }
  // bits per sample
  SHORT GetBitsPerSample() { return m_format.wBitsPerSample; }
  // 채널. 1은 모노, 2는 스테레오
  SHORT GetChannels() { return m_format.nChannels; }
  // 바이너리 데이터
  LPBYTE GetData() { return (LPBYTE)m_waveHdr.lpData; }
};
#include "Wave.h"
// 생성자
Wave::Wave() {
  // 멤버 변수 초기화
  memset(&m_format, 0x00, sizeof(WAVEFORMATEX));
  memset(&m_waveHdr, 0x00, sizeof(WAVEHDR));
  memset(&m_hWaveOut, 0x00, sizeof(HWAVEOUT));
  // WAVE_FORMAT_PCM에서는 무시되는 값
  m_format.cbSize = 0;
  // WAVE_FORMAT_PCM이라면 무압축이기 때문에 nSamplesPerSec와 같을 것이다.  
  m_format.nAvgBytesPerSec = m_format.nSamplesPerSec;
  // 라이브러리에서 실제 녹음된 사이즈를 구하는 함수(사용자가 사용하는 값이 아님)  
  m_waveHdr.dwBytesRecorded = 0;
  // 라이브러리에서 callback 함수 사용시 사용되는 status flag  
  m_waveHdr.dwFlags = 0;
  // 반복 재생시 사용됨 (사용하지 않는다.)  
  m_waveHdr.dwLoops = 0;
  // 예약 재생시 사용됨 (사용하지 않는다.)  
  m_waveHdr.reserved = 0;
}
// 파일 읽어오기
void Wave::OpenWavFile(const char* filename) {
  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*)&m_format.wFormatTag, 2);
  cout << "format->wFormatTag (fixed - 1) - " << m_format.wFormatTag << endl;
  // NumChannels  
  istream.read((char*)&m_format.nChannels, 2);
  cout << "format->nChannels - " << m_format.nChannels << endl;
  // sample rate    
  istream.read((char*)&m_format.nSamplesPerSec, 4);
  cout << "format->nSamplesPerSec - " << m_format.nSamplesPerSec << endl;
  // byte rate (SampleRate * block align)  
  int byteRate;
  istream.read((char*)&byteRate, 4);
  // block align  
  istream.read((char*)&m_format.nBlockAlign, 2);
  cout << "byteRate - " << byteRate << " =  format->nSamplesPerSec * format->nBlockAlign - " << m_format.nSamplesPerSec * m_format.nBlockAlign << endl;
  // bits per sample  
  istream.read((char*)&m_format.wBitsPerSample, 2);
  cout << "format->wBitsPerSample - " << m_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;
  // 실제 음악 데이터 읽어오기  
  m_waveHdr.dwBufferLength = subchunk2size / m_format.nChannels;
  m_waveHdr.lpData = (char*)malloc(m_waveHdr.dwBufferLength);
  istream.read(m_waveHdr.lpData, m_waveHdr.dwBufferLength);
  // 파일 닫기  
  istream.close();

  // byteRate는 1초의 데이터 길이  
  // WaveHeader->dwBufferLength는 데이터의 전체 길이  
  // WaveHeader->dwBufferLength / byteRate는 wav의 음악 길이가 나온다.  
  m_length = m_waveHdr.dwBufferLength / byteRate;
}
// 재생 함수
void Wave::Play() {
  cout << "Playing..." << endl;
  // waveOutOpen는 위 구조체로 장치를 Open하는 함수.  
  // 파라미터는 HWAVEOUT, 두번째는 장치 선택인데 보통은 WAVE_MAPPER를 넣어도 됩니다.  
  // 저는 특이하게 스피커가 여러개 있어서(노트북 기본 스피커, 해드셋 스피커) 가장 나중에 접속한 단자를 선택했습니다.  
  // 세번째 파라미터는 음성 포멧을 넣습니다.  
  // 마지막 파라미터는 WAVE_FORMAT_DIRECT를 설정해서 동기화된 소스를 작성합니다.  
  if (waveOutOpen(&m_hWaveOut, waveInGetNumDevs() - 1 /*WAVE_MAPPER*/, &m_format, 0, 0, WAVE_FORMAT_DIRECT))
  {
    // 에러 콘솔 출력  
    cout << "Failed to open waveform output device." << endl;
    // 접속 실패  
    return;
  }
  // 장치에 출력 준비를 알리는 함수  
  if (waveOutPrepareHeader(m_hWaveOut, &m_waveHdr, sizeof(WAVEHDR)))
  {
    // 에러 콘솔 출력  
    cout << "waveOutPrepareHeader error" << endl;
    // 장치 닫기  
    waveOutClose(m_hWaveOut);
    return;
  }
  // 출력 시작  
  if (waveOutWrite(m_hWaveOut, &m_waveHdr, sizeof(WAVEHDR)))
  {
    // 에러 콘솔 출력  
    cout << "waveOutWrite error" << endl;
    // 장치 닫기  
    waveOutClose(m_hWaveOut);
    return;
  }
  // WAVE_FORMAT_DIRECT를 선택했기 때문에 출력이 끝날때까지 기다려야 합니다.  
  // length로 음성의 길이를 알기 때문에 1초단위로 콘솔에 표시합니다.  
  for (int i = 0; i <= m_length; i++)
  {
    cout << "\r";
    // 플레이 초 표시  
    cout << "Sec - " << i;
    Sleep(1000);
  }
  // 장치 닫기  
  waveOutClose(m_hWaveOut);
}

소스는 길지만 wav 파일 읽어 오는 파일 소스에서 Wave 클래스를 만든 것밖에 없습니다. Wave 클래스의 주요 데이터는 음성 포멧(WAVEFORMATEX)과 음성 데이터 구조체(WAVEHDR), 장치 구조체(HWAVEOUT)입니다.

장치 구조체는 Wave 클래스 별로 두어서 여러개의 파일을 동시에 재생을 하면 동시에 소리가 재생이 되기 때문에 합성이 된 것처럼 들립니다.


그러나 제가 하고 싶은건 단순히 소리가 같이 들리는 것이 아니고 데이터를 믹싱하는 것이 목적이기 때문에 데이터를 합치는 함수를 만들겠습니다.

#include <iostream>  
#include "Wave.h"
using namespace std;
// Wave클래스를 믹싱
void mixing(Wave* pWave1, Wave* pWave2)
{
  // 믹싱 조건으로 파일 Channel이 같고 SampleRate가 같고, BitsPerSample이 같아야 처리됩니다.
  if ((pWave1->GetChannels() == pWave2->GetChannels()) && (pWave1->GetSampleRate() == pWave2->GetSampleRate()) && (pWave1->GetBitsPerSample() == pWave2->GetBitsPerSample()))
  {
    // 샘플 사이즈 구하기
    long sampleSize = min(pWave1->GetSize(), pWave2->GetSize())/ (pWave1->GetBitsPerSample() >> 3);
    switch (pWave1->GetBitsPerSample())
    {
    // BitsPerSample가 8인 경우
    case 8:
    {
      // 데이터 취득
      LPBYTE lpSrcData = pWave2->GetData();
      LPBYTE lpDstData = pWave1->GetData();
      float gain = log10(20.0f);
      for (long i = 0; i < sampleSize; i++)
      {
        // 주파수 합치기
        *lpDstData = (BYTE)(((*lpSrcData + *lpDstData) >> 1) * gain);
        lpSrcData++;
        lpDstData++;
      }
    }
    break;
    // BitsPerSample가 16인 경우
    case 16:
    {
      // 데이터 취득
      LPWORD lpSrcData = (LPWORD)pWave2->GetData();
      LPWORD lpDstData = (LPWORD)pWave1->GetData();
      for (long i = 0; i < sampleSize; i++)
      {
        float sample1 = (*lpSrcData - 32768) / 32768.0f;
        float sample2 = (*lpDstData - 32768) / 32768.0f;
        if (fabs(sample1 * sample2) > 0.25f)
        {
          // 주파수 합치기
          *lpDstData = (WORD)(*lpSrcData + *lpDstData);
        }
        else
        {
          // 주파수 합치기
          *lpDstData = fabs(sample1) < fabs(sample2) ? *lpSrcData : *lpDstData;
        }
        lpSrcData++;
        lpDstData++;
      }
    }
    break;
    }
  }
}

// 시작 함수  
int main()
{
  // 콘솔 출력
  cout << "********** Wave1 information **********" << endl;
  // Wave 인스턴스 생성
  Wave wave1;
  // 파일로 부터 wav파일 읽어오기
  wave1.OpenWavFile("d:\\work\\test1.wav");
  cout << endl;
  // 콘솔 출력
  cout << "********** Wave2 information **********" << endl;
  // Wave 인스턴스 생성
  Wave wave2;
  // 파일로 부터 wav파일 읽어오기
  wave2.OpenWavFile("d:\\work\\test2.wav");
  
  // wave파일 믹싱
  mixing(&wave1, &wave2);
  // 재생하기
  wave1.Play();
  return 0;
}

사실 저 주파수 합치기 에서 나오는 함수의 의미를 정확하게 저도 잘 모릅니다. 사실 CodeProject에서 믹싱 함수가 있어서 참조했습니다.

링크 - https://www.codeproject.com/Articles/29676/CWave-A-Simple-C-Class-to-Manipulate-WAV-Files


Wave파일 믹싱을 하는데 조건이 Channel과 SampleRate, BitsPerSample가 같아야 하는 조건이 있습니다. 동영상 편집기등을 보면 이런 특정 조건없이도 잘 믹싱이 되던데...

일단 파일을 OpenWavFile함수로 읽어 오면 파일 정보가 콘솔에 표시가 됩니다.


그리고 최종 믹싱된 데이터는 Wave에 저장이 되고 Play를 하면 믹싱된 음악 파일을 들을 수 있습니다.


여기까지 C++에서 Wave 파일 믹싱(Mixing)에 대한 글이었습니다.


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