基本思路:
将含有乐音的正弦信号保存在".wav"文件中,然后将其播放出来。
参考内容:
Python中的音频和数字信号处理(DSP)
没找到F调对应的频率,只找到了C调的频率,所以只能根据F调和C调的对应关系确定F调的频率。为了方便使用,将每个调及其对应频率做成如下字典:
note_fre = {'do_low': 349, 're_low': 392, 'mi_low': 440,'fa_low': 497, 'sol_low': 523, 'la_low': 587, 'si_low': 659,
'do': 698, 're': 784, 'mi': 880, 'fa': 988, 'sol': 1046, 'la': 1175, 'si': 1318, 'do_high': 1397}
确定了频率之后,便可以创建一个正弦信号以携带该音符,代码如下。
sin_signal = [np.sin(2 * np.pi * fre * x/sampling_rate) for x in range(num_samples)]
其中:
这里创建一个载有低音“do”( 频率349)的正弦信号然后采样并输出采样信号波形。为了方便展示效果这里采样率设为48000,采样数设置为240:
sin_signal = [np.sin(2 * np.pi * 349 * x/48000) for x in range(240)]
x = range(240)
plt.plot(x,sin_signal)
plt.show()
可以看到,“sin_signal”中虽然存储的是离散的数据,但是由于采样率足够大这些数据已经非常接近于正弦函数(如果仅凭肉眼观察,这就是一条正弦曲线)。
虽然已经创建了一个正弦信号并对其进行了采样,但是采样后的结果并不能直接用于创建音频。因为一段音频是由若各个音符连接而成的,如果仅仅是用上面的方法生成的音符组合一段音频,那在相邻的音符之间会听到“啪”的一声杂音,这是用于相位不连续产生了高频分量。为了解决这个问题,需要用包络修正每个音符,使得音符的连接处信号幅度为0。另外,不同的包络还会产生不同的乐器演奏效果。这里,尝试了三个包络:
在 [ 幅值最大值(12000),0] 之间平均取采样数(方便演示,这里设为100)个值,代码以及包络线如下:
env = np.linspace(12000, 0,100 )#包络,一次函数
X = range(100)
plt.plot(X, env)
plt.show()
这个包络线处理后的音符发出的声音类似于八音琴(应该是这种乐器吧)。
在 [e ln(12000),e0] 之间平均取采样数(方便演示,这里设为100)个值,代码以及包络线如下:
env = np.array([np.exp(x) for x in np.linspace(int(np.log(12000)), 0, 100)]) # 包络,指数函数
X = range(100)
plt.plot(X, env)
plt.show()
这个包络线处理过的音符个人感觉效果是最好的。
这个包络线呈一个梯形,前1/4由0直线上升至幅值最大值(12000),后1/4直线下降至0,中间1/2保持幅值最大值(12000)不变。这里生成100个点展示一下包络线的形状,代码及包络线如下:
env1 = np.linspace(0, 12000, 100*1//4)
env2 = np.full(100*2//4, 12000)
env3 = np.linspace(12000, 0,100*1//4)
env = np.concatenate([env1, env2, env3])
X = range(100)
plt.plot(X, env)
plt.show()
为一个音符添加包络的方法很简单,只需要将采样的信号与上面的指数函数做个乘法便可。下面展示一下采样信号在添加包络前后的曲线(采样数以及包络数都取5000):
sin_signal = [np.sin(2 * np.pi * 349 * x/48000) for x in range(5000)]
env1 = np.linspace(0, 12000, int(5000*1/4))
env2 = np.full(int(5000*2/4), 12000)
env3 = np.linspace(12000, 0, int(5000*1/4))
env = np.concatenate([env1, env2, env3])
X = range(5000)
plt.subplot(1,2,1)
plt.title("未加包络")
plt.plot(X,sin_signal)
plt.subplot(1,2,2)
plt.title("添加包络")
plt.plot(X, sin_signal*env)
plt.show()
将上述代码进行封装,得到my_note类。在实例化该类时,需传入采样数、采样率、最大幅值;generate_note函数用于创建一个音符,在调用时需传入音符以及时长;generate_audio根据传入的音符列表“notes”以及时长列表“times”创建一段音频,并将该音频写入文件“file_name”中。该代码存放在文件"my_song.py"中。
import numpy as np
import wave
import struct
class my_note:
def __init__(self, num_samples, sampling_rate, max_amplitude):
'''设置一拍采样数,采样率,最大幅值'''
self._note_fre = {'do_low': 349, 're_low': 392, 'mi_low': 440,
'fa_low': 497, 'sol_low': 523, 'la_low': 587, 'si_low': 659,
'do': 698, 're': 784, 'mi': 880, 'fa': 988, 'sol': 1046, 'la': 1175, 'si': 1318, 'do_high': 1397}
self._num_samples = num_samples
self._sampling_rate = sampling_rate
self._max_amplitude = max_amplitude
def generate_note(self, note, time):
'''创建含有一个音符的正弦信号'''
# 幅值渐变,保证两音符连接处幅值为0
# env = np.linspace(self._max_amplitude, 0, int(self._num_samples*time))#包络,一次函数
# env = np.array([np.exp(x) for x in np.linspace(int(np.log(self._max_amplitude)), 0, int(self._num_samples*time))]) # 包络,指数函数
# 包络,梯形(前1/4由0直线上升,后1/4直线下降至0,中间不变)
env1 = np.linspace(0, self._max_amplitude,
int(self._num_samples*time*1/4))
env2 = np.full(int(self._num_samples*time*2/4), self._max_amplitude)
env3 = np.linspace(self._max_amplitude, 0,
int(self._num_samples*time*1/4))
env = np.concatenate([env1, env2, env3])
# 根据音符频率及时间长度生成信号
sin_signal = [np.sin(2 * np.pi * self._note_fre[note] * x/self._sampling_rate)
for x in range(int(self._num_samples*time))]
return sin_signal*env
def generate_audio(self, file_name, notes, times):
sine_wave = np.array([])
for i in range(0, len(times)): # 创建正弦波并抽样
sine_wave = np.concatenate(
[sine_wave, self.generate_note(notes[i], times[i])])
# 写入文件
nframes = self._num_samples
comptype = "NONE"
compname = "not compressed"
nchannels = 1
sampwidth = 2
wav_file = wave.open(file_name, 'w')
wav_file.setparams((nchannels, sampwidth, int(
self._sampling_rate), nframes, comptype, compname))
for s in sine_wave: # 写入正弦波
wav_file.writeframes(struct.pack('h', int(s)))
下面,开始最后的工作,创建并播放音频,让“太阳照常升起”!
首先,设置采样数、采样率、幅值等参数。另外,根据之前准备好的《太阳照常升起》简谱创建一个“notes”列表存放音符,一个“times”列表存放音符对应的节拍长度(如果字典支持键重复的话,使用字典会更方便,至少不用担心一时大意音符和节拍长度对应不起来,但很可惜字典不支持键重复~~~)。
然后,为了节省时间,首先判断是否已经生成音频,如果生成则直接使用“pygame”播放;如果还未生成音频,则实例化一个“my_note”类并调用“generate_audio”方法生成音频然后播放。
(若想尝试不同的包络或者不同的采样率、幅值等,在重新运行代码之前一定要删除已生成的音频)
from my_song import my_note
import os
import sys
import pygame
from pygame.constants import QUIT
num_samples = 24000 # 采样数
sampling_rate = 48000.0 # 采样率
amplitude = 12000 # 最大幅值,最大值32767,需是整数
file_name = "sun.wav"
# 音符
notes = ['la_low', 'mi', 'mi', 'mi', 'fa',
'mi', 'mi', 'fa', 'sol',
'la', 'la', 'sol', 'sol',
'mi',
'la_low', 're', 're', 're', 'mi',
're', 'mi',
'sol', 'mi', 'sol', 'si_low', 'do',
'la_low',
'la_low', 'do', 'mi',
'la', 'sol', 'la', 'sol',
'la', 'sol', 'la', 'sol', 'sol',
'mi',
're', 'la', 'sol',
'mi', 're', 'mi',
're', 'la', 'fa',
'mi', 're', 'mi', 'sol', 'si', 'do',
'la_low', ]
# 音符对应的时长
times = [1, 1, 1, 3/4, 1/4,
5/2, 1/2, 1/2, 1/2,
3/2, 1/2, 1, 1,
4,
1, 1, 1, 3/4, 1/4,
2, 2,
3/4, 1/4, 2, 3/4, 1/4,
5,
1, 1, 1,
3/4, 1/4, 11/4, 1/4,
3/4, 1/4, 2, 3/4, 1/4,
5,
1, 1, 1,
3/4, 1/4, 4,
1, 1, 1,
3/4, 1/4, 1, 1, 3/4, 1/4,
4, ]
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode([600, 400])
pygame.display.set_caption("太阳照常升起")
background_img = pygame.image.load('image/image1.jpg')
screen.blit(background_img, (0, 0))
pygame.display.update()
if os.path.exists(file_name):
pass
else:
background_img = pygame.image.load('image/image2.jpg')
screen.blit(background_img, (0, 0))
pygame.display.update()
note = my_note(num_samples, sampling_rate, amplitude)
note.generate_audio(file_name, notes, times)
background_img = pygame.image.load('image/image3.jpg')
screen.blit(background_img, (0, 0))
pygame.display.update()
pygame.mixer.music.load(file_name)
pygame.mixer.music.play()
while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()