用Python演奏《国际歌》

一、背景与需求

前一段时间在B站看到一个视频,up主用matlab演奏出了《小星星》这首歌1。当时我就在想,python能不能做到这一点呢?《小星星》的谱子太简单了,我决定尝试演奏《国际歌》

二、必备知识

2.1 Python生成音乐的原理

想要生成一段音乐,首先要了解声音产生的原理:

声音是由物体振动产生的声波。是通过介质(空气或固体、液体)传播并能被人或动物听觉器官所感知的波动现象。最初发出振动(震动)的物体叫声源。声音以波的形式振动(震动)传播。声音是声波通过任何介质传播形成的运动。
声音是一种波。可以被人耳识别的声(频率在20 Hz~20000 Hz之间),我们称之为声音。

想要用Python生成一段音乐,道理其实非常简单。只要我们想办法生成这段音乐的波形数据,再将其写入文件保存,就可以播放了。

怎样生成一段声波呢?这可以通过Numpy库实现。

下面的代码用于生成一段正弦波并可视化:

import numpy as np 
import matplotlib.pyplot as plt

# 生成一条正弦波
x = np.linspace(0, 2*np.pi, 8192)
y = np.sin(x)
#可视化
plt.plot(x,y) 
plt.show()

可视化结果如图所示:
正弦波

当然,如果我们把这样一段正弦波写入文件并播放,是不会听到任何声音的。这是因为人耳可以听到的声音的频率范围在20到2万赫兹(Hz)之间,而上面这条正弦波的频率为1Hz,属于人耳听不到的次声波。

声波
接下来这段代码用于生成一条频率为440Hz的正弦波并可视化:

import numpy as np 
import matplotlib.pyplot as plt

frequency = 440  
x = np.linspace(0, 2*np.pi, 8192) 
y = np.sin(frequency * x)
plt.plot(x,y) 
plt.show()

结果如下:
440Hz
由于频率太大,生成的波周期太短,水平方向挤在了一起。我们不妨将图片放大看一看:
440Hz放大
如果将这条正弦曲线写入文件并播放,我们就可以听到声音了。这条440Hz的波对应的正是中央A2,也就是我们常说的la。

2.2 十二平均律

十二平均律3
(英语:Equal temperament),又称十二等程律,音乐律式的一种,也是当今最主流的律式。将一个八度平均分成十二等份,每等分称为半音,音高八度音指的是频率乘上二倍。八度音的频率分为十二等分,即是分为十二项的等比数列,也就是每个音的频率为前一个音的2的12次方根:

十二平均律
根据此表,我们可以计算出各个音符所对应的频率,从而生成对应的波。

2.3 简单乐理知识

想要用Python演奏国际歌,首先要对琴谱进行编码,将五线谱输入计算机中。这要求我们必须对乐理知识具备一定的了解。这里不作详细介绍,可参考 乐理知识以及musicXml属性介绍 这篇文章。

三、实现

3.1 对琴谱进行编码

我手里现有的国际歌琴谱是五线谱形式的。
国际歌五线谱
然而,计算机可不识谱。为了让它能理解琴谱的内容,我们必须首先对琴谱进行编码。这里我自行制定了一种编码格式,并手工对其进行了编码:
国际歌编码
其中每个section表示一个小节,每个音符用一个3维向量表示。如[0,1,3]表示一个持续3拍的中央C的do.

3.2 Music类

Music类用来实例化一首歌。其包含以下成员:

主要变量

  • name : 一个字符串,歌曲名称
  • staff : 一个Staff对象,歌曲的五线谱。
  • wave : np.array()数组,五线谱对应的波形。
  • Fs : 采样率
  • converter : 一个Converter对象,用来实现五线谱到波形的转换。

主要函数

  • load_staff(self,file_name) : 读取编码后的五线谱。
  • update_music(self) : 根据读取的五线谱更新波形。
  • save_music(self,file_name,file_type="wav") : 保存音频文件。
  • draw_wave(self) : 画出波形图。
class Music():
    def __init__(self, name=""):
        """
            Init an instance of MyMusic.

            Parameters
            ----------
                name : str, [optional]
                    Name of the music. Must be str.
        """
        #Judge the type of input.
        assert name is None or isinstance(name, str)

        self.__name = name
        self.__staff = None
        self.__wave = np.array([])
        self.__Fs = 8192
        self.__converter = Converter(self.__Fs)

    def load_staff(self,file_name):
        """
        Load the staff from a file.

        Parameters
        ----------
            file_name : str
                The file to read from.

        Returns
        -------
            state : bool
                Whether loaded the data successfully.
        """
        #Judge the input.
        assert isinstance(file_name, str), "file_name must be a string"
        assert len(file_name.split(".")) == 2, "Input error. \neg.'start.txt'"

        self.__staff = Staff(file_name)
        self.update_music()

    def save_staff(self,file_name):
        """
        Save the staff to a file.

        Parameters
        ----------
            file_name : str
                The file to save to.

        Returns
        -------
            state: bool
                Whether saved the data successfully.
        """

    def update_music(self):
        """
        Update the music(wave) by self.__staff.
        """
        self.__wave = self.__converter.gen_music(self.__staff)

    def save_music(self,file_name,file_type="wav"):
        """
        Save the music as an audio.

        Parameters
        ----------
            file_name : str
                The file to save to.
            file_type : str
                Type of the file to save to. Defaults to 'wav'
                and it means save the audio as "{file_name}.wav".

        Returns
        -------
            state : bool
                Whether successfully saved the music as an audio.
        """
        path = file_name.split(".")[0]+"."+file_type

        sf.write(path, self.__wave, self.__Fs, 'PCM_24')

    def play(self):
        """
        Play the music.
        """

    def draw_wave(self):
        """
        Draw the waveform according to self.__wave.
        """
        n = len(self.__wave)     # Number of samples.
        t = n/self.__Fs          # Range of t is [0,t]
        x = np.linspace(0, t, n)
        plt.plot(x,self.__wave)  # Draw the wave.
        plt.show()

3.3 Staff类

Staff类用于表示琴谱

主要变量

  • rythm : 琴谱的节奏。
  • loop : 储存琴谱循环信息。

主要函数

  • read(self) : 读取编码后的五线谱。
class Staff():

    def __init__(self, file_name):
        """
        Init an instance of a Staff.

        Parameters
        ----------
            f_name : str
                The name of the file.
            f_type : str
                The type of the file.
        """

        self.__sections = []
        self.__name ,self.__type = file_name.split(".")
        self.__text = ""
        self.__rythm = 60
        self.__loop = None
        self.__supported_type = {"txt"}            # The type of file supported.

        self.read()    # Read file.

    def read(self):
        """
        Init the staff from a file.
        """
        assert self.__type in self.__supported_type, "Sorry, this type of file is not supported."

        if self.__type == "txt":
            self.read_from_txt()

    def read_from_txt(self):
        """
        Load the staff from a txt file.

        Parameters
        ----------
            file_name : str ("xxx.txt")
                The full name of the file.
        """

        file_name = self.__name + "." + self.__type
        with open(file_name, "r") as file:
            self.__text = file.read()

        re_rythm = re.compile("rythm=([0-9]*)",re.S)
        re_loop = re.compile("loop=(.*?\))",re.S)
        re_section = re.compile("(<section.*?>.*?</section>)",re.S)
        re_section_att = re.compile("<section (.*?)>(.*)</section>",re.S)

        self.__rythm = int(re_rythm.findall(self.__text)[0])  # Find the rythm.
        self.__loop = eval(re_loop.findall(self.__text)[0])
        sections = re_section.findall(self.__text)       # Find all sections.

        for section in sections:
            # Create a temp dict to save the information of this section.
            dict_att = {}

            # Find the attributions and the notes of this section.
            match = re_section_att.findall(section)

            # Add every attribute to `dict_att`.
            attributes = match[0][0].split()
            for att in attributes:
                key, value = att.split("=")
                dict_att[key] = value

            # Create a list `notes` and add every note to this list.
            notes_temp = match[0][1].split("\n")
            notes = []
            for i in range(len(notes_temp)):
                note = notes_temp[i].strip(" ")
                note = note.strip("\t")
                if note:
                    notes.append(note)

            # Create a dict to save the information of this section, and add it to `self.__section`.
            self.__sections.append({"attribute":dict_att, "notes":notes})

        # print(self.__sections)

    @property
    def sections(self):
        return self.__sections

    @property
    def rythm(self):
        return self.__rythm

    @property
    def loop(self):
        return self.__loop

3.4 Converter类

Converter类用于实现五线谱到波形的转换。

主要变量

  • Fs : 采样率。

主要函数

  • read(self) : 读取编码后的五线谱。
  • get_frequency(self,major,scale) :计算给定音符的频率。
  • get_wave(self,major,scale,rythm=1) : 生成给定音符的波形。
  • gen_music(self,staff) : 拼接各音符的波形,生成整首音乐的波形。
class Converter():
    def __init__(self, Fs=8192):
        """
        Init an instance of Converter.

        Parameters
        ----------
            Fs : int [optional]
                The sampling rate.
        """
        self.__Fs = Fs

    def get_frequency(self,major,scale):
        """
        Calculate the Frequency.

        Parameters
        ----------
            major : int
                The major. For example, when it takes 0, it means C major.
            scale : int, range[-1,12]
                The scale. -1 means a rest, and 1 means "do", 12 means "si".

        Returns
        -------
            frequency : float
                The Frequncy calculated by the major and scale.
        """
        frequency = 440*(2**major)
        frequency = frequency*2**((scale-1)/12)
        return frequency

    def get_wave(self,major,scale,rythm=1):
        """
        Generate the wave of a note.

        Parameter
        ---------
            major : int
                The major. For example, when it takes 3, it means C major.
            scale : int, range[-1,12]
                The scale. -1 means a rest, and 1 means "do", 12 means "si".
            rythm : int
                The rythm of the note. When it takes 1.5, int means 1.5 unit-time per beat.

        Returns
        -------
            y : np.array
                The wave generated.

        """
        Pi = np.pi
        x = np.linspace(0, 2*Pi*rythm, int(self.__Fs*rythm), endpoint=False) # time variable

        # When scale==-1, it means a rest.
        if scale == -1:
            y = x*0
        else:
            frequency = self.get_frequency(major, scale)
            y = np.sin(frequency*x)*(1-x/(rythm*2*Pi))

        return y

    def gen_music(self,staff):
        """
        Play a piece of music based on section.

        Parameters
        ----------
            staff : class Staff.

        Returns
        -------
            wave : np.array
                The wave of the staff.
        """
        sections = staff.sections
        time = 60/staff.rythm
        loop_start, loop_end, loop_times, loop_sub = staff.loop

        wave = np.array([])
        section_wave_ls = []
        for section in sections:
            notes = section["notes"]
            wave_list = []
            for line_str in notes:
                line = [eval(note) for note in line_str.split()]
                line_wave = np.array([])
                for note in line:
                    major, scale, rythm = note
                    rythm *= time
                    y = self.get_wave(major, scale, rythm)
                    line_wave = np.concatenate((line_wave,y),axis=0)
                wave_list.append(line_wave)
            length = min([len(line_wave) for line_wave in wave_list])
            section_wave = wave_list[0][:length]
            for i in range(1,len(wave_list)):
                section_wave += wave_list[i][:length]
            # wave = np.concatenate((wave,section_wave),axis=0)
            section_wave_ls.append(section_wave)
            
        temp = [w for w in section_wave_ls[:loop_start-1]]
        for i in range(loop_times):
            for w in section_wave_ls[loop_start-1:loop_end]:
                temp.append(w)

        if loop_sub:
            section_wave_ls = temp[:-1] + [w for w in section_wave_ls[loop_end:]]
        else:
            section_wave_ls = temp + [w for w in section_wave_ls[loop_end:]]
        
        for w in section_wave_ls:
            wave = np.concatenate((wave,w), axis=0)

        return wave

四、项目代码

完整代码已经上传至github,传送门: Python演奏国际歌

五、不足与展望

  1. 对琴谱的编码方式有待优化。我采用的方法太过粗糙,只提取了原谱最关键的少量信息。此外,由于编码规则是我自己指定的,这种编码也缺乏通用性。接下来可以尝试MusicXML4编码。
  2. 琴谱编码需要人工进行,费时费力,后期可以采用图像识别技术自动识别并编码琴谱。
  3. 后期可以再写一个UI界面,提高用户体验度。

  1. 【Matlab番外篇】怎样用Matlab生成一段音乐 ↩︎

  2. 维基百科:A(音名) ↩︎

  3. 维基百科:十二平均律 ↩︎

  4. Songs and Schemas ↩︎

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值