Musicreater/docs/MSQ文件格式.md

9.5 KiB
Raw Blame History

MSQ 文件格式

MSQ 文件是 音·创 存储音符序列的一种格式,取自 MusicSeQuence。

现在 音·创 及其上游软件使用的是在 第二版 的基础上增设验证功能的 MSQ 第三版。

MSQ 第三版

第二版的码头是 MSQ@ ,这一版中,所有的字符串GB18030 编码进行编解码,数值以**大端序**存储。

MSQ 第三版的码头是 MSQ!

码头是文件前四个字节的内容,这一部分内容是可读的 ASCII 字串。因此,第三版的文件前四个字节的内容必为 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 文件应当如下排列其字节串:

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
    )