使用 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, &copy_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);
}
posted @ 2022-12-18 15:02  PeaZomboss  阅读(259)  评论(0编辑  收藏  举报