Gappsong的文章

  • 编程实现录音及保存为WAV音频文件

    背景之前自己录制视频教程的时候,从网上找过一些破解版录屏软件来使用。后来,我细想了一下,其实我自己就可以下一个简单的录屏小软件。于是,后来我也自己慢慢摸索着,从网上搜索资料,慢慢地开发了一个有基本的录音录屏功能的小程序。
    其中,本文的录音小程序是当时为了熟悉录音流程而特意开发来练手的。当然,本文演示的程序是重写版,也就是为了配合这篇文章而重新简化开发的。现在,我就把实现过程和原理整理成文档,分享给大家。
    函数介绍waveInOpen 函数
    打开制定的波形音频输入设备进行录制。
    MMRESULT waveInOpen( LPHWAVEIN phwi, UINT uDeviceID, LPCWAVEFORMATEX pwfx, DWORD_PTR dwCallback, DWORD_PTR dwCallbackInstance, DWORD fdwOpen);
    参数

    phwi指向接收识别打开的波形音频输入设备的句柄的缓冲区。 调用其他波形音频输入功能时,使用此手柄来识别设备。 如果为fdwOpen指定了WAVE_FORMAT_QUERY,则此参数可以为NULL。
    uDeviceID要打开的波形-音频输入设备的标识符。 它可以是设备标识符或打开的波形-音频输入设备的句柄。 您可以使用标志:WAVE_MAPPER,该功能选择一个能够以指定格式录制的波形-音频输入设备。
    pwfx指向WAVEFORMATEX结构的指针,用于标识用于记录波形音频数据的所需格式。 在waveInOpen返回后,您可以立即释放此结构。
    dwCallback指向固定回调函数,事件句柄,窗口句柄或在波形音频录制期间要调用的线程的标识符,以处理与录制进度相关的消息。 如果不需要回调函数,该值可以为零。 有关回调函数的更多信息,请参阅waveInProc。
    dwCallbackInstance用户实例数据传递给回调机制。 此参数不与窗口回调机制一起使用。
    fdwOpen打开设备的标志。 定义了以下值:




    VALUE
    MEANING




    CALLBACK_EVENT
    dwCallback参数是一个事件句柄


    CALLBACK_FUNCTION
    dwCallback参数是一个回调过程地址


    CALLBACK_NULL
    没有回调机制。 这是默认设置


    CALLBACK_THREAD
    dwCallback参数是线程标识符


    CALLBACK_WINDOW
    dwCallback参数是一个窗口句柄


    WAVEMAPPED_DEFAULT COMMUNICATION_DEVICE
    如果指定了此标志,并且uDeviceID参数为WAVE_MAPPER,该函数将打开默认通讯设备。该标志仅适用于uDeviceID等于WAVE_MAPPER


    WAVE_FORMAT_DIRECT
    如果指定了该标志,则ACM驱动程序不会对音频数据执行转换


    WAVE_FORMAT_QUERY
    该功能查询设备以确定它是否支持给定的格式,但它不会打开设备


    WAVE_MAPPED
    uDeviceID参数指定要由波形映射器映射到的波形 - 音频设备



    返回值

    如果成功返回MMSYSERR_NOERROR,否则返回错误。

    waveInProc 回调函数
    waveInProc 函数是与波形音频输入设备一起使用的回调函数。 此函数是应用程序定义的函数名称的占位符。 该函数的地址可以在 waveInOpen 函数的callback-address 参数中指定。
    函数声明
    void CALLBACK waveInProc( HWAVEIN hwi, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2);
    参数

    hwi处理与回调函数关联的波形音频设备。
    uMsg波形音频输入讯息。 它可以是以下消息之一:




    VALUE
    MEANING




    WIM_CLOSE
    使用waveInClose函数关闭设备时发送


    WIM_DATA
    当设备驱动程序完成使用 waveInAddBuffer 函数发送的数据块时发送,即填满缓冲区时。


    WIM_OPEN
    使用waveInOpen功能打开设备时发送




    dwInstance用waveInOpen指定的用户实例数据。
    dwParam1消息参数。
    dwParam2消息参数。

    返回值

    不返回任何数据。

    waveInStart 函数
    waveInStart功能在给定的波形-音频输入设备上启动输入。
    函数声明
    MMRESULT waveInStart( HWAVEIN hwi);
    参数

    hwi
    处理波形音频输入设备。

    返回值

    如果成功返回MMSYSERR_NOERROR,否则返回错误。

    waveInPrepareHeader 函数
    为波形音频输入准备一个缓冲区。
    函数声明
    MMRESULT waveInPrepareHeader( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh);
    参数

    hwi处理波形音频输入设备。pwh指向WAVEHDR结构的指针,用于标识要准备的缓冲区。cbwhWAVEHDR结构的大小(以字节为单位)。
    返回值

    如果成功返回MMSYSERR_NOERROR,否则返回错误。

    waveInUnprepareHeader 函数
    waveInUnprepareHeader函数清除waveInPrepareHeader函数执行的准备工作。 必须在设备驱动程序填充缓冲区并将其返回到应用程序后调用此函数。 在释放缓冲区之前必须先调用此函数。
    函数声明
    MMRESULT waveInUnprepareHeader( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh);
    参数

    hwi处理波形音频输入设备。pwh指向WAVEHDR结构,指定待清理的缓冲区。cbwhWAVEHDR结构的大小(以字节为单位)。
    返回值

    如果成功返回MMSYSERR_NOERROR,否则返回错误。

    waveInAddBuffer 函数
    向指定的波形-音频输入设备发送一个输入缓冲区。 当缓冲区填满时,通知应用程序。
    函数声明
    MMRESULT waveInAddBuffer( HWAVEIN hwi, LPWAVEHDR pwh, UINT cbwh);
    参数

    hwi处理波形音频输入设备。pwh指向标识缓冲区的WAVEHDR结构的指针。cbwhWAVEHDR结构的大小(以字节为单位)。
    返回值

    如果成功返回MMSYSERR_NOERROR,否则返回错误。
    备注

    填充缓冲区时,WHDR_DONE位在WAVEHDR结构的dwFlags成员中设置。在传递给此函数之前,必须使用waveInPrepareHeader函数准备缓冲区。

    实现原理整个录音程序的开发可以归纳为 3 个部分:开始录音、停止录音、将录音数据保存为音频文件。现在,我们就针对各个部分进行详细的讲解。
    录音部分
    首先,我们先设置波形音频数据格式 WAVEFORMATEX,调用 waveInOpen 函数自动选择音频设备,设置回调函数,并按照设置的波形音频数据格式打开音频设备,获取设备句柄
    然后,我们要配置多层缓冲区,这样可以提升获取音频数据的效率以及完整性。在配置多层缓冲区的时候,我们先使用 waveInPrepareHeader 函数准备缓冲区,在调用 waveInAddBuffer 函数将缓冲区添加到设备中,只要缓冲区的数据填满,就会通知应用程序,通过回调函数返回音频数据
    最后,在设置好接收音频的缓冲区后,我们便可以直接调用开始录音函数 waveInstart 来开始录音。那么,只要有缓冲区填满数据,就会通过回调函数中的 WIM_DATA 消息来从缓冲区中获取数据

    停止录音
    我们直接调用 waveInStop 函数停止录音
    然后,我们便开始释放多层缓冲区。在释放缓冲区之前,必须先调用 waveInUnprepareHeader 函数清楚 waveInPrepareHeader 函数的准备工作,这样才能正确释放缓冲区
    最后,我们便调用 waveInClose 函数关闭设备

    将音频数据保存为 .wav 音频文件将数据保存为 .wav 音频文件,只需要在音频数据之前添加一个 .wav 格式头即可。然后,创建文件写入数据。这样便可以保存问音频文件了。
    编码实现开始录音// 开始录制声音 采样参数:单声道 8kHz 8bit --> 1, 8000, 8HWAVEIN StartWaveRecord(WORD wChannels, DWORD dwSampleRate, WORD wBitsPerSample, DWORD dwSoundDataSize){ HWAVEIN hWaveIn = NULL; WAVEFORMATEX waveFormatEx = { 0 }; //波形声音的格式,单声道双声道使用WAVE_FORMAT_PCM.当包含在WAVEFORMATEXTENSIBLE结构中时,使用WAVE_FORMAT_EXTENSIBLE. waveFormatEx.wFormatTag = WAVE_FORMAT_PCM; //声道数量 waveFormatEx.nChannels = wChannels; //采样位数.wFormatTag为WAVE_FORMAT_PCM时,为8或者16 waveFormatEx.nSamplesPerSec = dwSampleRate; // 每秒的采样字节数.通过nSamplesPerSec * nChannels * wBitsPerSample / 8计算 waveFormatEx.nAvgBytesPerSec = dwSampleRate * wChannels * (wBitsPerSample / 8); // 每次采样的字节数.通过nChannels * wBitsPerSample / 8计算 waveFormatEx.nBlockAlign = wChannels * (wBitsPerSample / 8); // 采样位数.wFormatTag为WAVE_FORMAT_PCM时,为8或者16 waveFormatEx.wBitsPerSample = wBitsPerSample; // wFormatTag为WAVE_FORMAT_PCM时,忽略此参数 waveFormatEx.cbSize = 0; // 打开音频输入设备 MMRESULT mmRet = ::waveInOpen(&hWaveIn, WAVE_MAPPER, &waveFormatEx, (DWORD)WaveCallback, NULL, CALLBACK_FUNCTION); if (MMSYSERR_NOERROR != mmRet) { ShowError("waveInOpen"); return NULL; } // 配置多层缓冲区 for (int i = 0; i < BUFFER_LAYOUT_NUM; i++) { ::RtlZeroMemory(&g_waveHdr[i], sizeof(WAVEHDR)); BYTE *lpBuf = new BYTE[BUFFERLENGTH]; // 指向波形格式的缓冲区 g_waveHdr[i].lpData = (char *)lpBuf; // 缓冲区的大小 g_waveHdr[i].dwBufferLength = BUFFERLENGTH; // 用户数据 g_waveHdr[i].dwUser = i; // 为缓冲区提供的信息, 在waveInPrepareHeader函数中使用WHDR_PREPARED g_waveHdr[i].dwFlags = 0; mmRet = ::waveInPrepareHeader(hWaveIn, &g_waveHdr[i], sizeof(WAVEHDR)); if (MMSYSERR_NOERROR != mmRet) { ShowError("waveInPrepareHeader"); return NULL; } // 将缓冲区加入音频输入设备 mmRet = ::waveInAddBuffer(hWaveIn, &g_waveHdr[i], sizeof(WAVEHDR)); if (MMSYSERR_NOERROR != mmRet) { ShowError("waveInAddBuffer"); return NULL; } } // 申请录音数据的动态内存 if (0 >= dwSoundDataSize) { return NULL; } g_pSoundData = new BYTE[dwSoundDataSize]; if (NULL == g_pSoundData) { ShowError("new"); return NULL; } g_dwSoundDataSize = 0; // 开始录音 mmRet = ::waveInStart(hWaveIn); if (MMSYSERR_NOERROR != mmRet) { ShowError("waveInStart"); return NULL; } return hWaveIn;}
    停止录音// 停止录音void StopWaveRecord(HWAVEIN hWaveIn, BYTE **ppSoundData, DWORD *pdwSoundDataSize){ // 设备停止 ::waveInStop(hWaveIn); // 释放缓冲区 for (int i = 0; i < BUFFER_LAYOUT_NUM; i++) { ::waveInUnprepareHeader(hWaveIn, &g_waveHdr[i], sizeof(WAVEHDR)); delete g_waveHdr[i].lpData; g_waveHdr[i].lpData = NULL; } // 关闭设备 ::waveInClose(hWaveIn); // 返回数据 *ppSoundData = g_pSoundData; *pdwSoundDataSize = g_dwSoundDataSize;}
    保存音频数据为 .wav 文件// 保存音频数据为 .wav 音频文件BOOL SaveWave(char *lpszFileName, BYTE *pSoundData, DWORD dwSoundDataSize){ // 默认wav音频头部数据 WAVEPCMHDR wavePCMHdr = { { 'R', 'I', 'F', 'F' }, 0, { 'W', 'A', 'V', 'E' }, { 'f', 'm', 't', ' ' }, sizeof(PCMWAVEFORMAT), WAVE_FORMAT_PCM, 1, SAMPLE_RATE, SAMPLE_RATE*(SAMPLE_BITS / 8), SAMPLE_BITS / 8, SAMPLE_BITS, { 'd', 'a', 't', 'a' }, 0 }; wavePCMHdr.data_size = dwSoundDataSize; wavePCMHdr.size_8 = dwSoundDataSize + 32; // 保存为.WAV格式的音频文件 FILE *fp = NULL; fopen_s(&fp, lpszFileName, "w+"); if (NULL == fp) { ShowError("fopen_s"); return FALSE; } // 写入wav音频头部数据 fwrite(&wavePCMHdr, sizeof(WAVEPCMHDR), 1, fp); // 写入音频数据 fwrite(pSoundData, dwSoundDataSize, 1, fp); // 关闭文件 fclose(fp); return TRUE;}
    程序测试我们运行程序,开始进行录音测试并保存文件。程序提示录音成功,并成功保存文件。然后,我们打开保存的 520.wav 文件进行播放,成功播放刚才录音的声音。


    总结注意,在使用 waveInAddBuffer 函数之前,必须使用 waveInPrepareHeader 函数准备缓冲区。而且在释放缓冲区之前必须先调用 waveInUnprepareHeader 函数。
    而且,要特别注意一点就是,当我们通过回调函数中的 WIM_DATA 消息来从缓冲区中获取数据,获取我完毕之后,要调用 waveInPrepareHeader 函数以及 waveInAddBuffer 函数,重新把缓冲区放回设备中,否则,当缓冲区都填满数据的时候,便不能继续获取数据。
    参考参考自《Windows黑客编程技术详解》一书
    2  留言 2018-12-13 17:22:37
eject