我正在尝试实现类似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 ms的偏差太大。
答案 0 :(得分:4)
我看到一个大错误和一个小错误。最大的错误是您的代码假定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
,则会得到大约1041667ns。 iteration_time{1}
是指您希望Timer_class::Runner
中的每次循环迭代所花费的确切时间。
要纠正较大的错误,您需要休眠{em> 到time_point
,而不是休眠 for 进入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};
}
}
答案 1 :(得分:0)
我最终使用了@ howard-hinnant版本的delay,并减小了openal-soft中的缓冲区大小,这产生了巨大的变化,现在在120BPM(125 ms周期)内,1/16的波动约为+ -5 ms四分之一拍为+ -1 ms。有很多不足之处,但我想还可以