VB6使用waveOut系列API与基于FFT算法的简单音频处理
前段时间有个朋友找到我,想要了解音频播放和处理相关的知识。正好发现论坛现在缺乏这类内容,于是就有了这篇帖子。
音频基础——波形
计算机以固定的速率,依序将一连串的数值所描述的震动强度转换为对应强度的电流来让音箱或者耳机等播放设备的振动膜振动发声。这一连串的数据就叫波形数据 。任何带压缩的音频编码比如mp3、Vorbis、flac等,都 必须 转换为波形数据才能直接在硬件设备上正确播放,让你能听到其中的音频内容。
Windows下的音频播放API其实很多,现在介绍的waveOut系列API其实比较古老,但作为能直接控制底层音频设备进行波形数据的播放的API,它依然好用。顾名思义,wave是波形,out是输出。所以对应的,Windows同时还有waveIn系列API用于录音,可用于录制波形数据。
waveOut相关API的用法概述
首先你需要调用 waveOutOpen
先打开一个音频设备,然后根据打开的设备做你的操作去控制这个设备。它的第二个参数是设备号,你可以使用 waveOutGetNumDevs
判断计算机当前检测到的设备数量,然后使用 waveOutGetDevCaps
取得设备信息,再根据自己的情况来打开特定的设备去使用。
如何选择你要打开的设备呢?一个最简单的办法是打开设备号为 -1
的设备,也就是 WAVE_MAPPER
,它是默认设备,也就是你的电脑里绝大多数软件发声时使用的设备。但当你设计正经的软件给你的用户使用的时候,你其实应该提供一个菜单,允许用户选择你的输出设备。
在打开设备的时候,你需要提供一个 WAVEFORMATEX
结构体,用于描述你决定给你的设备提供何种格式的波形数据,以及你的设备应当以什么样的速率来播放这些波形数据。当你设置的数据格式不合理、你的设备不支持这种格式或者播放速率的时候,你会打不开设备,并得到错误代码。由错误代码可以判断打开设备失败的理由。
当你打开了设备,决定播放音频的时候,你需要准备一个以上的 WAVEHDR
结构体,并正确填写其中的内容。这个结构体的内容一部分由你来维护,一部分由waveOut系列API来维护。你使用这个结构体来传递你的波形数据的指针和波形数据的大小,而waveOut系列API使用这个结构体来存储当前的播放状态和队列信息。填写完结构体后,调用 waveOutPrepareHeader
将结构体设置为“已准备”的状态。
当你把若干 WAVEHDR
结构体准备好后,调用 waveOutWrite
即可立即开始播放。当你有多个已准备好的 WAVEHDR
结构体的时候,对每个结构体调用一次 waveOutWrite
可以让这些结构体进入播放的队列。播放的时候,队列一个接一个按顺序无缝连接播放。被播放完的 WAVEHDR
结构体的 dwFlags
成员的 WHDR_DONE
位被自动置1,你如果打开设备的时候提供了你的回调函数,你的回调函数会被调用,来通知你当前结构体的波形数据被播放完成。此时你既可以重新把这个结构体加入队列,使其再次被播放一次,也可以选择重新提供新的音频内容。如果你要重新提供新的音频内容,你需要先调用 waveOutUnprepareHeader
来让这个结构体变为“没准备”的状态,此时你即可删除旧的数据,重新提供一个指针来提供新的音频数据,然后用 waveOutPrepareHeader
将其设为已准备状态,再使用 waveOutWrite
将这个结构体重新加入队列。这样一来,你只需要两个 WAVEHDR
结构体就能实现流式音频播放。其中一个处于播放状态的时候,你给另一个准备接下来的波形数据,然后将其加入队列即可。你可以使用这种方法在不需要完整加载一整个WAV波形音频文件的情况下就能通过陆续读取文件内容并将其加入队列,来完成一整个音频文件的播放。
你不需要流式播放、只想播放一个简单的音效的时候,只使用一个 WAVEHDR
结构体即可,播放完后因为队列里没有更多的播放请求了,它会自动停止,除非你设置了 WHDR_BEGINLOOP
和 WHDR_ENDLOOP
用于指定循环播放。
在播放的时候,你可以调用 waveOutPause
暂停播放,调用 waveOutRestart
继续播放,调用 waveOutReset
立即终止播放。此外,你也可以调用 waveOutSetPlaybackRate
设置播放的速率和 waveOutSetVolume
改变左右声道的音量等。加快播放的速率会导致音调变高,减慢播放的速率会导致音调变低。
需要注意的是:waveOutSetPlaybackRate
和 waveOutSetPitch
虽然都是改变播放音调的API,但前者通过改变播放速率来实现,后者则是特定声卡才能支持的特殊功能,其可以改变音频数据中的人声的声调,甚至实现男声变女声、女声变男声。但并不是所有的音频设备都支持这个功能,因此如果设备不支持,waveOutSetPitch
会返回一个错误码 MMSYSERR_NOTSUPPORTED
(值为8)。
播放结束后,请一定调用 waveOutClose
来关闭设备。关闭前,记得把所有已准备状态的 WAVEHDR
通过调用 waveOutUnprepareHeader
来使其进入“没准备”的状态,然后才能关闭设备。否则你的程序可能会无法退出,任务管理器可能会杀不掉你的进程。
具体可以查看本贴提供的附件,是一套VB6编写的音频播放和处理相关的源码。请参考源码学习了解waveOut系列API的使用。
可以直接从微软官网查看它的所有API和对应的文档:
https://docs.microsoft.com/en-us/windows/win32/multimedia/waveform-functions
离散傅里叶变换(DFT)以及其优化版快速傅里叶变换(FFT)
离散傅里叶变换是一种被广泛应用的算法,它的作用是可以把波形数据转换为其频域数据。得到的频域数据用于描述区间内各个频率的正弦波以 何种相位和波幅 组合到一起,能还原为原始波形数据。如果只看波幅,你可以借此检测出一段音频中什么频率的正弦波含量多、什么频率的正弦波含量少。这个原理经常被用于各种各样的基于电磁波频率来区分信道的无线电通讯应用。在音频处理方面也被广泛使用,不少有损压缩算法就是通过将人耳不容易听到的频率的音频数据抹去,来减少音频文件的体积。
DFT算法本身非常简单:遍历每个频率值,然后对当前频率生成正弦波和余弦波,用这个正弦波和余弦波对整段波形做点乘运算,即可得当前频率值的正弦部分和余弦部分的统计值。其中余弦部分保持符号,正弦部分翻转符号,然后分别存储进一个复数的实部和虚部。最终得到的复数的数组即DFT算法的变换结果。
Type Complex_t
R As Single '实部
I As Single '虚部
End Type
'离散傅里叶变换
'N是时间,K是频率
Sub DFT(Src() As Single, Dst() As Complex_t)
Const PI As Double = 3.14159265358979
Const PI2 As Double = 6.28318530717959
Dim Num_Src As Long, N As Long
Dim Num_Dst As Long, K As Long
Num_Src = UBound(Src) + 1
Num_Dst = UBound(Dst) + 1
Dim Sum As Complex_t
Dim X As Single
For K = 0 To UBound(Dst)
For N = 0 To UBound(Src)
X = PI2 * K * N / Num_Src
Sum.R = Sum.R + Cos(X) * Src(N)
Sum.I = Sum.I - Sin(X) * Src(N)
Next
Dst(K) = Sum
Sum.R = 0
Sum.I = 0
Next
End Sub
由于需要遍历全部的频率值,并且需要在每个频率值里遍历波形,并且遍历波形的时候需要计算正弦和余弦值,DFT算法如果不经过优化,其计算速度相当缓慢,时间复杂度极高。
FFT算法应运而生,它巧妙地利用了蝶形算法对数据进行洗牌,然后将拆分为小块的数据进行DFT处理,得到的结果一样,但它极大地提升了效率,缺点是为了使用蝶形算法,你提供的原始数据的长度必须是2的N次方大小。不过你可以给原始数据尾部补零来强行应用FFT算法。
音频算法的实际应用范例
在游戏开发中,我们需要对音频数据做一些处理,来实现以下的几个常见的需求:
- 最基本的立体声效果——通过调整左右声道的响度来实现
- 回声效果——通过建立3D场景、判断回声的产生点,在其位置上设置根据音速计算的延迟播放的立体声源。
- 高音和低音部分的衰减效果——高音容易反射,低音容易衍射,或者穿过障碍物。
回到第一个需求,立体声效果并不是单纯靠调整左右声道的响度来实现的,实际上依然存在不同频率之间的响度衰减差异的关系。把人的脑袋和耳朵分别看作不同的障碍物,人的脑袋和耳朵可以阻挡、反射高音,而允许低音穿过。事实上,立体声还需要通过对高音和低音的响度调整来实现更真实的效果。
此时可以使用FFT对原始音效文件进行处理,先得到变换后的复数的数组,每个数组元素代表一个频率值的正弦波的含量和其相位。
得到数组后,我们可以使用FFT逆变换,来将其还原为音频数据。在这之前,我们复制一份FFT变换后的复数数组,得到两份数据。
对第一份数据根据频率从低到高做插值,用于使其低音到高音的响度乘算值从0线性过渡到1。而对第二份数据则根据频率从高到低做插值,使其低音到高音的响度乘算值从1线性过渡到0。然后我们对两份数据分别进行FFT逆变换,即可得到两个音频波形,其中一个存储高音部分,另一个存储低音部分,两个合起来一起播放的时候,高低音都有。在游戏音频算法里,通过控制两个音频波形的音量,来动态改变音效的高频和音频部分的含量。
源码
我设计的这个程序用于预览WAV的波形和编辑FFT衰减来试听波形处理的效果。
欢迎下载。