尝试用神经网络生成音乐游戏的谱面
2020/2/20 6:17:44
本文主要是介绍尝试用神经网络生成音乐游戏的谱面,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
写在前面
一直喜欢玩儿音乐游戏,从最早玩儿的节奏大师,再到cytus2,malody,lanota等等。但音游的谱面很难做,需要对音,配键型等等,很麻烦,于是想到了用AI来生成。
因为数据量比较少,而且都不算标注的数据,所以这只算是一个尝试。
效果视频:www.bilibili.com/video/av885…
(顺便B站求波关注)
音乐游戏有很多种,这里以Malody的4K下落模式来尝试。
关于Malody
Malody是一款音游模拟器,有很多的模式,玩过节奏大师的话,就会知道节奏大师有4K,5K,6K三个模式,这里我将只尝试4K模式,或者叫4K Mania模式。
Malody相比节奏大师来说,长条是不会换轨道的,也就是没有滑条。但是Malody会出现三押、四押的情况,而节奏大师一般只要两个手指就可以玩儿,而Malody的4k模式必须要用四个手指才行。
malody中有很多的键形,以下举例:
叠键型
切键型
纵连键型
面条键型
还有一些别的键型,这里就不举例了。
谱面文件分析
malody的谱面是以mcz后缀结尾的,其实它实际上是一个zip压缩包。
选取一个谱面通过python的zipfile解压mcz文件后,可以看到有三个文件。
jpg
是谱面的背景图片,mc
格式的是谱面,而ogg
则是音乐。
这里主要看一下mc这个文件。
这个文件其实就是json格式的,把它转换成python字典,可以看到这个字典一共有4个key。
mc_data["meta"]
歌曲基本信息
mc_data["time"]
这个主要是BPM虽时间的一个变化的信息,因为malody是有变速的,这里就不考虑变速了,只看第一个就行,可以看到这首歌的BPM是180。
mc_data["note"]
note也就是我们的按键了,有两种类型,一种就是一个note,还有long note,也就是长条。
可以看到这下面如果是有endbeat就是有长条。再来看下这个beat,一共由三个数字组成,这里以[27, 2, 4]为例。27代表的是第28个节拍(从0开始算),这个节拍需要通过BPM来算,这首歌BMP是180,也就是一分钟180个节拍,也就是秒一个节拍,那么这个27就是从秒到这段时间了。
再来看这个[27, 2, 4]中的2和4的含义:最后一个4其实代表的是一个节拍里面的小拍,这里一共是4小拍,这样把一个节拍又分成了四份,而2则代表第三个小拍(从0开始算)。
所以这个[27, 2, 4]代表的是秒这个时间点。
mc_data["note"][-1]
在note的最后一个元素中,很特殊,它会加载音乐,并设置offset。
offset表示使谱面节拍线对齐音乐节拍的最小前进量,单位为毫秒。这个其实就是一个对齐的数值。详情可以参考:www.bilibili.com/read/cv1869…
这里的offset是315ms,那么之前的[27, 2, 4]代表的其实就是秒这个点。
mc_data["extra"]
这个貌似没有用,就不管了。
数据集构建
因为要用AI做谱,这里首先要搞出一部分数据,这里选了20几个malody中Lv20-Lv25的谱面(都是些非常简单的歌曲)作为训练数据。
问题定义
这里我们的输入的是连续的音频,输出的是四个轨道的note,所以其实是一个序列输入到序列输出的情况。
输入特征特征构建
关于音乐方面的特征提取的基本知识可以看我之前的文章:使用Python对音频进行特征提取
这里序列拆分将会根据时间来,每个节拍可以分为四个时间点。也就是每个节拍内最多有4个打击点。
以BMP是180为例,那么一个节拍就是60/180=0.333秒。每个打击点之间的时间间隔就是0.333/4=0.08333秒。
然后打击点的特征也就是这0.08333秒的特征,这里通过mfcc来提取,并把0.08333秒分成两个部分,分别抽取mfcc特征,然后再拼在一起,当成音频的特征。
# x为音乐的时域信息,也就是一个列表 # sr为音频的采样频率 # position为第几个打击点 # offset为谱面的偏移 def get_audio_features(x, sr, bpm, position, offset): one_beat = 60 / bpm beat = position * one_beat / 4 - offset/1000 start = beat end = start + one_beat / 8 end2 = start + one_beat / 4 if start < 0: start = 0 start_index = int(sr * start) end_index = int(sr * end) end_index2 = int(sr * end2) features = [] mfcc1 = librosa.feature.mfcc(y=x[start_index:end_index], sr=sr, n_mfcc=32) mfcc2 = librosa.feature.mfcc(y=x[end_index:end_index2], sr=sr, n_mfcc=32) features += [float(np.mean(e)) for e in mfcc1] features += [float(np.mean(e)) for e in mfcc2] return features 复制代码
输入序列拆分
这里因为一个谱子会比较长,会有上千个打击点的判断,所以要把判断点切分开,每一轮50个。
输出格式
输出可以分为4种:
-
- 空,没有打击
-
- note,也就是打击点
-
- long note start,长条的开始
-
- long note continue,长条的连续
在特征中,可以用0,1,2,3表示这三种情况。
这里简化一下,我们把long note start和continue当成一个键,这样输出的结果就0,1,2三种情况。
键型编码
因为是4-Key的音游,所以每个位置有3种情况:空,打击,长条。
所以一共有中情况,可以通过一个4位的3进制数来表示,换算成10进制就是0到80。
比如下面这个键形:
用三进制表示就是:再比如下面这个:
用三进制表示就是:数据格式
综上,需要经过很多复杂的数据解析。
然后每一个输入是一个40*64的矩阵。(40为序列长度,64为特征维度)
每一个输出则是40*1的列表。
模型设计
上面的数据明显算是一个seq2seq的问题,可以用encoder-decoder这个模型。找一个机器翻译的代码就OK。不过有一点区别就是,这里我们的encoder的输入不需要经过embedding,因为我们已经用mfcc提取到特征了。
Encoder
class EncoderRNN(nn.Module): def __init__(self, hidden_size): super(EncoderRNN, self).__init__() self.hidden_size = hidden_size self.gru = nn.GRU(Feature_DIM, hidden_size) def forward(self, input_, hidden): input_ = input_.view(1, 1, -1) output, hidden = self.gru(input_, hidden) return output, hidden def initHidden(self): return torch.zeros(1, 1, self.hidden_size, device=device) hidden_size = 128 encoder = EncoderRNN(hidden_size).to(device) 复制代码
这里的encoder就是一层简单的GRU,因为输入是特征,所以相比机器翻译的seq2seq的encoder不需要加embedding。
encoder传播过程
对于encoder,这里需要跑一遍循环,然后拿到最后一个元素的hidden参数。
x1 = torch.from_numpy(np.array(X1[index])).to(device).float() # 输入特征 y1 = torch.from_numpy(np.array(Y1[index])).to(device).long() # label encoder_hidden = encoder.initHidden() for ei in range(max_length): _, encoder_hidden = encoder( x1[ei], encoder_hidden) 复制代码
Decoder
class DecoderRNN(nn.Module): def __init__(self, embedding, hidden_size, output_size): super(DecoderRNN, self).__init__() hidden_size = hidden_size # self.embedding = nn.Embedding(output_size, hidden_size) self.embedding = embedding self.gru = nn.GRU(50, hidden_size) self.out = nn.Linear(hidden_size, output_size) self.softmax = nn.LogSoftmax(dim=1) self.dropout = nn.Dropout(0.2) def forward(self, input_, hidden): output = self.embedding(input_).view(1, 1, -1) output = self.dropout(output) # output = F.relu(output) output, hidden = self.gru(output, hidden) output = self.softmax(self.out(output[0])) return output, hidden hidden_size = 128 decoder = DecoderRNN(embedding, hidden_size, 81).to(device) 复制代码
这里decoder也同样是一个rnn,对每一个输出,都会有一个结果输出,也就是键型的类别了。
“键型”语言模型
这里注意到在decoder里有一个embedding,这其实和NLP的embedding是非常类似的意思。词向量其实表达的就是一个词和什么词最接近。
这里的键型Embedding同样,什么样的键型更容易出现在一起。再加上RNN,学习的其实就是在已知前面的键型,和当前的音符特征的时候,下一个键型最可能是什么。
也就NLP里的的语言模型了。
decoder解码过程
对于decoder的解码过程其实也是跑一遍循环,不过输入的第一个hidden不是0,而是encoder的hidden。
decoder_input = torch.tensor([[0]], device=device) decoder_hidden = encoder_hidden for di in range(max_length): decoder_output, decoder_hidden = decoder( decoder_input, decoder_hidden) target = y1[di].view(-1) # print(decoder_output) # print(target) loss += F.nll_loss(decoder_output, target) decoder_input = target # Teacher forcing 复制代码
模型2.0
在经过我的测试之后,上面的效果并不好,生成出来的键非常不稳定。我想到的原因是数据实在是太少了,只有24首歌,而且数据噪声会比较大。
这里就不细说了,代码和上面是一样的,不过是多copy了几份。下面大概说一下我做了哪几件事情。
-
- 为了降低复杂度,我把长条和打击点分开,这样从之前的类就变成了类,大大减少了复杂性。
-
- 从一个模型变成两个模型。之前是直接判断键型,但很多时候0会比较多,所以效果不好。现在我加了一个分类器,先判断是否有长条或者打击点,再判断键型。这样如果有打击点或者长条,键型模型就会一定生成一个键型,而不会直接是空的了。
-
- 简化操作,先生成打击点,再生成长条。如果重合,长条会覆盖掉打击点。不过概率肯定不大,不影响大效果。
最后的效果看我B站视频就好了,一共生成了三首歌:China-P,惊蛰,春分。感觉总体效果还是可以的。
本文的代码:github.com/nladuo/AI_b…
(这个代码写的非常的乱,我感觉一般人也不会再去研究这个把,所以也就没有整理优化了。基本代码就是一个seq2seq,然后有一堆的数据构建解析的代码。)
这篇关于尝试用神经网络生成音乐游戏的谱面的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-20实战:30 行代码做一个网页端的 AI 聊天助手
- 2024-11-185分钟搞懂大模型的重复惩罚后处理
- 2024-11-18基于Ollama和pgai的个人知识助手项目:用Postgres和向量扩展打造智能数据库
- 2024-11-15我用同一个提示测试了4款AI工具,看看谁设计的界面更棒
- 2024-11-15深度学习面试的时候,如何回答1x1卷积的作用
- 2024-11-15检索增强生成即服务:开发者的得力新帮手
- 2024-11-15技术与传统:人工智能时代的最后一袭纱丽
- 2024-11-15未结构化数据不仅仅是给嵌入用的:利用隐藏结构提升检索性能
- 2024-11-15Emotion项目实战:新手入门教程
- 2024-11-157 个开源库助你构建增强检索生成(RAG)、代理和 AI 搜索