不一致 chrono::high_resolution_clock 延迟

Inconsistent chrono::high_resolution_clock delay

我正在尝试实现类似 MIDI 的时钟采样播放器。

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

排除音频后端影响:我已经尝试过 OpenAL 以及 SDL_mixer。

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 
    }

也就是说,您 "sleeping" 需要循环迭代的时间,然后在此基础上进行处理。

小错误是假设您可以用整数纳秒来表示理想的循环迭代时间。这个错误是如此之小,以至于它并不重要。然而,我通过向人们展示他们如何也可以摆脱这个错误来取悦自己。 :-)

首先让我们通过 exactly 代表理想化循环迭代时间来纠正小错误:

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>;

我对音乐一无所知,但我猜上面的代码是正确的,因为如果将 iteration_time{1} 转换为 nanoseconds,您将得到大约 1041667ns。 iteration_time{1} 旨在成为您希望 Timer_class::Runner 中循环的每次迭代所花费的精确时间。

要纠正较大的错误,您需要休眠 直到 一个 time_point,而不是休眠 duration。这里有一个通用实用程序可以帮助您做到这一点:

template <class Clock, class Duration>
void
delay_until(std::chrono::time_point<Clock, Duration> tp)
{
    while (Clock::now() < tp)
        ;
}

现在,如果您编码 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-soft 中减小了缓冲区大小,这产生了巨大的差异,现在 1/16 的波动约为 +-5 毫秒120BPM(125 毫秒周期)和四分拍 +-1 毫秒。有很多不足之处,但我觉得还可以