简要介绍WASAPI播放音频的方法

这篇具有很好参考价值的文章主要介绍了简要介绍WASAPI播放音频的方法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

正文

填一下之前挖的坑,这回就说说怎么用WASAPI播放声音吧。

本文完整代码可以在以下链接找到

https://gitcode.net/PeaZomboss/learnaudios

目录是demo/wasplay。

WASAPI介绍

参考链接https://learn.microsoft.com/en-us/windows/win32/coreaudio/wasapi,这个是英文原版的,建议阅读,https://learn.microsoft.com/zh-cn/windows/win32/coreaudio/wasapi是机翻的,有些地方不太好,当然也可以两份对照着阅读。

WASAPI是Windows Core Audio的一部分,是从Vista开始引入的,作为应用层最底层的音频API。所以现在用的DirectSound系列也好,waveXxx(也就是MME)也好,他们都是基于Core Audio的,而音频流的管理,则是通过WASAPI。

WASAPI是一个比较复杂的API,但是其方便了许多偏底层的音频开发,因为在这之前有WDM音频驱动和Kernel Streaming(内核流)这种模式,开发难度极高,甚至为了降低延迟就有厂商搞出了ASIO这玩意。

延迟对于音频开发来说非常重要,因为早期Windows没有一套像样的低延迟音频API,所以开发起来要另辟蹊径,费时费力,而现在有了WASAPI就不一样了,一套API就可以实现低延迟、高品质的音频输入输出了。

可以说微软推出WASAPI就是为了一统Windows音频开发的江湖。

WASAPI使用

现在我们看看WASAPI应该怎么用吧。

首先说明,WASAPI(或者说整个Core Audio)是基于Windows经典的COM组件对象模型,使用一系列接口来实现各类功能。所以我们要使用它,就必须先了解一些基础的COM知识。

其实之前讲DirectSound的时候也说到了这个,但是因为DirectSound有DirectSoundCreate这个函数,所以操作简单了不少,而用Core Audio就复杂一些了。

一些有关COM概念的介绍网上有不少不错的资料,这里就不多说了,比如
官方文档 https://learn.microsoft.com/zh-cn/windows/win32/com/component-object-model--com--portal
或者 https://blog.csdn.net/qq_40628925/article/details/118097146
还有 https://blog.csdn.net/wangqiulin123456/article/details/8026270(排版差了点)

COM在我们的使用中就是一个接口,这个接口类似于Java或者C#里的接口,在C++就是一个纯虚类,用C语言表示就是一个虚函数表,利用这个特性实现了跨编程语言、跨操作系统的功能。每个接口都继承自IUnknown类,有三个基本方法,一般最需要关注的就是Release的调用了,因为这涉及到内存释放,如果不调用就会造成泄漏。

使用COM的函数需要注意返回值,类型为HRESULT,实际上就是一个int,其中S_OK代表成功,其他不少返回值都有其特定的含义,具体在查API的时候可以看到,不过呢一般情况下都是会返回S_OK的,只有少部分会失败,我们只需关注容易失败的就行了。

在使用COM之前先要初始化,这个过程比较简单,调用CoInitializeEx函数即可,当然使用完了一会要调用CoUninitialize来撤销初始化。

其中CoInitializeEx有两个参数,第一个固定为NULL,第二个填0即可,默认就是多线程的,返回值一般不用管,基本上不会出错的;CoUninitialize没有参数也没有返回值。

然后需要调用CoCreateInstance来创建一个对象,这个函数比较复杂,这里是官方的介绍。

贴一下官方的函数原型

HRESULT CoCreateInstance(
  [in]  REFCLSID  rclsid,
  [in]  LPUNKNOWN pUnkOuter,
  [in]  DWORD     dwClsContext,
  [in]  REFIID    riid,
  [out] LPVOID    *ppv
);

第一个是CLSID,我们只要知道这是一个GUID常量就行了
第二个一般用NULL就行了
第三个一般用CLSCTX_ALL就行了
第四个一是要创建的对象的类型IID,也是一个GUID常量
第五个就是创建的对象指针的地址

下面贴出创建的例子

const GUID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
IMMDeviceEnumerator *enumerator = NULL;
CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **)&enumerator);

这样我们就有了一个IMMDeviceEnumerator对象指针了。

接着再

IMMDevice *device = NULL;
enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
enumerator->Release();

就能创建一个IMMDevice对象了,这里创建的是默认设备对象,也可以枚举所有设备选择其中一个,选择的过程一般交给用户来处理,具体可以去看看IMMDeviceEnumerator的介绍。

拿到了IMMDevice对象之后就可以创建IAudioClient对象了。

const GUID IID_IAudioClient = __uuidof(IAudioClient);
IAudioClient *aclient = NULL;
device->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&aclient);

前面说了这么多,终于到了我们需要的IAudioClient了。这是我们的核心部分,接口文档链接:https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nn-audioclient-iaudioclient

IAudioClient后面还有IAudioClient2和IAudioClient3,不过都是一些扩展功能,本文内容用不上。不过如果要用的话,就得用IUnknown的QueryInterface方法了,这个在之前讲DirectSound事件驱动模式的时候已经用过了。

这里是一个官方示例代码https://learn.microsoft.com/zh-cn/windows/win32/coreaudio/rendering-a-stream,不过呢虽然官方的代码很详细,但是不能直接跑,而且用的方法也不能体现WASAPI的优势(居然在循环里调用Sleep来等,而且这个时间是500ms)。而我们采用的是事件驱动机制,和之前用DirectSound是一样的,好处在于其延迟可以低至10ms左右,如果用独占流配合更好的设备的话,理论上可以更低(据说Win10共享流也可以更低),这对于录音来说是极其重要的。当然我们用共享流就行了,不然其他程序的声音就没了,独占流更适合专业性强的软件。

IAudioClient有若干方法,其中大部分都会用到,具体细节可以看文档说明。

首先我们要调用的是Initialize方法,不过在调用之前可以用IsFormatSupported来确认选定的格式是否受支持。也可以调用GetDevicePeriod来查看设备支持的处理周期。还可以调用GetMixFormat来获取系统内部的混音格式直接用,不过这样的话就得自己处理重采样了,对于专业的软件来说还是需要的,毕竟早期Windows自带的重采样质量稍差(其实DirectSound就默认支持重采样了,但是WASAPI一开始不支持,关于重采样的更多内容会在后文讨论)。除了以上三个方法可以在Initialize之前调用,其他的全部要先调用Initialize才能用。

官方文档上Initialize的原型:

HRESULT Initialize(
  [in] AUDCLNT_SHAREMODE  ShareMode,
  [in] DWORD              StreamFlags,
  [in] REFERENCE_TIME     hnsBufferDuration,
  [in] REFERENCE_TIME     hnsPeriodicity,
  [in] const WAVEFORMATEX *pFormat,
  [in] LPCGUID            AudioSessionGuid
);

第一个参数是共享模式还是独占模式的选项,就AUDCLNT_SHAREMODE_EXCLUSIVE、AUDCLNT_SHAREMODE_SHARED这俩,我们用AUDCLNT_SHAREMODE_SHARED共享模式就行了
第二个参数是控制流的选项,多个参数用按位或运算叠加,我们需要AUDCLNT_STREAMFLAGS_EVENTCALLBACK实现事件驱动,还有AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM和AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY(提供更高的质量)实现自动重采样
第三个参数是缓冲区持续时间长度(单位100纳秒),对于共享模式和事件驱动同时使用的情况,填0即可
第四个参数是设置设备处理周期,独占模式才要设置,填0即可
第五个参数是音频格式,支持WAVEFORMATEX和WAVEFORMATEXTENSIBLE,从要播放的文件获取
第六个参数是AudioSession类型GUID的指针,不用Session填NULL即可

调用完以后检查一下返回值,因为那么多方法里这个出错的概率比较大,当然规范的写法是每个方法都要检测返回值以避免错误。

之后需要调用SetEventHandle来指定接收事件的句柄,调用GetBufferSize来确定系统分配的缓冲区大小(单位是音频帧个数),也可以选择调用GetStreamLatency查看系统安排的延迟时间(单位100纳秒)(不过我发现这个方法似乎不会给出一个有效的值,一直是0,可能是bug)。

现在可以调用GetService来获取一个IAudioRenderClient对象,该对象仅提供两个方法:GetBufferReleaseBuffer,用来获取和释放需要填充的音频数据缓冲区指针。

在第一次调用Start之前用IAudioRenderClient提前获取缓冲区并填充数据可以降低播放的延迟,这样就可以开始播放了。

紧接着我们就要启动一个线程来等待事件的到来然后填充数据了。

要在线程中调用GetCurrentPadding来获取缓冲区已有的数据,因为缓冲区一般比较大,而实际上每10毫秒就会需要新的数据了,此时缓冲区内还有剩余数据没有播放。

具体代码如下:

device->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, (void **)&aclient);
HRESULT hr = aclient->Initialize(AUDCLNT_SHAREMODE_SHARED,
    AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |  AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY,
    0, 0, fmtex, NULL); // fmtex是要播放的文件的
// 这里最好检测一下hr结果,然后妥善处理
aclient->GetBufferSize(&blocksize);
aclient->SetEventHandle(hevents[0]);
aclient->GetService(IID_IAudioRenderClient, (void **)&arender);
BYTE *pdata;
arender->GetBuffer(blocksize, &pdata);
fill(pdata, blockalign * blocksize); // 注意这个blockalign是文件里获取的,实际数据大小等于帧数*每帧大小
arender->ReleaseBuffer(blocksize, 0);
hthread = CreateThread(0, 0, fill_thread, this, 0, NULL);
SetThreadPriority(hthread, THREAD_PRIORITY_TIME_CRITICAL); // 提高一下线程优先级
aclient->Start();

线程代码如下:

DWORD __stdcall fill_thread(void *obj)
{
    WASPlayer *player = (WASPlayer *)obj;
    while (1) {
        DWORD r = WaitForMultipleObjects(2, player->hevents, FALSE, INFINATE);
        BYTE *pdata;
        if (r == 0) {
            UINT32 padding;
            player->aclient->GetCurrentPadding(&padding); // 获取当前缓冲区已经填充的数据大小
            UINT32 frames = player->blocksize - padding; // 计算需要填充的大小
            UINT32 bytes = frames * player->blockalign;  // 计算出需要的字节数
            player->arender->GetBuffer(frames, &pdata);
            int filled = 0; // 实际填充的大小
            if (pdata) // 一般不会为空
                filled = player->fill(pdata, bytes);
            player->arender->ReleaseBuffer(frames, 0);
            if (filled < 0) {
                printf("[debug] No buffer\n");
                break;
            }
        }
        else if (r == 1) {
            printf("[debug] Set stop\n");
            break;
        }
        else {
            printf("[debug] Unknown\n");
            break;
        }
    }
    player->stop();
    printf("[debug] Thread end\n");
    return 0;
}

以上就是一些简单的说明和示例代码了,具体可以查看本文前面的完整代码。

录音说明

本文代码适当修改就可以实现录音功能了,还可以实现环回(Loopback)录音,不过Loopback和事件驱动模式下还有一些bug,据说win10修复了但是没有具体测试过。

这里贴一下官方文档:

麦克风录制 https://learn.microsoft.com/en-us/windows/win32/coreaudio/capturing-a-stream
Loopback录制 https://learn.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording

有兴趣可以去参阅一下。

关于重采样

这个单独拎出来讲,是因为内容比较多,几句话是说不完的,有关资料可以提供一个斯坦福大学CCRMA的Julius O. Smith教授的网站,介绍了关于数字音频重采样的理论和方法等内容。

链接在此:https://ccrma.stanford.edu/~jos/resample/resample.html

简单说的话,音频方面的重采样包括采样率转换、量化方式的转换和声道数等的转换(尽管大部分时候采样率转换可能是很多人说的意思)。打个比方,系统内部的混音格式是48000Hz、32位浮点数,而我要播放的文件是48000Hz、16位定点整数,那么只需要把每一帧的数据按量化最大值(16位整数是32767)进行除法转换到-1~1范围的浮点数就行了,比较容易;但如果我要播放的文件是44100Hz、16位的标准CD格式(这也是主流的格式),那就不仅仅是转换浮点数这么简单了,涉及到采样率转换这个比较棘手的问题。

还有比如32位浮点数或者24位整数转16位整数就涉及到抖动(Dither)这个概念,这是因为16位整数的量化误差相对较大,人为加入一些小噪音可以减少量化误差带来的影响。关于抖动,可以看看这篇文章:https://www.bilibili.com/read/cv13718097/。

高采样率转低采样率会造成混叠的现象(如96kHz到44.1kHz),理想情况下高于22.05kHz的频率应该被低通滤波器过滤掉,但实际上不存在这样的滤波器,所以在这种情况下多少会有混叠,如何减少这个现象也是一个重要的因素;反过来低采样率到高采样率还有产生镜像频率的问题。

关于采样率转换算法CCRMA网站上有详细的描述,网上也有不少开源代码,当然不同厂商也有自己的独家改进算法,不同软件采样率转换质量的比较,可以看这个:http://src.infinitewave.ca/。

如果把采样率转换和量化的转换放一起,那难度就更高了,不过一般情况下,Windows内部的混音器都是采用32位浮点数的,不同的是采样率,所以对Windows来说采样率转换可能才是重点。

当然不知到什么时候开始WASAPI有了AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM和AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY这俩,一个是自动重采样,同时使用另一个可以获得高质量的采样率转换,我们姑且认定其质量应该不会太差,但实际不敢保证,因为http://src.infinitewave.ca/并没有关于WASAPI的采样率转换质量对比,只有DirectSound的。

重采样这块可以挖个大坑,什么时候能填就不知道了(非专业,没怎么学过数字信号处理😅)。

总结

WASAPI看起来麻烦,其实并不难,实现简单的功能甚至比DirectSound还方便,但是关于WASAPI的说明介绍还是太少了,导致相关资料不好找,除了官方那份庞大的文档,而没有一份简单的入门介绍。于是在研究一段时间WASAPI后写了一些代码,并写出本文,希望起到抛砖引玉的效果。文章来源地址https://www.toymoban.com/news/detail-482051.html

到了这里,关于简要介绍WASAPI播放音频的方法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端实现页面自动播放音频方法

    前提 需要页面自动播放一段报警声音,将audio标签的 autoplay 属性设置为true,发现自动播放失败,并出现如下报错: Uncaught (in promise) DOMException: play() failed because the user didn’t interact with the document first. 为什么要禁止自动播放音频与视频 网页加载完成后立即播放音频(或带有音

    2024年02月09日
    浏览(48)
  • audio音频不能自动播放的解决方法

    由于浏览器限制的原因,不允许自动播放audio音频,尝试网上的方法后也没有进展(如果有解决方法,欢迎评论~) 一、首先创建 audio 标签 二、因为在页面刷新后需要先执行动画,动画完成后才去播放音乐,所以在执行 mounted 函数时,先加载音乐源 三、在动画完成后,进

    2024年02月11日
    浏览(114)
  • yolov5简要介绍

    YOLOV5 有不同的版本,不同版本的网络结构略有差异,但大致都差不多。这里以YOLOV5s 说明。 1、网络结构: Backbone : Focus + CSPX + SPP focus 作用: 通过slice操作, 将 W、H 上的信息融入到通道上,且在下采样过程不带来信息丢失。再使用3 × 3的卷积对其进行特征提取,使得特征提

    2024年02月08日
    浏览(51)
  • Socket简要介绍

    简介 Socket作为计算机术语翻译为“套接字”,而它更常见的含义是:插座。 Socket就像一个电话插座,负责连通两端的电话,进行点对点通信,让电话可以进行通信,端口就像插座上的孔,端口不能同时被其他进程占用。而我们建立连接就像把插头插在这个插座上,创建一个

    2024年02月06日
    浏览(48)
  • 目标检测简要介绍

    目标检测是计算机视觉领域的一项重要任务,其目的是在图像或视频中自动识别和定位特定目标。本教程将介绍目标检测的基础知识和常用算法,旨在帮助读者快速掌握目标检测的核心概念和实现方法。 目标检测基础知识 图像表示和处理 目标检测的定义和分类 目标检测的评

    2024年02月01日
    浏览(43)
  • 简要介绍django框架

    Django是一个高级的Python Web框架,它鼓励快速开发和干净、实用的设计。 Django遵循MVC(模型-视图-控制器)设计模式,使得开发者能够更轻松地组织代码和实现功能。以下是Django框架的一些主要组件: 1. 模型(Model) :模型是数据的抽象表示,用于定义数据结构。在Django中,

    2024年02月05日
    浏览(43)
  • 简要介绍YOLOv5算法

    Yolov5算法是目前应用最广泛的目标检测算法之一,它基于深度学习技术,在卷积神经网络的基础上加入了特征金字塔网络和SPP结构等模块,从而实现了高精度和快速检测速度的平衡。   Yolov5算法主要分为三个部分:Backbone网络、Neck网络和Head网络。其中,Backbone网络是整个算法

    2024年02月16日
    浏览(46)
  • 机器学习:简要介绍及应用案例

    机器学习是一种人工智能(AI)的分支,它致力于研究和开发系统,使它们能够从经验中学习并自动改善。这种学习过程使机器能够适应新的数据,识别模式,做出决策和预测,而无需明确的编程。 机器学习的主要目标是通过算法和统计模型,使计算机系统能够执行特定任务

    2024年01月17日
    浏览(53)
  • C++——list的简要介绍

    详细请看(https://cplusplus.com/reference/list/list/?kw=list) 1.list是一个可以在常数范围内在任意位置,进行插入和删除的序列式容器,并且此容器可以前后双向迭代。 2.list的底层实质是一个双向链表结构,双向链表里每个元素的存放都互不相关,在节点中可以通过指针来指向前一个

    2024年02月12日
    浏览(60)
  • cv2.getAffineTransform()简要介绍

    先了解cv2.warpAffine()+cv2.getRotationMatrix2D() cv2.getAffineTransform( pts1 , pts2 ) 仿射变换,指一个向量空间进行线性变换+平移变成另外一个向量空间,它需要一个变换矩阵,而由于仿射变换较为复杂,一般很难找出这个矩阵,于是opencv提供了cv2.getAffineTransform() cv2.getAffineTransForm()通过找

    2024年02月16日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包