音乐波谱生成
实现一个由代码生成,《送别》的音频文件. 本文设涉及编码、音乐以及一些数学公式。实现从底层波函数生成到音乐音频的生成。不依赖第三方API。其中的数学、音乐知识点,具体细节不展开讨论,可以私聊。
一.根据频率生成波谱
def generate_wave_func(v, c=0):
# v 频率 单位 HZ, c 相位。 生成正弦波
a = 2 * numpy.pi / (1 / v)
def inner(x):
return numpy.sin(a * x + c)
return inner
def superelipse_points(n=1.0, a=1.0, b=1.0, lins=1000):
# 超椭圆方程, 返回第一象限的数值
X1 = numpy.linspace(0, a, lins)
Y1 = (1 / b) * numpy.power((1 - numpy.power(X1 / a, n)), 1 / n)
return X1, Y1
def wave_decay(data, i0=0, i1=-1, decay=0.8):
# 波谱衰减函数
# i0, i1 不包含 i1
# print(data.shape)
if i1 == -1:
i1 = data.shape[0] - 1
n = (i1 - i0)
x, y = superelipse_points(n=decay, a=1, b=1, lins=n)
for ch in range(data.shape[1]):
data[i0: i1, ch] = data[i0: i1, ch] * y
return data
##
import wave
import numpy
import matplotlib.pyplot as plt
"""
file_path moonlight.wav
nchannels 2 # 声道
sampwidth 2 # 采样深度 字节
framerate 44100 # 采样速率 HZ
nframes 16052015 # 音频总帧数
comptype NONE # 压缩类型
compname not compressed # 压缩类型名称
data.shape (16052015, 2)
"""
class WaveObj(object):
# 对 wave模块 封装
def __init__(self, nchannels=None, sampwidth=None,
framerate=None, nframes=None, data=None, **kwargs):
"""
nchannels: 声道 int
sampwidth: 采样字节长度(音频强度) bytes
framerate: 采样率 HZ
nframes: 总帧数 int
data: 音频数据 numpy 数组 shape = (nframes, nchannels)
"""
self.nchannels = nchannels
self.sampwidth = sampwidth
self.framerate = framerate
self.nframes = nframes
self.comptype = kwargs.get("comptype", 'NONE')
self.compname = kwargs.get("compname", 'not compressed')
self.data = data
self.file_path = ""
# 第一帧时间标记为0, 所以总时间需要减少一帧计算
if self.nframes and self.framerate:
# self.total_time = (nframes - 1) / framerate
self.total_time = nframes / framerate
# if self.nframes and self.framerate:
# self.times = numpy.arange(0, nframes) * (1 / framerate)
# self.total_time = self.times[-1]
def init_from_file(self, path):
self.file_path = path
with wave.open(path, "rb") as w:
self.nchannels = w.getnchannels()
self.sampwidth = w.getsampwidth()
self.framerate = w.getframerate()
self.nframes = w.getnframes()
self.comptype = w.getcomptype()
self.compname = w.getcompname()
data = w.readframes(self.nframes) # bytes
data = numpy.frombuffer(data, dtype=numpy.int16)
data = numpy.reshape(data, (self.nframes, self.nchannels))
self.data = data
# self.times = numpy.arange(0, self.nframes) * (1 / self.framerate)
self.total_time = self.nframes / self.framerate
@property
def times(self):
if not hasattr(self, "_times"):
self._times = numpy.arange(0, self.nframes) * (1 / self.framerate)
return self._times
return self._times
def save(self, path):
if not path.endswith(".wav"):
path += path + ".wav"
with wave.open(path, mode="wb") as w:
w.setparams((self.nchannels,
self.sampwidth,
self.framerate,
self.nframes,
self.comptype,
self.compname))
w.writeframes(self.data.tobytes())
def print_params(self):
print("file_path", self.file_path)
print("nchannels", self.nchannels),
print("sampwidth", self.sampwidth),
print("framerate", self.framerate),
print("nframes", self.nframes),
print("comptype", self.comptype),
print("compname", self.compname)
print("data.shape", self.data.shape)
def show_wave(self):
plt.plot(self.times, self.data[:, ...])
# plt.scatter(d.times, d.data[:, 0])
plt.show()
def create_wave_data_by_func(func, t0, t1, framerate, channels=2, batch=True):
# 生成波谱
# t0, t1, 单位 s framerate 单位 n/s
n = int((t1 - t0) * framerate)
x = numpy.linspace(t0, t1, n)
if not batch:
y = numpy.array([func(i) for i in x])
else:
y = func(x)
y = y[..., numpy.newaxis].repeat(channels, axis=1)
return y
二.计算小字一组音的频率
# 大调音阶频率关系
ionian = [0, 2, 4, 5, 7, 9, 11, 12] # c1, d1, e1, f1, g1, a1, b1
du = 2 ** (1 / 12) # 一度音程倍率
a1 = 440 # 标准音 a1
c1 = 440 / (du ** 11) # 小字一组 c1 频率
c1toc2 = [int(c1 * (du ** i)) for i in ionian] # 小字一组音阶 + c2
print(c1toc2)
[233, 261, 293, 311, 349, 391, 440, 466]
三.根据简谱生成波谱数据
以下是送别的简谱
T = 0.5 # 一拍的时间
# 根据简谱,做一些简单变换,方便处理,比如 1*表示 高八度的 1, 第二个是以数值标注的节拍时值
data = [
"5-1",
"3-0.5",
"5-0.5",
"1*-2",
"6-1",
"1*-1",
"5-2",
"5-1",
"1-0.5",
"2-0.5",
"3-1",
"2-0.5",
"1-0.5",
"2-4",
"5-1",
"3-0.5",
"5-0.5",
"1*-1.5",
"7-0.5",
"6-1",
"1*-1",
"5-2",
"5-1",
"2-0.5",
"3-0.5",
"4-1.5",
"*7-0.5",
"1-4",
"6-1",
"1*-1",
"1*-2",
"7-1",
"6-0.5",
"7-0.5",
"1*-2",
"6-0.5",
"7-0.5",
"1*-0.5",
"6-0.5",
"6-0.5",
"5-0.5",
"3-0.5",
"1-0.5",
"2-4",
"5-1",
"3-0.5",
"5-0.5",
"1*-1.5",
"7-0.5",
"6-1",
"1*-1",
"5-2",
"5-1",
"2-0.5",
"3-0.5",
"4-1.5",
"*7-0.5",
"1-4",
]
def create_music_wave(data):
T = 0.5 # 一拍时间 单位 s
framerate = 44100
sampwidth = 2
v_list = [i * 2 for i in c1toc2]
music = []
for i in data:
key, time = i.split("-")
key_id = int(key.replace("*", "")) - 1
time = float(time)
starts = key.count("*")
mark = -1 if key.startswith("*") else 1
v = v_list[key_id] * (2 ** (starts * mark))
print(f"音符:{key} 频率:{v}, 时间:{time}")
func = generate_wave_func(v)
nots_wave = create_wave_data_by_func(func, 0, time, framerate,
channels=2
)
nots_wave = nots_wave * (2 ** (sampwidth * 8 - 1))
# 做音高衰减
nots_wave = wave_decay(nots_wave, decay=1.1)
nots_wave = nots_wave.astype("int16")
music.append(nots_wave)
return numpy.concatenate(music)
music_wave = create_music_wave(data)
音符:5 频率:698, 时间:1.0
音符:3 频率:586, 时间:0.5
音符:5 频率:698, 时间:0.5
…
# def __init__(self, nchannels=None, sampwidth=None,
# framerate=None, nframes=None, data=None, **kwargs):
wave_obj = WaveObj(data=music_wave,nframes=music_wave.shape[0],
nchannels=music_wave.shape[1],
framerate = 44100,
sampwidth = 2
)
wave_obj.show_wave()
wave_obj.save("送别.wav")