[C++] Window 음성 레코더 프로그램 작성하는 방법(스팩트럼 미완성)


Development note/C , C++ , MFC  2020. 5. 11. 20:13

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


이 글은 C++에서Window 음성 레코더 프로그램 작성하는 방법(스팩트럼 미완성)에 대한 글입니다.


예전에 제가 Wav파일 작성하는 방법이랑 C++에서 Window 폼을 작성하는 방법에 대해 설명한 적이 있습니다.

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

링크 - [C++] C++에서 Window Form을 생성하는 방법(Win32Api, MFC)


사실 이 글은 제가 마이크로 음성을 받으면 윈도우 화면에 스팩트럼을 그리는 식의 단순한 작업인 줄 알았는데, 스팩트럼 파형이 생각보다 제대로 된 이미지가 나오지 않네요.

제가 이 쪽으로 전문이 아니다 보니 이 소리 파형과 스팩트럼에 대해서 전혀 이해를 하지 못하네요..

그래서 단순히 Wav의 데이터 중에 위쪽은 가장 큰 소리, 아래쪽은 가장 작은 소리의 스팩트럼을 표시하는 것으로 작성했습니다. (아마 이런 단순한 작업은 아닐 듯 싶은데.. 자세히 아시는 분 있으면 댓글로 알려주세요.)

#include <stdio.h>
#include <Windows.h>
#include <fstream>
using namespace std;
#pragma comment(lib, "winmm.lib")
// 음성을 입력하는 장치 구조체
HWAVEIN hWaveIn;
// 음성 데이터 구조체
WAVEHDR WaveInHdr;
// 음성 포멧 지정
WAVEFORMATEX pFormat;
// 상태 플러그
BOOL PLAY = false;
// 버튼 핸들 (녹음 버튼)
HWND RecButton;
// 버튼 핸들 (정지 버튼)
HWND StpButton;

// 윈도우 메시지 함수
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// 녹음하기 위한 초기화 함수
void InitRecord(HWND);
// 녹음을 종료하는 함수
void StopRecord(HWND);
// 파일 저장
void SaveWavFile(const char*, WAVEFORMATEX*, PWAVEHDR);
// 버퍼
typedef struct BufferNode
{
  // raw 데이터 총 길이
  int length;
  // raw 데이터 버퍼
  void* buffer;
  // 샘플링 인덱스
  int index = 0;
  // 스팩트럼 샘플링
  short max[425];
  short min[425];
};
// 실행 함수
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR LpszCmdParam, int nCmdShow)
{
  // 윈도우 핸들이다
  HWND hWnd;
  // 윈도우 메시지이다.
  MSG Message;
  // 윈도우를 정의하는 구조체이다.
  WNDCLASS WndClass;
  // 윈도우의 초기 설정하는 값이다.
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  // 윈도우의 메인 바탕색
  WndClass.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
  // 윈도우의 기본 마우스 커서 형태
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  // 윈도우의 아이콘
  WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  // 윈도우의 인스턴스
  WndClass.hInstance = hInstance;
  // 윈도우의 메시지 callbak 함수
  WndClass.lpfnWndProc = WndProc;
  // 윈도우를 인식하기 위한 클래스 명
  WndClass.lpszClassName = L"Main";
  // 윈도우 메뉴 이름
  WndClass.lpszMenuName = NULL;
  // 윈도우 스타일
  WndClass.style = CS_HREDRAW | CS_VREDRAW;
  // 레지스트리 등록
  RegisterClass(&WndClass);
  // 윈도우 생성
  hWnd = CreateWindowEx(WS_EX_WINDOWEDGE, WndClass.lpszClassName, WndClass.lpszClassName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 440, 300, NULL, (HMENU)NULL, hInstance, NULL);
  // 윈도우를 띄운다.
  ShowWindow(hWnd, SW_SHOWDEFAULT);
  // 메시지 루프
  while (GetMessage(&Message, NULL, 0, 0))
  {
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  // 녹음 중이라면
  if (PLAY) {
    // 마이크 리소스를 종료한다.
    StopRecord(hWnd);
  }
  return 0;
}
// 녹음하기 위한 초기화 함수
void InitRecord(HWND hWnd) 
{
  // 무압축 소리 데이터 설정
  pFormat.wFormatTag = WAVE_FORMAT_PCM;
  // 소리 채널 수, 1이면 모노, 2이면 스테레오입니다.
  pFormat.nChannels = 2;
  // 샘플링 데이터 횟수.
  // nSamplesPerSec에는 8.0 kHz, 11.025 kHz, 22.05 kHz, 44.1 kHz가 있습니다.
  pFormat.nSamplesPerSec = 44100;
  // WAVE_FORMAT_PCM이라면 무압축이기 때문에 nSamplesPerSec와 같을 것이다.
  pFormat.nAvgBytesPerSec = 44100;
  // 1회 샘플링에 사용되어지는 데이터 비트수, 8 또는 16
  pFormat.wBitsPerSample = 16;
  // 샘플링에 사용되는 바이트 단위의 메모리 크기
  pFormat.nBlockAlign = pFormat.nChannels * (pFormat.wBitsPerSample / 8);
  // WAVE_FORMAT_PCM에서는 무시되는 값
  pFormat.cbSize = 0;
  // waveInOpen는 위 구조체로 장치를 Open하는 함수.
  if (waveInOpen(&hWaveIn, waveInGetNumDevs() - 1 /*WAVE_MAPPER*/, &pFormat, (DWORD_PTR)hWnd, 0, CALLBACK_WINDOW)) 
  {
    // 에러 발생시 메시지 박스
    MessageBox(hWnd, L"waveInOpen error", L"Button", MB_OK);
    return;
  }
  // 데이터 구조체 설정
  WaveInHdr.dwBufferLength = pFormat.nAvgBytesPerSec;
  // 실제 데이터를 할당한다.
  WaveInHdr.lpData = (char*)malloc(WaveInHdr.dwBufferLength);
  // 라이브러리에서 실제 녹음된 사이즈를 구하는 함수(사용자가 사용하는 값이 아님)
  WaveInHdr.dwBytesRecorded = 0;
  // 라이브러리에서 callback 함수 사용시 사용되는 status flag
  WaveInHdr.dwFlags = 0;
  // 반복 재생시 사용됨 (사용하지 않는다.)
  WaveInHdr.dwLoops = 0;
  // 예약 재생시 사용됨 (사용하지 않는다.)
  WaveInHdr.reserved = 0;
  // 버퍼 설정.
  WaveInHdr.dwUser = (ULONG_PTR)new BufferNode();
  BufferNode* bn = (BufferNode*)WaveInHdr.dwUser;
  bn->buffer = (char*)malloc(1);
  bn->length = 0;
  // 샘플링 배열 초기화
  memset(&bn->max, 0x00, sizeof(bn->max));
  memset(&bn->min, 0x00, sizeof(bn->min));
  // 장치에 녹음 준비를 알리는 함수
  if (waveInPrepareHeader(hWaveIn, &WaveInHdr, sizeof(WAVEHDR)))
  {
    // 에러 발생시 메시지 박스
    MessageBox(hWnd, L"waveInPrepareHeader error", L"Button", MB_OK);
    // 장치 닫기
    waveInClose(hWaveIn);
    return;
  }
  // 상태 플래그 true로 변경
  PLAY = true;
}
// 녹음을 종료하는 함수
void StopRecord(HWND hWnd) 
{
  // 상태 플래그 false로 변경
  PLAY = false;
  // 장치 닫기
  waveInClose(hWaveIn);
  // 버퍼 취득
  BufferNode* bn = (BufferNode*)WaveInHdr.dwUser;
  // 버퍼의 데이터를 설정한다.
  WaveInHdr.dwBufferLength = bn->length;
  WaveInHdr.lpData = (char*)bn->buffer;
  // 파일 저장
  SaveWavFile("c:\\work\\temp.wav", &pFormat, &WaveInHdr);
  // 동적 할당된 버퍼를 메모리 해제한다.
  delete bn->buffer;
  delete bn;
}
// 윈도우 메시지 함수
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
  // 생성 구조체 취득
  CREATESTRUCT* cs = (CREATESTRUCT*)lParam;
  switch (iMessage)
  {
  case WM_CREATE:
  {
    // 버튼 생성
    RecButton = CreateWindowEx(0, L"button", L"Record", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 7, 175, 100, 25, hWnd, (HMENU)999, cs->hInstance, NULL);
    StpButton = CreateWindowEx(0, L"button", L"Stop", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 314, 175, 100, 25, hWnd, (HMENU)998, cs->hInstance, NULL);
    // 정지 버튼 비활성화
    EnableWindow(StpButton, FALSE);
  }
  return 0;
  case WM_COMMAND:
  {
    switch (LOWORD(wParam))
    {
    // Record 버튼을 눌렀을 경우
    case 999:
      // 녹음 버튼 비활성화
      EnableWindow(RecButton, FALSE);
      // 정지 버튼 활성화
      EnableWindow(StpButton, TRUE);
      // 장치 초기화
      InitRecord(hWnd);
      break;
    // Stop 버튼을 눌렀을 경우
    case 998:
      // 녹음 버튼 활성화
      EnableWindow(RecButton, TRUE);
      // 정지 버튼 활성화
      EnableWindow(StpButton, FALSE);
      // 장치 정지 및 파일 저장
      StopRecord(hWnd);
    }

  }
  return 0;
  case WM_PAINT:
  {
    PAINTSTRUCT ps;
    // GDI 화면 DC취득
    HDC hdc = BeginPaint(hWnd, &ps);
    if (hdc)
    {
      RECT rc;
      rc.top = 35;
      rc.left = 0;
      rc.bottom = 145;
      rc.right = 425;
      // 스팩트럼 창 그리기
      FillRect(hdc, &rc, (HBRUSH)(COLOR_WINDOW + 2));
      // 재생 중일 경우
      if (PLAY == TRUE)
      {
        // 팬 취득
        HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0, 200, 0));
        // 오브젝트 선택
        SelectObject(hdc, hPen);
        // 버퍼 취득
        BufferNode* bn = (BufferNode*)WaveInHdr.dwUser;
        // 스팩트럼을 그리기 위한 계산식
        // 스팩트럼 최대치
        float normalmaxv = MAXSHORT;
        // 스팩트럼 넓이 계산
        int width = rc.right - rc.left;
        // 스팩트럼 높이 계산
        int height = rc.bottom - rc.top;
        // 스팩트럼 x선 좌표
        int center = rc.top + (height / 2);
        // 스팩트럼 최대 길이
        int maxbar = height / 2;
        // 커서 이동
        MoveToEx(hdc, rc.left + 0, center, NULL);
        // 스팩트럼 배열에서 취득한다.
        for (int i = 0; i < bn->index; i++) 
        {
          // 커서 이동
          MoveToEx(hdc, rc.left + i, center, NULL);
          // 스팩 트럼 크기만큼 그리기
          LineTo(hdc, rc.left + i, center - (maxbar * (bn->max[i] / normalmaxv)));
          LineTo(hdc, rc.left + i, center - (maxbar * (bn->min[i] / normalmaxv)));
        }
        // 오브젝트 삭제
        DeleteObject(hPen);
      }
      // GDI 화면 DC 삭제
      DeleteDC(hdc);
      // 그리기 종료
      EndPaint(hWnd, &ps);
    }
  }
  return 0;
  case WM_DESTROY:
  {
    PostQuitMessage(0);
  }
  // 장치가 waveInPrepareHeader되면 호출
  case MM_WIM_OPEN:
  {
    // 장치가 초기화 되면 버퍼를 설정한다.
    if (waveInAddBuffer(hWaveIn, &WaveInHdr, sizeof(WAVEHDR))) 
    {
      // 메시지 박스 출력
      MessageBox(hWnd, L"waveInAddBuffer error", L"Button", MB_OK);
      // 장치 해제
      waveInClose(hWaveIn);
      return 0;
    }
    // 녹화를 시작한다.
    if (waveInStart(hWaveIn))
    {
      // 메시지 박스 출력
      MessageBox(hWnd, L"waveInStart error", L"Button", MB_OK);
      // 장치 해제
      waveInClose(hWaveIn);
      return 1;
    }
    return TRUE;
  }
  // 녹음 중일 때 호출
  case MM_WIM_DATA:
  {
    // 녹음 상태가 아니면 종료
    if (PLAY == false) 
    {
      waveInClose(hWaveIn);
      return TRUE;
    }
    // 음성 데이터 구조체 취득
    WAVEHDR* WaveInHdr = (WAVEHDR*)lParam;
    // 버퍼 취득
    BufferNode* bn = (BufferNode*)WaveInHdr->dwUser;
    // 녹음된 사이즈만큼 메모리 재할당을 한다.
    bn->buffer = realloc(bn->buffer, bn->length + WaveInHdr->dwBytesRecorded);
    // 파일 버퍼로 복사
    memcpy((char*)bn->buffer + bn->length, WaveInHdr->lpData, WaveInHdr->dwBytesRecorded);
    // 버퍼 길이 수정
    bn->length += WaveInHdr->dwBytesRecorded;
    // 샘플링
    // 제가 여기 계산을 잘 몰라서 임의로 최대값, 최소값으로 설정했습니다.
    short max = MINSHORT;
    short min = MAXSHORT;
    // 데이터를 취득한다.
    for (int i = 0; i < WaveInHdr->dwBytesRecorded - 1; i+=2) {
      // 데이터 비트를 취득
      char* p = (char*)WaveInHdr->lpData + i;
      // char형 에서 short형으로 변경
      short res = *p << 8 | *(p + 1);
      // 최대 값은 max에 swap한다.
      if (max < res) {
        max = res;
      }
      // 최소 값은 min에 swap한다.
      if (res < min) {
        min = res;
      }
    }
    // 샘플링 데이터에 설정
    bn->max[bn->index] = max;
    bn->min[bn->index] = min;
    bn->index++;
    // 버퍼를 설정한다.
    if (waveInAddBuffer(hWaveIn, WaveInHdr, sizeof(WAVEHDR)))
    {
      MessageBox(hWnd, L"waveInPrepareHeader error", L"Button", MB_OK);
      waveInClose(hWaveIn);
      return TRUE;
    }
    // 다시 그리기 - WM_PAINT 호출
    RECT rc;
    GetClientRect(hWnd, &rc);
    InvalidateRect(hWnd, &rc, TRUE);
    return TRUE;
  }

  return 0;
  }
  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}

// 녹음된 데이터를 파일로 작성하기 위한 함수
void SaveWavFile(const char* filename, WAVEFORMATEX* format, PWAVEHDR WaveHeader)
{
  // output stream을 할당
  ofstream ostream;
  // 파일 열기
  ostream.open(filename, fstream::binary);
  int subchunk1size = 16;
  int byteRate = format->nSamplesPerSec * format->nBlockAlign;
  int subchunk2size = WaveHeader->dwBufferLength * format->nChannels;
  int chunksize = (36 + subchunk2size);
  // wav파일 구조체대로 작성한다.
  ostream.seekp(0, ios::beg);
  // chunk id
  ostream.write("RIFF", 4);
  // chunk size (36 + SubChunk2Size))
  ostream.write((char*)&chunksize, 4);
  // format
  ostream.write("WAVE", 4);
  // subchunk1ID
  ostream.write("fmt ", 4);
  // subchunk1size (무압축 PCM이면 16 고정)
  ostream.write((char*)&subchunk1size, 4);
  // AudioFormat (무압축 PCM이면 1 고정)
  ostream.write((char*)&format->wFormatTag, 2);
  // NumChannels
  ostream.write((char*)&format->nChannels, 2);
  // sample rate
  ostream.write((char*)&format->nSamplesPerSec, 4);
  // byte rate (SampleRate * block align)
  ostream.write((char*)&byteRate, 4);
  // block align
  ostream.write((char*)&format->nBlockAlign, 2);
  // bits per sample
  ostream.write((char*)&format->wBitsPerSample, 2);
  // subchunk2ID
  ostream.write("data", 4);
  // subchunk2size (NumSamples * nBlockAlign)
  ostream.write((char*)&subchunk2size, 4);
  // 실제 음악 데이터 작성
  ostream.write(WaveHeader->lpData, WaveHeader->dwBufferLength);
  // 파일 닫기
  ostream.close();
}

이 소스를 실행 해 보겠습니다.

Record 버튼을 누르면 녹음이 시작되면서 스팩트럼이 보입니다. 다시 Stop을 누르면 스팩트럼이 정지하면서 파일이 저장됩니다.

녹음 파일도 잘 생성이 되었습니다.


참조 - https://sijoo.tistory.com/290

참조 - https://www.dreamincode.net/forums/topic/208153-sound-recorder-using-the-low-level-windows-api-in-c/


혹시, 스팩트럼 사양에 대해서 자세히 아시는 분 있으면 댓글로 알려주세요.


여기까지 C++에서Window 음성 레코더 프로그램 작성하는 방법(스팩트럼 미완성)에 대한 글이었습니다.


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