简体   繁体   中英

Inconsistent chrono::high_resolution_clock delay

I'm trying to implement a MIDI-like clocked sample player.

There is a timer, which increments pulse counter, and every 480 pulses is a quarter, so pulse period is 1041667 ns for 120 beats per minute. Timer is not sleep-based and running in separate thread, but it seems like delay time is inconsistent: period between samples played in a test file is fluctuating +- 20 ms (in some occasions period is OK and steady, I can't find out dependency of this effect).

Audio backend influence is excluded: i've tried OpenAL as well as 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
}

Am i doing something wrong in terms of an approach? Is there any better way to implement timer of this kind? Deviation higher than 4-5 ms is too much in case of audio.

I see a large error and a small error. The large error is that your code assumes that the main processing in Runner consistently takes zero time:

    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 
    }

That is, you're "sleeping" for the time you want your loop iteration to take, and then you're doing processing on top of that.

The small error is presuming that you can represent your ideal loop iteration time with an integral number of nanoseconds. This error is so small that it doesn't really matter. However I amuse myself by showing people how they can get rid of this error too. :-)

First lets correct the small error by exactly representing the idealized loop iteration time:

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

I know nothing of music, but I'm guessing the above code is right because if you convert iteration_time{1} to nanoseconds , you get approximately 1041667ns. iteration_time{1} is intended to be the precise amount of time you want each iteration of your loop in Timer_class::Runner to take.

To correct the large error, you need to sleep until a time_point , as opposed to sleeping for a duration . Here's a generic utility to help you do that:

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

Now if you code Timer_class::Runner to use delay_until instead of sleep_ns , I think you'll get better results:

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

I ended up using @howard-hinnant version of delay, and reducing buffer size in openal-soft, that's what made a huge difference, fluctuations is now about +-5 ms for 1/16th at 120BPM (125 ms period) and +-1 ms for quarter beats. Leaves a lot to be desired, but i guess it's okay

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM