You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Pd中复现MIDI‘双音符触发消息’标准?

在Pure Data中实现自定义MIDI按键交换规则

需求规则

我们将MIDI消息抽象为(物理按键, 音符, 力度),其中物理按键仅用于辅助说明,不属于标准MIDI消息组成部分,具体规则如下:

  • 若发送(a,0,1)后直接发送(b,0,2)(中间无(a,0,0)的释放消息),释放按键a时实际发送(b,0,2),释放按键b时实际发送(a,0,2),而非原本的(a,0,1)。
  • 单独按住按键a未按b,释放a时发送(a,0,0);单独按住b未按a,释放b时发送(b,0,0)。

已尝试的实现方案

数组索引方案

测试仅使用MIDI音符0至9,制作了以MIDI音符为索引、力度为元素的数组补丁,意图将数组读取的力度值输出到noteout,而非直接使用notein的力度输入,但无法正常工作。问题表现为:力度为0的消息无法传递,或音符消息与力度消息读取顺序错误(二者来自不同输出口,无法用trigger同步控制)。

补丁结构大致为:notein拆分出音符和力度,音符连接tabread的索引端口,力度连接tabwrite的输入端口,同时音符和tabread的输出连接到noteout,但逻辑上存在消息不同步和力度0无法处理的问题。

自定义子补丁替代tabwrite

曾怀疑是冷/热端口问题,制作了自定义子补丁替代tabwrite:用select区分力度0和非0,结合bang触发写入操作,但依然无法解决核心的消息同步和交换逻辑问题。

解决方案

方案一:优化数组实现,解决消息同步问题

核心是保证音符和力度消息同步触发,同时维护每个音符的激活状态与交换目标:

  1. 用trigger f f捆绑notein的输出,确保音符和力度消息同步触发后续逻辑。
  2. 创建三个数组:
    • active:存储音符是否处于激活(按住)状态,1为激活,0为释放。
    • velocity:存储每个激活音符的力度值。
    • swap_target:存储每个音符对应的交换目标音符索引。
  3. 按下音符(力度>0)时:
    • 遍历active数组,找到所有已激活的音符,将当前音符设为它们的swap_target,同时将它们设为当前音符的swap_target。
    • 用tabwrite更新active数组对应位置为1,记录当前力度到velocity数组。
  4. 释放音符(力度=0)时:
    • 读取swap_target数组,若存在交换目标且目标仍处于激活状态:
      • 发送目标音符+对应力度的noteon消息,再发送目标音符的noteoff消息。
      • 发送当前音符的noteoff消息。
    • 若没有交换目标,直接发送当前音符的noteoff消息。
    • 用tabwrite将active数组对应位置设为0,清空swap_target对应位置。

基础框架补丁示例:

#N canvas 0 0 400 350 10;
#X obj 50 50 notein;
#X obj 50 80 trigger f f;
#X obj 120 80 select 0;
#X obj 120 110 bang;
#X obj 180 80 select 1;
#X obj 180 110 bang;
#X obj 50 140 tabwrite active;
#X obj 50 170 tabwrite velocity;
#X obj 50 200 tabread active;
#X obj 50 230 tabread velocity;
#X obj 50 260 noteout;
#X obj 250 80 counter 0 9;
#X obj 250 110 tabread active;
#X connect 0 0 1 0;
#X connect 0 1 1 1;
#X connect 1 0 2 0;
#X connect 1 0 5 0;
#X connect 1 0 6 0;
#X connect 1 1 7 1;
#X connect 2 1 3 0;
#X connect 3 0 8 0;
#X connect 3 0 9 0;
#X connect 5 0 11 0;
#X connect 11 0 12 0;
#X connect 8 0 10 0;
#X connect 9 0 10 1;
#X connect 10 0 10 2;

注:需补充遍历数组时的交换逻辑判断,可结合spigot控制激活状态的读取与更新。

方案二:无数组的状态机实现

适合少量音符的场景,用send/receive结合float对象维护每个音符的状态:

  1. 为每个音符创建单独的float对象,存储其激活状态(1/0)和交换目标音符。
  2. 按下音符时:
    • 通过receive检查其他音符的激活状态,若有已激活的音符,互相设置交换目标。
    • 记录当前力度,发送该音符的noteon消息。
  3. 释放音符时:
    • 若存在交换目标且目标仍处于激活状态:
      • 发送交换目标音符+对应力度的noteon消息,再发送目标的noteoff消息。
      • 发送当前音符的noteoff消息。
    • 无交换目标则直接发送当前音符的noteoff消息。
    • 重置自身及交换目标的状态变量。

核心逻辑示例(以音符0和1为例):

#N canvas 0 0 400 300 10;
#X obj 50 50 notein;
#X obj 50 80 route 0 1;
#X obj 100 80 float 0;
#X obj 150 80 float 0;
#X obj 100 110 send note0_active;
#X obj 150 110 send note1_active;
#X obj 200 80 receive note0_active;
#X obj 250 80 receive note1_active;
#X obj 100 140 select 0;
#X obj 150 140 select 0;
#X obj 100 170 noteout 0 0;
#X obj 150 170 noteout 0 0;
#X obj 100 200 float 0;
#X obj 150 200 float 0;
#X connect 0 0 1 0;
#X connect 1 0 2 0;
#X connect 1 1 3 0;
#X connect 2 0 4 0;
#X connect 3 0 5 0;
#X connect 6 0 9 0;
#X connect 7 0 8 0;
#X connect 8 1 11 0;
#X connect 9 1 10 0;

注:需添加力度存储模块和交换目标的逻辑判断,用trigger同步音符与力度消息。

内容的提问来源于stack exchange,提问作者Julián

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 21:13:11