C++ PortAudio多轨音频播放下不同音轨序列的时序同步实现方法
时序交错实现方案
你当前的串行执行逻辑之所以无法达到预期,是因为所有轨道的事件被按顺序排队执行,不会互相穿插。优先推荐单线程事件调度器方案,不要使用多线程:多线程受操作系统调度抖动影响,很难满足音频场景下毫秒级的时序精度要求,还会额外引入MIDI流访问冲突、轨道时钟同步的问题。
具体实现步骤如下:
- 预处理所有轨道的序列,把相对时间的sleep转换为全局绝对时间戳
把每条轨道的seq按顺序累加sleep时长,生成「时间戳(单位:beat,从play触发时刻的0开始计算)+ 事件类型+参数」的结构,比如你的示例转换后得到的事件列表为:- 0beat:track1 noteOn 70
- 0beat:track2 noteOn 30
- 0.125beat:track2 noteOff 30
- 0.125beat:track2 noteOn 30
- 0.25beat:track1 noteOff 70
- 0.25beat:track1 noteOn 77
- 0.25beat:track2 noteOff 30
- 0.25beat:track2 noteOn 31
- 0.75beat:track1 noteOff 77
- 0.75beat:track2 noteOff 31
- 把所有事件按时间戳从小到大排序,相同时间戳的事件执行顺序不影响效果
- 单线程循环调度事件:维护全局当前时间,每次取出时间最早的待执行事件,计算当前时间到该事件时间的差值,高精度等待对应时长后执行所有该时间点的事件,重复该流程直到所有事件执行完毕
简化实现代码示例:
#include <queue> #include <chrono> #include <vector> // 事件结构 struct Event { double beat_time; // 绝对beat时间 int track_id; bool is_note_on; int note; // 排序用,时间小的排前面 bool operator<(const Event& other) const { return beat_time > other.beat_time; // 适配最小堆的反向定义 } }; // 按BPM把beat数转换为毫秒值 double bpm_to_ms(double beats, int bpm) { return beats * 60000.0 / bpm; } void play(int bpm, std::priority_queue<Event> event_queue, MidiStream& stream) { auto start_time = std::chrono::high_resolution_clock::now(); while (!event_queue.empty()) { auto next_event = event_queue.top(); // 计算事件需要触发的绝对墙上时间 auto trigger_time = start_time + std::chrono::milliseconds((long long)bpm_to_ms(next_event.beat_time, bpm)); // 高精度等待到触发时间 std::this_thread::sleep_until(trigger_time); // 执行所有当前时间点的事件 while (!event_queue.empty() && event_queue.top().beat_time == next_event.beat_time) { auto ev = event_queue.top(); event_queue.pop(); if (ev.is_note_on) { stream.noteOn(ev.note, ev.track_id); } else { stream.noteOff(ev.note, ev.track_id); } } } }
需要学习的相关知识领域
- 软音序器/合成器的事件驱动架构设计
- 最小堆(优先级队列)的使用,用来高效获取最近待触发的事件
- 高精度计时与定时接口的使用,C++场景下可参考
std::chrono库的相关用法 - 实时系统基础概念,了解音频场景下低抖动、高时序精度的要求
- 若后续需要支持运行中动态修改序列,可学习无锁队列的用法,实现控制线程与音频调度线程的安全数据交互
内容的提问来源于stack exchange,提问作者panthyon
相关产品推荐
相关产品推荐

