关于声音输出api的那些事-聊一下系统混音的动态采样率


#1

多媒体在 PC 上向来都不是个 EASY 的事情。

如何将声音输送到声卡,也是一个不 EASY 的事情。让我们来 8 一 8 声音输出 API 的那些事。

第一个出现的声音 API 是 OSS。UNIX 系统的发明,被 BSD 和 Linux 发扬广大,当然,也是 Linux 让它死翘翘的。

OSS 借用了 UNIX 里 “一切都是文件” 的概念,把声卡模拟成一个 /dev/dsp 设备,多块声卡就是 dsp0 dsp1 …

要播放声音? 打开 dsp 设备。往里面 write 数据就可以了。 设置 比特率? 用 ioctl 设置即可。

最简单的接口,也是最没用的接口。因为应用程序完全没法对声音的播放进行控制。OSS 是个阻塞的接口, write 后,要声音播放完毕才返回。显然很糟糕,因为在 write 和下一次 write 的微小时间间隔内,声音出现了不连续。 解决办法就是使用异步的 write 。 write 完了,用 ioctl 轮询,看还剩下多少数据。注意,是轮询。 轮询到还剩下不到 1ms 就播放玩了,马上再 write 一个 buffer 过去。 这就是 OSS 接口下的播放。

同 OSS 同时期出现的 Windows 上的接口就是 WaveOut 了。 同 OSS 没有啥本质区别。 WaveOutWrite 也是阻塞的 。。。。。 同直写 /dev/dsp 设备有区别么? 基本没有。

Linux 那群开发者很活跃,也是最早把 UNIX 玩成 PC OS 的。当然 OSS 这个声音接口实在是太简单了,于是他们搞了一个 ALSA 接口。

ALSA 先进了一些,有了 PLL 接口。除了可以向声卡发送数据,还可以为数据附带时钟信息。注意,可以为数据附带时钟信息。意味着你可以不用精确的等待到一个时间再送出数据,而是指定一块数据在指定的时间播放。还可以设定播放到某个 byte 的时候,产生时钟信号通知程序,好让程序能继续准备下一块 buffer 继续播放。

然后 asla 又提供了一个高级的接口, libasound.solibasound.so 可以读取 /etc/asound.conf /usr/share/alsa/alsa.conf ~/.asound.conf 配置文件,还可以加载各种插件,插件又可以通过配置文件配置。相当的高级。可以说, alsa 是为专业的音频应用准备的。

不论是 OSS 还是 ALSA , 在民用声卡上都有一个致命的缺点: 他们不支持混音。OSS 和 ALSA 都只是声卡的接口。如果声卡本身不支持硬件混音,那么程序就只能独占声卡。表现为只能有一个程序出声。

好在 alsa 有一个补救措施,那就是 libasound.so 。 alsa 不推荐直接使用 alsa 的内核接口编程,而是使用 libasound.so 的接口。 如果使用了 libasound.so 的接口,用户可以配置一个 叫 “dmix” 的插件。然后把 dmix 模拟出来的声卡作为默认声卡。 大家都输出到 dmix , dmix 再收集多个程序的声音,一起混音然后输出给硬件。

这个问题看似解决了。实际上没有解决。 受限于alsa的接口, dmix 的实现质量相当的糟糕。首先,dmix 为了混音,强制重采样。重采样导致了音质的劣化。其次, dmix 没有 C/S 结构。通过共享内存协作完成混音和共享声卡,一个程序的 dmix 出问题,会导致所有正在发声的程序崩溃。

同期, Windows 折腾出了一个 DirectX ,这个接口集里,有一个叫 DirectSound 的子集。

那么 Windows 怎么解决声卡独占问题? 答案是 Windows Audio 这个服务。OSS 是个驱动,应用程序通过 /dev/dsp 直接使用这个驱动。 alsa 也是驱动,应用程序被鼓励使用 libasound.so 间接使用这个驱动。

Windows 也有一个驱动,叫 Kernel Stream 。但是,应用程序不被鼓励使用。反而使用 DirectSound 和 WaveOut 这样的高级接口。通过 DirectSound 和 WaveOut ,将数据输出到 Windows Audio 这个服务,这个服务完成声音混音,再通过 Kernel Stream 输出给声卡。DirectSound 同时还有个独占模式,可以绕过混音,直接输出给声卡。但就多数程序而言,都需要经过系统混音。

在 Vista 及以上的系统里, M$ 又搞了一个 WASAPI 这个接口。。。并让 DirectSound 通过 WASAPI 间接出声。这直接劣化了 DS 的音质。并在API文档里宣布 DS 过时 。。。 M$ 变脸可真快啊。。。

除非直接使用 kernel stream ,否则其他 API 下,声音都经过系统混音。如果你的声音不是系统混音所使用的那个采样率,重采样是不可避免的。 在 Windows 里,声音属性里可以设置系统混音所使用的采样率。默认设置是 16bit/44100hz。也就是说,非 44100hz 的声音都被系统重采样了。

同 Windows 一样, alsa 的 dmix 插件也是默认 16bit/44100hz 的采样率,非这个采样率就被重采样后再混音。

不可避免的带来音质的劣化。

ALSA 的另一个缺点,在使用了多个声卡后(蓝牙和 HDMI 普及了啊),也开始暴露。那就是不能动态切换输出声卡。我希望我带上蓝牙耳机后,声音能从扬声器直接转移到耳机里播放。

但是 ALSA 的先天缺陷导致无法实现这个功能。播放器必须重新打开蓝牙声卡 。。。 也就是说,把切换输出的责任全部交给播放器实现。播放器可以实现,问题是所有的播放器都必须实现。另一个问题是,用户必须到各个播放器下依次调节。非常麻烦。

呆着解决ALSA固有缺陷的目的, PulseAudio 又出现了。

PulseAudio 解决办法也是提供一个混音服务。混音服务后台执行,独占 声卡的访问权限。其他程序要播放声音的,都需要将声音数据交给 pulseaudio 。由 pulseaudio 执行混音。

那么,重采样问题怎么解决呢?

pulseaudio 提供了一个 动态采样率 模式。简单说明一下工作原理

  1. A 程序播放 44100hz 的声音, 提交给 pulseaudio

  2. pulseaudio 查询到声卡支持 44100hz 采样率播放,直接输出

  3. A 程序接着播放 48000hz 声音,提交给 pulseaudio

  4. pulseaudio 查询到声卡是否支持 48000hz 采样率播放,不支持则重采样到 44100 后输出。 如果声卡支持,则 直接输出。那么,现在支持,好,设定声卡为 48000模式,然后直接输出。

  5. A 程序播放 48000 声音的同时, B 程序开始播放 44100 的声音。

  6. pulseaudio 将 B 程序的声音重采样到 48000, 然后混音输出。

  7. A 程序退出, B 程序继续播放 44100 的声音

  8. pulseaudio 将 B 程序的声音重采样到 48000, 然后混音输出。

  9. B 程序暂停或停止播放,然后重新或继续播放44100 的声音

  10. pulseaudio将设定声卡为 44100模式,然后直接输出。

  11. A 程序又播放 48000 声音

  12. pulseaudio 将 A 程序的声音重采样到 44100, 然后混音输出。

  13. 用户切换到蓝牙耳机。只支持 32000hz 采样率。

  14. pulseaudio 将 A 程序的 48000hz 重采样到 32000hz , B 的 44100 重采样到 32000hz , 混音后输出给蓝牙耳机。

同样是系统混音, pulseaudio 支持动态采样率调节。依据当前声卡支持的采样率和当前程序提交的数据,动态修改内部混音的采样率。尽量减小不必要的重采样过程

我们看看 Windows 如何解决这个问题:

因为系统混音无法避免,一群发烧声音的人开发了 ASIO , 注意,这个不是 Boost.Asio , 而是 Audio Stream IO 的缩写。 ASIO 是一个非 M$ 产的声音驱动。使用 ASIO 输出,意味着声音数据绕过了一切 M$ 产的代码,直接到达声卡(原装驱动也得卸载, lol, 禁用 windows audio 服务, lol ) 。当然,也要付出 ASIO 只能支持有限的几款声卡的代价。一些发烧友认为这样可以提升音质。。。。。。我只能说,呵呵。

因为从 Vista 开始, Windows Audio 服务也开始支持 pulseaudio 类似的 “动态采样率” 了。因为系统重采样导致的声音劣化,已经不存在了。

顺便提醒一下列位: google 这个傻逼搞的 android 不使用 pulseaudio , 自己搞了个 audiofinger , 不支持动态采样率, 还是会强制重采样到 44100hz 。 我只能说, Google 的一群傻逼。