Непоследовательная задержка chrono::high_resolution_clock

Я пытаюсь реализовать MIDI-подобный проигрыватель сэмплов с синхронизацией.

Есть таймер, который увеличивает счетчик импульсов, и каждые 480 импульсов составляют четверть, поэтому период импульса составляет 1041667 нс при 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 мс для аудио слишком много.

Вы знаете, что используете (скорее всего) многозадачную операционную систему? Ваша операционная система может прервать любой процесс в любое время, чтобы переключить ЦП на другую фоновую задачу, прежде чем возобновить свой процесс, знаете ли вы об этом? Ожидание дрожания всего в несколько миллисекунд для вашего процесса, работающего в операционной системе общего назначения, кажется несколько нереалистичным. Существуют пользовательские, специальные «операционные системы реального времени», которые гарантируют процессор в реальном времени для процессов. Если вам нужна такая точность, вам нужно использовать специальную ОС для этой цели.

Sam Varshavchik 01.06.2019 15:44

@Sam Varshavchik Ну, я предположил, что это возможно, основываясь на существовании такого программного обеспечения: цифровые звуковые рабочие станции уже существуют, и они прекрасно обрабатывают такие последовательные события даже в ОС потребительского уровня. Многозадачность, конечно, влияет на все, но... 20 мс для 8-потокового 4ГГц процессора - это слишком много.

ub0baa 01.06.2019 15:55

Их основная логика, скорее всего, написана на оптимизированном вручную языке C или ассемблере.

Sam Varshavchik 01.06.2019 15:56

да, но 20 мс неприемлемо, планирование потоков гораздо более детальное, чем это.

Avin Kavish 01.06.2019 15:58

Я бы использовал std::chrono::steady_clock, поскольку «высокое разрешение» часто представляет собой просто системные часы, которые подвержены внешним колебаниям.

Galik 01.06.2019 15:58

И почему вы не используете std::this_thread::sleep_until?

Galik 01.06.2019 15:59

После того, как вы переключитесь на steady_clock, я думаю, ваша задержка должна быть только +, тогда вы можете написать фрагмент кода, чтобы найти эту задержку и вычесть ее из вашего времени вращения. sleep_until не дает никаких гарантий на следующее пробуждение. Ожидание вращения - лучший способ пойти. stackoverflow.com/questions/45571180/…. Загрузка ЦП будет мешать этому, но это относится ко всем программам DAW/DJ, работающим в Windows. Если вам нужна последовательная обработка звука, используйте Mac, как это делает большинство ди-джеев (таких как я), и купите внешнюю звуковую карту, предназначенную для цифровой обработки звука.

Avin Kavish 01.06.2019 16:00

Самая первая звуковая карта DJ, которую я купил, использовала этот драйвер, это практически лучшее решение для Windows, кроме переключения на основной звук на Mac. sweetwater.com/sweetcare/articles/…

Avin Kavish 01.06.2019 16:16

@Avin Kavish, спасибо, steady_clock работает немного лучше, по крайней мере, измеренное время сейчас правильное (1041700) нс. Но период выходного аудио по-прежнему составляет 487 мс или 511 мс. Никаких других значений, только эти два. Похоже, что-то в коде съедает эти 24 мс. Я пытался отключить графический интерфейс (ругательства). но это не имело значения. В конце концов я надеюсь втиснуть его в какой-нибудь одноплатный компьютер, так как это всего лишь пробный плеер.

ub0baa 01.06.2019 16:30

@ ub0baa: особенно с API-интерфейсами Windows, вы часто можете наблюдать некоторое квантование значений времени, которые претендуют на то, чтобы быть в нс. Например, если вы пишете цикл занятости, который вызывает high_resolution_clock::now() или steady_clock::now(), вы вполне можете увидеть, как одно и то же значение повторяется много раз, а затем внезапный скачок на целых 24 мс, о которых вы упоминали выше, затем это повторяется в течение того же периода и снова прыгает. Десять строк кода, и вы можете проверить, происходит ли это на вашей собственной машине....

Tony Delroy 01.06.2019 16:59

@AvinKavish Также нет гарантий, что спин-блокировка прекратится при запуске пользовательского кода. Я имею в виду, что вы можете быть правы в том, что это лучший способ. Но используя std::thread::sleep_until в моей системе, я достигаю точности в районе 0.08 миллисекунды (при компиляции в другом месте).

Galik 01.06.2019 17:01

да, ответ Говарда должен сделать это, поскольку цикл ожидает пока времени, а не для периода времени, тогда задержка вызовов API не должна быть проблемой. Но у меня есть ощущение, что сам API может вводить случайную внутреннюю задержку, которая все равно будет проблемой.

Avin Kavish 01.06.2019 17:15

@Galik О, это интересно, я думаю, мне стоит самому поэкспериментировать. Вы видите теорию, стоящую за этим, верно? Спин-блокировка явно не передает управление, но sleep_until уступает планировщику.

Avin Kavish 01.06.2019 17:22

Можно поэкспериментировать с континуумом вращения и сна, спя до некоторого короткого времени, пока вы не должны проснуться, а затем вращаясь, пока не придет время. Можно варьировать значение «раннего пробуждения» от 0 до всего времени, которое вы должны спать.

Howard Hinnant 01.06.2019 17:28
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
14
366
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я вижу большую ошибку и маленькую ошибку. Большая ошибка заключается в том, что ваш код предполагает, что основная обработка в 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>;

Я ничего не знаю о музыке, но я предполагаю, что приведенный выше код верен, потому что, если вы преобразуете iteration_time{1} в nanoseconds, вы получите примерно 1041667 нс. iteration_time{1} предназначен для точного определения времени, которое вы хотите, чтобы каждая итерация вашего цикла в Timer_class::Runner занимала.

Чтобы исправить большую ошибку, вам нужно спать до a time_point, а не спать для a 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, вот что имело огромное значение, колебания теперь составляют около +-5 мс для 1/16 при 120 ударах в минуту (период 125 мс) и +- 1 мс для четверти доли. Оставляет желать лучшего, но я думаю, что это нормально

Другие вопросы по теме