如何在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触发写入操作,但依然无法解决核心的消息同步和交换逻辑问题。
解决方案
方案一:优化数组实现,解决消息同步问题
核心是保证音符和力度消息同步触发,同时维护每个音符的激活状态与交换目标:
- 用
trigger f f捆绑notein的输出,确保音符和力度消息同步触发后续逻辑。 - 创建三个数组:
active:存储音符是否处于激活(按住)状态,1为激活,0为释放。velocity:存储每个激活音符的力度值。swap_target:存储每个音符对应的交换目标音符索引。
- 按下音符(力度>0)时:
- 遍历
active数组,找到所有已激活的音符,将当前音符设为它们的swap_target,同时将它们设为当前音符的swap_target。 - 用
tabwrite更新active数组对应位置为1,记录当前力度到velocity数组。
- 遍历
- 释放音符(力度=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对象维护每个音符的状态:
- 为每个音符创建单独的
float对象,存储其激活状态(1/0)和交换目标音符。 - 按下音符时:
- 通过
receive检查其他音符的激活状态,若有已激活的音符,互相设置交换目标。 - 记录当前力度,发送该音符的
noteon消息。
- 通过
- 释放音符时:
- 若存在交换目标且目标仍处于激活状态:
- 发送交换目标音符+对应力度的
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
相关产品推荐
相关产品推荐

