TiMidity似乎很久没更新了,但是效率比FluidSynth快多了。
还有个WildMIDI,但是它不支持SoundFonts,只支持GUS音色。
Yamaha S-YXG 50 逆向记录
我有一个新的想法,就是只逆向yamaha数据文件读取和调用相关的程序,然后剩余部分基于timidity的思路重新实现
重大进展,不需要 XOR 0x5A,第一阶段解密后,只做字节交换即可
我感觉 yamaha 有点鸡贼,做这个异或运算可能是想防内存 dump,所以在音频输出阶段很可能会再做一次异或运算,等我后续逆向结果
opencode+鸡爪(Ghidra)真好用~
我已经在着手写合成器了,目前是把 MIDI 处理部分的架构搭好了,接下来计划参考 QXGEdit 的代码进一步核实 tbl 文件内置数据结构
希望能最终实现对于 S-YXG50 的完整替代,可以跨平台使用,然后实现对于 S-YXG2006LE 的数据文件兼容,尽可能让这个只支持 XGLite 的软件实现尽可能对 FullXG 的支持(没有的乐器就退回基础乐器了)
后续实现 ALSA/Pipewire/JACK/CoreAudio 的多平台支持
最近跟SysEx干上了,但还好的是,无论是XG还是GS,他们的SysEx 本质上都是在指定内存地址写入数据,也就是说我可以借此了解到这类合成器的内存结构
接下来应该尝试实现对于tbl文件的读取了,但我需要先完成一个通用的内存结构,可以同时兼容S-YXG50和2006LE(甚至是更早期的 S-YXG 100 等)
我最近有点卡住了,根据AI逆向的结果,好像prevoice表是变长结构……这就很难受了
顺带,2006LE的数据结构也是奇葩,也是大量变长结构……
项目地址在这里 https://github.com/madaha-dev/madaha ,有兴趣的也可以一起看看
告诉你一个坏消息:2006LE 的数据文件里面是缺少了部分乐器的,Full XG 真的是“臣妾做不到”了
现在 S-YXG50 已经基本打通了从数据到波形的链路。但是 2006LE 的数据格式十分蛋疼,到目前为止卡在了半路上,目前只能确定里面有哪些乐器,但乐器到最终波形这部分,AI 也蒙圈了,2006LE 在这部分用了间接寻址,很难找函数
S-YXG50 TBL 文件格式分析
概述
Yamaha S-YXG50 软件合成器的音色库文件(.tbl)包含全部乐器定义、波形参数和鼓映射数据。
本文档基于 Ghidra 反编译 + GDB 动态追踪验证,完整还原了 TBL 文件的结构和解析流程。
两个版本:
-
sxgbin41.tbl— 4MB 版本,含 9 个 16-bit 采样 -
sxgbin21.tbl— 2MB 版本,全部 8-bit 采样
1. 文件头部 (0x00–0x63)
偏移 大小 字段
0x00 0x10 Header: "MU50 4MB V2.0" / "MU50 2MB V2.0"
0x10 0x0E 波形文件名: "SXGWAVE4.TBL" / "SXGWAVE2.TBL"
0x1E 0x01 解码标志 (0=加密, 1=已解码)
0x20 0x44 segLength[17] — 17 个 int32 LE, 每个数据段的字节长度
头部之后,17 个数据段从偏移 0x64 开始连续排列。
2. 数据段总表
| seg | 偏移 | 大小 | 类型 | 用途 |
|---|---|---|---|---|
| 00 | 0x000064 | 0x0080 | uint8[128] | GS 鼓组号表 |
| 01 | 0x0000E4 | 0x0080 | uint8[128] | XG 鼓组号表 |
| 02 | 0x000164 | 0x0080 | uint8[128] | XG SFX 鼓组号表 |
| 03 | 0x0001E4 | 0x0080 | uint8[128] | GM2 鼓组号表 |
| 04 | 0x000264 | 0x1F00 | uint16[31][128] | 鼓映射表 |
| 05 | 0x002164 | 0x2490 | DrumData[312] | 鼓参数表 (30B/条) |
| 06 | 0x0045F4 | 0x00AE | uint16[87] | GM2 SFX Program 表 |
| 07 | 0x0046A2 | 0x0080 | uint8[128] | GS Bank MSB → selector |
| 08 | 0x004722 | 0x0080 | uint8[128] | XG Bank MSB → selector |
| 09 | 0x0047A2 | 0x0080 | uint8[128] | XG Bank LSB → selector |
| 10 | 0x004822 | 0x0080 | uint8[128] | XG Melody Voice LSB |
| 11 | 0x0048A2 | 0x1800 | uint16[24×128] | GS 程序表 |
| 12 | 0x0060A2 | 0x3800 | uint16[56×128] | XG 程序表 |
| 13 | 0x0098A2 | 0xFB2A | PreVoiceDef[] | pre-voice 定义表 (基表) |
| 14 | 0x0193CC | 0x5CAE | PreVoiceDef[] | pre-voice 定义表 (扩展) |
| 15 | 0x01F07A | 0x01EC | uint16[246] | 波形偏移索引表 |
| 16 | 0x01F266 | 0x4410 | WaveEntry[1089] | 波形参数表 (16B/条) |
3. 核心数据结构
3.1 Pre-voice 定义 (seg13/seg14)
pre-voice 定义是音色描述的最小单元。每个定义以 2 字节头开始:
偏移 大小 字段
+0 2 header: [bitmask, count_control]
- header[0]: bitmask (不直接用于迭代)
- header[1] bit1: 0→1个元素, 1→2个元素
+2 78×N 元素块 (N = count, 按 index 0,1 访问)
3.2 元素 (Element) — 78 字节
| 偏移 | 大小 | 字段 | 说明 |
|---|---|---|---|
| 0 | 1 | seg15_idx | dataSeg15 索引 (0-245) |
| 1 | 1 | key_center | 音高中心 |
| 2 | 1 | key_span | 音高范围 (匹配: abs(note - center) <= span) |
| 3 | 1 | vel_center | 力度中心 |
| 4 | 1 | vel_span | 力度范围 |
| 5 | 1 | lfo_wave | LFO 波形选择 (mask 0x7) |
| 6 | 1 | vel_threshold | 力度阈值 |
| 7 | 1 | pitch_offset | 音高偏移 (signed) |
| 8 | 1 | vol_offset | 音量偏移 (signed) |
| 9-10 | 2 | pitch_fine | 音高微调 |
| 11-14 | 4 | (未知) | 未确认 |
| 15 | 1 | pitch_mode | 调音模式 (FUN_100140f0 读取) |
| 16 | 1 | range_base | 范围基值 |
| 17 | 1 | mono_mode | 单音模式标志 |
| 18-21 | 4 | sample_slot | 采样槽工作区 |
| 22-77 | 56 | synth_params | 合成参数 (EG/Filter/LFO) |
3.3 WaveEntry (seg16) — 16 字节
| 偏移 | 大小 | 字段 | 说明 |
|---|---|---|---|
| 0 | 1 | velocity | 力度敏感度 (0=最响) |
| 1 | 1 | baseKey | 基准音高 (MIDI key) |
| 2 | 1 | tone | 音分微调 |
| 3-5 | 3 | negOffset | 采样负偏移 (24-bit BE) |
| 6-8 | 3 | loopStart | 循环起始偏移 (24-bit BE, 0=无循环) |
| 9-11 | 3 | baseAddr | 采样基地址 (24-bit BE) |
| 12 | 1 | sampleFlags | bit7=1→8-bit PCM; bit7=0→16-bit LE PCM |
| 13 | 1 | (保留) | 恒为 0 |
| 14 | 1 | keyStart | 键位下限 |
| 15 | 1 | keyEnd | low7=范围上限, bit7=1→末层 |
计算:
startAddr = baseAddr - negOffset // PCM 起始位置
loopPoint = baseAddr + loopStart // 循环折返点
sampleLen = loopPoint - startAddr // 总采样长度
attackLen = baseAddr - startAddr // 攻击段长度 (= negOffset)
loopLen = loopStart // 循环段长度 (= 0 则无循环)
3.4 SXGWAVE 波形文件
PCM 数据存储在 SXGWAVE*.TBL 中,采样率 22050Hz,单声道。
| sampleFlags & 0x80 | 格式 |
|---|---|
| 1 (0x80) | 8-bit 无符号 PCM (中心值 128) |
| 0 (0x00) | 16-bit 小端有符号 PCM (仅 sxgbin41 部分条目) |
4. 程序解析流程
MIDI (bankMSB, bankLSB, program, note, velocity)
│
├─ selector = XGBankMSBTable[bankMSB] // seg08
│ if selector == 1: selector = XGBankLSBTable[bankLSB] // seg09
│
├─ prevoiceIdx = XGProgramTable[selector×128 + program] // seg12
│
├─ byte_offset = prevoiceIdx × 2 // Ghidra: *2 (uint16_t* 指针算术)
│
├─ elementCount = (seg13[byte_offset+1] & 2) ? 2 : 1 // header[1] bit1
│
├─ 按 INDEX 遍历 element[0..count-1]:
│ if abs(note - elem[1]) <= elem[2] → key 匹配
│ if abs(vel - elem[3]) <= elem[4] → vel 匹配
│ 匹配 → 存入 matched[]
│
├─ 对每个匹配 element:
│ ├─ range = MIDI key // FUN_100140f0
│ ├─ init element params // vtable[0x51c]
│ ├─ entry = seg15[elem[0]] // waveform_FUN_10004df0
│ │ while (entry.keyEnd.range < range && !entry.keyEnd.bit7)
│ │ entry++ // 链扫描 (16B 步进)
│ │ return entry // 匹配的 WaveEntry*
│ └─ WaveEntry* → voice // vtable[0x38c]
│
└─ 多个 element → 多个 voice → 混音输出
关键点
-
元素个数:
header[1] bit1 = 0 → 1个; = 1 → 2个 - 元素访问: 按 INDEX 0,1(非 bitmask 顺序)
-
范围匹配:
abs(note - center) <= span(center+span,非传统 min/max) -
链扫描 用 MIDI key 作为 range,按
(keyEnd & 0x7F) < range推进 - 力度分层 在 element 级别实现:不同 vel 范围的 element 指向不同 chain
- 多层叠音: count=2 时两个 element 各自走完整处理链,产生两个 voice,混音输出
5. DrumData (seg05) — 30 字节
| 偏移 | 大小 | 字段 | XG NRPN | 说明 |
|---|---|---|---|---|
| 0 | 1 | pitchCoarse | 0x00 | 粗调音高 (64=±0) |
| 1 | 1 | pitchFine | 0x01 | 微调 |
| 2 | 1 | level | 0x02 | 音量 |
| 3 | 1 | alternateGroup | 0x03 | 交替分组 |
| 4 | 1 | pan | 0x04 | 声像 (64=center) |
| 5 | 1 | reverbSend | 0x05 | 混响发送 |
| 6 | 1 | chorusSend | 0x06 | 合唱发送 |
| 7 | 1 | variationSend | 0x07 | 变化效果发送 |
| 8 | 1 | keyAssign | 0x08 | 0=Single, 1=Multi |
| 9 | 1 | rcvNoteOff | 0x09 | 接收 Note Off |
| 10 | 1 | rcvNoteOn | 0x0A | 接收 Note On |
| 11 | 1 | filterCutOff | 0x0B | 滤波器截止 |
| 12 | 1 | filterResonance | 0x0C | 滤波器共鸣 |
| 13 | 1 | egAttack | 0x0D | EG 起音 |
| 14 | 1 | egDecay1 | 0x0E | EG 衰减1 |
| 15 | 1 | egDecay2 | 0x0F | EG 衰减2 |
| 16 | 1 | isSFXSound | — | 0=SFX, -1=Drum |
| 17 | 1 | sfxSoundID | — | SFX 音色 ID |
| 18 | 1 | baseKey | — | 基准音高 |
| 19-20 | 2 | negOffset | — | 采样负偏移 (16-bit BE) |
| 21 | 1 | (保留) | — | — |
| 22-23 | 2 | loopEnd | — | 循环结束点 (16-bit BE) |
| 24-26 | 3 | baseAddr | — | 采样基地址 (24-bit BE) |
| 27 | 1 | sampleRate | — | 采样率标识 |
| 28-29 | 2 | (未知) | — | 未确认 |
6. 验证与调试
GDB 动态追踪结果(S-YXG50.exe, DLL base 0x10000000):
# Prog 0 (Acoustic Grand Piano), note=60 vel=100
FUN_10017060: elem[0]=37[1]=0[2]=127[3]=1[4]=127 Note[1]=60[3]=100[10]=0
FUN_100140F0: elem[15]=0[16]=60 Note[1]=60[6]=77[10]=0
WAVE: seg15_idx=37 range=60
# MSB=0 LSB=41 Prog=0 (Dream) — 双采样叠层
17000# RET: matchCount=2
MATCH# elem[0]=236 seg15=236
MATCH# elem[0]=37 seg15=37
WAVE# seg15_idx=236 range=60
WAVE# seg15_idx=37 range=60
确认结果:
- Note[1] = MIDI key, Note[3] = velocity, Note[10] = 0 (vel 匹配开启)
- chain 扫描的 range = MIDI key
- matchCount = 匹配的 element 数(最大 2)
- 每个匹配 element 独立创建 voice
7. 解析器
sxgparser.py — Python 解析和提取工具。
# 解析一个音符
python3 sxgparser.py from_veg/sxgbin21.tbl --note 0 0 0 60 100
# 提取 WAV
python3 sxgparser.py from_veg/sxgbin21.tbl --note 0 0 0 60 100 --extract
# 指定叠加层 (Dream: --element 0 或 --element 1)
python3 sxgparser.py from_veg/sxgbin21.tbl --note 0 41 0 60 100 --extract --element 1
# 使用 GS 模式
python3 sxgparser.py from_veg/sxgbin21.tbl --gs --note 0 0 0 60 100
程序匹配率
| TBL | 匹配率 | 格式 |
|---|---|---|
| sxgbin21.tbl | 128/128 | 全部 8-bit
|
| sxgbin41.tbl | 128/128 | 119×8-bit + 9×16-bit
|
叠层乐器 (50/128 程序)
多个 element 叠层的乐器。用 --element N 选择提取哪层。
| Prog | 名称 | Layer 0 | Layer 1 |
|---|---|---|---|
| 7 | Clavinet | 0x0D432A |
0x06C727 |
| 10 | Music Box | 0x04522D |
0x0B677F |
| 18 | Rock Organ | 0x04965B |
0x0481BE |
| 39 | Synth Bass 2 | 0x0AFD7D |
0x02F8CA |
| 55 | Orchestra Hit | 0x1808F5 |
0x14C707 |
| 62 | SynthBrass 1 | 0x173B53 |
0x055D82 |
| 68 | Oboe | 0x0D675A |
0x0D6836 |
| 76 | Blown Bottle | 0x0163EE |
0x0B7AEB |
| 77 | Shakuhachi | 0x03EBA1 |
0x1D9F8C |
| 80 | Lead 1 | 0x055A44 |
0x055D82 |
| 82 | Lead 3 | 0x03EBA1 |
0x1AD9C6 |
| 84 | Lead 5 | 0x0D5650 |
0x0A1AF9 |
| 88 | Pad 1 | 0x0B677F |
0x1B5C8A |
| 91 | Pad 4 | 0x1B5C8A |
0x16FCCA |
| 94 | Pad 7 | 0x1AD9C6 |
0x0B06C2 |
| 100 | FX 5 | 0x1AD9C6 |
0x0CFB16 |
| 101 | FX 6 | 0x0B06C2 |
0x1E74B7 |
| 114 | Steel Drums | 0x0B582D |
0x1EB938 |
8. 致谢
- Ghidra SRE — 静态分析
- GDB + Wine — 动态追踪验证
- Yamaha S-YXG50 — 优秀的软件合成器
- TBL解析工具
现在S-YXG50我觉得基本上应该是通了,那么接下来我应该考虑去解决S-YXG2006LE了
如果能将这个也解析清楚,那么Madaha最难的部分基本上已经过去了
效果器什么的,原理基本上差不太多,根据 XG Spec写就是了
我不得不吐槽一下 S-YXG2006LE 的数据格式,搞了一个什么鬼的树结构……直接把AI干烧了
逆向笔记:Yamaha S-YXG2006LE sxgbnw6l.tbl 文件格式
注意:以下内容为AI生成,可能存在错误,我已经调教了很久,才达到现在这个效果
前言
Yamaha S-YXG2006LE 是 Yamaha 开发的 XGLite 标准 VST 合成器。其音色参数存储在 sxgbnw6l.tbl (716,608 B) 中,格式标记为 UTG VPRM。波形数据文件 (xgdat6l.tbl) 不在本文讨论范围。
方法
- 静态分析: Ghidra 反编译 S-YXG2006LE.DLL (x86 PE, 基址 0x10000000)
-
运行时验证: Linux + Wine, VST 宿主 MidiPlayer6, 通过
/proc/PID/mem读取进程内存确认关键指针和数据结构
文件全局布局
| 偏移 | 大小 | 说明 |
|---|---|---|
| 0x0000 | 0x0018 | 头部 ASCII: "UTG VPRM 06 07 28 15 28\0"
|
| 0x0018 | 0x0028 | 段偏移表: 10×uint32, [7]=[9]=0xFFFFFFFF
|
| 0x0040 | 0x4000 | Section 0: bank 映射表 byte[bankMSB×128+bankLSB] → bank_group
|
| 0x4040 | 0x5700 | Section 1: 索引表 ushort[bank_group×128+prog] → sec2_idx
|
| 0x9740 | 0xBD8 | Section 2: 描述符偏移表 uint32[754]
|
| 0xA308 | 0x28BE0 | Section 2: 描述符数据区 |
| 0x32EE8 | 0x489A4 | Section 3: 波形参数 (内部偏移表 + offset_table + 树节点 + per-note数据) |
| 0x7B88C | 0x00200 | Section 4: keyzone 调制表 4×128B
|
| 0x7BA8C | 0x21400 | Section 5: velzone 调制表 1064×128B
|
| 0x9CE8C | 0x09B64 | Section 6: Seg5 波形参数表 1989×20B
|
| 0xA69F0 | 0x04550 | Section 7: 鼓定义 |
段偏移表位于 params+0x18,DLL 通过 *(params+0x24)、*(params+0x28) 等访问各段基址。
运行时验证: params 地址通过搜索进程内存中 "UTG VPRM" 签名定位,确认其内容与文件完全一致。
描述符 (乐器定义)
查找链
bankMSB + bankLSB → Sec0[bankMSB×128+bankLSB] = bank_group (byte, 0xFF=空)
→ Sec1[bank_group×128+prog] = sec2_idx (ushort, 0xFFFF=空)
→ Sec2[sec2_idx] = 描述符绝对文件偏移 (uint32)
→ 描述符数据
DLL 函数: sec1_sec2_instr_descriptor_lookup @ 0x1002C8B0。
描述符头部 (40B)
| 偏移 | 大小 | 说明 |
|---|---|---|
| +0x00 | uint16 | 固定 0x0000
|
| +0x02 | uint16 | 类型标记: 0x0001=TYPE0, 0x0002=TYPE1 |
| +0x04 | uint16 | 哨兵 0xFFFF
|
| +0x06 | uint16 | drum_kit: 0xFFFF=旋律, 0-N=鼓 |
| +0x08 | uint16 | 固定 0xFFFF
|
| +0x0A-0x0D | 保留 | |
| +0x0E | uint16 |
[7:0]=group_id, [15:8]=type_marker (0x31) |
| +0x10 | uint16 |
[7:0]=flags, [15:8]=zone_count |
| +0x12 | uint16 |
[7:0]=fine_tune, [15:8]=root_note |
| +0x14 | uint16 |
[7:0]=param_type, [15:8]=volume |
| +0x16 | uint16 |
[7:0]=pan, [15:8]=0x40 |
| +0x18 | uint32 | 哨兵 |
| +0x1C | uint16 | EG_param |
| +0x1E-0x27 | 保留/固定值 |
ZoneBlock (16B)
紧接头部的 N 个 ZoneBlock,每 zone 一个:
| 偏移 | 大小 | 说明 |
|---|---|---|
| +0x00 | uint32 | wave_id — 波形 ID |
| +0x04 | uint8 | keyzone_idx — Section 4 块索引 |
| +0x05 | uint8 | velzone_idx — Section 5 块索引 |
| +0x06 | uint8 | velMax |
| +0x07 | uint8 | flags |
| +0x08 | int32 | 固定 0x00004000 或 0xFFFFFFFF
|
| +0x0C | uint32 | 哨兵 0xFFFFFFFF
|
Per-zone 额外数据 (16B/zone)
ZoneBlock 段之后紧接每 zone 的额外数据,这是 Seg5 索引的真正来源:
| 偏移 | 大小 | 说明 |
|---|---|---|
| +0x00 | uint32 | (seg5_idx << 16) \| param_word |
| +0x04 | uint32 | 固定 0x00004000
|
| +0x08 | uint32 | 哨兵 0xFFFFFFFF
|
| +0x0C | uint32 | 表引用 |
运行时验证: 读取进程内存中所有 141 个 waveform entries,确认 field14=params,seg5_get_type1_wid_based 使用 f1_lo = per-zone 高16位。126/128 程序 seg5_idx=383 (0x017F),余下 drum/effects 为 1 或 2。
Seg5 波形参数表 (Section 6)
位置: params + 0x9CE8C。1989 条 × 20B。
| 偏移 | 大小 | 说明 |
|---|---|---|
| +0x00 | uint32 | loopStart — 循环起点 |
| +0x04 | uint32 | negOffset — attack 长度 |
| +0x08 | uint32 | alloc_size — PCM 段大小 |
| +0x0C | uint32 | loopEnd — 循环长度 |
| +0x10 | uint32 | flags: [7:0]=fine_tune, [15:8]=root_note |
寻址公式
DLL 函数 seg5_get_type1_wid_based @ 0x1002C750:
int seg5_get_type1_wid_based(int entry, uint f1_lo) {
if (-1 < (char)(f1_lo >> 8)) {
// 路径 A: f1_lo 高字节 < 0x80
return *(*(entry + 0x2c) + 0x30) // offset_table[6] = 0x9CE8C
+ (f1_lo & 0xFFFF) * 0x14 // seg5_idx × 20
+ *(entry + 0x2c); // + params
}
if ((f1_lo & 0xf000) == 0xc000) {
// 路径 B: 未在标准音色中触发
return *(*(entry + 0x34) + 0x243c) + (f1_lo & 0xfff) * 0x14;
}
// 路径 C: 未在标准音色中触发
return *(*(entry + 0x24) + 0x10) + (f1_lo & 0x7fff) * 0x14 + *(entry + 0x24);
}
其中 entry+0x2c = field14 = params 基址。*(entry+0x2c) + 0x30 = *(params + 0x30) = 0x9CE8C。
简化:
Seg5[seg5_idx] 地址 = params + 0x9CE8C + seg5_idx × 20
逆向确认: 通过 DLL 反编译 + 运行时内存读取 Seg5[127] 和 Seg5[383] 验证偏移量公式。
Waveform entry (运行时内存结构)
stride 0x38 (56B), 共 141 个条目。waveform_table_init @ 0x1002B220 中创建。
+0x00..+0x23: 函数指针表 (9×uint32)
+0x14: seg5_get_type1_wid_based
+0x20: tree_vtab20_sec3_ptr_get
+0x24: null (0)
+0x28: filetype (uint32: 1 或 2)
+0x2c: field14 = params_ptr
运行时验证: 进程内存中读取到 140 个 filetype=1 entries, 1 个 filetype=2, 全部 field14 = 同一个 params 地址。
Section 3 — 树遍历
Section 3 开头 40B = 10 个内部偏移:
ioff = [0x38D68, 0x38E18, 0x38E34, 0x38E50, 0x38E6C,
0x38E88, 0x38EA4, 0x38EC0, 0x38F4C, 0x38F68]
Section 3+0x28 = offset_table[6048] (uint32)。每 wave_id 一条,两个用途:
- per-note block:
data[sec3 + offset_table[wid] + note]— 取该 note 的 PCM block 号 - 树遍历入口
树节点 (28B)
Section 3 内部偏移指向的树节点:
+0x00: uint32 f0
+0x04: uint32 f1
+0x08: uint32 f2
+0x0C: uint32 f3 (byte[0] bit7 = 分支方向)
+0x10: 4B 左子节点 (大端序)
+0x14: uint32
+0x18: 4B 右子节点 (大端序)
遍历算法
tree_traverse_wave_id_to_seg5 @ 0x1002C320:
首次迭代: 入口节点地址直接使用
每次迭代:
1. 方向: f3.byte[0] & 0x80 → 1=左子(+0x10), 0=右子(+0x1C)
2. 子值: 4字节大端序 & 0x7FFFFFFF
3. 叶子判断: 子值.byte[0] < 0x80 (有符号无符号均可)
叶子 → 返回子值指针; 否则继续
4. 解析下一节点:
tree_vtab20_sec3_ptr_get @ 0x1002C910:
bit30=0: params + *(uint32*)(params + sec3_off + 子值×4)
bit30=1: *(entry+0x24) + *(*(entry+0x24)+4) + (子值&0x3FFFFFFF)×0xB0
逆向确认: DLL 代码反编译。但该遍历到 Seg5 索引的完整映射链路尚未在运行时验证。
Section 4/5 — 调制参数
Section 4: 4块 × 128B, Section 5: 1064块 × 128B。值范围 0-255,为连续调制值(非开关)。
示例: Grand Piano Zone 0 的 keyzone 从 note=0 的值 10 平稳递增到 note=127 的值 65。对应 Zone 的 velzone 从 vel=0 的值 126 平滑递减到 vel=127 的值 0。
精确 DSP 含义不确定: 所有 zone 在匹配的 note+velocity 下同时激活,keyzone/velzone 值控制音量/滤波/EG 参数。其具体算法需要分析渲染循环。
Zone 模型: 声音叠加
- 一个乐器描述符有 N 个 ZoneBlock (Grand Piano = 16)
- 对给定 (note, velocity),所有满足
kz[note] != 0 && vz[vel] != 0的 zone 同时激活 - Grand Piano note=60, vel=100: 10 个 zone 同时激活,各自有独立 wave_id
- 各 zone 共用同一 seg5_idx (同组波形几何参数)
- velzone 值平滑变化,无切换式门限 → 对应听感上的"仅音量变化"
确认: 通过文件数据分析 128 个程序的 per-zone seg5_idx 全部归一(383/1/2 三组),velzone 值呈连续分布。
当前实现的限制: parser 仅返回第一个非零 zone,未实现多 zone 叠加。
PCM 偏移
当 per-note block 值 block > 0 时:
PCM 样本偏移 = (block × 24092 + 11) // 22
当 block = 0 (波表模式) 时:
攻击段: sample_start = Seg5[seg5_idx].loopStart - Seg5[seg5_idx].negOffset
循环段: loop_start = Seg5[seg5_idx].loopStart
loop_end = Seg5[seg5_idx].loopStart + Seg5[seg5_idx].loopEnd
常数 24092 和 22 未在 DLL 代码中找到为字面常量,为经验推导。公式对所有 128 程序验证通过,但推导方式为猜测。这些常数可能来自编译器优化后的魔数乘法,或在波形解码阶段隐含确定。
验证总结
| 项目 | 状态 | 方法 |
|---|---|---|
| 段偏移表 | 确认 | DLL 代码 + 运行时 params 读取 |
| 查找链 (Sec0→1→2→描述符) | 确认 | DLL 代码 |
| 描述符头部结构 | 确认 | DLL 代码 |
| ZoneBlock 结构 | 确认 | DLL 代码 + 运行时验证 |
| Per-zone 额外数据 | 确认 | 文件分析 |
| Seg5 寻址公式 | 确认 | DLL 代码 + 运行时 Seg5 读取 |
| field14 = params | 确认 | 运行时 waveform entry 遍历 |
| Waveform entry 结构 | 确认 | 运行时内存读取 |
| Zone 模型 (叠加) | 确认 | per-zone seg5_idx 一致性 + velzone 连续性 |
| 树遍历算法 | 逆向确认 | DLL 代码, 未在运行时验证完整映射 |
| keyzone/velzone DSP 含义 | 不确定 | 需进一步分析渲染循环 |
| PCM 偏移公式常数 | 猜测 | DLL 中未找到字面量 |
| 路径 B/C | 未测试 | 标准 GM 音色未触发 |
| Section 7 鼓定义 | 未分析 |
我目前在尝试实现S-YXG50的功能了,2006LE 因为代码比较复杂,这个等后续处理
不过万幸的是,2006LE 有 mac 版本,甚至还带了调试符号……(Yamaha也有马大哈时刻)
报告一下进度
- S-YXG50 的读取部分完成了
- MIDI处理线程基本上完成了
- 正在实现 Tone Generator 部分,目前在实现 Oscillator 部分,这部分是产生声音的
官方文档里面对于该部分的描述:
%% Tone Generator 的结构图,这个是 Mermaid 语法
flowchart LR
subgraph Main
direction LR
%% 音频串行链路 (主音频流)
Osc[Oscillator] --> LPF
LPF --> HPF
HPF --> Amp[Amplifier]
Amp --> EQ
EQ --> Pan
end
Pan --> Out((Output))
%% 可选模块组 (对应图中的 Option 括号)
subgraph Opt1["Option"]
direction TB
CutoffHPF[Cutoff]
CutoffHPF --> HPF
end
subgraph Opt2["Option"]
direction TB
EQC[EQ]
EQC --> EQ
end
subgraph Params
direction LR
subgraph OscParams
direction TB
PEG["PEG<br>Key On<br>Initial Level<br>Attack Time<br>Release Time<br>Release Level<br>Key Off"]
Port["Portamento"]
PitchBend["Pitch Bend"]
Pitch["Pitch"]
Delay["Delay"]
end
%% 滤波控制信号 (Filter)
subgraph LPFParams
direction TB
Cutoff["Cutoff"] --> LPF
FEG["FEG<br>Key On<br>Attack Time<br>Decay Time<br>Release Time<br>Key Off"] --> LPF
Res["Resonance"] --> LPF
end
%% 音量控制信号 (Volume)
subgraph AmpParams
AEG["AEG<br>Key On<br>Attack Time<br>Decay Time<br>Sustain Level<br>Release Time<br>Key Off"]
Volume["Volume"]
end
%% 声像控制信号 (Pan)
subgraph PanParams
PanCtrl["Pan"]
end
LFO["LFO (~)"] --> Delay
LFO --> LPF
LFO --> Amp
end
%% 音高控制信号汇聚 (Pitch)
PEG --> Osc
Port --> Osc
PitchBend --> Osc
Pitch --> Osc
%% LFO 输出,分配到多个模块
Delay --> Osc
%% 音量控制信号 (Volume)
AEG --> Amp
Volume --> Amp
%% 声像控制信号 (Pan)
PanCtrl --> Pan
现在的进度
Tone Generator 我让 AI 帮我实现了(不知道实现效果如何)
接下来把完整的混音管线实现了,基本上就可以开始测试了
希望能成功
在AI的帮助下,现在madaha可以播放钢琴曲了
控制器/RPN/NRPN等我还没测试,鼓和SFX我还完全没动,但是总体看起来是有希望了
我录了一段,可以听听 (48000Hz,快速Sinc插值)
https://pan.baidu.com/s/1HWaNGpda4p4trmK9z3fwag?pwd=2333
由于deepseek涨价,后续开发会慢下来了……不过目前还是能播放钢琴曲的
软件版的SYXG50和MU50真机实际上差的还是不少
加上本贴主要对SYG软件全部重构,或许用处不大(目测)
mame的mu50值得参考的是:他们逆向了整个swp00的meg模块,所有的效果器已经全部是纯cpp代码实现而不是vliw解释器,此外,swp00的awm2对于采样的envelope等操作也值得借鉴

