创建一个正弦波
学习如何创建正弦波并将其转换为音频文件。
在本节课中,我们将学习如何生成一个正弦波并将其保存为 wav 文件。在此之前,先回顾一些基本理论。
一些定义
频率:频率是指正弦波每秒重复的次数。在本课中,我们将使用 1KHz 的频率。
采样率:大多数现实世界的信号是模拟信号,而计算机是数字设备。因此我们需要一个转换器将模拟信号转换为数字表示。关键点在于采样率,即转换器每秒对模拟信号进行采样的次数。
虽然我们现在完全在数字环境中操作,采样率的重要性相对降低,但它对于正弦波公式仍然必不可少。我们将使用 48000 这个值,这是专业音频设备常用的采样率。
正弦波公式:
是我们要计算的 x 轴样本对应的 y 轴值。A 是振幅。我们稍后再讨论这一点。 是常数 。f 是频率。t 是我们的样本。由于需要将其转换为数字形式,我们将 t 除以采样率。
振幅:通常人们会随机选择一个 A 值,比如 11。但这对我们不适用。我们生成的正弦波是浮点数,虽然这足以绘制图形,但在写入文件时无法正常工作。这是因为我们处理的是整数。查看 WAV 文件格式,它们是以 16 位短整型(short integer)存储的。如果写入浮点数,将无法正确表示。
为解决这个问题,我们必须将浮点数转换为定点数。可以通过乘以一个固定常数来实现。如何确定这个常数?一个有符号 16 位数的最大值是
(即 )。
最高位用于表示符号,剩下 15 位。我们把 2 的 15 次方减去 1,因为计算机从 0 开始计数。
如果我们希望达到满量程音频,应乘以 32767。但此时,要使音量达到满量程的一半,就需要使用 16000 的振幅。
绘制音频信号
我们已经设置好了所声明的变量。
在下方代码框“绘制频率波形”中,请注意第 18 行的高亮代码。
我们可以用普通的 for 循环来编写这段代码,但我们想展示列表推导式的强大功能,因为这是 Python 最强大的特性之一。
如果理解得当,下面这行代码其实很简单。让我们逐步拆解一下。最好同时打开源代码以便对照。
1)第一点是公式被放在 [] 中,这意味着最终结果会被转换成一个列表。
1[ for x in range(num_samples)]
2)range() 函数生成从 0 到 num_samples 的一系列数字。本质上,我们将遍历所有样本数量,用变量 x 从 0 到 48000。
1[np.sin(2 * np.pi * frequency * x/sampling_rate)]
3)这表示对每个生成的 x 值,都代入正弦波公式进行计算。
12 * pi * f * t
所以再看一遍第 18 行代码:
它表示在 0 到 num_samples 的范围内生成 x,对每个 x 值计算其正弦值。我们可以把这个值看作 y 轴的数值。所有这些值都会被放入一个列表中。
现在我们可以绘制出这个正弦波了。
1import numpy as np 2import wave 3import struct 4import matplotlib.pyplot as plt 5 6 7def create_wav(): 8 # 频率是波形每秒重复的次数 9 frequency = 1000 10 11 num_samples = 48000 12 13 # 模数转换器的采样率 14 sampling_rate = 48000.0 15 16 amplitude = 16000 17 18 sine_wave = [np.sin(2 * np.pi * frequency * x/sampling_rate) for x in range(num_samples)] 19 20 # 绘制图形 21 x = range(num_samples) 22 plt.plot(x[:300], sine_wave[:300]) 23 plt.show() 24 25 26 27if __name__ == "__main__": 28 create_wav() 29 plt.savefig('output/plot.png')
创建 wav 文件
现在我们已经看到正弦波以图形方式呈现,接下来就是将其写入文件。
为此,我们将使用 Python 内置的 wave 库。首先设置参数。
1)nframes 是帧数或样本数。
2)comptype 与 compname 表示数据未压缩。
3)nchannels 是声道数,这里为 1。
4)sampwidth 是每个样本的字节宽度。
如前所述,WAV 文件通常是 16 位,即每个样本占 2 字节。
1wav_file = wave.open(file, 'w') 2 3wav_file.setparams((nchannels, sampwidth, int(sampling_rate), nframes, comptype, compname))
我们打开文件并设置参数。
1for s in sine_wave: 2 wav_file.writeframes(struct.pack('h', int(s * amplitude)))
这部分可能需要进一步解释。我们正在逐个样本地写入 sine_wave。writeframes 是用于写入正弦波数据的函数。
以上内容都很简单,接下来的代码则更复杂,需要更加专注!
1struct.pack('h', int(s * amplitude))
我们来分步解析:
1int(s * amplitude)
这里的 s 是我们要写入的 sine_wave 的单个样本。我们在此处将其乘以振幅(用于转换为定点数)。虽然可以提前完成,但这一操作在写入文件时最为关键。
目前我们拥有的只是一组数字列表。如果直接写入文件,音频播放器将无法读取。
Struct是 Python 的一个库,用于将我们的数据打包为二进制格式。
代码中的 h 表示 16 位整数。下面我们通过 IPython 示例来理解打包的作用。
以下以 0.5 为例。我们计算 0.5 的正弦值,并通过乘以 16000 转换为定点数。如果我们直接写入文件,只会写入字符串 "7664",这是错误的。所以我们来看看 struct 的作用:
1struct.pack('h', 7664)
在下方可运行代码组件中运行代码并观察结果。
\x 符号表示该数值为十六进制。可以看到,struct 已将数字 7664 转换为两个十六进制值:0xf0 和 0x1d。
为什么使用 0xf0 0x1d?
因为将 7664 转换为十六进制正好是 0xf01d。
为什么是两个值?
因为我们使用的是 16 位整数,无法用一个字节容纳,因此 struct 将其拆分为两个部分。
现在数值已转为十六进制,其他程序(包括我们的音频播放器)都可以正确读取。
回到我们的代码:
请查看下方代码块中的第 31–32 行。
这将把我们的正弦波样本写入文件 test.wav,并以 16 位音频格式打包。
运行以下代码,查看输出结果。
1import numpy as np 2import wave 3import struct 4import matplotlib.pyplot as plt 5 6 7def create_wav(): 8 # 频率是波形每秒重复的次数 9 frequency = 1000 10 11 num_samples = 48000 12 13 # 模数转换器的采样率 14 sampling_rate = 48000.0 15 16 amplitude = 16000 17 file = "output/test.wav" 18 19 sine_wave = [np.sin(2 * np.pi * frequency * x/sampling_rate) for x in range(num_samples)] 20 21 22 nframes = num_samples 23 comptype = "NONE" 24 compname = "not compressed" 25 nchannels = 1 26 sampwidth = 2 27 28 wav_file = wave.open(file, 'w') 29 wav_file.setparams((nchannels, sampwidth, int(sampling_rate), nframes, comptype, compname)) 30 31 for s in sine_wave: 32 wav_file.writeframes(struct.pack('h', int(s * amplitude))) 33 34 wav_file.close() 35 36if __name__ == "__main__": 37 create_wav()
运行后,output 目录下会生成 test.wav 文件。
使用任意音频播放器(例如 Windows Media Player、VLC 等)播放该文件,应该能听到一段短暂的音调。
使用 Audacity
现在我们需要验证音调的频率是否正确。我们将使用 Audacity,一款开源音频编辑软件。Audacity 的一个有趣功能是可以分析音频文件的频率。
打开文件后,你会看到如下图像。
当音频文件在 Audacity 中打开时,可能需要多次放大才能看清频率波形。有时甚至需要放大两次才能完整显示图形。
我们看到的是一个正弦波。
注意:波形最高点约为 0.5,而最大值为 1.0。记住,我们已将
sine_wave乘以 16000,这对应于满量程的频率图。
现在来查找频率。
可以使用“频谱图”功能查看当前图形。
可以看到峰值出现在约 1000 Hz 处,这正是我们创建 wav 文件时设定的频率。