안녕하세요. 명월입니다.
이 글은 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 음성 레코더 프로그램 작성하는 방법(스팩트럼 미완성)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Development note > C , C++ , MFC' 카테고리의 다른 글
[C++] 테스리스 게임 만들기 (0) | 2021.03.11 |
---|---|
[C++] Wav 구조체를 이용한 음성 채팅(MCI - 미디어 컨트롤 인터페이스) (0) | 2020.05.25 |
[C++] Wave 파일 믹싱(Mixing) (0) | 2020.05.19 |
[C++] wav파일을 재생하는 방법(MCI - 미디어 컨트롤 인터페이스) (3) | 2020.04.30 |
[C++] 녹음기 프로그램 작성하는 방법(MCI - 미디어 컨트롤 인터페이스) (1) | 2020.04.29 |
[C++] opencv를 사용하는 방법(image를 동영상, 캠 이미지를 동영상으로 작성) (0) | 2020.04.27 |
[C++] Bitmap 파일 작성과 읽는 방법(화면 스크린 샷을 하는 방법) (0) | 2020.04.27 |
[C++] 필수 라이브러리 Boost 설치하기(boost::asio::threadpool 예제) (0) | 2020.04.23 |