使用 DirectSound 录制麦克风音频
使用 DirectSound 录制麦克风音频
本文所有代码均可在以下仓库找到
https://gitcode.net/PeaZomboss/learnaudios
目录是demo/dscapture
之前那篇文章简单介绍了DirectSound,并用其实现了对WAV格式文件的播放操作,本文将继续聚焦于DirectSound,但目标变成了用其实现对麦克风音频的录制,并将其保存为WAV格式的文件。
DirectSound录制的方法和播放的方法其实差不多,都是使用循环+多缓冲的方法,在实际写代码的过程中没有什么很大的不同,也无需过多解释,所以本文主要就是展示一些关键代码。
关于IDirectSoundCapture
等接口的具体定义还是建议直接看文档就行了。
https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee416960(v=vs.85)
接口定义
接口和播放器的非常相似,仅有部分小改动
// 参数1:缓冲区指针 // 参数2:缓冲区大小(单位字节) // 参数3:用户自定义指针 typedef void (*CopyBufCallback)(void *, int, void *); DWord WINAPI copy_thread(void *parameter); class Recorder { private: IDirectSoundCapture *dsc; IDirectSoundCaptureBuffer *dscb; CopyBufCallback copybuf_func; void *copybuf_ctx; WAVEFORMATEX fmtex; DWord block_size; DWord buf_size; HANDLE events[3]; HANDLE h_copy_thread; // copy_thread线程句柄 bool recording; // 录制中(包括暂停) bool suspended; // 是否挂起,用于暂停 void copy_buf(void *ptr, int len); // 方便调用 public: friend DWord WINAPI copy_thread(void *parameter); Recorder(); Recorder(GUID *device); ~Recorder(); void set_block_size(DWord bs); void set_fmt(const WAVEFORMATEX &fmtex); void set_copy_buf_callback(CopyBufCallback copy, void *ctx); void start(); void stop(); void pause(); void resume(); };
具体实现
对于录制功能的实现,使用简单的双缓冲即可,目前暂未发现一些问题。
线程实现如下:
DWord WINAPI copy_thread(void *parameter) { Recorder *recorder = (Recorder *)parameter; void *ptr; DWord len; while (true) { DWORD res = WaitForMultipleObjects(3, &recorder->events[0], FALSE, INFINITE); if (res == 0) { // 录制第一段缓冲区时保存第二段 recorder->dscb->Lock(recorder->buf_size, recorder->buf_size, &ptr, &len, NULL, NULL, 0); recorder->copy_buf(ptr, len); recorder->dscb->Unlock(ptr, len, NULL, 0); } else if (res == 1) { // 同理保存第一段 recorder->dscb->Lock(0, recorder->buf_size, &ptr, &len, NULL, NULL, 0); recorder->copy_buf(ptr, len); recorder->dscb->Unlock(ptr, len, NULL, 0); } else { // 保存剩下部分 DWord pos; recorder->dscb->Stop(); recorder->dscb->GetCurrentPosition(&pos, NULL); // 根据当前录制位置确定是在第一段还是第二段缓冲区 if (pos > recorder->buf_size) recorder->dscb->Lock(recorder->buf_size, pos - recorder->buf_size, &ptr, &len, NULL, NULL, 0); else recorder->dscb->Lock(0, pos, &ptr, &len, NULL, NULL, 0); recorder->copy_buf(ptr, len); recorder->dscb->Unlock(ptr, len, NULL, 0); break; } } printf("Recording thread end.\n"); return 0; }
然后是start()
的实现:
void Recorder::start() { if (dscb || recording) return; buf_size = block_size * fmtex.nBlockAlign; DSCBUFFERDESC dscbd; memset(&dscbd, 0, sizeof(DSCBUFFERDESC)); dscbd.dwSize = sizeof(DSCBUFFERDESC); dscbd.dwBufferBytes = buf_size * 2; // 使用双缓冲 dscbd.lpwfxFormat = &fmtex; HRESULT hr = dsc->CreateCaptureBuffer(&dscbd, &dscb, NULL); if (FAILED(hr)) { printf("Can not create capture buffer\n"); return; } void *ptr; DWord len; dscb->Lock(0, 0, &ptr, &len, NULL, NULL, DSCBLOCK_ENTIREBUFFER); memset(ptr, 0, len); // 清空整个缓冲区 dscb->Unlock(ptr, len, NULL, 0); IDirectSoundNotify *dsn; hr = dscb->QueryInterface(_iid_IDirectSoundNotify, (void **)&dsn); if (FAILED(hr)) { printf("Can not query direct sound notify\n"); return; } DSBPOSITIONNOTIFY dsbpn[2]; dsbpn[0].dwOffset = 0; dsbpn[0].hEventNotify = events[0]; dsbpn[1].dwOffset = buf_size; dsbpn[1].hEventNotify = events[1]; dsn->SetNotificationPositions(2, &dsbpn[0]); // 设置通知位置 dsn->Release(); recording = true; dscb->Start(DSCBSTART_LOOPING); printf("Recording ...\n"); DWord copy_thread_id = 0; h_copy_thread = CreateThread(NULL, 0, copy_thread, this, 0, ©_thread_id); printf("Record thread handle: %d, thread id: %d\n", h_copy_thread, copy_thread_id); suspended = false; }
stop()
,pause()
,resume()
的实现:
void Recorder::stop() { if (recording) { printf("Stopping...\n"); SetEvent(events[2]); WaitForSingleObject(h_copy_thread, 1000); // 等待线程结束 dscb->Release(); dscb = NULL; recording = false; printf("Done...\n"); } } void Recorder::pause() { if (recording && !suspended) { dscb->Stop(); suspended = true; printf("pause\n"); } } void Recorder::resume() { if (recording && suspended) { dscb->Start(DSCBSTART_LOOPING); suspended = false; printf("resume\n"); } }
主程序
为了方便将数据写入文件,使用全局变量如下:
FILE *fp; DWord total_len = 0; // 保存实际写入的数据长度(单位字节)
然后是保存缓冲区数据的方法:
void copy_buf(void *buf, int len, void *ctx) { int wt_size = fwrite(buf, 1, len, fp); total_len += wt_size; // 累加写入的长度 }
然后就是主函数:
int main() { char fn[256]; SYSTEMTIME st; GetLocalTime(&st); // 获取当前时间 // 根据时间生成文件名 sprintf(fn, "%d-%d-%d_%02d%02d%02d.wav", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); fp = fopen(fn, "wb"); RIFFHeader rh; strncpy(rh.id.chr, "RIFF", 4); rh.size = 0; // 暂时填零 strncpy(rh.type.chr, "WAVE", 4); fwrite(&rh, sizeof(RIFFHeader), 1, fp); // 写入RIFF头 RIFFChunkHeader rch; strncpy(rch.id.chr, "fmt ", 4); rch.size = sizeof(WAVEFORMATEX) - sizeof(WORD); // 不要多余的cbSize字段 fwrite(&rch, sizeof(RIFFChunkHeader), 1, fp); // 写入fmt头 WAVEFORMATEX fmtex; fmtex.wFormatTag = 1; fmtex.nChannels = 2; fmtex.nSamplesPerSec = 16000; fmtex.wBitsPerSample = 16; fmtex.nBlockAlign = 4; fmtex.nAvgBytesPerSec = 16000 * 4; fmtex.cbSize = 0; fwrite(&fmtex, sizeof(WAVEFORMATEX) - sizeof(WORD), 1, fp); // 写入fmt内容 strncpy(rch.id.chr, "data", 4); rch.size = 0; // 同理 fwrite(&rch, sizeof(RIFFChunkHeader), 1, fp); // 写入data头 Recorder recorder; recorder.set_fmt(fmtex); recorder.set_copy_buf_callback(copy_buf, NULL); recorder.start(); int command = 0; printf("Input 'q' to quit, 'p' to pause, 'r' to resume\n"); do { command = getchar(); if (command == 'p') recorder.pause(); else if (command == 'r') recorder.resume(); } while (command != 'q'); recorder.stop(); printf("Total got %d (bytes) data\n", total_len); // 回到文件头写入实际的大小信息 DWord riff_size = total_len + 44 - 8; // 头部共占用44字节内容 fseek(fp, 4, SEEK_SET); // 此处使用硬编码,实际使用不推荐 fwrite(&riff_size, 4, 1, fp); // 重新写入实际的大小 fseek(fp, 40, SEEK_SET); // 同上 fwrite(&total_len, 4, 1, fp); fclose(fp); printf("Saved to file \"%s\", size: %d\n", fn, riff_size + 8); }
分类:
音频相关
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)