Yamaha S-YXG 50 逆向记录

Yamaha S-YXG50 逆向工程架构文档

以下是我烧掉 150 亿 token 的结果
因为是 AI 分析的,可能有不准确的地方

目录

  1. 概述
  2. 程序架构
  3. MIDI 到音频的完整数据流
  4. 效果器链运作机制
  5. 核心效果器算法
  6. 关键数据结构
  7. 音色表系统
  8. 附录

概述

Yamaha S-YXG50 是 Yamaha 公司于 2003 年发布的一款软件波表合成器,以 DLL 形式存在,主要用作 VST 插件。它的核心功能是接收 MIDI 音乐信号,实时合成出高质量的音频输出。

支持的 MIDI 标准:

  • GM(通用 MIDI):最基础的 MIDI 标准,定义了 128 种乐器和一套鼓组
  • GS(Roland 标准):GM 的扩展,增加了更多音色和鼓组
  • XG(Yamaha 标准):Yamaha 自己的扩展标准,支持更丰富的音色编辑和效果器控制

基本能力:

  • 同时处理 16 个 MIDI 通道,每个通道可以演奏不同的乐器
  • 最大 128 复音,即同时可以有 128 个音符在发声
  • 内置三组效果器:混响、合唱、变奏
  • 44.1kHz 采样率、立体声浮点音频输出
  • 基于波表的音色系统,音色数据存储在外部文件中

程序架构

整体结构

S-YXG50 采用分层架构设计,从上到下分为四个主要层次:

第一层:VST 插件外壳(CVstEffect)

这是与宿主程序(如 Cubase、FL Studio)对接的最外层。它负责接收宿主发来的各种消息,比如"打开插件"、“关闭插件”、“处理 MIDI 事件”、"设置采样率"等。这一层本身不做任何音频处理,只是一个翻译官,把宿主的消息翻译成内部引擎能理解的指令。

CVstEffect 内部持有一个 CSynthEngine 实例,所有实际工作都委托给它完成。

第二层:合成器核心引擎(CSynthEngine)

这是整个合成器的大脑。它协调各个子系统的工作:

  • 持有 MIDI 处理器,负责解析 MIDI 消息
  • 持有声音管理器,负责分配和回收声音资源
  • 持有效果处理器,负责给音频加效果
  • 持有音色表,存储所有乐器的音色数据

每当宿主请求一帧音频时,CSynthEngine 就会指挥各个子系统协同工作,最终输出立体声采样。

第三层:功能子系统

这一层包含四个独立的子系统,各司其职:

MIDI 处理器(CMidiHandler):接收原始 MIDI 字节流,解析出有意义的音乐事件。比如它能识别出"第 5 通道按下中央 C,力度 100"这样的指令,并通知声音管理器去触发对应的声音。

声音管理器(CVoiceManager):管理一个包含 128 个声音槽的池子。当收到 Note On 指令时,它会从池子里找一个空闲的声音槽,初始化它的参数,开始播放。当收到 Note Off 指令时,它会让对应的声音进入释放状态。每个声音槽都是一个独立的 DSP 处理单元,有自己的振荡器、包络、滤波器等。

效果处理器(CEffectsProcessor):对所有声音混合后的音频进行效果处理。它包含混响、合唱、变奏三组效果器,每组都可以独立设置类型和参数。

音色表管理器(CVoiceTable):负责加载和管理音色数据文件。它读取 sxgbin41.tbl 中的音色参数(比如用什么波形、包络怎么设置、滤波器怎么调)和 sxgwave4.tbl 中的实际波形采样数据。

第四层:DSP 处理模块

这是最底层的信号处理库,提供各种基础的音频处理功能:

  • 振荡器:生成或读取波形数据
  • 滤波器:改变音频的频率特性
  • 包络发生器:控制声音的音量随时间变化
  • 混响效果器:模拟空间反射,让声音有空间感
  • 合唱效果器:通过延迟调制让声音更丰满
  • 混音器:将多个声音混合成最终的立体声输出

源代码组织

源代码按照功能模块组织在不同的目录中:

include/ 目录存放所有头文件,定义了核心数据结构和接口。最重要的包括 types.h(基础类型)、engine.h(合成器引擎)、voice.h(声音对象)、effects.h(效果器接口)、voice_table.h(音色表)。

midi/ 目录存放 MIDI 消息处理相关的代码。核心文件 midi_processing.cpp 负责解析各种 MIDI 消息,包括 Note On/Off、控制变化、程序变化、系统专属消息等。

voice/ 目录存放声音引擎相关的代码,是整个项目中文件最多、代码量最大的部分。voice_engine.cpp 是声音引擎的核心,voice_rendering.cpp 处理声音的渲染,voice_param.cpp 管理声音参数,voice_state.cpp 管理声音的状态转换(激活、释放、静音等)。

effects/ 目录存放效果器相关的代码。effects_algorithms.cpp 实现了各种效果器算法,effects_params.cpp 处理效果器参数的设置和读取。

dsp/ 目录存放底层 DSP 处理代码。oscillator.cpp 实现波形生成,filters.cpp 实现各种滤波器,envelope.cpp 实现包络发生器,reverb.cpp 实现混响,chorus.cpp 实现合唱,mixer.cpp 实现混音。

channel/ 目录存放通道管理相关的代码。每个 MIDI 通道都有自己的参数状态(音量、声像、弯音等),这些代码负责管理它们。

audio/ 目录存放音频参数管理和命令分发的代码。audio_params.cpp 是最大的单个文件,包含了 DSP 处理器的大部分实现。

xg/ 目录存放 XG/GS 兼容层的代码。这些代码确保 S-YXG50 能正确响应各种 XG 和 GS 专属的 MIDI 消息和系统专属指令。

mfc/ 目录存放 MFC(Microsoft Foundation Classes)相关的工具函数。原始 DLL 使用了 MFC 框架,这些函数处理内存管理、字符串操作等基础功能。

core/ 目录存放 DLL 入口点、运行时工具等核心功能。


MIDI 到音频的完整数据流

总体流程

从 MIDI 信号进入 DLL 到最终输出音频,需要经过以下主要阶段:

  1. MIDI 事件接收
  2. MIDI 消息解析
  3. 通道参数更新
  4. 声音触发与分配
  5. 声音初始化
  6. 波形渲染
  7. 效果器处理
  8. 混音输出

阶段一:MIDI 事件接收

宿主程序通过 VST 接口将 MIDI 事件传递给插件。这些事件被打包在一个事件数组中,每个事件包含事件类型、时间戳和 MIDI 数据字节。

S-YXG50 在音频处理循环的开始处检查是否有待处理的 MIDI 事件。如果有,就逐个取出处理。

阶段二:MIDI 消息解析

对于每个 MIDI 事件,系统首先解析它的状态字节(第一个字节的高四位),确定这是什么类型的消息:

Note On(音符开启):表示某个键被按下。数据包含通道号、音符号(0-127,对应钢琴的 88 个键)和力度(按键的力度,0-127)。力度为 0 的 Note On 等同于 Note Off。

Note Off(音符关闭):表示某个键被释放。数据包含通道号和音符号。

Control Change(控制变化):表示某个控制器的值发生了变化。常见的控制器包括音量(CC#7)、声像(CC#10)、调制轮(CC#1)、延音踏板(CC#64)等。每个 CC 消息包含控制器编号和新的值。

Program Change(程序变化):表示切换乐器音色。数据包含新的音色编号(0-127)。

Pitch Bend(弯音):表示弯音轮的位置变化。弯音值是一个 14 位的数(0-16383),中间值 8192 表示没有弯音。

System Exclusive(系统专属):用于厂商特定的扩展功能。S-YXG50 通过系统专属消息接收 XG、GS、GM 的各种高级设置。

阶段三:通道参数更新

每个 MIDI 通道都有自己的参数状态,包括音量、声像、弯音、调制深度、滤波器截止频率、包络参数等。当收到控制变化消息时,系统会更新对应通道的对应参数。

这些参数存储在全局参数块中,供后续的声音渲染使用。比如音量参数会影响声音的响度,声像参数会影响声音在左右声道的分布,弯音参数会影响声音的音高。

阶段四:声音触发与分配

当收到 Note On 消息时,声音管理器需要为这个音符分配一个声音槽。声音池共有 128 个声音槽,每个 MIDI 通道最多可以同时使用 32 个声音。

分配过程如下:

  • 首先在对应通道的声音槽范围内寻找空闲的槽位
  • 如果找到空闲槽位,直接使用
  • 如果没有空闲槽位,需要"偷取"一个正在发声的槽位(通常选择最早开始发声的)

分配成功后,声音槽被标记为"激活"状态。

阶段五:声音初始化

声音槽分配好后,需要初始化它的各种参数:

音色参数加载:从音色表中查找对应的音色数据。查找时需要考虑当前通道选择的乐器编号、Bank Select(音色库选择)的 MSB 和 LSB 值。如果是鼓组通道(第 10 通道),则从鼓组数据中查找。

振荡器初始化:设置波形数据的起始地址、循环点位置、采样率等。如果波形采样率与输出采样率不同,还需要设置采样率转换参数。

包络初始化:根据音色数据中的包络参数,计算 Attack、Decay、Sustain、Release 各阶段的速率和目标值。

滤波器初始化:根据音色数据中的滤波器参数,计算滤波器的截止频率和共振值。

增益初始化:根据力度、音量、声像等参数,计算初始的左右声道增益值。

阶段六:波形渲染

声音初始化完成后,进入波形渲染阶段。系统以 128 采样为一个处理块,逐块进行渲染。

波形读取:振荡器从波形数据中读取采样值。读取速率由音高决定——音高越高,读取速率越快,波形被"压缩",频率升高。由于读取速率通常不是整数,需要使用线性插值来计算中间值,保证声音平滑。

循环处理:大多数乐器音色都有循环区域。当读取位置到达循环结束点时,会跳回循环起始点继续读取,这样声音可以持续任意长时间。有些音色是单次播放(如钢琴),到达终点后就停止。

包络应用:包络发生器根据当前所处的阶段(Attack、Decay、Sustain、Release)计算一个音量因子,乘到波形数据上。Attack 阶段音量从 0 上升到最大值;Decay 阶段从最大值下降到 Sustain 电平;Sustain 阶段保持在 Sustain 电平;Release 阶段从当前电平下降到 0。

S-YXG50 为每个声音维护三组包络:主包络控制整体音量,合唱发送包络控制送往合唱效果器的信号量,混响发送包络控制送往混响效果器的信号量。

滤波器应用:双二阶滤波器(Biquad Filter)改变音频的频率特性。最常用的是低通滤波器,它让低频通过而衰减高频,使声音变得更暗、更柔和。滤波器的截止频率可以由 MIDI CC#74(Brightness)控制。

增益与声像:根据力度、通道音量、表情、声像等参数,计算最终的左右声道增益。声像控制声音在左右声道的分布——声像值为中间时左右声道音量相等,偏左时左声道更响,偏右时右声道更响。

输出到缓冲区:渲染好的采样数据被写入多个缓冲区——主输出缓冲区(左右声道)、合唱发送缓冲区、混响发送缓冲区。这些缓冲区将在后续的效果器处理阶段使用。

阶段七:效果器处理

当所有活跃的声音都渲染完成后,系统将它们的输出混合在一起,然后送入效果器链处理。效果器处理的详细机制在下一章描述。

阶段八:混音输出

效果器处理完成后,进入最终的混音输出阶段:

效果返回混合:将效果器处理后的信号(混响返回、合唱返回、变奏返回)与干信号混合在一起。

主音量应用:将混合后的信号乘以主音量系数。

限幅处理:防止信号超出有效范围。如果采样值超过最大值(131071)或低于最小值(-131072),就将其限制在范围内,避免削波失真。

输出到宿主:将最终的立体声音频数据写入宿主提供的输出缓冲区。


效果器链运作机制

效果器架构概述

S-YXG50 的效果器系统由三组独立的效果器组成:混响(Reverb)、合唱(Chorus)、变奏(Variation)。每组效果器可以独立设置类型和参数。

效果器的信号流如下:

每个声音渲染完成后,它的输出被分成多个部分:

  • 主输出:直接送到最终混音器
  • 混响发送:送到混响效果器
  • 合唱发送:送到合唱效果器
  • 变奏发送:送到变奏效果器

每组效果器处理完后,输出信号被送回最终混音器,与干信号混合。发送量由 MIDI CC 控制(CC#91 控制混响发送,CC#93 控制合唱发送,CC#94 控制变奏发送)。

混响效果器

混响模拟声音在空间中的反射效果,让声音听起来像是在某个房间里演奏。

支持的混响类型:

  • Hall(大厅):模拟音乐厅的大空间,混响时间长,声音温暖丰满
  • Room(房间):模拟普通房间,混响时间较短,声音紧凑
  • Stage(舞台):模拟舞台环境,声音明亮清晰
  • Plate(板式):模拟金属板混响器,声音明亮,衰减均匀

可调参数:

  • 混响时间:声音衰减到听不见所需的时间
  • 阻尼:高频衰减的速度,阻尼越大高频衰减越快,声音越暗
  • 扩散:反射的密度,扩散越大声音越平滑
  • 预延迟:直达声与第一次反射之间的时间差

合唱效果器

合唱效果通过将信号延迟并调制,产生多个略有差异的声音叠加的效果,让单个乐器听起来像是多个乐器在同时演奏。

支持的合唱类型:

  • Chorus(合唱):标准合唱效果,延迟时间较长(10-30ms),没有或很少反馈
  • Flanger(镶边):延迟时间很短(1-10ms),有较高的反馈,产生"喷气机"效果
  • Symphonic(交响):多个延迟线叠加,产生更丰满的合唱效果

工作原理:

  • 输入信号被写入一个延迟缓冲区
  • 一个低频振荡器(LFO)不断调制读取位置,使得延迟时间在一定范围内周期性变化
  • 读取出来的信号与原始信号混合,产生音高微调的效果
  • 左右声道使用不同相位的 LFO,产生立体声扩展效果

变奏效果器

变奏效果器是一个通用的效果器插槽,可以加载各种不同类型的效果。支持的效果类型非常丰富:

调制类效果:

  • Chorus/Flanger/Symphonic:与合唱效果器类似
  • Phaser(相位器):使用全通滤波器链产生梳状滤波效果
  • Tremolo(颤音):周期性调制音量
  • Auto Pan(自动声像):周期性调制声像位置

空间类效果:

  • Delay/Echo(延迟/回声):将信号延迟后与原始信号混合
  • Early Reflections(早期反射):模拟房间的前几次反射
  • Gate Reverb(门混响):混响突然截止的效果

失真类效果:

  • Distortion(失真):硬削波,产生强烈的谐波失真
  • Overdrive(过载):软削波,产生温暖的失真效果
  • Amp Simulator(音箱模拟):模拟吉他音箱的音色

动态类效果:

  • Compressor(压缩器):减小动态范围,让响的和轻的部分差距变小
  • Limiter(限幅器):防止信号超过阈值

频率类效果:

  • 3-Band EQ / 2-Band EQ:多段均衡器,可以调整不同频段的音量
  • Auto Wah(自动哇音):周期性调制滤波器截止频率

其他效果:

  • Rotary Speaker(旋转扬声器):模拟 Leslie 音箱的旋转效果
  • Pitch Change(变调):改变音高
  • Karaoke(卡拉 OK):消除人声

效果参数管理

效果器的参数通过一个专门的状态结构来管理。这个结构存储了所有效果器的当前设置,包括每种效果器的类型编号和各个参数值。

参数值通常是 0-127 范围的整数,对应 MIDI 控制器的值范围。有些参数使用 16 位编码,可以表示更精细的控制。


核心效果器算法

混响算法

S-YXG50 的混响采用经典的 Schroeder 混响结构,这是一种在数字音频领域广泛使用的混响算法。

信号处理流程:

  1. 预延迟:输入信号首先经过一个短延迟(5-20ms)。这模拟了从声源到墙壁的首次反射的时间差,增加了"空间感"。预延迟越长,听起来空间越大。

  2. 早期反射:接下来模拟房间的前几次反射。这些反射是离散的回声,每个回声有不同的延迟时间和音量。早期反射帮助听觉系统判断空间的大小和形状。

  3. 梳状滤波器组:这是混响的核心部分。8 个并联的梳状滤波器,每个都有不同的延迟长度(大约 30-45ms)。梳状滤波器的工作原理是将信号延迟后与原始信号混合,产生一系列共振峰。多个不同延迟长度的梳状滤波器并联,产生的共振峰相互补充,形成密集的混响尾音。每个梳状滤波器都有反馈(控制混响时间)和阻尼(控制高频衰减)。

  4. 全通滤波器组:4 个串联的全通滤波器。全通滤波器的特点是让所有频率都通过,但会改变相位。这一步的目的是增加混响的"扩散感",让声音更平滑,减少梳状滤波器带来的"金属感"。

  5. 阻尼处理:根据阻尼参数调整高频的衰减速度。阻尼越大,高频衰减越快,混响听起来越"温暖"、越"暗"。这模拟了真实房间中声波被墙壁材料吸收的特性。

不同混响类型的差异:

大厅混响有较长的混响时间(约 0.7 秒),较高的扩散和密度,预延迟约 20ms,适合营造宏大的空间感。

房间混响混响时间较短(约 0.3-0.5 秒),扩散和密度中等,预延迟约 5-10ms,适合营造紧凑的房间感。

板式混响扩散很高,声音明亮均匀,没有明显的早期反射,适合人声和乐器的混响处理。

合唱算法

信号处理流程:

  1. 延迟线:输入信号被写入一个循环缓冲区。缓冲区长度由最大延迟时间决定(10-30ms)。

  2. LFO 调制读取位置:一个低频振荡器(通常是正弦波或三角波)不断改变读取位置。这使得读出的信号相对于写入的信号有周期性的时间偏移,从而产生音高微调效果。

  3. 反馈(可选):部分输出信号反馈回输入,增加效果的"厚度"。合唱模式通常没有或很少反馈,镶边模式有较高的反馈(0.7-0.9)。

  4. 立体声扩展:左右声道使用相位差 90 度的 LFO,使得左右声道的调制不同步,产生宽广的立体声效果。

  5. 混合:将调制后的信号与原始信号按一定比例混合。

合唱与镶边的区别:

合唱的延迟时间较长(10-30ms),LFO 速率较慢(0.1-5Hz),没有或很少反馈,产生的效果是声音变得更丰满、更宽广。

镶边的延迟时间很短(1-10ms),LFO 速率更慢(0.1-2Hz),有较高的反馈,产生的效果是那种典型的"喷气机"声音,带有明显的共振峰。

延迟算法

信号处理流程:

  1. 输入信号被写入延迟线
  2. 延迟线的输出按反馈系数送回输入
  3. 干信号和延迟信号按混合比例混合后输出

延迟时间可以从 1 毫秒到 1 秒以上。反馈系数控制重复次数——0% 时只有一个回声,接近 100% 时回声会重复很多次逐渐衰减。

相位器算法

信号处理流程:

  1. 输入信号通过 4-12 个串联的全通滤波器
  2. LFO 调制全通滤波器的中心频率
  3. 部分输出信号反馈回输入
  4. 调制后的信号与原始信号混合

全通滤波器会改变信号的相位但不改变幅度。当多个全通滤波器串联时,某些频率的相位变化会相互抵消,而另一些频率会增强或减弱,形成梳状滤波效果。LFO 不断改变这个梳状滤波的频率位置,产生动态的"嗖嗖"声。

颤音与自动声像算法

颤音:LFO 直接调制信号的音量。当 LFO 值大时音量大,LFO 值小时音量小,产生周期性的音量波动。

自动声像:LFO 调制信号在左右声道的分配。当 LFO 值大时声音偏右,LFO 值小时声音偏左,产生声音在左右之间移动的效果。

失真与过载算法

信号处理流程:

  1. 输入信号先经过增益放大
  2. 放大后的信号通过非线性函数进行削波
  3. 削波后的信号通过低通滤波器去除高频谐波
  4. 处理后的信号与原始信号按混合比例混合

过载使用软削波函数(如双曲正切函数),信号在接近阈值时逐渐被压缩,产生的失真比较温暖、平滑。

失真使用硬削波,信号超过阈值后直接被截断,产生的失真比较强烈、粗糙。

削波会产生大量谐波,听起来很刺耳,所以后面通常会接一个低通滤波器来柔化音色。

均衡器算法

S-YXG50 的均衡器采用三段设计:

低频搁架:可以提升或衰减某个频率以下的所有频率。典型频率范围是 20-500Hz。就像一个可以整体调高或调低的低音旋钮。

中频峰值:可以提升或衰减某个中心频率附近的一个频带。中心频率和带宽(Q 值)都可以调整。这是最灵活的 EQ 段,可以针对特定频率进行精确调整。

高频搁架:可以提升或衰减某个频率以上的所有频率。典型频率范围是 2kHz-20kHz。就像一个可以整体调高或调低的高音旋钮。

压缩器算法

信号处理流程:

  1. 检测输入信号的电平(可以是 RMS 平均值或峰值)
  2. 将检测到的电平与阈值比较
  3. 如果电平低于阈值,增益为 1(不压缩)
  4. 如果电平高于阈值,按照压缩比降低增益
  5. 用攻击时间和释放时间平滑增益变化
  6. 将计算出的增益应用到信号上

压缩比决定了压缩的强度。比如 4:1 的压缩比意味着输入信号每增加 4dB,输出只增加 1dB。无限大:1 的压缩比就是限幅器,输出永远不会超过阈值。

攻击时间决定了增益下降的速度——攻击时间短,压缩反应快,能抓住瞬态峰值;攻击时间长,压缩反应慢,保留更多瞬态。

释放时间决定了增益恢复的速度——释放时间短,压缩恢复快,可能产生"抽吸"效果;释放时间长,压缩恢复慢,动作更平滑。

旋转扬声器算法

这个效果模拟 Leslie 音箱的旋转扬声器,常见于风琴音色。

工作原理:

Leslie 音箱有两组扬声器——高频号角和低频低音箱——它们会旋转。旋转产生两个效果:

多普勒效应:当扬声器朝向听者运动时,接收到的频率会升高;当远离时频率会降低。这产生了音高调制效果。

幅度调制:旋转时,扬声器有时朝向听者(音量大),有时背向听者(音量小)。这产生了音量调制效果。

S-YXG50 分别模拟高频和低频的旋转效果,然后将它们混合。左右声道分别模拟不同的旋转位置,产生环绕感。LFO 速率可以切换快速和慢速。

早期反射算法

早期反射模拟房间中直达声之后的前几次墙壁反射。与混响不同,早期反射是离散的回声,而不是密集的混响尾音。

工作原理:

使用多抽头延迟线,每个抽头代表一次反射。每个抽头有不同的延迟时间和增益,模拟不同距离和角度的反射。

小房间的反射间隔短、衰减快;大房间的反射间隔长、衰减慢。走廊会产生强侧向反射。


关键数据结构

声音 DSP 对象(VoiceDSPObject)

这是每个活跃声音的核心数据结构,大小为 780 字节(0x30C)。每个正在发声的音符都有一个这样的对象实例。

主要字段说明:

状态标志

  • active 字段表示这个声音槽是否在使用中
  • flags 字段用位标志表示声音的各种状态——是否正在发声、是否正在释放、是否静音等

音频参数

  • sampleRate 存储当前的采样率
  • rateScale 存储速率缩放因子,由力度决定
  • coeffTable 指向系数表,系数表存储了各种 DSP 处理需要的参数

振荡器状态

  • 包含波形数据指针、循环点位置、当前采样位置等
  • samplePos 和 sampleFrac 记录精确的读取位置(整数部分和小数部分)

滤波器状态

  • 22 个浮点数存储滤波器的内部状态
  • 双二阶滤波器需要存储前两个输入和输出的值

噪声发生器

  • 4 组 23 位线性反馈移位寄存器(LFSR)
  • 用于生成白噪声,模拟某些乐器的噪声成分

包络电平

  • 四个浮点数分别存储主包络、合唱包络、混响包络和备用主包络的当前电平

增益值

  • 四组当前增益和目标增益(分别对应主输出 L/R 和发送 L/R)
  • 增益平滑算法逐步将当前增益移向目标增益,避免突变产生咔嗒声

DSP 函数指针表

  • 29 个函数指针,构成一个处理链
  • 每个函数负责一个特定的 DSP 任务
  • 系统按顺序调用这些函数,完成整个声音的渲染

引擎布局结构(EngineLayout)

这个结构描述了 DLL 内部引擎对象的内存布局。它是连接各个子系统的桥梁。

关键字段:

  • voiceManager 指针:指向声音管理器
  • midiHandler 指针:指向 MIDI 处理器
  • effectsProcessor 指针:指向效果处理器
  • voiceTable 指针:指向音色表管理器
  • midiResetMode:MIDI 重置模式(0=GM,1=XG,2=GS)
  • isXGLite:XG Lite 模式标志

效果器布局结构(EffectsLayout)

这个结构描述了效果处理器内部的参数存储布局。所有效果器参数都以字节为单位存储在连续的内存区域中。

主要参数组:

  • 混响参数:时间、阻尼、扩散、预延迟
  • 合唱参数:速率、深度、反馈、延迟
  • 延迟参数:时间、反馈、电平
  • 变奏参数:类型和最多 4 个通用参数
  • 均衡器参数:低频增益、中频增益、高频增益、中频频率
  • 插入效果参数:20 个通用参数
  • 主效果参数:4 个通用参数

MIDI 参数块(MidiParamBlock)

存储当前活跃通道的 MIDI CC 值。这是一个全局数据块,所有通道共享,当切换通道时需要更新。

存储的参数包括:

  • 声像(CC#10)
  • 音量(CC#7)
  • 弯音值(0-16383)
  • 滤波器截止频率(CC#74)
  • 调制轮(CC#1)
  • 滤波器共振(CC#71)
  • 包络起音(CC#73)
  • 包络衰减(CC#75)
  • 表情(CC#11)
  • 包络释放(CC#72)
  • 合唱发送(CC#93)
  • 混响发送(CC#91)
  • 变奏发送(CC#94)
  • 弯音灵敏度(RPN 0)
  • 调音(RPN 1/2)
  • 当前力度

音色表系统

音色表文件结构

S-YXG50 使用两个音色数据文件:

sxgbin41.tbl:音色参数表,约 4MB。包含各种乐器的参数定义、鼓组配置、音色索引等。文件内部被分成 17 个数据段,每个段存储不同类型的数据。

sxgwave4.tbl:波形数据表,约 4MB。包含所有乐器的实际音频采样数据。这些采样是单声道的,以 22050Hz 或 44100Hz 采样率录制。文件经过简单的编码保护,加载时需要解码。

音色参数表的数据段

鼓组表(dataSeg00-03):四张鼓组表,分别对应 GS、XG、XG SFX、GM2 标准。每张表 128 个字节,每个字节对应一个音符,值是鼓组编号(用于索引鼓组数据)。

鼓组映射(dataSeg04):XG 鼓组的映射表,31 个鼓组 × 128 个音符。每个条目是 16 位,指向鼓组参数数据的位置。

鼓组默认参数(dataSeg05):每个鼓键的默认参数,包括音高偏移、电平、声像、效果发送量、滤波器设置、包络设置等。每个鼓键 30 字节。

SFX 索引表(dataSeg06):特殊音效的索引表,用于快速查找 SFX 音色。

Bank Select 表(dataSeg07-10):存储 GS 和 XG 标准的 Bank Select MSB/LSB 映射关系。当收到 Bank Select 消息时,系统通过这些表确定实际要加载的音色。

音色索引表(dataSeg11-12):GS/GM 索引表和 XG 索引表。这些表将程序编号(和可能的 Bank Select 值)映射到具体的音色参数位置。

基础音色参数表(dataSeg13):存储所有基础音色的参数,每个音色的参数大小可变。这些参数包括波形选择、音高设置、包络参数、滤波器参数等。

扩展音色参数表(dataSeg14):存储扩展音色的参数,结构与基础参数表类似。

采样参数索引(dataSeg15):将音色映射到具体的采样描述符。

采样描述符(dataSeg16):每个描述符 16 字节,描述一个采样片段的属性。

采样描述符详解

每个采样描述符包含以下信息:

力度分层:同一个音符可能有多个采样,按力度分层。比如轻按和重按会触发不同的采样,以模拟真实乐器的力度变化。

基准音高:这个采样原本录制时的音高。播放时需要根据请求的音高进行音高转换。

采样起始地址:波形数据在文件中的起始位置。使用 3 字节大端序编码。

循环结束点:循环播放的结束位置。到达这个位置后会跳回循环起始点继续播放。

采样率指示:标明这个采样是以 22050Hz 还是 44100Hz 录制的。如果采样率与输出采样率不同,播放时需要进行采样率转换。

音域范围:这个采样适用的音高范围。超出这个范围的音符会使用相邻的采样。

波形数据解码

sxgwave4.tbl 文件经过简单的编码保护。解码过程如下:

对文件中的每个字节,依次进行以下操作:

  1. 与当前密钥和字节索引进行异或运算
  2. 交换高四位和低四位
  3. 与固定值 0x5C 异或
  4. 密钥在 0x5D 和 0xA2 之间交替切换

这个解码算法在 DLL 中实现,加载波形文件时自动执行。

音色查找流程

当需要为某个音符查找音色时,系统执行以下步骤:

  1. 确定当前通道是旋律通道还是鼓组通道
  2. 如果是旋律通道,根据 Bank Select MSB、LSB 和程序编号,在索引表中查找对应的音色参数
  3. 如果是鼓组通道,根据鼓组编号和音符号,在鼓组映射表中查找对应的鼓键参数
  4. 从音色参数中获取波形索引
  5. 根据波形索引找到采样描述符
  6. 从采样描述符中获取采样起始地址、循环点等信息
  7. 使用这些信息初始化声音的振荡器

附录

MIDI CC 编号与功能对照

CC#0 Bank Select MSB:选择音色库的高 7 位
CC#1 Modulation:调制轮,通常用于控制颤音深度
CC#7 Volume:通道音量
CC#10 Pan:声像位置,控制声音在左右声道的分布
CC#11 Expression:表情,通常与音量配合使用
CC#64 Sustain:延音踏板,按下后音符不会在释放键时停止
CC#71 Resonance:滤波器共振,增强截止频率附近的频率
CC#72 Release:包络释放时间
CC#73 Attack:包络起音时间
CC#74 Cutoff:滤波器截止频率
CC#75 Decay:包络衰减时间
CC#91 Reverb Send:送往混响效果器的信号量
CC#93 Chorus Send:送往合唱效果器的信号量
CC#94 Variation Send:送往变奏效果器的信号量
CC#120 All Sound Off:立即停止所有声音
CC#121 Reset All:重置所有控制器到默认值
CC#123 All Notes Off:让所有正在发声的音符进入释放状态

XG 效果类型代码

混响类型(0x0100-0x0500):

  • Hall 1/2:大厅混响
  • Room 1/2/3:房间混响
  • Stage 1/2:舞台混响
  • Plate 1/2:板式混响
  • GM Reverb:通用 MIDI 默认混响

合唱类型(0x4100-0x4300):

  • Chorus 1/2/3/4:不同风格的合唱
  • Flanger 1/2:镶边效果
  • GM Chorus:通用 MIDI 默认合唱

变奏类型(0x4100-0x5D02):

  • 合唱/镶边/交响:调制类效果
  • 相位器:梳状滤波效果
  • 旋转扬声器:Leslie 音箱模拟
  • 颤音/自动声像:周期性调制
  • 失真/过载:谐波失真
  • 均衡器:频率调整
  • 延迟/回声:时间延迟效果
  • 早期反射/门混响:空间效果
  • 卡拉 OK:人声消除

采样率转换原理

当波形采样率(如 22050Hz)与输出采样率(44100Hz)不同时,需要进行采样率转换。

转换使用线性插值方法:假设输入采样率是输出采样率的一半,那么每产生一个输出采样,读取位置只前进半个输入采样。当读取位置落在两个输入采样之间时,根据距离两个采样的比例计算出一个中间值。

比如读取位置在第 100 个和第 101 个输入采样之间的 0.3 位置,那么输出值 = 第100个采样 × 0.7 + 第101个采样 × 0.3。


文档版本:1.0
生成日期:2026-06-07
基于 S-YXG50.dll 逆向工程分析

2 Likes

事实上逆向下来,我发现AI并没有很好执行我的意图,RVA的标注都对不上……
而且我在想,就XG给的那种小容量音色库,实际音质能做到什么程度呢?
我想如果真的要做这种midi音源,我还是应该考虑一下能不能用rust去重写timidity

1 Like

TiMidity似乎很久没更新了,但是效率比FluidSynth快多了。
还有个WildMIDI,但是它不支持SoundFonts,只支持GUS音色。

我有一个新的想法,就是只逆向yamaha数据文件读取和调用相关的程序,然后剩余部分基于timidity的思路重新实现

2 Likes

重大进展,不需要 XOR 0x5A,第一阶段解密后,只做字节交换即可

我感觉 yamaha 有点鸡贼,做这个异或运算可能是想防内存 dump,所以在音频输出阶段很可能会再做一次异或运算,等我后续逆向结果

opencode+鸡爪(Ghidra)真好用~

2 Likes

我的猜测应该是正确的,在 0x10034DFB0x10034E5E 这两个函数也有 XOR 0x5A 的操作

@gaozhe3321 你按照我的方法应该能拿到 PCM 格式的采样文件

1 Like

我已经在着手写合成器了,目前是把 MIDI 处理部分的架构搭好了,接下来计划参考 QXGEdit 的代码进一步核实 tbl 文件内置数据结构
希望能最终实现对于 S-YXG50 的完整替代,可以跨平台使用,然后实现对于 S-YXG2006LE 的数据文件兼容,尽可能让这个只支持 XGLite 的软件实现尽可能对 FullXG 的支持(没有的乐器就退回基础乐器了)
后续实现 ALSA/Pipewire/JACK/CoreAudio 的多平台支持

1 Like

最近跟SysEx干上了,但还好的是,无论是XG还是GS,他们的SysEx 本质上都是在指定内存地址写入数据,也就是说我可以借此了解到这类合成器的内存结构

接下来应该尝试实现对于tbl文件的读取了,但我需要先完成一个通用的内存结构,可以同时兼容S-YXG50和2006LE(甚至是更早期的 S-YXG 100 等)

1 Like

我最近有点卡住了,根据AI逆向的结果,好像prevoice表是变长结构……这就很难受了

顺带,2006LE的数据结构也是奇葩,也是大量变长结构……

项目地址在这里 https://github.com/madaha-dev/madaha ,有兴趣的也可以一起看看

1 Like

告诉你一个坏消息: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 → 混音输出

关键点

  1. 元素个数: header[1] bit1 = 0 → 1个; = 1 → 2个
  2. 元素访问: 按 INDEX 0,1(非 bitmask 顺序)
  3. 范围匹配: abs(note - center) <= span(center+span,非传统 min/max)
  4. 链扫描 用 MIDI key 作为 range,按 (keyEnd & 0x7F) < range 推进
  5. 力度分层 在 element 级别实现:不同 vel 范围的 element 指向不同 chain
  6. 多层叠音: 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 :white_check_mark:
sxgbin41.tbl 128/128 119×8-bit + 9×16-bit :warning:

叠层乐器 (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解析工具
2 Likes

现在S-YXG50我觉得基本上应该是通了,那么接下来我应该考虑去解决S-YXG2006LE了

如果能将这个也解析清楚,那么Madaha最难的部分基本上已经过去了

效果器什么的,原理基本上差不太多,根据 XG Spec写就是了

1 Like

我不得不吐槽一下 S-YXG2006LE 的数据格式,搞了一个什么鬼的树结构……直接把AI干烧了

1 Like

逆向笔记:Yamaha S-YXG2006LE sxgbnw6l.tbl 文件格式

:warning: 注意:以下内容为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 固定 0x000040000xFFFFFFFF
+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 一条,两个用途:

  1. per-note block: data[sec3 + offset_table[wid] + note] — 取该 note 的 PCM block 号
  2. 树遍历入口

树节点 (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

:warning: 常数 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 鼓定义 未分析
1 Like

我目前在尝试实现S-YXG50的功能了,2006LE 因为代码比较复杂,这个等后续处理

不过万幸的是,2006LE 有 mac 版本,甚至还带了调试符号……(Yamaha也有马大哈时刻)

2 Likes

报告一下进度

  1. S-YXG50 的读取部分完成了
  2. MIDI处理线程基本上完成了
  3. 正在实现 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
3 Likes

现在的进度

Tone Generator 我让 AI 帮我实现了(不知道实现效果如何)

接下来把完整的混音管线实现了,基本上就可以开始测试了

希望能成功

1 Like

在AI的帮助下,现在madaha可以播放钢琴曲了

控制器/RPN/NRPN等我还没测试,鼓和SFX我还完全没动,但是总体看起来是有希望了

我录了一段,可以听听 (48000Hz,快速Sinc插值)
https://pan.baidu.com/s/1HWaNGpda4p4trmK9z3fwag?pwd=2333

3 Likes

由于deepseek涨价,后续开发会慢下来了……不过目前还是能播放钢琴曲的

1 Like

其实你可以考虑看看mame的代码,mu50基本完全模拟了,4m完整版rom
mu50|690x327

1 Like