文章介绍wave模块的用法、生成一定频率音频的算法实现。
wave
模块提供了一个处理 wav 声音格式的便利接口, 可获取wav文件头信息, 从文件读取数据, 也可直接将bytes
格式的数据写入wav文件。
wave.open(file, mode=None)
函数接收两个参数, file
为文件名或文件对象, mode
可取"r",“rb”,“w”,“wb"四个值, 其中"r"和"rb”, "w"和"wb"效果完全相同, 如下:
>>> wave.open('音乐.wav','r')
<wave.Wave_read object at 0x0355E810>
>>> wave.open('音乐.wav','rb')
<wave.Wave_read object at 0x0358C5D0>
>>> wave.open('test.wav','w')
<wave.Wave_write object at 0x0355E810>
>>> wave.open('test.wav','wb')
<wave.Wave_write object at 0x0358C5B0>
以读模式打开的文件会返回Wave_read
对象, 写模式打开时会返回Wave_write
对象。
wave文件由许多帧组成, 每一帧长度为1或2字节。
Wave_read.getnchannels()
返回声道数量(1 为单声道,2 为立体声)
Wave_read.getsampwidth()
返回采样字节长度 (每一帧的字节长度)。
Wave_read.getframerate()
返回采样频率。
Wave_read.getnframes()
返回音频总帧数。
Wave_read.getcomptype()
和Wave_read.getcompname()
返回压缩类型。
Wave_read.readframes(n)
读取并返回以 bytes 对象表示的最多 n 帧音频。
Wave_read.tell()
返回当前文件指针位置。
Wave_read.setpos(pos)
设置文件指针到指定位置。
Wave_write.setnchannels(n)
设置声道数。
Wave_write.setsampwidth(n)
设置采样字节长度为 n。
Wave_write.setframerate(n)
设置采样频率为 n。
Wave_write.setnframes(n)
设置总帧数为 n。 如果与之后实际写入的帧数不一致此值将会被更改( 如果输出流不可查找则此更改尝试将引发错误)。
Wave_write.setcomptype(type, name)
设置压缩格式。目前只支持 NONE 即无压缩格式。
Wave_write.tell()
返回当前文件指针,其指针含义和 Wave_read.tell() 以及 Wave_read.setpos() 是一致的。
Wave_write.writeframesraw(data)
写入bytes
格式的音频数据但不更新 nframes。data
的长度必须不大于setnframes
设定的总帧数, 否则会引发错误。
Wave_write.writeframes(data)
写入bytes
格式的音频帧并确保 nframes 是正确的。 这里data
的长度可以大于setnframes
设定的总帧数。
Wave_write.close()
确保 nframes 是正确的,并在文件被 wave 打开时关闭它。 此方法会在对象收集时被调用。 如果输出流不可查找且 nframes 与实际写入的帧数不匹配时引发异常。
程序先将两段音频中的数据读入data1
和data2
中, 再将读取的数据写入result.wav
。两段音频的采样频率、采样字节长度需要一致。
import wave
sampwidth = 1
framerate = 22050
with wave.open('音乐.wav','rb') as f1:
sampwidth = f1.getsampwidth()
framerate = f1.getframerate()
nframes1=f1.getnframes()
data1=f1.readframes(nframes1)
with wave.open('音乐.wav','rb') as f2:
nframes2=f2.getnframes()
data2=f2.readframes(nframes2)
with wave.open('result.wav','wb') as fw:
fw.setnchannels(1)
fw.setsampwidth(sampwidth)
fw.setframerate(framerate)
#fw.setnframes(nframes1+nframes2)
fw.writeframesraw(data1)
fw.writeframesraw(data2)
现在开始制作自己的声音。程序生成一段频率为200Hz, 长度为1.8秒的蜂鸣声。
import wave
from winsound import PlaySound,SND_FILENAME
file = 'test.wav'
len_= 1.8 # 秒
frequency = 200
sampwidth = 1 #每一帧宽度(采样字节长度)
framerate = 22050 # 采样频率 (越大音质越好)
length = int(framerate * len_ * sampwidth)
para = [0b00000000]*(framerate//frequency//2*sampwidth)\
+[0b11111111]*(framerate//frequency//2*sampwidth) # 音频的一小段
data=bytes(para)
# 生成wav文件
with wave.open(file,'wb') as f:
f.setnchannels(1)
f.setsampwidth(sampwidth)
f.setframerate(framerate)
#f.setnframes(length)
f.writeframes(data * (length // len(data))) # 调用writeframes函数无需预先设置nframes, 但调用writeframesraw函数需要
PlaySound(file,SND_FILENAME) # 播放生成的wav
上述程序有缺点, 如para
中0b0000000
和0b11111111
的长度是整数且相同, 导致生成的声音频率不精确; 缺乏对正弦波的支持等。
这里增加了生成器, 用于生成音频帧。
import wave,math
from winsound import PlaySound,SND_FILENAME
def generate(T,total,volume,sine=False):
# T: 周期, total 总长度, 都以帧为单位
if not sine:
h = T / 2
for i in range(total):
if i % T >= h:
yield volume
else:
yield 0
else:
# 计算方法: sin 的 T = 2*pi / w
w = 2 * math.pi / T; r = volume / 2
for i in range(total):
yield int(math.sin(w * i) * r + r)
file = 'test.wav'
len_= 1.8 # 秒
frequency = 200
sampwidth = 1
framerate = 22050
sine=True
volume = 255 # 音量, 0 - 255
data = bytes(generate(framerate / frequency, int(framerate*len_),
volume,sine)) # bytes能接收0-255整数型的迭代器
with wave.open(file,'wb') as f:
f.setnchannels(1)
f.setsampwidth(sampwidth)
f.setframerate(framerate)
f.writeframes(data)
PlaySound(file,SND_FILENAME)
与Python内置的音频合成对比:
import winsound
# Beep(freq,duration),参数分别是频率和毫秒为单位的持续时间
winsound.Beep(200,1800)
发现, 前述程序很好地仿真了调用内置的Beep
函数发声。
但音质有区别, 这是采样字节长度为1(只有8位)导致的, 还需要加大采样字节长度。
最终的程序如下:
import wave,math,struct
from winsound import PlaySound,SND_FILENAME
def generate(T,total,volume,sampwidth,sine=False):
# T: 周期, total 总长度, 以帧为单位
volume = min(volume * 2**(sampwidth*8),2**(sampwidth*8) - 1)
if not sine:
h = T / 2
for i in range(total):
if i % T >= h:
yield volume
else:
yield 0
else:
w = 2 * math.pi / T; r = volume / 2
for i in range(total):
# T = 2*pi / w
yield int(math.sin(w * i) * r + r)
file = 'test.wav'
len_= 1.8 # 秒
frequency = 200
sampwidth = 2
framerate = 22050
sine=True
volume = 255
# 8位的wav文件的一帧是无符号8位整数, 而16位的一帧是有符号的整数(-32768至32767)。
if sampwidth == 1: # 8位
lst = list(generate(framerate / frequency, int(framerate*len_),
volume,sampwidth,sine))
data = bytes(lst)
elif sampwidth == 2:
data = b'' # 16位
lst = list(generate(framerate/frequency,
int(framerate*len_),
volume,sampwidth,sine))
for digit in lst:
data += struct.pack(',digit - 32768)
with wave.open(file,'wb') as f:
# --snip-- (省略)
PlaySound(file,SND_FILENAME)
使用matplotlib
库查看生成的声波:
import matplotlib.pyplot as plt
# --snip--
plt.plot(range(len(lst)),lst)
plt.show()
写在最后:
程序还可再做改进, 例如模拟各种乐器的音色, 也就是细微改变生成的声波形状。如果程序中加入共振峰, 还可实现简单的语音合成?
但是, Windows系统已经自带了语音合成, 何必再开发一个呢?
下篇: Python 调用Windows内置的语音合成,并生成wav文件