mirror of
https://github.com/TriM-Organization/Musicreater.git
synced 2025-09-05 20:06:23 +00:00
150 lines
12 KiB
Python
150 lines
12 KiB
Python
# 音乐序列文件格式
|
||
|
||
音·创 库的音符序列文件格式包含两种,一种是常规的音乐序列存储采用的 MSQ 格式,另一种是为了流式读取音符而采用的 FSQ 格式。
|
||
|
||
## MSQ 数据格式
|
||
|
||
MSQ 格式是 音·创 库存储音符序列的一种字节码格式,取自 **M**usic**S**e**Q**uence 类之名。
|
||
|
||
现在 音·创 库及其上游软件使用的是在 第二版 的基础上增设校验功能的 MSQ 第三版。
|
||
|
||
### MSQ 第三版
|
||
|
||
第三版 MSQ 格式的码头是 `MSQ!` ,这一版中,所有的**字符串**皆以 _**GB18030**_ 编码进行编解码,**数值**皆是以 _**大端字节序**_ 存储的无符号整数。
|
||
|
||
码头是字节码前四个字节的内容,这一部分内容是可读的 ASCII 字串。因此,第三版的字节码中前四个字节的内容必为 `MSQ!`。
|
||
|
||
第二版 MSQ 取 `MSQ@` 作为码头是因为美式键盘上 @ 是 Shift+2 键 按下取得的,故代表 MSQ 第二版。
|
||
|
||
你猜为什么第三版是 `MSQ!`。
|
||
|
||
#### 元信息
|
||
|
||
| 信息名称 | 西文代号 | 位长(多少个 0 或 1) | 支持说明 |
|
||
| ------------------------------ | -------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **码头** | _无_ | 32 位 | 值为 `MSQ!` |
|
||
| **音乐名称长度** | music_name_length | 6 位 | 支持数值 0~63 |
|
||
| **最小音量** | minimum_volume | 10 位 | 支持数值 0~1023,注意,这里每个 1 代表最小音量的 0.001 个单位,即取值是此处表示数字的千分倍 |
|
||
| **是否启用高精度音符时间控制** | enable_high_precision_time | 1 位 | 1 是启用,反之同理 |
|
||
| **总音调偏移** | music_deviation | 15 位 | 支持数值 -16383~16383,这里也是表示三位小数的,和最小音量一样。这里 15 位中的第一位(从左往右)是正负标记,若为 1 则为负数,反之为正数,后面的 14 位是数值 |
|
||
| **音乐名称** | music_name | 依据先前定义 | 最多可支持 31 个中文字符 或 63 个西文字符,其长度取决于先前获知的 “音乐名称长度” 的定义 |
|
||
|
||
在这一元信息中,**音乐名称长度**和**最小音量**合计共 2 字节;**高精度音符时间控制启用**和**总音调偏移**合计共 2 字节;因此,除**音乐名称**为任意长度,前四字节内容均为固定。
|
||
|
||
#### 音符序列
|
||
|
||
每个序列前 4 字节为一个用以表示当前通道中音符数量的值,也就是**通道音符数**(notes_count)。也即是说,一个通道内的音符可以是 0~4294967295 个。
|
||
|
||
在这之后,就是这些数量的音符了,其中每个音符的信息存储方式如下
|
||
|
||
| 信息名称 | 西文代号 | 位长 | 支持说明 |
|
||
| ---------------------------- | ------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **乐器名称长度** | name_length | 6 位 | 支持数值 0~63 |
|
||
| **Midi 音高** | note_pitch | 7 位 | 支持数值 0~127 |
|
||
| **开始时刻** | start_tick | 17 位 | 单位 二十分之一秒,即约为 1 命令刻;支持数值 0~131071 即 109.22583 分钟 合 1.8204305 小时 |
|
||
| **音符持续刻数** | duration | 17 位 | 同上 |
|
||
| **是否作为打击乐器** | percussive | 1 位 | 1 是启用,反之同理 |
|
||
| **响度(力度)** | velocity | 7 位 | 支持数值 0~127 |
|
||
| **是否启用声像位移** | is_displacement_included | 1 位 | 1 是启用,反之同理 |
|
||
| **时间精度提升值**(非必含) | high_time_precision | 8 位 | 支持数值 0~255,若在 元信息 中启用**高精度音符时间控制**,则此值启用,代表音符时间控制精度偏移,此值每增加 1,则音符开始时刻向后增加 1/1250 秒 |
|
||
| **乐器名称** | sound_name | 依据先前定义 | 最多可支持 31 个中文字符 或 63 个西文字符,其长度取决于先前获知的 “乐器名称长度” 的定义 |
|
||
| **声像位移**(非必含) | position_displacement | 共三个值,每个值 16 位 共 48 位 | 若前述**是否启用声像位移**已启用,则此值启用;三个值分别代表 x、y、z 轴上的偏移,每个值支持数值 0~65535,注意,这里每个 1 代表最小音量的 0.001 个单位,即取值是此处表示数字的千分倍 |
|
||
|
||
#### 序列校验
|
||
|
||
_第三版新增_
|
||
|
||
在每个音符序列结尾包含一个 128 位的校验值,用以标识该序列结束的同时,验证该序列的完整性。
|
||
在这 128 位里,前 64 位是该通道音符数的 XXHASH64 校验值,以 3 作为种子值。
|
||
后 64 位是整个通道全部字节串的 XXHASH64 校验值(包括通道开头的音符数),以 该通道音符数 作为种子值。
|
||
|
||
#### 总体校验
|
||
|
||
_第三版新增_
|
||
|
||
在所有有效数据之后,包含一个 128 位的校验值,用以标识整个字节串结束的同时,验证整个字节码数据的完整性。
|
||
|
||
该 128 位的校验值是 包括码头在内的元信息的 XXHASH64 校验值(种子值是全曲音符数) 对于前述所有校验值彼此异或的异或 所得值之 XXHASH128 校验值,以 全曲音符总数 作为种子值。
|
||
|
||
请注意,是前述每个 XXHASH64 校验值的异或(每次取 XXHASH64 都计一遍),也就并非是每个序列结尾,那个已经合并了的 128 位校验值再彼此异或。对于这个异或值,再取其种子是 全曲音符数 的 XXHASH128 校验字节码。
|
||
|
||
听起来很复杂?我来举个例子。以下是该算法的伪代码。我们设:
|
||
|
||
- `meta_info` : `bytes` 为 元信息字节串
|
||
- `note_seq_1` : `bytes` 为 第一个音符序列的编码字节串
|
||
- `note_seq_2` : `bytes` 为 第二个音符序列的编码字节串
|
||
- `XXH64(bytes, seed)` : `bytes` 为 XXHASH64 校验函数
|
||
- `XXH128(bytes, seed)` : `bytes` 为 XXHASH128 校验函数
|
||
- `XOR(bytesLike, bytesLike)` : `bytes` 为 异或 函数
|
||
- `note_count` : `int` 为 全曲音符数
|
||
- `seq_1_note_count` : `int` 为 第一个音符序列的音符数
|
||
- `seq_2_note_count` : `int` 为 第二个音符序列的音符数
|
||
|
||
为了简化,我们假设只有两个序列,实际上每个通道都是一个序列(最多 16 个序列)
|
||
|
||
那么,一个完整的 MSQ 文件应当如下排列其字节串:
|
||
|
||
```assembly
|
||
ADD meta_info
|
||
ADD note_seq_1
|
||
ADD XXH64(seq_1_note_count, 3)
|
||
ADD XXH64(note_seq_1, seq_1_note_count)
|
||
ADD note_seq_2
|
||
ADD XXH64(seq_2_note_count, 3)
|
||
ADD XXH64(note_seq_2, seq_2_note_count)
|
||
ADD XXH128(
|
||
XOR(
|
||
XOR(
|
||
XXH64(meta_info, note_count),
|
||
XOR(
|
||
XXH64(seq_1_note_count, 3),
|
||
XXH64(note_seq_1, seq_1_note_count)
|
||
),
|
||
),
|
||
XOR(
|
||
XXH64(seq_2_note_count, 3),
|
||
XXH64(note_seq_2, seq_2_note_count),
|
||
)
|
||
),
|
||
note_count
|
||
)
|
||
```
|
||
|
||
## FSQ 数据格式
|
||
|
||
FSQ 格式是 音·创 库存储音符序列的一种字节码格式,取自 **F**lowing Music **S**e**q**uence 之名。
|
||
|
||
现在 音·创 库及其上游软件使用的是在 MSQ 第三版 的基础上进行流式的兼容性变动。
|
||
|
||
### FSQ 第一版
|
||
|
||
第一版的码头是 `FSQ!` ,这一版中,所有的**字符串**皆以 _**GB18030**_ 编码进行编解码,**数值**皆是以 _**大端字节序**_ 存储的无符号整数。
|
||
|
||
码头是字节串前四个字节的内容,这一部分内容是可读的 ASCII 字串。因此,这一版的文件前四个字节的内容必为 `FSQ!`。
|
||
|
||
因为这一版本是在 MSQ 第三版的基础上演变而来的,因此取自 MSQ 码头的 `MSQ!` 而改作 `FSQ!`。
|
||
|
||
#### 元信息
|
||
|
||
FSQ 第一版的元信息是在 MSQ 第三版的元信息的基础上增加了一个占 5 字节的全曲音符总数。
|
||
|
||
也就是说,与 MSQ 第三版一致的,在这一组信息中,**音乐名称长度**和**最小音量**合计共 2 字节;**高精度音符时间控制启用**和**总音调偏移**合计共 2 字节;因此,除**音乐名称**为任意长度,前四字节内容均为固定。而最后增加 5 字节作为全曲音符总数。
|
||
|
||
#### 音符序列
|
||
|
||
FSQ 格式不包含音符的通道信息,在读取处理时默认将同一乐器的音符视为同一通道。也就是说,仅存在一个序列。其中每个音符的信息存储方式与 MSQ 第三版一致。
|
||
|
||
音符序列的存储顺序是按照音符的**开始时间**进行排序的。
|
||
|
||
但是注意!有可能一个较长的音符的开始到结束时间内还包含有音符,此时如有要适配的读取器,还请继续读取直到下一个音符的开始时间大于此较长音符的结束时间。
|
||
|
||
在每 100 个音符后,插入一段 32 位的 XXHASH32 校验码,其所校验的内容为这一百个音符中每个音符的第 6 个字节彼此异或之结果,种子值为这一百个音符中每个音符的第 2 个字节的彼此异或结果。
|
||
|
||
若最后不满足 100 个音符,则不插入上述校验码。
|
||
|
||
#### 总体校验
|
||
|
||
在所有有效数据之后,包含一个 128 位的校验值,用以标识整个字节串结束的同时,验证整个字节码数据的完整性。
|
||
|
||
该 128 位的校验值是 包括码头在内的元信息的 XXHASH64 校验值(种子值是全曲音符数) 对于前述所有 XXHASH32 校验值彼此异或的异或 所得值之 XXHASH128 校验值,以 全曲音符总数 作为种子值。
|