audio: master-clock playback timing, output clock compensation, buffer config, interpolated speed

- playback timer uses the audio output device as its master clock: the
  PortAudio callback counts consumed frames (including underrun
  zero-fill) so video cannot drift away from what is heard; wall clock
  remains as fallback when no clocked output is running
- output clock compensates for device output latency; new Preferences >
  Audio buffer size setting (0 = auto)
- SampleBuffer::speed() now uses linear interpolation instead of
  nearest-neighbor sampling
- regression tests: audio-clock driven timer (fwd/rev/speed), wall
  clock fallback, interpolation correctness
This commit is contained in:
2026-07-19 21:44:34 +08:00
parent 0c02ff0d77
commit a7ddc0f114
16 changed files with 266 additions and 8 deletions
+32 -2
View File
@@ -71,6 +71,10 @@ int output_callback(const void *input, void *output, unsigned long frame_count,
max_read - read_count);
}
// Count all frames leaving the device (including zero-filled underrun
// frames) so this can serve as the playback master clock
device->add_output_frames(frame_count);
return paContinue;
}
@@ -106,10 +110,14 @@ bool AudioManager::push_to_output(const AudioParams &params,
PaStreamParameters p = get_port_audio_params(params, output_device_);
// 0 = let PortAudio choose the buffer size
const unsigned long frames_per_buffer =
OAK_CONFIG("AudioOutputBufferSize").toUInt();
PaError r = Pa_OpenStream(&output_stream_, nullptr, &p,
output_params_.sample_rate(),
paFramesPerBufferUnspecified, paNoFlag,
output_callback, output_buffer_);
frames_per_buffer, paNoFlag, output_callback,
output_buffer_);
if (r != paNoError) {
// Unhandled error
//qCritical() << "Failed to open output stream:" << Pa_GetErrorText(r);
@@ -141,6 +149,28 @@ void AudioManager::clear_buffered_output()
output_buffer_->clear();
}
double AudioManager::seconds() const
{
if (!output_stream_ || !Pa_IsStreamActive(output_stream_)) {
return -1.0;
}
double seconds = double(output_buffer_->output_frames_consumed()) /
double(output_params_.sample_rate());
// Compensate for output latency so the clock reflects what is audible
if (const PaStreamInfo *info = Pa_GetStreamInfo(output_stream_)) {
seconds -= info->outputLatency;
}
return qMax(0.0, seconds);
}
void AudioManager::reset_output_clock()
{
output_buffer_->reset_output_frames();
}
PaSampleFormat AudioManager::get_port_audio_sample_format(SampleFormat fmt)
{
switch (fmt) {