
在
上一篇文章中,我描述了解析未知二进制数据格式时的推理路线。 使用十六进制编辑器Synalaze It!,我演示了如何解析二进制文件的标头并突出显示主要数据块。 由于在SNG格式的情况下,这些块形成了层次结构,因此我设法在语法中使用了递归,从而以易于理解的方式自动构建其树形视图。
在本文中,我将介绍一种用于直接解析音乐数据的类似方法。 使用Hex编辑器的内置功能,我将创建一个数据转换器的原型,使其成为常见和简单的Midi格式。 对于看似简单的转换时间样本的任务,我们将不得不面对许多陷阱和困惑。 最后,我将解释如何使用获得的结果和二进制文件的语法为将来的转换器生成部分代码。
解析音乐数据
因此,是时候弄清楚音乐数据如何存储在.SNG文件中了。 我在上一篇文章中部分提到了这一点。 合成器文档指出,SNG文件最多可以包含128首“歌曲”,每首歌曲由16个音轨和一个主音轨组成(用于记录全局事件和更改主音效)。 与Midi格式不同,在音乐格式中,音乐事件仅以特定的时间增量彼此跟随,而SNG格式包含音乐小节。
小节是一系列音符的容器。 小节尺寸以乐谱表示。 例如4/4-表示小节包含4个节拍,每个节拍的持续时间等于四分音符。 简而言之,这样的小节将包含4个四分音符或2个半音符或8个八分音。
SNG文件中的小节用于编辑内置合成器音序器中的音轨。 使用菜单,您可以删除,添加和复制曲目中任何位置的小节。 您还可以循环循环或更改其尺寸。 最后,您可以简单地开始记录任何小节的曲目。
让我们尝试看看所有这些如何存储在二进制文件中。 “歌曲”的通用容器是SGS1块。 每首歌曲的数据存储在块SDT1中:

SPR1和BMT1块存储常规的歌曲设置(速度,节拍器设置)和单个音轨设置(音色,效果,琶音器设置等)。 我们对TRK1块感兴趣-它包含音乐事件。 但是您需要进一步降低层次结构级别-才能阻止MTK1

最后,我们找到了轨道-这些是MTE1块。 让我们尝试在合成器上记录一段短持续时间的空轨道,再记录一段更长的空轨道,以了解有关二进制形式的小节信息的存储方式。

措施似乎存储为八字节结构。 添加一些注意事项:

因此,我们可以假设所有事件都以相同的形式存储。 MTE块的开头仍包含未知信息,然后八字节结构的序列结束。 打开语法编辑器,并创建一个大小为8个字节的
事件结构。
添加继承
childChunk的
mte1Chunk结构,并在
数据结构中放置指向
事件的链接。 我们指示该
事件可以无限次重复。 接下来,通过实验,我们在开始跟踪事件流之前找出了几个字节的大小和目的。 我得到以下内容:

在块MTE1的开始处,存储了跟踪事件的数量,其数量以及事件的维度。 应用语法后,该块开始看起来像这样:

让我们继续进行事件流。 在分析了具有不同注释顺序的几个文件之后,将出现以下图片:
# | 型式 | 二进制表示 |
---|
1个 | 击败1 | 01 00 00 ... |
2 | 注意事项 | 09 00 3C ... |
3 | 注意事项 | 09 00 3C ... |
4 | 注意事项 | 09 00 3C ... |
5 | 节拍2 | 01 C3 90 ... |
6 | 注意事项 | 09 00 3C ... |
7 | 赛道尽头 | 03 88 70 ... |
看起来第一个字节编码事件的类型。 将
类型字段添加到
事件结构。 让我们再创建两个继承
事件的结构:
measure和
note 。 我们为它们分别指定相应的固定值。 最后,在
mte1Chunk块的
数据中添加指向这些结构的链接。

应用更改:

好吧,我们取得了良好的进展。 仍然需要了解音符的高度和强度是如何编码的,以及每个事件相对于其他事件的时移。 让我们再次尝试通过合成器菜单将文件与导出到midi的结果进行比较。 这次,我们对单击笔记的事件特别感兴趣。

太好了! 音符的音高和压力似乎以与Midi格式完全相同的方式编码,只有几个字节。 将适当的字段添加到语法。
不幸的是,暂时的转变并非如此简单。
我们处理持续时间和增量
在midi格式中,NoteOn和NoteOff事件是分开的。 音符的持续时间取决于这些事件之间的时间差。 在SNG格式的情况下,没有NoteOff事件的类似物,持续时间和时间增量值必须存储在一个结构中。
为了了解它们的存储方式,我在合成器上录制了多个不同持续时间的音符序列。

显然,我们需要的数据位于事件结构的最后4个字节中。 用肉眼看不到规则性,因此我们在编辑器中选择了我们感兴趣的字节,然后使用“数据面板”工具。
显然,音符的持续时间和时移都由一对字节(UInt16)编码。 在这种情况下,字节顺序是相反的-Little Endian。 比较了足够的数据量之后,我发现此处的时间增量不是从上次事件(如midi)开始而是从时钟开始算起。 如果音符在下一个小节中结束,则在当前小节中其长度将为0x7fff,在下一个小节中将以相同的增量0x7fff重复该音符,并从新小节的开始测量持续时间。 相应地,如果一个音符听起来有几个小节,则在每个中间音节中,其持续时间和增量将等于0x7fff。
小电路
时间增量/持续时间的单位在单元中进行计数。 音符1听起来正常,而音符2在第二小节和第三小节中继续发声。 我认为,所有这些看起来都有些曲折。 另一方面,连奏以类似的方式在音乐符号中以连续的方式指示连续若干音符的音符。
在哪个“鹦鹉”中有一个持续时间? 像midi一样,在这里使用tic。 从文档中可以知道,一股股票的持续时间为480滴答。 以每分钟100拍的速度和4/4的速度,四分音符的持续时间为(60/100)= 0.6秒。 因此,一刻的持续时间为0.6 / 480 = 0.00125秒。 标准的4/4节拍将以100 bpm的速度持续4 * 480 = 1920滴答或2.4秒。
所有这些将对我们将来有用。 同时,将持续时间和增量添加到我们的
音符结构中。 另外,请注意,间歇结构中有一个字段可存储事件数。 另一个字段包含度量的序列号-将它们添加到
度量结构中。

转换器原型
现在,我们有足够的信息来尝试转换数据。 专业版中的十六进制编辑器Synalaze It允许您使用python或lua编写脚本。 创建脚本时,您需要确定我们要使用的内容:语法本身,磁盘上的单个文件或以某种方式处理解析的数据。 不幸的是,每个模板都有一些限制。 该程序提供了许多用于工作的类和方法,但是并非所有模板都可以访问所有这些类和方法。 也许这是文档中的一个缺陷,但是我还没有找到如何加载文件列表的语法,解析它们以及使用结果结构导出数据的方法。
因此,我们将创建一个脚本来处理解析当前文件的结果。 该模板实现了三种方法:init,terminate和processResult。 后者被自动调用,并且递归地传递解析过程中接收到的所有结构和数据。
要在Midi中写入转换后的数据,我们使用Python MIDI工具包(https://github.com/vishnubob/python-midi)。 由于我们正在实施概念证明,因此我们将不执行音符持续时间和增量持续时间的转换。 相反,我们设置固定值。 持续时间为0x7fff或类似增量的笔记暂时被丢弃。
内置脚本编辑器的功能非常有限,因此所有代码都必须放在一个文件中。
gist.github.com/bkotov/71d7dfafebfe775616c4bd17d6ddfe7b因此,让我们尝试转换文件并听取我们得到的信息
嗯...结果很有趣。 当我尝试拟定它的外观时,我想到的第一件事是无结构的音乐。 我将尝试给出一个定义:
非结构化音乐-一种基于和声而具有简化结构的音乐。 音符持续时间和音符之间的间隔被取消或减小为相同的值。一种和声。 使其具有珍珠色(类似于白色,蓝色,红色,粉红色等),似乎没有人采用这种组合。
也许我们应该尝试在我的数据上训练神经网络,也许结果会很有趣。
热身的任务
这一切都很棒,但是主要问题仍然没有解决。 我们需要将音符持续时间转换为NoteOff事件,并将事件相对于小节开始的时间偏移转换为相邻事件之间的时间增量。 我将尝试更正式地提出问题的条件。
挑战赛:
1
1
2
3
...
N
2
...
N
1
...
: 1
: 1920
: Int
: Int
: 9
: 0-127
: 0-127
: 0-1920 0xFF
: 0-1920 0xFF
, , 0xFF, =0xFF . , . = = 0xFF.
.
midi. :
:
: 9
: 0-127
: 0-127
: Int
:
: 8
: 0-127
: 0-127
: Int
任务有些简化。 在实际的SNG文件中,每个量度可以具有不同的维度。 除了“音符开/关”事件外,流中还将发生其他事件,例如,踩下延音踏板或使用pitchBend更改音高。
在下一篇文章中(如果有的话),我将为这个问题提供解决方案。
当前结果
由于脚本解决方案无法扩展到任意数量的文件,因此我决定在Swift中编写一个控制台转换器。 如果我编写了一个双向转换器,那么在代码中创建的语法结构对我很有用。 您可以使用Synalize It内置的相同脚本功能将它们导出为C结构或任何其他语言! 选择语法模板时,将自动创建一个带有此类导出示例的文件。

目前,转换器已完成了99%(以在功能方面适合我的形式)。 我计划将代码和语法放在github上。
一个例子,一切都已开始,
您可以在这里收听 。
这首歌听起来如何现成的。