前言
在Windows上实现录音比较简单的方法是使用winmm,其中的waveIn模块就可以打开录音设备,获取PCM数据,进行声音录制。本文将介绍waveIn录音的具体实现,以及如何避免死锁。
一、需要的对象及方法
需要用到的头文件
#include"windows.h"
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib ")
1.对象
//声音采集对象
HWAVEIN _waveIn;
//声音数据的缓存
WAVEHDR _wavehdrs[2];
//声音格式
WAVEFORMATEX _waveFormat;
2.方法
//打开声音输入设备
waveInOpen
//注册缓冲区
waveInPrepareHeader
//注销缓冲区
waveInUnprepareHeader
//缓冲区加入使用
waveInAddBuffer
//开始采集
waveInStart
//停止采集
waveInStop
//停止采集
waveInReset
//关闭设备
waveInClose
二、整体流程
整体流程大致如下:
三、关键实现
1.设置声音格式
WAVEFORMATEX WaveInitFormat(WORD nCh, DWORD nSampleRate, WORD bitsPerSample)
{
WAVEFORMATEX waveFormat;
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nChannels = nCh;
waveFormat.nSamplesPerSec = nSampleRate;
waveFormat.nAvgBytesPerSec = nSampleRate * nCh * bitsPerSample / 8;
waveFormat.nBlockAlign = nCh * bitsPerSample / 8;
waveFormat.wBitsPerSample = bitsPerSample;
waveFormat.cbSize = 0;
return waveFormat;
}
2.子线程中打开设备
使用子线程开启的方式可以有效避免死锁问题。
//子线程,通过CreateThread启动下列线程
DWORD WINAPI ThreadProc(LPVOID p)
{
MMRESULT result = waveInOpen(&_waveIn, 0, &waveFormat, GetCurrentThreadId(), (DWORD)NULL, CALLBACK_THREAD);
//开启消息循环
//略
//消息循环中监听3个消息
switch (msg.message){
case WIM_OPEN:break;
case WIM_DATA:break;
case WIM_CLOSE:break;
}
//开启消息循环--end
return 0;
}
3.停止采集
通过发送WIM_CLOSE消息停止采集。
PostThreadMessage(GetThreadId(_hThread), WIM_CLOSE, 0, 0);
在子线程的消息循环中:
case WIM_CLOSE:
waveInStop(_waveIn);
waveInReset(_waveIn);
waveInClose(_waveIn);
break;
四、封装成对象
将采集功能封装成一个通用工具,方便在任意地方使用。
1.接口设计
接口设计如下:
#pragma once
#include<vector>
#include<string>
#include<functional>
/************************************************************************
* @Project: AC::SoundCollector
* @Decription: 音频采集工具
* @Verision: v1.0.0.1
* @Author: Xin Nie
* @Create: 2021/12/30 13:34:00
* @LastUpdate: 2021/1/7 23:06:00
************************************************************************
* Copyright @ 2020. All rights reserved.
************************************************************************/
namespace AC {
/// <summary>
/// 声音格式
/// </summary>
class SoundFormat {
public:
/// <summary>
/// 声道数
/// </summary>
int Channels;
/// <summary>
/// 采样率
/// </summary>
int SampleRate;
/// <summary>
/// 位深
/// </summary>
int BitsPerSample;
};
/// <summary>
/// 声音采集设备
/// </summary>
class SoundDevice {
public:
/// <summary>
/// 设备Id
/// </summary>
int Id;
/// <summary>
/// 设备名称
/// </summary>
std::string Name;
/// <summary>
/// 声道数
/// </summary>
int Channels;
/// <summary>
/// 支持的格式
/// </summary>
std::vector<SoundFormat> SupportedFormats;
};
/// <summary>
/// 声音采集对象
/// </summary>
/// <summary>
/// 声音采集对象
///这是一个功能完整声音采集对象,所有接口通过了测试。
///SoundCollectorTest方法包含了所有接口的测试,修改代码后,用其验证功能是否正常。
///非线程安全,所有方法需确保在单线程中调用,即比如:Start和Stop不能在两个线程中同时调用。
/// </summary>
class SoundCollector {
public:
/// <summary>
/// 采集开始事件参数
/// </summary>
class StartedEventArgs
{
public:
/// <summary>
/// 采集声音数据的格式
/// </summary>
SoundFormat Format;
};
/// <summary>
/// 采集数据到达事件
/// </summary>
class DataArrivedEventArgs :public StartedEventArgs
{
public:
/// <summary>
/// 声音数据
/// </summary>
unsigned char* Data;
/// <summary>
/// 数据长度
/// </summary>
int DataLength;
};
/// <summary>
/// 错误事件参数
/// </summary>
class ErrorEventArgs
{
public:
/// <summary>
/// 错误内容
/// </summary>
std::string Message;
};
/// <summary>
/// 采集开始事件
/// </summary>
std::function<void(void*, StartedEventArgs*)> Started;
/// <summary>
/// 采集数据到达事件
/// </summary>
std::function<void(void*, DataArrivedEventArgs*)> DataArrived;
/// <summary>
/// 采集结束事件
/// </summary>
std::function<void(void*, void*)>Stoped;
/// <summary>
/// 错误事件
/// </summary>
std::function<void(void*, ErrorEventArgs*)> Error;
/// <summary>
/// 构造方法
/// </summary>
SoundCollector();
/// <summary>
/// 构造方法
/// </summary>
/// <param name="deviceId">声音设备Id,0为默认设备</param>
SoundCollector(int deviceId);
/// <summary>
/// 构造方法
/// </summary>
/// <param name="deviceId">声音设备Id,0为默认设备</param>
/// <param name="channels">采集的声道数</param>
/// <param name="sampleRate">采集的采样率</param>
/// <param name="bitsPerSample">采集的位深</param>
SoundCollector(int deviceId, int channels, int sampleRate, int bitsPerSample);
/// <summary>
/// 析构方法
/// </summary>
~SoundCollector();
/// <summary>
/// 开始采集
/// </summary>
bool Start(int channels, int sampleRate, int bitsPerSample);
/// <summary>
/// 停止采集
/// </summary>
void Stop();
/// <summary>
/// 异步停止采集
/// </summary>
void BeginStop();
/// <summary>
/// 设置采集速率
/// </summary>
/// <param name="timesPerSecond">采集速率单位:次/秒</param>
void SetFrequency(int timesPerSecond);
/// <summary>
/// 获取采集速率,数据回调频率。
/// </summary>
/// <returns>采集速率,单位:次/秒</returns>
int GetFrequency();
/// <summary>
/// 获取声道数
/// </summary>
/// <returns>声道数</returns>
int GetChannels();
/// <summary>
/// 获取采样率
/// </summary>
/// <returns>采样率,单位:hz</returns>
int GetSampleRate();
/// <summary>
/// 获取位深
/// </summary>
/// <returns>位深,单位:bits</returns>
int GetBitsPerSample();
/// <summary>
/// 获取当前是否在采集中
/// </summary>
/// <returns>是否已停止</returns>
bool GetIsStoped();
/// <summary>
/// 获取声音设备列表
/// </summary>
/// <returns></returns>
static std::vector<SoundDevice> GetDeives();
private:
void* _implement = nullptr;
};
}
2.具体实现
https://download.csdn.net/download/u013113678/72818898
五、使用示例
采集声音并保存为wav文件,其中的WavWapper对象参考《C++ 将音频PCM数据封装成wav文件》
#include"SoundCollector.h"
#include "WavWapper.h"
#include<Windows.h>
int main()
{
//运行测试程序
//AC::SoundCollectorTest();
//获取设备列表
auto devices = AC::SoundCollector::GetDeives();
if (devices.size() > 0)
{
printf("设备名称:%s\n", devices[0].Name.c_str());
//初始化采集对象
AC::SoundCollector sc(devices[0].Id);
//wav封装对象
AC::WavWapper ww;
//注册事件
sc.Started = [&](auto s, auto e) {
printf("开始录制:Channels %d SampleRate %d BitsPerSample %d\n", e->Format.Channels, e->Format.SampleRate, e->Format.BitsPerSample);
//根据声音格式创建wav文件
ww.CreateWavFile("sound.wav", e->Format.Channels, e->Format.SampleRate, e->Format.BitsPerSample);
};
sc.DataArrived = [&](auto s, auto* e) {
//采集的数据写入wav文件
ww.WriteToFile(e->Data, e->DataLength);
};
sc.Stoped = [&](auto s, auto e) {
//关闭文件
ww.CloseFile();
printf("录制完成!\n");
};
sc.Error = [&](auto s, auto e) {
printf("%s\n",e->Message.c_str());
};
//开始采集
if (sc.Start(2, 44100, 16))
{
Sleep(20000);
//结束采集
sc.Stop();
}
}
}
总结
以上就是今天要讲的内容,使用waveIn实现声音采集,实现过程还是相对较简单的,但还是有些细节需要注意,比如使用方法回调的方式打开设备,关闭时很容易造成死锁,经过一番尝试发现子线程中打开设备才是比较好的方式。总的来说,waveIn实现的声音采集模块是能够支持音频实时流和录制开发的。