C++ 时钟不一致:高分辨率时钟延迟

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

我正在尝试实现一个类似MIDI的时钟样本播放器

有一个定时器,它增加脉冲计数器,每480个脉冲是四分之一,因此脉冲周期为1041667纳秒,每分钟120次。 计时器不是基于睡眠的,并且在单独的线程中运行,但似乎延迟时间不一致:在测试文件中播放的样本之间的周期是波动的+-20毫秒,在某些情况下,周期是正常和稳定的,我无法找出这种效果的依赖性

排除了音频后端的影响:我尝试了OpenAL和SDL_混音器

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到你应该睡觉的整个时间。