找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 2813|回复: 2

【VB6】使用waveOut系列API播放音频,并使用FFT算法改变频谱

[复制链接]

1109

主题

1649

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
244
威望
743 点
宅币
24180 个
贡献
46222 次
宅之契约
0 份
在线时间
2294 小时
注册时间
2014-1-26
发表于 2021-9-11 08:23:53 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

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_BEGINLOOPWHDR_ENDLOOP 用于指定循环播放。

在播放的时候,你可以调用 waveOutPause 暂停播放,调用 waveOutRestart 继续播放,调用 waveOutReset 立即终止播放。此外,你也可以调用 waveOutSetPlaybackRate 设置播放的速率和 waveOutSetVolume 改变左右声道的音量等。加快播放的速率会导致音调变高,减慢播放的速率会导致音调变低。

需要注意的是:waveOutSetPlaybackRatewaveOutSetPitch 虽然都是改变播放音调的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衰减来试听波形处理的效果。

欢迎下载。


waveOut.png

程序运行起来是下图这个样子,观察其中的DFT窗口,你可以在其上点击鼠标来创建节点,或者点选已有的节点来拖拽节点,点击右键可以删除节点。这些节点用于编辑不同频率音频的响度,编辑好了后,点“播放”应用编辑并试听音效。

WaveDFT.png

WaveDFT_bin.zip (28.18 KB, 下载次数: 8)
WaveDFT_src.zip (46.38 KB, 下载次数: 6, 售价: 10 个宅币)
回复

使用道具 举报

55

主题

271

回帖

9330

积分

用户组: 管理员

UID
77
精华
16
威望
237 点
宅币
8199 个
贡献
251 次
宅之契约
0 份
在线时间
253 小时
注册时间
2014-2-22
发表于 2021-9-16 05:10:29 | 显示全部楼层
WAV、BMP、AVI:三个上古时代的文件格式,年龄加起来应该有100岁了。
回复 赞! 1 靠! 0

使用道具 举报

1109

主题

1649

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
244
威望
743 点
宅币
24180 个
贡献
46222 次
宅之契约
0 份
在线时间
2294 小时
注册时间
2014-1-26
 楼主| 发表于 2021-9-17 19:30:27 | 显示全部楼层
美俪女神 发表于 2021-9-16 05:10
WAV、BMP、AVI:三个上古时代的文件格式,年龄加起来应该有100岁了。

可能没有
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-3-28 22:42 , Processed in 0.046657 second(s), 36 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表