C++ 时钟不一致:高分辨率时钟延迟
我正在尝试实现一个类似MIDI的时钟样本播放器 有一个定时器,它增加脉冲计数器,每480个脉冲是四分之一,因此脉冲周期为1041667纳秒,每分钟120次。 计时器不是基于睡眠的,并且在单独的线程中运行,但似乎延迟时间不一致:在测试文件中播放的样本之间的周期是波动的+-20毫秒,在某些情况下,周期是正常和稳定的,我无法找出这种效果的依赖性 排除了音频后端的影响:我尝试了OpenAL和SDL_混音器C++ 时钟不一致:高分辨率时钟延迟,c++,delay,midi,chrono,C++,Delay,Midi,Chrono,我正在尝试实现一个类似MIDI的时钟样本播放器 有一个定时器,它增加脉冲计数器,每480个脉冲是四分之一,因此脉冲周期为1041667纳秒,每分钟120次。 计时器不是基于睡眠的,并且在单独的线程中运行,但似乎延迟时间不一致:在测试文件中播放的样本之间的周期是波动的+-20毫秒,在某些情况下,周期是正常和稳定的,我无法找出这种效果的依赖性 排除了音频后端的影响:我尝试了OpenAL和SDL_混音器 void Timer_class::sleep_ns(uint64_t ns){ auto
void Timer_class::sleep_ns(uint64_t ns){
auto start = std::chrono::high_resolution_clock::now();
bool sleep = true;
while(sleep)
{
auto now = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(now - start);
if (elapsed.count() >= ns) {
TestTime = elapsed.count();
sleep = false;
//break;
}
}
}
void Timer_class::Runner(void){
// this running as thread
while(1){
sleep_ns(BPMns);
if (Run) Transport.IncPlaybackMarker(); // marker increment
if (Transport.GetPlaybackMarker() == Transport.GetPlaybackEnd()){ // check if timer have reached end, which is 480 pulses
Transport.SetPlaybackMarker(Transport.GetPlaybackStart());
Player.PlayFile(1); // period of this event fluctuates severely
}
}
};
void Player_class::PlayFile(int FileNumber){
#ifdef AUDIO_SDL_MIXER
if(Mix_PlayChannel(-1, WaveData[FileNumber], 0)==-1) {
printf("Mix_PlayChannel: %s\n",Mix_GetError());
}
#endif // AUDIO_SDL_MIXER
}
我在方法上做错了什么吗?有没有更好的方法来实现这种计时器?
对于音频,大于4-5毫秒的偏差太大。我看到一个大错误和一个小错误。最大的错误是,您的代码假定Runner中的主处理始终花费零时间:
if (Run) Transport.IncPlaybackMarker(); // marker increment
if (Transport.GetPlaybackMarker() == Transport.GetPlaybackEnd()){ // check if timer have reached end, which is 480 pulses
Transport.SetPlaybackMarker(Transport.GetPlaybackStart());
Player.PlayFile(1); // period of this event fluctuates severely
}
也就是说,在循环迭代所需的时间内,您一直在睡觉,然后在此基础上进行处理
小错误是假设您可以用整数纳秒表示理想的循环迭代时间。这个错误太小了,根本不重要。然而,我也向人们展示了如何摆脱这个错误,以此自娱自乐-
首先,让我们通过精确表示理想循环迭代时间来纠正小错误:
using quarterPeriod = std::ratio<1, 2>;
using iterationPeriod = std::ratio_divide<quarterPeriod, std::ratio<480>>;
using iteration_time = std::chrono::duration<std::int64_t, iterationPeriod>;
现在,如果您将Timer_class::Runner编码为使用delay_until而不是sleep_ns,我认为您将获得更好的结果:
void
Timer_class::Runner()
{
auto next_start = std::chrono::steady_clock::now() + iteration_time{1};
while (true)
{
if (Run) Transport.IncPlaybackMarker(); // marker increment
if (Transport.GetPlaybackMarker() == Transport.GetPlaybackEnd()){ // check if timer have reached end, which is 480 pulses
Transport.SetPlaybackMarker(Transport.GetPlaybackStart());
Player.PlayFile(1);
}
delay_until(next_start);
next_start += iteration_time{1};
}
}
我看到了一个很大的错误。最大的错误是,您的代码假定Runner中的主处理始终花费零时间:
if (Run) Transport.IncPlaybackMarker(); // marker increment
if (Transport.GetPlaybackMarker() == Transport.GetPlaybackEnd()){ // check if timer have reached end, which is 480 pulses
Transport.SetPlaybackMarker(Transport.GetPlaybackStart());
Player.PlayFile(1); // period of this event fluctuates severely
}
也就是说,在循环迭代所需的时间内,您一直在睡觉,然后在此基础上进行处理
小错误是假设您可以用整数纳秒表示理想的循环迭代时间。这个错误太小了,根本不重要。然而,我也向人们展示了如何摆脱这个错误,以此自娱自乐-
首先,让我们通过精确表示理想循环迭代时间来纠正小错误:
using quarterPeriod = std::ratio<1, 2>;
using iterationPeriod = std::ratio_divide<quarterPeriod, std::ratio<480>>;
using iteration_time = std::chrono::duration<std::int64_t, iterationPeriod>;
现在,如果您将Timer_class::Runner编码为使用delay_until而不是sleep_ns,我认为您将获得更好的结果:
void
Timer_class::Runner()
{
auto next_start = std::chrono::steady_clock::now() + iteration_time{1};
while (true)
{
if (Run) Transport.IncPlaybackMarker(); // marker increment
if (Transport.GetPlaybackMarker() == Transport.GetPlaybackEnd()){ // check if timer have reached end, which is 480 pulses
Transport.SetPlaybackMarker(Transport.GetPlaybackStart());
Player.PlayFile(1);
}
delay_until(next_start);
next_start += iteration_time{1};
}
}
我最终使用了@howard hinnant版本的延迟,并在openal软件中减小了缓冲区大小,这就是造成巨大差异的原因,现在在120BPM 125毫秒周期时,1/16的波动大约为+-5毫秒,四分之一拍的波动大约为+-1毫秒。还有很多需要改进的地方,但我想没关系,我最终使用了@howard hinnant版本的延迟,并在openal soft中减少了缓冲区大小,这就是造成巨大差异的原因,现在在120BPM 125毫秒周期时,1/16的波动约为+-5毫秒,四分之一拍的波动约为+-1毫秒。还有很多需要改进的地方,但我想没关系您知道您最有可能使用多任务操作系统吗?您的操作系统可以随时中断任何进程,以便将CPU切换到另一个后台任务,在恢复进程之前,您是否意识到这一点?对于您的进程来说,在通用操作系统上运行只需要几毫秒的抖动似乎有些不切实际。有一些定制的、特殊用途的实时操作系统可以保证进程的实时CPU处理。如果你需要那么高的精度,你就需要使用专用的操作系统来达到这个目的。@Sam Varshavchik我假设这是可能的,因为这种软件的存在:数字音频工作站已经存在,即使在消费级操作系统下,它们也能完全很好地处理这种一致的事件。一心多用显然会影响一切,但是。。。对于8线程4GHz CPU来说,20毫秒的时间太多了。它们的核心逻辑很可能是用手工优化的C编写的,或者汇编语言。我会使用std::chrono::Stadium_时钟,因为高分辨率的时钟通常只是受外部波动影响的系统时钟。人们可以通过在短时间内睡觉来试验旋转与睡眠的连续体,直到你应该醒来,然后旋转到真正的时间。您可以将早起量从0更改为您应该睡眠的整个时间。您是否知道您最有可能使用的是多任务操作系统?您的操作系统可以随时中断任何进程,以便将CPU切换到另一个后台任务,在恢复进程之前,您是否意识到这一点?对于您的进程来说,在通用操作系统上运行只需要几毫秒的抖动似乎有些不切实际。有一些定制的、特殊用途的实时操作系统可以保证进程的实时CPU处理。如果你需要那么高的精度,你需要使用专用的操作系统来达到这个目的。@Sam Varshavchik我假设这是可能的,因为有这样的软件存在:数字音频工作站是
ready exist,即使在消费级操作系统下,他们也完全可以处理此类一致性事件。一心多用显然会影响一切,但是。。。对于8线程4GHz CPU来说,20毫秒的时间太多了。它们的核心逻辑很可能是用手工优化的C编写的,或者汇编语言。我会使用std::chrono::Stadium_时钟,因为高分辨率的时钟通常只是受外部波动影响的系统时钟。人们可以通过在短时间内睡觉来试验旋转与睡眠的连续体,直到你应该醒来,然后旋转到真正的时间。你可以改变早起量,从0到你应该睡觉的整个时间。