正文

填一下之前挖的坑,这回就说说怎么用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后写了一些代码,并写出本文,希望起到抛砖引玉的效果。

简要介绍WASAPI播放音频的方法的更多相关文章

  1. iOS 播放音频的几种方法

    Phone OS 主要提供以下了几种播放音频的方法: System Sound Services AVAudioPlayer 类 Audio Queue Services OpenAL 1. Syst ...

  2. FFmpeg 入门(3):播放音频

    本文转自:FFmpeg 入门(3):播放音频 | www.samirchen.com 音频 SDL 提供了播放音频的方法.SDL_OpenAudio 函数用来让设备播放音频,它需要我们传入一个包含了所 ...

  3. 【转】Android播放音频MediaPlayer的几种方式介绍

    接下来笔者介绍一下Android中播放音频的几种方式,android.media包下面包含了Android开发中媒体类,当然笔者不会依次去介绍,下面介绍几个音频播放中常用的类: 1.使用MediaPl ...

  4. 【Android】播放音频的几种方式介绍

    接下来笔者介绍一下Android中播放音频的几种方式,android.media包下面包含了Android开发中媒体类,当然笔者不会依次去介绍,下面介绍几个音频播放中常用的类: 1.使用MediaPl ...

  5. 使用WindowsAPI实现播放PCM音频的方法

    这篇文章主要介绍了使用WindowsAPI实现播放PCM音频的方法,很实用的一个功能,需要的朋友可以参考下 本文介绍了使用WindowsAPI实现播放PCM音频的方法,同前面一篇使用WindowsAP ...

  6. SDL开发笔记(二):音频基础介绍、使用SDL播放音频

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  7. 简要介绍BASE64、MD5、SHA、HMAC几种方法。

    加密解密,曾经是我一个毕业设计的重要组件.在工作了多年以后回想当时那个加密.解密算法,实在是太单纯了.     言归正传,这里我们主要描述Java已经实现的一些加密解密算法,最后介绍数字证书.     ...

  8. MFC中使用SDL播放音频没有声音的解决方法

    本文所说的音频是指的纯音频,不包含视频的那种. 在控制台中使用SDL播放音频,一般情况下不会有问题. 但是在MFC中使用SDL播放音频的时候,会出现没有声音的情况.经过长时间探索,没有找到特别好的解决 ...

  9. Windows基础-使用XAudio2播放音频(本质是WASAPI)

    对于常见的音频播放,使用XAudio2足够了. 时间是把杀猪刀,滑稽的是我成了猪 早在Windows Vista中,M$推出了新的音频架构UAA,其中的CoreAudio接替了DSound.WaveX ...

  10. 小程序安卓端播放不了音频解决方法wx.createInnerAudioContext()

    在小程序播放音频时,使用组件wx.createInnerAudioContext(),安卓端无法播放音频. 我的情况:播放服务器上传来的音频,格式为mp3.首先查看你的格式是否符合文档要求 在安卓端进 ...

随机推荐

  1. npm卸载"Tracker idealTree already exists"

    问题 使用npm卸载babel插件的时候执行命令npm uninstall babel...出现如下报错 npm ERR! Tracker "idealTree" already ...

  2. 【Advanced Installer】打包winfrom程序出现您没有任何数字签名的实用程序。请安装平台 SDK。错误

    出现这个问题的原因是设置了磁铁,此功能只会在win8.1上有效.也就是开始菜单里面的磁铁图 只需要把这个删除掉就可以解决了

  3. 自动注册实体类到EntityFramework Core上下文,并适配ABP及ABP VNext

    继上篇文章(EF Core懒人小技巧之拒绝DbSet)之后,最近笔者把这个小功能单独封装成一个扩展方法并开源,欢迎交流和Star~ GitHub: EntityFrameworkCore.Extens ...

  4. Google Chrome(谷歌浏览器)安装使用

    谷歌浏览器官网https://www.google.cn/chrome/ Chrome是由Google开发的一款简单便捷的网页浏览工具.谷歌浏览器(Google Chrome)可以提帮助你快速.安全的 ...

  5. SQL注入问题、视图、触发器、事物、存储过程、函数、流程控制、索引相关概念、索引数据结构、慢查询优化、

    目录 SQL注入问题 视图 触发器 事物 存储过程 函数 流程控制 索引相关概念 索引数据结构 慢查询优化 测试装备 联合索引 全文检索 插入数据 更新数据 删除数据 主键 外键 重命名表 事物 安全 ...

  6. 解决RockyLinux和Centos Stream 9中firefox无法播放HTML视频问题

    如题在测试两种centos后续系统时,发现firefox无法播放HTML视频问题.经过一番折腾找到了解决的办法,具体解决如下: 首先下载VLC $sudo yum install vlc 而后重启浏览 ...

  7. 终于定制出顺手的Obsidian斜杠命令

    wolai.语雀.思源笔记等笔记软件,有一个特别好用的功能,通过斜杠打开快速输入面板,让我们快速输入markdown.插入图片外链.插入文件.插入iframe等,十分方便. 但当我使用obsidian ...

  8. [OpenCV实战]36 使用OpenCV在视频中实现简单背景估计

    目录 1 时间中值滤波 2 使用中值进行背景估计 3 帧差分 4 总结和代码 5 参考 许多计算机视觉应用中,硬件配置往往较低.在这种情况下,我们必须使用简单而有效的技术.在这篇文章中,我们将介绍一种 ...

  9. 使用Jiralert实现AlertManager告警对接Jira

    简介 Alertmanager 处理由客户端应用程序(如 Prometheus server)发送的警报.它负责去重(deduplicating),分组(grouping),并将它们路由(routin ...

  10. 【Redis 技术探索】「数据迁移实战」手把手教你如何实现在线 + 离线模式进行迁移Redis数据实战指南(离线同步数据)

    离线迁移 与在线迁移相比,离线迁移适宜于源实例与目标实例的网络无法连通的场景,或者源端实例部署在其他云厂商Redis服务中,无法实现在线迁移. 存在的问题 由于生产环境的各种原因,我们需要对现有服务器 ...