跳到正文
本页内容

MIDI 与事件时序

正确处理音符标识、自动化以及密集事件块的乐器开发指南。

Vesty 将 VST3 事件转换为强类型的 Event,通过 ProcessContextProcessContext64 交给内核。MIDI synth 示例 是一个单音乐器,包含时序、表情和 SysEx 测试,可作为起点。

声明输入

乐器需要声明 PluginKind::Instrument,并在插件总线布局中配置事件输入总线。插件只接收其支持的事件类型和总线。VST3 通过参数变化传递已映射的 MIDI 控制器:使用参数的 with_midi_mapping 声明映射,不要假设所有宿主都会转发原始 MIDI CC 字节。参见参数指南和示例中的 program 参数。

通道编号从零开始(0..=15),键号范围为 0..=127,音符事件带有宿主的 note_id。复音乐器应优先使用宿主提供的有效音符 ID,并为没有可用 ID 的事件定义通道与键号匹配策略。释放声部时,除了键号,还要匹配通道。

在事件之间渲染

偏移量相对于当前 process 调用的音频范围。不要先应用所有事件,再渲染整个缓冲区。

例如,一个 32 帧上下文包含偏移 8 的 NoteOn 和偏移 24 的 NoteOff:

范围操作
采样 0–7按之前的声部状态渲染
采样 8 之前应用 NoteOn
采样 8–23渲染激活的声部
采样 24 之前应用 NoteOff
采样 24–31渲染释放后的声部状态

内核可采用以下循环结构;其中 renderapply 代表你自己的、不会分配内存的 DSP 函数:

Text
cursor = 0for event in context.events(): render(cursor .. event.sample_offset()) apply(event) cursor = event.sample_offset()render(cursor .. context.audio().frames())

按传入顺序消费相同偏移的事件。在相同偏移处,参数事件先于音符列表中的事件;各来源内部保持稳定顺序。跨调用保留振荡器和包络状态,在 prepare() 中更新依赖采样率的状态。

需要遵循 MIDI 语义时,将零力度 NoteOn 视为释放事件,示例已实现此行为。适配器会过滤无效的总线、通道、键号、偏移,以及非有限的 NoteOn 力度或压力值;NoteOff 的非有限力度会被置为零,保留释放事件。

密集事件与零帧调用

512 是单批缓存大小,不是宿主音频块的事件总量上限。 当宿主发送更多事件时,Vesty 使用预分配存储连续交付多批事件。因此一个宿主音频块可能触发多次内核调用。

当超过 512 个事件位于同一采样时,该位置前面的批次可能没有音频帧。必须消费这些事件;只能跳过音频生成,不能在处理事件之前直接返回。原生 process_f64 同样遵守此规则。

宿主主动发出的零帧参数刷新是另一种情况:适配器只更新参数状态,不调用音频内核,即使处理已停止也能接受刷新。

溢出路径会为每一批重新扫描宿主列表。内存保持有界,但 CPU 开销会随事件总量和批次数增加。避免产生冗余自动化点,并在目标 DAW 的密集编曲中进行性能分析。消除事件数量截断不等于消除音频回调的执行时限。

载荷限制

载荷当前契约
每批事件缓存512 个;其余事件由后续批次继续交付
SysEx每事件最多 256 字节;检查 data_lentruncated
音符表情文本最多 64 个 UTF-16 代码单元;检查 text_len

不要把截断的 SysEx 前缀当成完整消息解释。示例在验证实验性厂商 ID 和消息边界之前会拒绝截断消息。更大的 SysEx 载荷需要单独设计 API 与存储;事件分批不会扩大单个事件的载荷。

验证乐器行为

以不同块大小处理同一事件序列,并比较生成的采样。覆盖多通道、重复音符、零力度 NoteOn、表情、自动化以及末尾的 NoteOff。还应测试零帧事件上下文,以及同一时间戳超过 512 个事件的情况。

仓库的适配器回归测试覆盖 4,800 个混合事件、同一时间戳的 2,050 个事件、f32 与 f64 路径、内存分配检查和音频缓冲区重叠。运行:

Bash
cargo test -p vesty-vst3 --features vst3-bindingscargo test -p vesty-example-midi-synth

本地测试需要配合真实 DAW 发布证据。在目标宿主中验证音符释放、自动化回放、传输状态变化和编辑器开关行为。