MIDI 与事件时序
正确处理音符标识、自动化以及密集事件块的乐器开发指南。
Vesty 将 VST3 事件转换为强类型的 Event,通过 ProcessContext 和 ProcessContext64 交给内核。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 | 渲染释放后的声部状态 |
内核可采用以下循环结构;其中 render 与 apply 代表你自己的、不会分配内存的 DSP 函数:
Textcursor = 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_len 和 truncated |
| 音符表情文本 | 最多 64 个 UTF-16 代码单元;检查 text_len |
不要把截断的 SysEx 前缀当成完整消息解释。示例在验证实验性厂商 ID 和消息边界之前会拒绝截断消息。更大的 SysEx 载荷需要单独设计 API 与存储;事件分批不会扩大单个事件的载荷。
验证乐器行为
以不同块大小处理同一事件序列,并比较生成的采样。覆盖多通道、重复音符、零力度 NoteOn、表情、自动化以及末尾的 NoteOff。还应测试零帧事件上下文,以及同一时间戳超过 512 个事件的情况。
仓库的适配器回归测试覆盖 4,800 个混合事件、同一时间戳的 2,050 个事件、f32 与 f64 路径、内存分配检查和音频缓冲区重叠。运行:
Bashcargo test -p vesty-vst3 --features vst3-bindingscargo test -p vesty-example-midi-synth
本地测试需要配合真实 DAW 发布证据。在目标宿主中验证音符释放、自动化回放、传输状态变化和编辑器开关行为。