안녕하세요. 명월입니다.
이 글은 C++에서 녹음기 프로그램 작성하는 방법(MCI - 미디어 컨트롤 인터페이스)에 대한 글입니다.
C++에서 마이크를 통해 음성을 녹음하는 방법은 여러 가지가 있으나 대표적인 건 DirectX 라이브러리로 DirectSound를 이용하여 녹음하는게 정석입니다.
저도 DirectX를 다루어 본지가 너무 오래돼서 이제는 거의 기억이 잘 안 나네요.. 언제 다시 라이브러리 SDK를 열어서 다시 공부해야겠습니다.
DirectX를 이용하지 않고 오디오에 접근하는 방법이 있는데, WinApi의 MCI로 접근하는 방법입니다.
WinApi의 MCI로 접근하는 방법은 따로 라이브러리를 다운로드 할 것 없이 바로 접근이 가능합니다.
그럼 WinApi로 마이크로 부터 데이터를 받고 wav파일로 작성하는 방법에 대해서 작성하겠습니다.
#include <iostream>
#include <Windows.h>
#include <fstream>
using namespace std;
// wav에 접근하기 위해서는 필요합니다.
#pragma comment(lib, "winmm.lib")
// 함수 예약 - Callback 함수, 음성 녹음 시간을 지정해 놓고 녹음하는 게 아니기 때문에 동적인 콜백 함수가 필요한다.
void CALLBACK waveInProc(HWAVEIN, UINT, DWORD, DWORD, DWORD);
// 함수 예약 - 녹음된 데이터를 파일로 작성하기 위한 함수
void SaveWavFile(const char*, WAVEFORMATEX*, PWAVEHDR);
// 메모리 구조체
typedef struct BufferNode
{
int length;
void* buffer;
};
// 시작 함수
int main()
{
// 음성을 입력하는 장치 구조체
HWAVEIN hWaveIn;
// 음성 데이터 구조체
WAVEHDR WaveInHdr;
// 음성 포멧 지정
WAVEFORMATEX pFormat;
// 무압축 소리 데이터 설정
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하는 함수.
// 파라미터는 HWAVEIN, 두번째는 장치 선택인데 보통은 WAVE_MAPPER를 넣어도 됩니다.
// 저는 특이하게 마이크가 여러개 있어서(노트북 기본 마이크, 해드셋 마이크) 가장 나중에 접속한 단자를 선택했습니다.
// 세번째 파라미터는 음성 포멧을 넣고, call함수를 넣습니다.
// 마지막 파라미터는 CALLBACK_FUNCTION를 설정해서 callback함수를 사용한다는 것을 설정합니다.
if (waveInOpen(&hWaveIn, waveInGetNumDevs() - 1 /*WAVE_MAPPER*/, &pFormat, (DWORD_PTR)waveInProc, 0, CALLBACK_FUNCTION))
{
// 에러 콘솔 출력
cout << "Failed to open waveform input device." << endl;
// 접속 실패
return 1;
}
// 데이터 구조체 설정
// 버퍼의 길이를 설정한다. 무조건 1초 단위로 설정합니다.
// 위 구조체에서 샘플링을 44100 바이트로 했기 때문에 그 크기를 넣는다.
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;
// 버퍼 설정. 실제 녹음은 1초 단위로 데이터가 나오는데.. 데이터를 연결할 필요가 있다.
// WaveInHdr.dwUser는 원래 버퍼를 설정하는 변수는 아니지만, 사용하지 않는 변수이기 때문에 재활용
WaveInHdr.dwUser = (ULONG_PTR)new BufferNode();
BufferNode* bn = (BufferNode*)WaveInHdr.dwUser;
// 동적으로 메모리를 재할당하기 때문에 1byte만 할당하자
bn->buffer = (char*)malloc(1);
// 초기 데이터 길이는 0이다.
bn->length = 0;
// 장치에 녹음 준비를 알리는 함수
if (waveInPrepareHeader(hWaveIn, &WaveInHdr, sizeof(WAVEHDR)))
{
// 에러 콘솔 출력
cout << "waveInPrepareHeader error" << endl;
// 장치 닫기
waveInClose(hWaveIn);
return 1;
}
// 장치에 메모리를 할당을 하는 함수
if (waveInAddBuffer(hWaveIn, &WaveInHdr, sizeof(WAVEHDR)))
{
// 에러 콘솔 출력
cout << "waveInAddBuffer error" << endl;
// 장치 닫기
waveInClose(hWaveIn);
return 1;
}
// 녹음 시작
if (waveInStart(hWaveIn))
{
// 에러 콘솔 출력
cout << "waveInStart error" << endl;
// 장치 닫기
waveInClose(hWaveIn);
return 1;
}
// 콘솔 출력
cout << "Recording..." << endl;
// 아무 키를 누르고 엔터를 치면 녹음을 중단하고 바로 파일로 저장한다.
while (true)
{
cout << "If press any key, the record will save to the file." << endl;
char input = -1;
cin >> input;
if (input != -1)
{
break;
}
}
// callback에 종료를 알리는 Flag 설정.
WaveInHdr.dwFlags = WHDR_DONE;
// 녹음 종료
waveInStop(hWaveIn);
// 장치에 녹음 해제를 알리는 함수
waveInUnprepareHeader(hWaveIn, &WaveInHdr, sizeof(WAVEHDR));
// 장치 닫기
waveInClose(hWaveIn);
// 구조체의 메모리는 동적 할당 되었기 때문에 해제한다. 더이상 필요가 없음.
delete WaveInHdr.lpData;
// 버퍼의 데이터를 설정한다.
WaveInHdr.dwBufferLength = bn->length;
WaveInHdr.lpData = (char*)bn->buffer;
// 파일을 저장한다.
SaveWavFile("c:\\work\\temp.wav", &pFormat, &WaveInHdr);
// 동적 할당된 버퍼를 메모리 해제한다.
delete bn->buffer;
delete bn;
return 0;
}
// Callback 함수, 음성 녹음 시간을 지정해 놓고 녹음하는 게 아니기 때문에 동적인 콜백 함수가 필요한다.
void CALLBACK waveInProc(HWAVEIN hWaveIn, UINT uMsg, DWORD dwInstance, DWORD dwParam1, DWORD dwParam2)
{
switch (uMsg)
{
// 데이터가 호출되면 메시지가 WIM_DATA로 호출된다.
case WIM_DATA:
{
// 음성 데이터 구조체 취득
WAVEHDR* WaveInHdr = (WAVEHDR*)dwParam1;
// 버퍼를 가져온다.
BufferNode* bn = (BufferNode*)WaveInHdr->dwUser;
// WaveInHdr->dwBytesRecorded는 녹음된 사이즈이다.
// 녹음된 사이즈만큼 메모리 재할당을 한다.
bn->buffer = realloc(bn->buffer, bn->length + WaveInHdr->dwBytesRecorded);
// 1초 단위로 샘플링이 오기 때문에 마지막 녹음된 위치의 그 뒤에 메모리 복사를 한다.
// 1초에는 bn->length가 0이기 때문에 그대로 녹음된 1초가 memcpy함.
// 2초에는 bn->length가 1초의 길이만큼 들어가니 앞의 1초 뒤에 1초가 memcpy함.
// 3초는 2초 뒤에...
memcpy((char*)bn->buffer + bn->length, WaveInHdr->lpData, WaveInHdr->dwBytesRecorded);
// 길이를 늘려준다.
bn->length += WaveInHdr->dwBytesRecorded;
// 장치에 녹음 준비를 알리는 함수
if(waveInPrepareHeader(hWaveIn, WaveInHdr, sizeof(WAVEHDR)))
{
// 에러 콘솔 출력
cout << "waveInPrepareHeader error" << endl;
// 장치 닫기
waveInClose(hWaveIn);
return;
}
// 장치에 메모리를 할당을 하는 함수
if(waveInAddBuffer(hWaveIn, WaveInHdr, sizeof(WAVEHDR)))
{
// 에러 콘솔 출력
cout << "waveInPrepareHeader error" << endl;
// 장치 닫기
waveInClose(hWaveIn);
return;
}
}
break;
}
}
// 녹음된 데이터를 파일로 작성하기 위한 함수
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();
}
실행을 하면서 바로 녹음이 시작됩니다. 녹음은 아무 데이터 하나를 넣고 엔터를 치면 종료하고 파일을 작성합니다.
wav 파일이 작성되었습니다.
실행해 보면 녹음한 내용이 들리는 것을 확인할 수 있습니다.
참조 - https://www.dreamincode.net/forums/topic/208153-sound-recorder-using-the-low-level-windows-api-in-c/
참조 - https://docs.microsoft.com/en-us/windows/win32/api/mmeapi/ns-mmeapi-waveformatex
여기까지 C++에서 녹음기 프로그램 작성하는 방법(MCI - 미디어 컨트롤 인터페이스)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Development note > C , C++ , MFC' 카테고리의 다른 글
[C++] Wav 구조체를 이용한 음성 채팅(MCI - 미디어 컨트롤 인터페이스) (0) | 2020.05.25 |
---|---|
[C++] Wave 파일 믹싱(Mixing) (0) | 2020.05.19 |
[C++] Window 음성 레코더 프로그램 작성하는 방법(스팩트럼 미완성) (0) | 2020.05.11 |
[C++] wav파일을 재생하는 방법(MCI - 미디어 컨트롤 인터페이스) (3) | 2020.04.30 |
[C++] opencv를 사용하는 방법(image를 동영상, 캠 이미지를 동영상으로 작성) (0) | 2020.04.27 |
[C++] Bitmap 파일 작성과 읽는 방법(화면 스크린 샷을 하는 방법) (0) | 2020.04.27 |
[C++] 필수 라이브러리 Boost 설치하기(boost::asio::threadpool 예제) (0) | 2020.04.23 |
[C++] 환경 설정 파일(ini)을 사용하는 방법 (0) | 2020.04.22 |