文章将讨论 DG-LAB APP 波形导出功能生成的二维码的编码方式及其所包含的信息。
编码方式逆向由白神成太_Tanuki完成,结构分析由博主完成。

二维码的编码方式

此部分内容由 B 站 UP 白神成太_Tanuki 完成

DG-LAB APP 波形导出功能可以生成如上二维码,直接扫描二维码可以得到一串 URL:

https://www.dungeon-lab.com/app-download.php#DGLAB-PULSE#1F8B0800000000000000F3752E2FF771712CF6CD42C299E5953E2E9EC5BE30B9E0F20A1F1757043F241228565CE113E259EE93E50814732D01EA2BF575712CF20D364088399B94FB3A83D481E58A6172001D79E0BD74000000

URL 后面的 16 进制串即为编码后的波形数据

1F8B0800000000000000F3752E2FF771712CF6CD42C299E5953E2E9EC5BE30B9E0F20A1F1757043F241228565CE113E259EE93E50814732D01EA2BF575712CF20D364088399B94FB3A83D481E58A6172001D79E0BD74000000

B 站 UP白神成太_Tanuki发现,这是一串经过 gzip 压缩的字符串,gzip 解压缩后的结果是一串 base64 编码的字符串

>>> import gzip
>>> import base64
>>> bytes.fromhex("1F8B0800000000000000F3752E2FF771712CF6CD42C299E5953E2E9EC5BE30B9E0F20A1F1757043F241228565CE113E259EE93E50814732D01EA2BF575712CF20D364088399B94FB3A83D481E58A6172001D79E0BD74000000")
b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xf3u./\xf7qq,\xf6\xcdB\xc2\x99\xe5\x95>.\x9e\xc5\xbe0\xb9\xe0\xf2\n\x1f\x17W\x04?$\x12(V\\\xe1\x13\xe2Y\xee\x93\xe5\x08\x14s-\x01\xea+\xf5uq,\xf2\r6@\x889\x9b\x94\xfb:\x83\xd4\x81\xe5\x8aar\x00\x1dy\xe0\xbdt\x00\x00\x00'
>>> gzip.decompress(b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xf3u./\xf7qq,\xf6\xcdB\xc2\x99\xe5\x95>.\x9e\xc5\xbe0\xb9\xe0\xf2\n\x1f\x17W\x04?$\x12(V\\\xe1\x13\xe2Y\xee\x93\xe5\x08\x14s-\x01\xea+\xf5uq,\xf2\r6@\x889\x9b\x94\xfb:\x83\xd4\x81\xe5\x8aar\x00\x1dy\xe0\xbdt\x00\x00\x00')
b'MCwwLDAsMjAsMjAsMjAsMiwyLDIsMCwwLDAsMSwxLDEsMCwwLDAsMTYsMSsxLTIwLjAwLDEtMjAuMDArMS0wLjAwLDEtMC4wMCsxLTAuMDAsMS0wLjAw'
>>> base64.b64decode(b'MCwwLDAsMjAsMjAsMjAsMiwyLDIsMCwwLDAsMSwxLDEsMCwwLDAsMTYsMSsxLTIwLjAwLDEtMjAuMDArMS0wLjAwLDEtMC4wMCsxLTAuMDAsMS0wLjAw')
b'0,0,0,20,20,20,2,2,2,0,0,0,1,1,1,0,0,0,16,1+1-20.00,1-20.00+1-0.00,1-0.00+1-0.00,1-0.00'

可以推出波形数据编码的流程:base64 编码 → gzip 压缩 → 转为16进制串

下面的 Python 函数可以实现解码:

def reverse_parse(encoded_str):
    # 十六进制转换字节序列
    hex_bytes = bytes.fromhex(encoded_str)
    # gzip 解压缩
    decompressed_bytes = gzip.decompress(hex_bytes)
    # base64 解码
    decoded_bytes = base64.b64decode(decompressed_bytes)
    # 转字符串
    decoded_str = decoded_bytes.decode('utf-8', errors='ignore')

    return decoded_str

信息的含义

分析得到的原始信息不难发现,完整的波形数据包含四部分:元数据、小节1形状、小节2形状和小节3形状,四部分间使用 + 分割

频率1

在“固定”模式下为恒定不变的脉冲频率

在“节内渐变”、“元内渐变”和“元间渐变”三种模式下充当脉冲频率的最小值

频率2

在“固定”模式下该项失效

在“节内渐变”、“元内渐变”和“元间渐变”三种模式下充当脉冲频率的最大值

滑块值 $x \in \mathbb{Z} \cap [0, 83]$ 与脉冲频率的关系符合以下分段函数:
\[ f(x) = \begin{cases} x + 10 & 0 \le x \le 40 \\ 2x - 30 & 40 < x \le 55 \\ 5x-195 & 55 < x \le 59 \\ 10x-490 & 59 < x \le 69 \\ 33.321x-2099.4 & 69 < x \le 75 \\ 50x-3350 & 75 < x \le 79 \\ 100x-7300 & 79 < x \le 83 \end{cases} \]

脉冲元数量

标识各个小节内的脉冲元数量(范围 2~30),具体脉冲元数量以小节形状数据为准

小节时长

各个小节的时长(范围 0~100,对应 0.1s~10s)

频率模式

标识各个小节的频率模式

1 - 固定
2 - 节内渐变:小节的全部持续时间内频率会逐渐从频率1渐变到频率2
3 - 元内渐变:小节每个脉冲元的全部持续时间内频率会逐渐从频率1渐变到频率2
4 - 元间渐变:小节每个脉冲元内部的频率是固定的,但是第一个脉冲元到最后一个脉冲元的频率会从频率1渐变到频率2

节开关

标识第2、3小节是否启用:0 - 禁用,1 - 启用

休息时长

在播放完最多三个小节后,暂停输出的时长(范围 0~100,对应 0.0s~1.0s)

?

未知,值为 16

播放速率

播放速率可以设置波形播放速度,目前 APP 提供的速度倍率有:1x、2x、4x

此处为对应倍率的值(1x、2x、4x 对应1、2、4),其他值无效

脉冲元

脉冲元的结构为 类型-强度

类型:0 - 限制脉冲元(不可自由滑动,由 APP 自动计算),1 - 自由脉冲元(可自由滑动)
强度:0~20,允许2位小数

起始与结束脉冲元类型为可自由滑动脉冲元,0 无效


例子

0,10,0,16,45,20,4,5,2,0,0,0,1,2,1,1,0,48,16,1+1-20.00,0-13.33,0-6.67,1-0.00+1-20.00,1-20.00,1-20.00,1-20.00,1-20.00+1-0.00,1-0.00

小节1:脉冲频率固定10ms,脉冲形状从强度20渐变到0(共4个脉冲元)
小节2:脉冲频率从20ms节内渐变至60ms,脉冲形状为5个强度20的脉冲元
小节3:未启用
休息时长:0.48s(0.5s)