WinAMP 的多设备 ASIO 输出插件






4.80/5 (9投票s)
一个微型WinAMP输出DLL,它使用官方ASIO SDK的C++替代品,支持多个ASIO设备。
引言
如果有人想知道,从本文中提到的缩写词/技术数量来看,实际上有一个核心主题,其他所有主题都源于此,那就是ASIO(Steinberg的音频相关技术,而不是网络相关的Boost库)。
对要求苛刻的低级C++编程和高保真数字音频充满热情,摆弄ASIO似乎是测试和提高我技能的好主意。由于这不是一篇纯粹的理论文章,以下故事有一个实际的二进制产品,一个可用的WinAMP ASIO输出插件。实现这个目标,同时努力不只是重新发明轮子或重复现有解决方案,而是做得更好和/或不同,这构成了本文及其背后源代码的核心内容。
大多数库都有“问题”(无论是客观缺陷还是主观偏好)的悲惨现实,实际上可以成为像这个项目一样的学习项目中的“积极”驱动力。最主要的“缺陷”激发了这个项目以及克服它的愿望,那就是ASIO SDK的官方声明:它不支持多个(活动)ASIO驱动程序。第二个,或者更好的,一个“并行”的缺陷,是大多数API的丑陋和糟糕/过时/“典型的C风格”设计,或者另一方面,许多更现代设计的库的臃肿,这些库通常散发着“谁在乎,今天的计算机足够快”的心态。我试图测试并证明这两个极端并不是唯一可用的方法,并且代码既高效又遵循现代设计模式和习惯用法,换句话说,对于开发人员和硬件来说都相对易读和易于处理的代码是可能存在的。出于上述原因,你会看到我愉快地忽略了Knuth、Hoare和Sutter,并做了“万恶之源”。但在我们深入探讨之前,有必要先介绍一下“一点点”背景知识……
背景
简而言之,ASIO是Audio Stream Input Output的缩写,是Steinberg Media Technologies GmbH开发并公开发布的一种技术,旨在克服以前音频流API中固有的缺陷,这些缺陷阻碍了个人计算机上低延迟、采样精确的软件应用程序的创建(当然,采样精确部分对于像这个简单的播放应用程序来说更为重要)。本文假设读者已经对API及其相关术语有所了解。
故事的“有效”方面。
给我我的这个。
如引言中所述,我在SDK文档中注意到的第一件事是关于“不幸地”仅支持单个活动设备/驱动程序。仔细观察后,发现了两个罪魁祸首。第一个是缓冲区切换回调,它通常缺少“void * pUserData
”对this
指针的“模拟”,另一个是SDK本身的实现,它以典型的C语言方式使用全局变量来存储活动驱动程序COM接口的指针。
第一个问题无法直接解决,因为它属于API的二进制规范,因此必须采用变通方法。由于每个驱动程序/设备都会创建自己的线程来运行缓冲区切换回调,因此通常且显而易见的方法是使用线程本地存储来存储`this`指针(指向ASIO设备实例)。对于这种“线程本地单例”模式,使用了`TLSObject<>`类。该类仅包装TLS实现和“先设置全局再设置线程本地”使用模式的同步,并不解决何时设置TLS值的问题。如果我们放弃每次在缓冲区切换回调中检查TLS指针是否已设置并在未设置时设置它的选项,那么我们还剩下两种解决方案。一种是使用“万能”方法,即在驱动程序的DLL中钩住kernel32.dll导入的CreateThread函数。理论上,这并非完全万无一失,因为驱动程序的DLL理论上不必是专门的COM服务器,并且只托管驱动程序,因此我们可能会拦截不需要的CreateThread调用。此方法使用`ImportsHooker`类(它包装了API/导入钩子代码),并在CreateThread透传辅助函数中设置TLS值。
另一种解决方案是通过 ASIO 回调设置/传递给驱动程序的方式提供(您填充一个函数指针结构并将其传递给驱动程序)。机会窗口来自于某些驱动程序不复制此结构而只复制指向该结构的指针这一事实。这确实会使驱动程序在每次回调调用时增加一次指针解引用,并使我们/“主机”需要为整个播放期间保存该结构,但它也提供了在播放线程激活时在运行时切换/更改回调的可能性。这用于通过辅助透传回调启动播放,这些回调在首次调用时设置 TLS 值,然后将回调指针切换为指向正确的缓冲区切换函数。当然,此方法不适用于复制 ASIOCallbacks 结构的驱动程序。
在开发后期,我才注意到thunks的概念,这很可能成为下一版本中将要实现和使用的解决方案,因为它似乎既简单又高效(与原生TLS实现相比可能并非如此,但遗憾的是,在此类DLL项目中,Vista之前的Windows上无论如何都无法使用原生TLS)。
第二个问题,与ASIO SDK相关,可以通过绕过/废弃ASIO SDK对IASIO COM接口的C封装,直接使用COM接口和/或将其适当地封装在一个类中来解决。由于原始ASIO SDK中存在其他缺陷,例如相当丑陋的驱动程序列表,它只是“重新发明轮子”(即std::list),我决定完全废弃它,并尝试从头开始创建一个更好的,仅使用原始SDK中的枚举和类型定义。
WA <-> ASIO 交互
在深入了解“ASIO++ SDK”的细节和用法之前,我们应该首先了解Wasiona代码其余部分的背景。大部分核心内容来自WinAMP SDK(“又一个丑陋的C API”)和ASIO(++) SDK之间的交互。正如人们所预期的那样,两者并非自然契合,而是需要一定程度的适应。最大的问题在于WinAMP使用过时的轮询系统。它(输入插件)不断轮询输出插件(“我们”)“我能写入多少?”,然后如果`CanWrite()`回调返回的大小足够大就写入,否则就休眠一段未指定毫秒数,然后再次轮询。这当然不能与ASIO缓冲区切换回调(或任何低延迟用途)正常工作,因为它们必须在延迟毫秒数内返回,而这可能低至一毫秒,远低于输入插件在轮询和写入循环中使用的通常休眠时间。其次,WinAMP写入回调使用交错的单缓冲区格式传输样本,而ASIO API为每个音频通道使用单独的缓冲区,因此需要进行去交错处理。第三,ASIO设备/驱动程序具有固定的输出样本类型,因此还必须进行样本类型转换过程(例如16位整数到32位浮点数),“异国”采样率可能还需要重采样。所有这三个问题的通常不愉快的解决方案当然是在两者之间引入一个新的缓冲区,在WinAMP `Write()`回调中完成所有必要的转换,并将完全处理过的数据保存到中间缓冲区中,从而最大限度地减少缓冲区切换回调所需的工作量。
与所有音频应用程序一样,中间缓冲区当然需要是一个循环缓冲区。这里选择的实现(由 `MultiChannelBuffer` 类包装/提供)不使用双指针(例如 DirectSound),因为这会带来额外的 if-then 子句/代码路径,并以两次传递的方式进行复制,从而降低了使用向量/SIMD 指令处理/复制数据的潜力。相反,它依赖于使用比读取/写入块大几倍的缓冲区大小,然后在达到缓冲区末尾时,将剩余的一两个“小”块从缓冲区末尾复制/移动到缓冲区开头。这以 CPU 时间为代价使用了更多的内存,因为如果额外的 `memcpy()` 调用(每个通道)发生得足够少(总缓冲区大小远大于读取/写入块),则可以忽略不计,好处是始终拥有一个线性/“不间断”的“前置”缓冲区。因此,通过调整缓冲区大小,我们调整了内存和 CPU 使用率之间的权衡。所有通道缓冲区都使用单个物理缓冲区(`sizeof() = sizeof( channel ) * numberOfChannels`)以提高引用局部性。
然而,事实证明,这种设计(使用单个指针和“零星”的 `memcpy()` 而不是两个指针)在 WinAMP 及其可视化系统上存在问题。似乎可视化时序与输入插件到输出插件的写入流“时序”之间存在某种奇怪的耦合。在所描述的 `MultiChannelBuffer` (MCB) 设计中,此流并非以恒定速度“流动”。相反,发生的情况是输入插件相对快速地填充 MCB,然后等待(因为 MCB 拒绝任何进一步的写入),直到数据被消耗,MCB 执行“移到开头”操作,此时(几乎)整个缓冲区再次可供输入插件填充,这正是它所做的,同样在相对短的时间内完成。因此,密集的写入周期与空闲周期交替出现,这似乎“混淆”了 WA 的可视化逻辑,并导致其开始卡顿。目前,已在 `CanWrite()` 回调中添加了一个快速变通方法,如果写入数据领先播放数据超过 50 毫秒,则不允许进一步写入,这使得写入时间和输出时间值保持足够接近,从而使可视化再次流畅。
拥有多个ASIO设备意味着缓冲区将由多个读取器/消费者在不同时间以不同大小的块从不同线程读取。这需要特殊的逻辑来跟踪缓冲区的状态(已读部分、已写部分和未使用部分)并解决来自不同线程的访问问题。多读取器问题通过`MultiChannelBuffer::Reader`成员类进行建模,通过该类跟踪每个读取器的状态/位置(关系类似于Dinkumware STL安全版本中迭代器和容器的关系),以便每个读取器都可以访问其未读数据,并且缓冲区可以知道所有读取器已读取哪些数据(并且可以安全地覆盖)。
图形用户界面
WinAMP 插件中还有一个部分,它本身不直接与 ASIO 相关联,那就是(配置)GUI。在研究轻量级/高效解决方案时,我决定尝试一下 WTL。它有可能成为 MFC 应有的样子,我曾寄予厚望,但最终它确实让我有些失望。看起来“兼容性”/与 MFC 的相似性比现代设计具有更高的优先级,因此它在设计层面引入了一些丑陋之处,它仍然使用 ATL 构造而不是 STL,非封装的公共成员访问,(遗留的)指针而不是引用(以及类似的 C 风格遗留),预先声明的全局“模块对象”,并且它(当然,大多数其他库也一样)做出某些不可配置的、设计与效率之间的权衡,这些权衡不一定“对每个人都好”或像这个小型项目所必需的(范围从“手动”预创建属性表页,到即使那是开发人员的编译时知识,也总是根据窗口状态切换代码路径,再到引入 SEH、用于各种 COM、窗口、ATL 等数据的 ATL 容器代码、虚函数、关键段和自定义内存分配器)。
可以理解/正常的是,它也有一些影响这个项目的bug。例如,`AtlCreateSimpleToolBar() / CFrameWindowImplBase<>::CreateSimpleToolBarCtrl()` 函数不能正确处理垂直工具栏(它没有正确设置按钮换行状态),但最重要的问题是让 `PropertySheet`“控件”作为对话框子控件工作(之后还要让它使用 XP 风格的白色背景)。这些问题的修复以及其他 GUI/WTL 帮助程序和/或“更好”的实现可以在 GUI 模块中的函数和类中找到。我不会用细节来混淆这段文字,因为我认为代码及其用法应该从 `ConfigDlg` 模块中的接口、注释和用法中足够清楚。
音量控制
最后,至少对于更重要的问题而言,存在音量控制问题。ASIO驱动程序没有义务支持`kAsioSetOutputGain`“`futureCall()`”,因此必须使用替代方法来更改音量。Windows提供了waveOut/MME和mixerAPI接口,`waveOutSetVolume()`似乎是“默认”选择,但事实证明,一些(ASIO)声卡驱动程序不支持旧版MME API,因此这些功能对它们不起作用(即使Windows音量控制也无法正常工作)。在这种情况下使用mixerAPI控制主音量可以是“替代替代”解决方案。因此,我包含了所有三种选项(MME、mixerAPI和ASIO),用户可以选择使用哪个API来控制音量和平衡。不幸的是,mixerAPI是我不得不使用的最丑陋的API之一,我无论如何都无法让平衡功能与其配合(如果有人成功了,我将不胜感激提供帮助/输入)。最初的想法是允许为每个选定的设备独立配置音量设置方法,但事实证明,这不是一项容易完成的任务。由于ASIO设备/驱动程序的名称理论上不必与MME设备/驱动程序名称相似,并且由于ASIO设备根本不需要MME驱动程序,或者可以有多个MME驱动程序,甚至可以是同时模拟多个设备的“ASIO仿真器”(如ASIO4All),因此没有确定性的链接可以推断出ASIO驱动程序对应的MME驱动程序用于音量控制。出于上述原因,当前的次优且有望是临时解决方案是waveOut和mixerAPI方法控制(仅)默认Windows音频设备的音量。
故事的“高效”方面。
性能是整个ASIO理念的主要部分,因此在相关项目,特别是任何包装器/扩展/库代码中投入精力并“紧凑地做事”是合乎逻辑的。
在实际进行时间关键处理的代码部分中,速度优化当然是首要目标。除了代码中“微小的技巧和黑客”以及通过设计而不是同步来解决线程问题的全局努力(整个项目中没有临界区)之外,所有这些都集中在WinAMP输入-ASIO输出数据路径中。`MultiChannelBuffer`类的逻辑已经介绍过,这让我们只剩下针对不同IO样本类型组合的`Write()`回调,而这些回调归结为去交错处理。这似乎是一个学习MMX的好机会,所以我决定编写一个MMX版本的16位到32位去交错和转换函数。我搜索了一个已经存在的解决方案,但没有找到,所以我从头开始编写了我的,如果更有经验的MMX程序员能给我一些反馈,我一点也不介意 ;)
在空间优化上花费了相当多的时间(副标题中的“微小”部分指的是这个,而不是过大的配置对话框:),因为即使从一开始就使用了一些更传统的“合理化”方法(稍后将提到),第一个构建仍然显示出不令人满意的结果。
我试图从不同的角度解决这个问题。替代的“ASIO++ SDK”以及整个项目中的其他代码都依赖于模板和小型内联函数,以使编译时知识保持为编译时知识。这还有一个好处,通过将代码分组到更多更小的函数中,使代码更具可读性。正如代码(或反汇编窗口)中可能显而易见的那样,`IASIO` COM 接口周围的包装器几乎完全对优化器/内联器透明,而 SDK 的其余部分有望提供比原始 SDK 更紧凑的二进制文件以及更强大、更易于使用的源代码。
在这方面,一个重要的决定是COM智能指针类的选择。MSVC“原生”的`_com_ptr<>`实际上被证明是最糟糕的选择,因为它包含冗余的偏执检查和C++异常的使用。ATL的`CComPtr<>`稍好/“更轻”,但仍不尽人意,因此我决定编写一个定制的`COMSmartPtrWrapper`。顾名思义,它可以包装原始COM接口指针以及其他COM智能指针类,因此可以用于轻松地单一配置点来选择和测试不同的(智能)指针实现。我试图最小化臃肿的STL类的使用,即`std::string`和`std::vector`(当然没有使用流),并成功地完全删除了`std::string`,只在MCB中保留了一个`std::vector`,而这个`std::vector`在下一版本中也可能会被删除。
如前所述,当更传统的方法用尽时,结果仍然不尽人意,CRT的静态和动态链接之间的差异显示了主要原因。是时候转向Mike_V的“Tiny C Runtime Library”项目提供的替代方案了。开箱即用的产品并不完全足够,主要是因为Dinkumware的STL和ATL与微软的CRT以及项目使用的其他C++功能的耦合。为了解决这个问题,我修改了我的代码和TLibC库。TLibC不幸地是在Mike_V不知情或未经同意的情况下修改的(当我有时,我会尝试联系他,也许我们可以统一我们的努力)。
以下是TLibC所做的更改列表:
alloc.cpp
– 更改为使用Windows XP的低碎片堆,并且只调用`GetProcessHeap()`一次。file.cpp
- 重构后不再需要通过 `_init_file()` 手动初始化 STD 句柄。initterm.cpp
– 重构后不再使用动态分配的退出函数指针列表,因为它仍然使用固定大小的列表;将两个索引变量替换为单个指针。crt0t*.cpp
和libct.h
– 进行了上述两个文件更改所需的更改。math.cpp
– 添加了 `ldiv()` 函数。- `memory.cpp` – 添加了`alloca()`所需的`_resetstkoflw()`(一个朴素的实现);添加了微软汇编内存例程所需的`__sse2_available` DWORD;禁用了原始的“手动”内存例程,转而使用微软汇编优化的例程(这会稍微增加二进制文件大小,但我认为非常值得)。
newdel.cpp
– 次要代码复用重构。sprintf.cpp
– 添加了次要健全性检查和 `_snprintf` 的宽字符版本。string.cpp
– 添加了 `_String_base::_Xlen()`、`_String_base::_Xran()`、`_String_base::_Xinvarg()` 函数,这些函数是 Dinkumware 的 `std::string` 实现报告/处理错误所必需/使用的。- 添加了 `memory_s.h` 和 `string_s.h`,其中包含一些 Microsoft “安全” CRT 函数的实现。
- .vcproj - 转换为VC9.0项目,调整了优化开关,并添加了MASM支持以及Microsoft P4内存例程.obj文件和汇编源文件。
- 遍布文件
- 添加了 `__declspec(selectany)` 标识符以帮助链接器移除未使用的对象。
- 添加了警告禁用 pragma 指令。
- 添加了 #pragma function() 指令,以便在“启用内联函数”设置为“开”时进行编译。
- 进行了各种小的风格更改和重构。
在“我的故事”方面,我首先专注于移除 C++ 异常及其相关的幕后代码。我个人没有使用它们,但 STL 和 ATL 库使用了,所以仅仅尝试在没有 C++ 异常支持的情况下编译确实导致编译器发出了相当多的抱怨。然而,在深入探讨我做了什么之前,可能需要解释一下我为什么要这样做。在这个项目中,唯一可能实际抛出的(C++)异常是 STL 的 `std::bad_alloc`。这个小型 DLL 执行的少量小分配(唯一的例外是 MCB 中的“大”`std::vector`)实际失败的可能性相对较小,我认为这足以排除异常处理的臃肿,特别是因为在像这样的项目中,除了尝试显示消息框然后终止之外,遇到 `std::bad_alloc` 也无能为力。这与在空指针上发生访问冲突而崩溃,“以 -1 退出”或因未处理的异常而被 Windows 终止并没有太大区别/更好。
无论如何,当然没有标准或文档化的方法来禁用STL和ATL中的异常,但是深入挖掘源代码可以找到STL的_HAS_EXCEPTIONS宏和ATL的_ATL_NO_EXCEPTIONS宏。将它们定义为零确实可以禁用这两个库使用C++异常,但它带来了其他问题(正如大多数未文档化且可能不太完善的功能所做的那样)。我想到的两个更重要的问题是STL头文件吐出的大量警告(这相对容易修复,只需在预编译头文件中添加一些pragma指令)以及`<xstddef>`中的`_THROW`和`_RAISE`宏的非抛出版本,它们在没有异常的情况下使用,并且在它们的某个内部,导致`std::string`代码被包含在二进制文件中(分配`std::string`以在`std::bad_alloc`的情况下显示消息确实看起来“奇怪”,我必须说),这让我无法接受,于是诞生了UglyHack#1:一个`disableDinkumwareSTLExceptions.hpp`头文件,它将上述宏重新定义为执行简单的`exit( -1 )`,并强制包含到每个编译单元中,从而有效地解决了问题。这摆脱了C++异常,但SEH仍然存在,ATL和`alloca()`是罪魁祸首。摆脱`alloca()`相对容易,但ATL没有提供“无SEH”构建,修改ATL源代码确实不尽如人意。但一切并未丢失,因为仔细观察可以发现,atlbase.h头文件中的SEH代码仅用于处理在Windows XP之前的版本上锁定关键区域时可能发生的结构化异常,这对于该项目来说是完全无用的,因为它无论如何都不支持这些Windows版本。这催生了一系列新的UglyHack,它们被归类在一个名为`TINY_ATL`的新宏下(其详细信息可以在WTL模块中找到)。当它被定义时,许多事情都会改变
- 所有涉及SEH的代码以及 `CriticalSection`(对于只有一个窗口且仅从单个线程访问的项目来说,这是另一个完全无用的“功能”)都使用典型的预处理器技巧从构建中删除,
- ATL 静态库已从构建中排除,并且 WTL.cpp 中定义了其所需的最小导出:`CAtlBaseModule` 的构造函数和析构函数,以及 `_stdcallthunk` 的自定义分配器和释放器。此外,`CAtlBaseModule`、`CAtlComModule` 和 `CAtlWinModule` 全局变量使用 `__declspec(selectany)` 标识符定义,以使链接器能够在未使用时将其删除,
- `CAppModule _Module` 全局变量未(自动)定义,因为它单独会使二进制文件增加 10 kB。
我知道有很多人在经历这一切之后,额头上可能会写着“你疯了吗?”,但在我看来,一个从96 KB开始,最终达到28.5 KB的DLL(并且仍有改进空间),说明我没有疯,而是微软的CRT臃肿且做得不对/耦合度过高/过于单一(我怀疑如果静态版本用/GL和/LTCG构建会有所帮助)。
另一方面,那些不介意亲自动手的人可以查看 .vcproj 中列出的发布构建的预处理器宏列表,以寻找其他一些“隐藏的宝藏”。
使用代码
要求
在使用/构建项目之前,您必须满足一些要求。您将需要:
- Windows XP Service Pack 2 或更高版本。
- Microsoft VisualC++ 9.0 SP1,或者至少是“SP0”+“Feature pack”。由于代码使用了TR1功能,任何低于此版本都将无法工作。此外,您还需要安装CRT源代码和ATL。
- ASIO SDK 2.2
- WinAMP SDK
- WTL 8.0
- 修改后的 TLibC (已包含)。
遗憾的是,还有两个问题您必须手动解决:
- 由于 Microsoft Macro Assembler 在其命令行解析中存在一个 bug,即不支持“包含路径”(/I) 参数中带有空格的路径,因此我无法使用 VC 宏(即 $(VCInstallDir)crt\src...)来指定 Microsoft CRT .asm 文件所需的额外包含目录,以使其在每个人的计算机上都能开箱即用(因为 VC 宏包含带有空格的长文件名)。在此问题解决之前,您将不幸需要手动编辑 TLibC(子)项目的属性,并在 Microsoft Macro Assembler/高级/包含路径下插入您的 MSVC CRT 源目录的短路径(例如 c:\progra~1\sys\vs\vc\crt\src)。
- 由于 Visual Studio 解决方案文件存储了包含/引用项目文件的相对路径,我无法解耦“out_wasiona”和“tlibc”项目的位置。目前,tlibc 项目相对于 WASIO.sln 文件位于“..\3rdParty\TLibC”,因此您必须手动修复此问题以适应您的个人源代码树和目录结构。再次,如果有人知道更智能的解决方案(在 Visual Studio 构建系统内),我将不胜感激。
最后,您需要将所使用的SDK的路径添加到Visual Studio的包含路径中。
代码
下载 Wasiona_source.ZIP - 263.92 KB
就 ASIO 而言,作为这个项目的核心可重用部分,所有类目前都在 `ASIODevices.hpp` 头文件中。
列表中的第一个是前面提到并解释过的 `IASIOPtr` 类型定义。另一个值得一提的是“`NativeIASIOPtr`”类型定义。它的声明对于经验丰富的 COM 程序员来说可能看起来很奇怪,但那是我创建 `_com_ptr<>` 类类型定义的唯一方法,因为 Steinberg 在他们的 COM 规范中把事情搞得一团糟。第一个错误(也许不那么重要)是方法不返回 `HRESULT`,第二个是驱动程序的 CLSID 也用作 IID,从而使得(正确)独立于驱动程序的智能指针类型定义成为不可能。说到这里,我还必须抱怨 ASIO SDK 中的整个 COM 业务。由于它旨在实现跨平台兼容性,我必须说我不明白他们为什么只为 Wintel 平台选择 COM(一个简单的 DLL 接口规范就足够了),但更令人烦恼的是他们完全错过了 COM、接口和面向对象设计的重点。最糟糕的例子可能是 `IASIO::future()` 方法(它应该作为 API 的扩展点,使用“80年代 C 风格”的模式发送消息和 void 指针并在 switch case 中解析它们,而 COM 开箱即用提供了通过新方法扩展接口的功能)以及最终将其全部封装在一个 C API 中这一事实。
接下来是 `ASIODriverList` 类,它是 `SubKeys` 类的一个薄包装器,而 `SubKeys` 类又是一个 Windows 注册表(子)键的包装器,它为其子键提供了类似 STL 随机访问容器的接口(与 ASIO SDK 中的替代方案不同,它不进行分配,也不预加载子键)。`ASIODriverList` 类只自动选择/打开 ASIO 注册表键,并提供一个新的实用成员函数。
与之配合使用的是 `ASIODriverData` 类,它可以根据您从 `ASIODriverList` 找到的可用驱动程序中选择的驱动程序(名称)来构建。除了提供特定 ASIO 驱动程序的可用信息外,它还提供一个用于创建所描述驱动程序实例的实用成员。
你可能不会使用 `ASIODriverData::createInstance()` 返回的简单 `IASIOPtr`,而是会使用下一个 `ASIODriver` 类,它试图提供更清晰的 C++ 接口以及方便的辅助函数和实用方法,同时跟踪驱动程序的状态(并确保你正确使用它)。
最后,为了实际使用您的ASIO驱动程序进行播放、录制和流媒体传输,您需要使用“最胖”的`ASIODevice`类,它通过ASIO缓冲区、ASIO回调和TLS功能扩展了`ASIODriver`。它还提供了`supportsCallbackSwitching()`成员函数,您可以使用它来查询驱动程序是否支持本文前面提到的回调切换技巧。如果支持,您可以使用`startUsingCallbackSwitching()`来启动您的设备;如果不支持,您必须首先(一次)调用`hookDriverCreateThread()`,然后调用`start()`。
附:值得一提的是,代码演变的不同阶段遗留了一些未使用的各种(成员)函数重载,它们尚未被移除,因为它们可能再次变得有用,或者可以简单地视为“库导出”。
用法
加载和启动设备的简单场景大概是这样的(为简洁起见,省略了大部分错误处理)
// Load driver list. ASIODriverList const drivers; if ( drivers.empty() ) { // no ASIO drivers found, handle/notify... }
// Choose the first driver (or a different one by index). ASIODriverData const driverData( drivers[ 0 ] ); // Or find one by name. ASIODriverData const driverData( drivers.getDriverData( “my driver” ) ); // Or search for it in a loop. for( ... drivers.begin() ... drivers.end() ... ) { ... }
// After you have selected your driver you create an instance wrapped // in one of the three flavours, depending on your needs: IASIOPtr pIASIO( driverData.createInstance() ); // ...or... ASIODriver asioDriver( driverData.createInstance() ); // ...or... ASIODevice asioDevice ( ...pointers to your callback functions..., driverData.createInstance(), parentWindowHWND );
// For most uses you will probably be able to use the utility setOutputFormat() // member function that will also automatically create the buffers of the // default/driver prefered size. asioDevice.setOutputFormat( desiredOutputSampleRate, desiredNumberOfOutputChannels ); if ( asioDevice.supportsCallbackSwitching() ) asioDevice.startUsingCallbackSwitching(); else { asioDevice.hookDriverCreateThread(driverData.path().c_str() ); asioDevice.start(); }
附:WinAMP 代码也被组织并拆分为类和模块,以便在其他项目中重用,但我认为其可重用部分相对简单,并且仅从代码中就能理解,因此我将不再延长文章。
兴趣点、愿望和待办事项
该项目当然还有很多工作要做才能成熟。除了代码中可以找到的各种待办事项,需要处理的主要领域大致可归类如下:
- 代码清理
- 查找所有剩余的未封装的公共成员访问
- 清理代码和文件命名约定中剩余的不一致之处(大写字母、成员变量名等)
- 项目应该拆分为不同的库,以便正确(重)用以及更轻松的开发和更新
- 目前,该项目与MSVC编译器及其特性联系相当紧密,如果能最小化甚至消除这种联系(当然没有副作用)会很好。
- 利用Boost库(例如BOOST_STATIC_ASSERTs、侵入式容器等)来改进代码
- 添加所有剩余和缺失的 `Write()` 回调和去交错函数,以支持“所有”IO 样本类型组合(目前仅支持 16 位和 24 位输入样本分辨率以及 ASIOSTInt32LSB 输出样本类型),所有基础工作已经完成,为附加 IO 样本类型组合添加支持仅需为 `WriterImpl
::Write()` 成员函数编写一个新的特化,而模板魔法将自动处理其余部分并使用新函数 - 尝试找到一种方法来实现每设备音量控制方法
- 在ASIO++ SDK中通过异常添加错误报告,因为它可能更适合大型项目;添加一个编译时开关来选择错误处理和报告的级别和机制。
- 添加编译时开关(以及所需的对代码的支持和更改),以选择单设备和多设备构建(目前,除了明显的空间开销外,在使用单个设备时支持多个设备的成本几乎降至TLS访问,因为代码会自动将WA Write()回调切换为直接访问该设备,而不是遍历所有设备),这对于ASIO++ SDK尤其需要,至少在添加thunking功能之前是这样。
- 添加重采样支持
- 添加对任意数量通道文件播放的支持,将它们映射到不同的物理输出通道,扩展和下混
- 增加设备同步和时钟源使用的支持(目前,在多设备场景中,只有在最小延迟值下才能获得可接受的体验),由于只有一个“真正的”ASIO声卡设备(Terratec火线Aureon),并通过ASIO4ALL和板载Realtek芯片模拟另一个设备,我缺乏开发和测试此功能所需的硬件支持
- 添加交叉淡入淡出功能(类似SQRSoft的插件,但最好不需要分配巨大的缓冲区)
当然,欢迎所有反馈。
历史
2009-02-10 首次公开发布并提交到 CodeProject。