·  阅读 365

@TOC

# 二、必备知识

## 2.1 Python生成音乐的原理

``````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()

``````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()

# 三、实现

## 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)

"""
Load the staff from a file.

Parameters
----------
file_name : str

Returns
-------
state : bool
"""
#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.

"""
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":

"""
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:

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

# 五、不足与展望

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