摘要

  • RTP MIDI 区分短暂的演奏瑕疵与持续的状态错误。漏掉一个音,与漏掉结束一个音的指令,不能仅按丢包数量判断损失。
  • 恢复日志携带修复所需的历史信息,不靠重传每个旧包,也不是完整的演奏档案;接收端有时应当跳过已经迟到的起音。
  • 检查点范围、会话初始状态、静默期间的保护包和本地播放策略共同决定恢复效果。协议规定共同的责任边界,却不替所有乐器选择同一套听感取舍。

正确的恢复,也可能什么都不补奏

一条 NoteOn 指令在网络里丢失了。接收端稍后得知,音乐家刚才确实按下过那个音。直觉上的补救似乎很简单:既然找回了这段历史,就把音符播放出来。

但音乐不是一份等待补齐的表格。起音的时刻已经过去,迟来的声音可能比原来的空缺更扰乱演奏。RTP MIDI 的恢复设计允许接收端面对某些漏掉的起音时不再发声,同时更新内部记录,使后续判断仍与恢复后的历史相容。知道一件事曾经发生,不等于现在应该再做一次。

这不是允许接收端随意忽略指令,而是协议目标本来就比逐条重放更精确。2011 年 6 月的 RFC 6295 要求恢复系统处理会持续下去的演奏错误,却没有承诺所有曾经错过的声音都能及时回来。某个音符没有被补奏,可以是恢复工作的合理结果,而不是恢复机制没有运行。

同样丢一个包,后果却不对称

MIDI 传送的是控制乐器的符号指令,不是已经录好的音频波形。NoteOn 通常开始一个音,NoteOff 通常结束它;控制器消息还可以改变音量等状态。接收端根据这些指令和自己的乐器状态产生声音。网络中一小段信息的缺席,因而可能影响远超过这段信息本身的时间。

2006 年 11 月的 RFC 4695 已经把这一区别写成恢复设计的基础。丢失 NoteOn,可能只是少响一个音,影响通常不越过这个音原本应当持续的时间。丢失 NoteOff,则可能让本该停止的音一直延续。丢失控制器 7 的通道音量设置,还可能让之后所有音符都过响或过轻。

这三种情况不能只写成“丢了一个包”。第一种损失有一个随演奏自行过去的边界,后两种可能把错误带到未来。具体乐器的包络、踏板状态以及后续指令会改变实际听感,因此“丢停音指令”并非在所有条件下都必然产生无限长的声音。但对协议设计者而言,不能把侥幸出现的后续纠正当作保证。

恢复日志的任务,就是把可能没有结束时间的错误,压回有结束时间的瑕疵。它不是要让过去没有发生过损失,而是不能让一次损失永久接管接收端。

传输知道序号,乐器才知道后果

2003 年的 RFC 3550 给 RTP 提供了序号、时间戳、负载识别与接收监测等共同工具,同时明确不保证数据必达、按时到达或按序到达。序号可以帮助发现缺口,却不会告诉系统:这次缺口只漏了一次起音,还是把音量永久留在了错误的档位。

RTP MIDI 把这种应用知识放入负载格式。使用恢复日志的流,会在每份负载中携带相应的日志部分。日志从一个检查点开始,编码后续恢复所需的历史信息。接收端发现丢失后,把自己已经知道的历史,与结束这次缺口的包所带来的日志比较,再发出必要的本地纠正指令。

这里没有要求把每个丢失的网络包重新发一遍。接收端需要的,是让乐器不再持续错误的足够信息,而不是运输过程的完整复制件。一个本地生成的 NoteOff,便可能结束由远端指令缺失造成的问题。

也因此,正确解读时间戳和正确解释音乐状态是两项工作。包里有时间,并不意味接收端还能让已经过去的时刻重新出现;包里有历史,也不意味历史中的每一次动作都应该现在执行。

检查点不是整场演出的备份

假设接收端拿到序号为 I 的包,日志的检查点是 C。日志覆盖的是 C 至 I−1 的相关指令历史,不包括当前包 I 中的新指令。当前命令与恢复信息同在一份负载中,但承担不同角色。如果 C 就是 I,检查点历史可以是空的。

规范中“能够恢复任意数量的丢包”有一个重要限定:丢失发生在所覆盖的检查点历史以内。它不是对无限长断线作出的保证。接收端需要查看检查点范围,不能仅凭“这份包带着日志”就假定过去所有缺口都已经被照顾。

日志本身也不是逐条保存全部音符。处理常见起音、停音交替模式的 Chapter N,使用近期音符状态等结构来支持恢复。NoteOn 日志不携带原指令的精确执行时刻,其 Y 位只是建议是否还值得播放这个音。这个提示属于音符日志,不能与顶层日志头中同名、用于指示另一种结构是否存在的位混为一谈。

更复杂的同音重叠模式,需要额外的 Chapter E 支持。吉他或管乐控制器可能产生与钢琴式交替起停不同的序列。把一个简化例子的恢复算法直接说成所有 MIDI 乐器的通用答案,会抹去协议特意保留的差异。

没有新音符,仍然可能需要新包

2006 年同月发布的信息类文档 RFC 4696,把困难放进一个很具体的例子:NoteOff 所在的包丢失了,而音乐家接下来五秒都没有再发出指令。如果没有后续 RTP 包,接收端可能要等下一次动作,才能从序号看见那个缺口。这五秒的安静,在另一端却可能变成五秒不该存在的持续声音。

文档讨论了携带空 MIDI 指令列表的保护包。它不要求音乐家为了网络而多弹一个音,却可以在间歇中继续提供序号推进与恢复日志。空指令列表不等于空日志,更不等于整份负载没有工作可做。

实现指南举过在短暂静默后较密集发送、随后逐渐拉长间隔的方案。那些定时数值是设计示例,不是全网统一的强制节拍。RFC 6295 后来描述的 guardtime 参数,是用 RTP 时间戳单位表达相邻包之间的最大间隔;它列出的常见范围也明确不是规范性上下限。

更关键的是,拥塞控制优先于所请求的保护包频率。2003 年的 RFC 3551 已经讨论 RTP 流在共享网络上与其他流量公平共存的责任。减少一个乐器的错误持续时间,并不能自动取得无限发送的权利。保护演奏与保护共同网络,需要同时放进方案里。

接收到了,并不等于适合再执行

RFC 4696 是实现指南,不是另一份强制所有接收端照做的标准。它介绍的一个接收器,会避免播放迟到的起音,同时处理其他有助于纠正持续状态的命令;面对乱序旧包,则选择忽略,以免重复已经通过日志执行过的纠正。

这套例子有自己的适用条件,包括特定网络特征、受限的命令集合以及不使用播放缓冲区。文档甚至指出,该设计并未完全满足一个要求接收端具备缓冲能力的互通安排。它的价值在于展示取舍,不在于让所有应用复制同一套实现。

规范的要求更窄,也更坚实:接收端要识别序号中断,修复由丢失造成的持续错误,并且不能在处理乱序包时再制造这类错误。使用播放缓冲的实现,可以等到真正渲染之前再处理恢复,因为所谓“丢失”的包可能只是晚到,仍来得及使用。立即纠正与稍作等待之间,没有脱离具体播放条件的唯一答案。

还有一个更细的限制。停音与延音踏板操作的先后关系,并不总能从日志中完整恢复。接收端有时可以借助自己的历史推断;无法推断时,规范倾向于避免留下踏板维持的错误长音,哪怕会造成短暂的不悦。这不是把艺术判断交给网络,而是承认信息已经不足以无损重现艺术判断。

新加入的人,可能缺少很久以前的设置

默认的闭环策略使用接收端反馈,让发送端缩短需要保留的检查点历史。RTCP 报告中的最高已接收扩展序号,可以帮助决定哪些历史不再需要反复携带。2017 年的信息类设计指南 RFC 8088 特别提到了 RTP MIDI 的状态摘要与这种修剪机制,指出它适合高度依赖状态的符号媒体。

但缩短历史也带来进入会话的责任。一个通道音量设置可能很早就发过,旧接收端都知道了;新接收端现在加入,却只拿到近期的包。未来的数据持续到达,不代表它的初始音量就是正确的。发送端得知新接收端存在后,要保证它不会以这种持续错误开始,必要时重新提供当前控制器状态。

未知的多播接收端更难处理。它可以谨慎避免卡住的音,却不能凭空猜出从未收到过的正确音量。协议在这里没有把“加入成功”伪装成“历史齐全”。恢复范围与参与者是否已被纳入反馈安排,是实际的运行条件。

文档保存设计,不替演奏效果作证

RFC 6295 在 2011 年取代了 RFC 4695,修正图示、语法和配置例子等问题,同时保留了这一恢复思路。它提到某些应用当时尚未成为主流,那是对 2011 年的判断,不是今天的市场统计。作者对若干修订未发现互通问题的说明,也不能扩大为所有实现都正确的证明。

IANA 的 audio/rtp-midi 登记 提供共同名称、参数与文档参照,记录的是实时 MIDI 传送格式,不是某场演出成功率的认证。登记、标准发表、实际采用和听众听到的效果,是不同层次的事实。

这段历史留下的不是“恢复总比丢失好”这样宽泛的结论。更有用的区分是:网络可以交付足够信息,让错误不再延续;它却不能把错过的时刻归还。实时系统的成熟,有时就体现在它知道哪些动作不该补做,以及哪些错误不能继续留下。