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 逆向工程分析

1 Like

事实上逆向下来,我发现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