用Python演奏《太阳照常升起》

用Python演奏《太阳照常升起》

  • 一、准备工作
    • 1.1 找谱子,看谱子
    • 1.2 确定频率
  • 二、创建载有一个音符的正弦信号
    • 2.1 创建正弦信号并采样
    • 2.2 包络
      • 2.2.1 一次函数,木质八音琴?
      • 2.2.2 指数函数,木鱼?八音琴?
      • 2.2.3 梯形函数
      • 2.2.4 音符添加包络
  • 三、最终代码
    • 3.1 my_note类,创建音频
    • 3.2 演奏音频,让“太阳照常升起”

基本思路:
将含有乐音的正弦信号保存在".wav"文件中,然后将其播放出来。

参考内容:
Python中的音频和数字信号处理(DSP)

一、准备工作

1.1 找谱子,看谱子

用Python演奏《太阳照常升起》_第1张图片
谱子里面有一些专业的乐理知识,问了下度娘,大概意思如下:

  • 基本的时间长度是一拍(即一个正常音符的演奏时长),一拍的具体时长可以自己确定(比如一拍1s或2s)
  • 1=F:F调
  • 音符下面加一点,低音;音符不加点,中音;音符上面加点,高音
  • 音符:音符下面加一条下划线代表时长缩小一半,加两条下划线时长缩小1/4
  • 音符—:音符后面一条横线代表时长延长1倍,两条横线时长延长2倍
  • 音符.:音符后面加一点代表延长1/2拍
  • 两音符上面一个括号:两音符相同时,代表延音,只演奏前一个,演奏时长为这两个音符时长的和
    例如,第一小节(两个竖线之间为一节):
    第一个音符:低音“la”,1拍
    第二个音符:中音“mi”,1拍
    第三个音符:中音“mi”,1拍
    第四个音符:中音“mi”,3/4拍(加一点延长半拍:3/2拍,再加下划线减半(3/2拍减半):3/4拍)
    第五个音符:中音“fa”,1/4拍

1.2 确定频率

没找到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}

二、创建载有一个音符的正弦信号

2.1 创建正弦信号并采样

确定了频率之后,便可以创建一个正弦信号以携带该音符,代码如下。

sin_signal = [np.sin(2 * np.pi * fre * x/sampling_rate) for x in range(num_samples)]

其中:

  • np.pi:圆周率π
  • fre:频率,不同的频率决定了不同的音符
  • sampling_rate:采样率,正弦信号是一个模拟信号,计算机无法存储,所以需要将模拟信号转换为数字信号。转换的方法是在正弦信号上每隔一段固定的时间取一个函数值,间隔越小,取样后的信号越接近原始信号。(采样率要达到被采样信号最大频率的两倍以上才能完整保留原始信号中的信息)
  • 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()

用Python演奏《太阳照常升起》_第2张图片
可以看到,“sin_signal”中虽然存储的是离散的数据,但是由于采样率足够大这些数据已经非常接近于正弦函数(如果仅凭肉眼观察,这就是一条正弦曲线)。

2.2 包络

虽然已经创建了一个正弦信号并对其进行了采样,但是采样后的结果并不能直接用于创建音频。因为一段音频是由若各个音符连接而成的,如果仅仅是用上面的方法生成的音符组合一段音频,那在相邻的音符之间会听到“啪”的一声杂音,这是用于相位不连续产生了高频分量。为了解决这个问题,需要用包络修正每个音符,使得音符的连接处信号幅度为0。另外,不同的包络还会产生不同的乐器演奏效果。这里,尝试了三个包络:

2.2.1 一次函数,木质八音琴?

在 [ 幅值最大值(12000),0] 之间平均取采样数(方便演示,这里设为100)个值,代码以及包络线如下:

env = np.linspace(12000, 0,100 )#包络,一次函数
X = range(100)

plt.plot(X, env)
plt.show()

用Python演奏《太阳照常升起》_第3张图片
这个包络线处理后的音符发出的声音类似于八音琴(应该是这种乐器吧)。

2.2.2 指数函数,木鱼?八音琴?

在 [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()

用Python演奏《太阳照常升起》_第4张图片
这个包络线处理后的音符发出的声音类似于木鱼或者八音琴。

2.2.3 梯形函数

这个包络线处理过的音符个人感觉效果是最好的。
这个包络线呈一个梯形,前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()

用Python演奏《太阳照常升起》_第5张图片

2.2.4 音符添加包络

为一个音符添加包络的方法很简单,只需要将采样的信号与上面的指数函数做个乘法便可。下面展示一下采样信号在添加包络前后的曲线(采样数以及包络数都取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()

用Python演奏《太阳照常升起》_第6张图片

三、最终代码

3.1 my_note类,创建音频

将上述代码进行封装,得到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)))

3.2 演奏音频,让“太阳照常升起”

下面,开始最后的工作,创建并播放音频,让“太阳照常升起”!
首先,设置采样数、采样率、幅值等参数。另外,根据之前准备好的《太阳照常升起》简谱创建一个“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()

你可能感兴趣的:(勿庸制作,python,dsp)