Я пытаюсь реализовать 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 Ну, я предположил, что это возможно, основываясь на существовании такого программного обеспечения: цифровые звуковые рабочие станции уже существуют, и они прекрасно обрабатывают такие последовательные события даже в ОС потребительского уровня. Многозадачность, конечно, влияет на все, но... 20 мс для 8-потокового 4ГГц процессора - это слишком много.
Их основная логика, скорее всего, написана на оптимизированном вручную языке C или ассемблере.
да, но 20 мс неприемлемо, планирование потоков гораздо более детальное, чем это.
Я бы использовал std::chrono::steady_clock, поскольку «высокое разрешение» часто представляет собой просто системные часы, которые подвержены внешним колебаниям.
И почему вы не используете std::this_thread::sleep_until?
После того, как вы переключитесь на steady_clock, я думаю, ваша задержка должна быть только +, тогда вы можете написать фрагмент кода, чтобы найти эту задержку и вычесть ее из вашего времени вращения. sleep_until не дает никаких гарантий на следующее пробуждение. Ожидание вращения - лучший способ пойти. stackoverflow.com/questions/45571180/…. Загрузка ЦП будет мешать этому, но это относится ко всем программам DAW/DJ, работающим в Windows. Если вам нужна последовательная обработка звука, используйте Mac, как это делает большинство ди-джеев (таких как я), и купите внешнюю звуковую карту, предназначенную для цифровой обработки звука.
Самая первая звуковая карта DJ, которую я купил, использовала этот драйвер, это практически лучшее решение для Windows, кроме переключения на основной звук на Mac. sweetwater.com/sweetcare/articles/…
@Avin Kavish, спасибо, steady_clock работает немного лучше, по крайней мере, измеренное время сейчас правильное (1041700) нс. Но период выходного аудио по-прежнему составляет 487 мс или 511 мс. Никаких других значений, только эти два. Похоже, что-то в коде съедает эти 24 мс. Я пытался отключить графический интерфейс (ругательства). но это не имело значения. В конце концов я надеюсь втиснуть его в какой-нибудь одноплатный компьютер, так как это всего лишь пробный плеер.
@ ub0baa: особенно с API-интерфейсами Windows, вы часто можете наблюдать некоторое квантование значений времени, которые претендуют на то, чтобы быть в нс. Например, если вы пишете цикл занятости, который вызывает high_resolution_clock::now() или steady_clock::now(), вы вполне можете увидеть, как одно и то же значение повторяется много раз, а затем внезапный скачок на целых 24 мс, о которых вы упоминали выше, затем это повторяется в течение того же периода и снова прыгает. Десять строк кода, и вы можете проверить, происходит ли это на вашей собственной машине....
@AvinKavish Также нет гарантий, что спин-блокировка прекратится при запуске пользовательского кода. Я имею в виду, что вы можете быть правы в том, что это лучший способ. Но используя std::thread::sleep_until в моей системе, я достигаю точности в районе 0.08 миллисекунды (при компиляции в другом месте).
да, ответ Говарда должен сделать это, поскольку цикл ожидает пока времени, а не для периода времени, тогда задержка вызовов API не должна быть проблемой. Но у меня есть ощущение, что сам API может вводить случайную внутреннюю задержку, которая все равно будет проблемой.
@Galik О, это интересно, я думаю, мне стоит самому поэкспериментировать. Вы видите теорию, стоящую за этим, верно? Спин-блокировка явно не передает управление, но sleep_until уступает планировщику.
Можно поэкспериментировать с континуумом вращения и сна, спя до некоторого короткого времени, пока вы не должны проснуться, а затем вращаясь, пока не придет время. Можно варьировать значение «раннего пробуждения» от 0 до всего времени, которое вы должны спать.





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