From 7cde0b0f1a477cd6154750a77965a61e120d4e4b Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Mon, 13 Jul 2026 17:46:15 +0800 Subject: [PATCH] Fix invalid channel layout (0x0) in audio processor and add playback diagnostics - Add FixChannelLayout() helper to AudioProcessor::Open() to fall back to a default native channel layout when the input/output layout is unspecified, custom, or has a zero mask. This prevents FFmpeg's abuffer/aformat filters from rejecting 'channel_layouts=0x0' on Linux. - Log audio processor open parameters, viewer audio queue state, and AudioManager::PushToOutput device/stream status to help diagnose silent playback on Linux. --- app/audio/audiomanager.cpp | 16 ++++++- app/audio/audioprocessor.cpp | 81 +++++++++++++++++++++++++++++------- app/widget/viewer/viewer.cpp | 32 +++++++++++++- 3 files changed, 112 insertions(+), 17 deletions(-) diff --git a/app/audio/audiomanager.cpp b/app/audio/audiomanager.cpp index 71f8d1ade..dfc78455f 100644 --- a/app/audio/audiomanager.cpp +++ b/app/audio/audiomanager.cpp @@ -93,6 +93,11 @@ int InputCallback(const void *input, void *output, unsigned long frameCount, bool AudioManager::PushToOutput(const AudioParams ¶ms, const QByteArray &samples, QString *error) { + qDebug() << "AudioManager::PushToOutput: device=" << output_device_ + << "sample_rate=" << params.sample_rate() + << "channels=" << params.channel_count() + << "bytes=" << samples.size(); + if (output_device_ == paNoDevice) { if (error) *error = tr("No output device is set"); @@ -113,10 +118,14 @@ bool AudioManager::PushToOutput(const AudioParams ¶ms, if (r != paNoError) { // Unhandled error //qCritical() << "Failed to open output stream:" << Pa_GetErrorText(r); + qCritical() << "AudioManager::PushToOutput: Pa_OpenStream failed:" + << Pa_GetErrorText(r); if (error) *error = Pa_GetErrorText(r); return false; } + qDebug() << "AudioManager::PushToOutput: opened stream with" + << params.channel_count() << "channels"; output_buffer_->set_bytes_per_frame(output_params_.samples_to_bytes(1)); } @@ -124,7 +133,9 @@ bool AudioManager::PushToOutput(const AudioParams ¶ms, output_buffer_->write(samples); if (!Pa_IsStreamActive(output_stream_)) { - Pa_StartStream(output_stream_); + PaError r = Pa_StartStream(output_stream_); + qDebug() << "AudioManager::PushToOutput: Pa_StartStream returned" + << r << Pa_GetErrorText(r); } return true; @@ -365,6 +376,9 @@ AudioManager::AudioManager() PaDeviceIndex output_device = FindConfigDeviceByName(true); PaDeviceIndex input_device = FindConfigDeviceByName(false); + qDebug() << "AudioManager: selected output device index=" << output_device + << "input device index=" << input_device; + SetOutputDevice(output_device); SetInputDevice(input_device); diff --git a/app/audio/audioprocessor.cpp b/app/audio/audioprocessor.cpp index 586c725c5..4f1b6f51e 100644 --- a/app/audio/audioprocessor.cpp +++ b/app/audio/audioprocessor.cpp @@ -33,6 +33,47 @@ extern "C" { namespace olive { +/** + * @brief Ensure an AudioParams has a usable native channel layout mask. + * + * FFmpeg's abuffer/aformat filters reject channel_layout=0x0 / unspecified + * layouts (e.g. when the user config or a source stream reports a mask of 0). + * If the provided layout is not a valid native mask, fall back to a default + * layout derived from the channel count (stereo when unknown). + */ +static AudioParams FixChannelLayout(const AudioParams ¶ms) +{ + AudioParams result = params; + const AVChannelLayout &layout = params.channel_layout(); + + bool needs_fix = false; + if (!av_channel_layout_check(&layout)) { + needs_fix = true; + } else if (layout.order != AV_CHANNEL_ORDER_NATIVE) { + needs_fix = true; + } else if (layout.u.mask == 0) { + needs_fix = true; + } + + if (needs_fix) { + int channels = params.channel_count(); + if (channels <= 0) { + channels = 2; + } + + qWarning() << "AudioProcessor: fixing invalid/unspecified channel layout" + << "(channels=" << params.channel_count() << ") -> default" + << channels << "channel layout"; + + AVChannelLayout fallback; + av_channel_layout_default(&fallback, channels); + result.set_channel_layout(fallback); + av_channel_layout_uninit(&fallback); + } + + return result; +} + AudioProcessor::AudioProcessor() { filter_graph_ = nullptr; @@ -59,16 +100,25 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to, return false; } - from_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(from.format()); - to_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(to.format()); + AudioParams from_fixed = FixChannelLayout(from); + AudioParams to_fixed = FixChannelLayout(to); + + qDebug() << "AudioProcessor::Open: from sample_rate=" + << from_fixed.sample_rate() << "channels=" + << from_fixed.channel_count() << "layout_mask=0x" << Qt::hex + << from_fixed.channel_layout().u.mask << "to sample_rate=" + << to_fixed.sample_rate() << "channels=" << to_fixed.channel_count() + << "layout_mask=0x" << to_fixed.channel_layout().u.mask << Qt::dec; // Set up audio buffer args char filter_args[200]; + from_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(from_fixed.format()); + to_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(to_fixed.format()); snprintf( filter_args, 200, "time_base=%d/%d:sample_rate=%d:sample_fmt=%d:channel_layout=0x%" PRIx64, - 1, from.sample_rate(), from.sample_rate(), from_fmt_, - from.channel_layout().u.mask); + 1, from_fixed.sample_rate(), from_fixed.sample_rate(), from_fmt_, + from_fixed.channel_layout().u.mask); int r; @@ -120,18 +170,19 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to, } // Create conversion filter - auto ch1 = from.channel_layout(); - auto ch2 = to.channel_layout(); - if (from.sample_rate() != to.sample_rate() || - av_channel_layout_compare(&ch1, &ch2) || from.format() != to.format() || - (to.format().is_planar() && + auto ch1 = from_fixed.channel_layout(); + auto ch2 = to_fixed.channel_layout(); + if (from_fixed.sample_rate() != to_fixed.sample_rate() || + av_channel_layout_compare(&ch1, &ch2) || + from_fixed.format() != to_fixed.format() || + (to_fixed.format().is_planar() && create_tempo)) { // Tempo processor automatically converts to packed, // so if the desired output is planar, it'll need // to be converted snprintf(filter_args, 200, "sample_fmts=%s:sample_rates=%d:channel_layouts=0x%" PRIx64, - av_get_sample_fmt_name(to_fmt_), to.sample_rate(), - to.channel_layout().u.mask); + av_get_sample_fmt_name(to_fmt_), to_fixed.sample_rate(), + to_fixed.channel_layout().u.mask); AVFilterContext *c; r = avfilter_graph_create_filter(&c, avfilter_get_by_name("aformat"), @@ -182,9 +233,9 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to, in_frame_ = av_frame_alloc(); if (in_frame_) { - in_frame_->sample_rate = from.sample_rate(); + in_frame_->sample_rate = from_fixed.sample_rate(); in_frame_->format = from_fmt_; - in_frame_->ch_layout = from.channel_layout(); + in_frame_->ch_layout = from_fixed.channel_layout(); in_frame_->pts = 0; } else { qCritical() << "Failed to allocate input frame"; @@ -199,8 +250,8 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to, return false; } - from_ = from; - to_ = to; + from_ = from_fixed; + to_ = to_fixed; return true; } diff --git a/app/widget/viewer/viewer.cpp b/app/widget/viewer/viewer.cpp index 37ef50491..cceb1e318 100644 --- a/app/widget/viewer/viewer.cpp +++ b/app/widget/viewer/viewer.cpp @@ -601,6 +601,14 @@ void ViewerWidget::UpdateAudioProcessor() .toString() .toStdString())); + qDebug() << "ViewerWidget::UpdateAudioProcessor: from sample_rate=" + << ap.sample_rate() << "channels=" << ap.channel_count() + << "layout_mask=0x" << Qt::hex << ap.channel_layout().u.mask + << "to sample_rate=" << packed.sample_rate() + << "channels=" << packed.channel_count() + << "layout_mask=0x" << packed.channel_layout().u.mask + << Qt::dec; + audio_processor_.Open( ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_)); } @@ -844,9 +852,14 @@ void ViewerWidget::QueueNextAudioBuffer() // Clamp queue end by zero and the audio length queue_end = std::clamp(queue_end, rational(0), GetConnectedNode()->GetAudioLength()); + qDebug() << "ViewerWidget::QueueNextAudioBuffer: time=" + << audio_playback_queue_time_.toDouble() << "end=" + << queue_end.toDouble() + << "audio_length=" << GetConnectedNode()->GetAudioLength().toDouble(); if ((playback_speed_ > 0 && queue_end <= audio_playback_queue_time_) || (playback_speed_ < 0 && queue_end >= audio_playback_queue_time_)) { // This will queue nothing, so stop the loop here + qDebug() << "ViewerWidget::QueueNextAudioBuffer: nothing to queue"; if (prequeuing_audio_) { DecrementPrequeuedAudio(); } @@ -865,6 +878,8 @@ void ViewerWidget::QueueNextAudioBuffer() void ViewerWidget::ReceivedAudioBufferForPlayback() { + qDebug() << "ViewerWidget::ReceivedAudioBufferForPlayback: queue_size=" + << audio_playback_queue_.size(); while (!audio_playback_queue_.empty() && audio_playback_queue_.front()->HasResult()) { RenderTicketWatcher *watcher = audio_playback_queue_.front(); @@ -872,6 +887,10 @@ void ViewerWidget::ReceivedAudioBufferForPlayback() if (watcher->HasResult()) { SampleBuffer samples = watcher->Get().value(); + qDebug() << "ViewerWidget::ReceivedAudioBufferForPlayback: got buffer" + << "allocated=" << samples.is_allocated() + << "sample_count=" << samples.sample_count() + << "channels=" << samples.audio_params().channel_count(); if (samples.is_allocated()) { // If the samples must be reversed, reverse them now if (playback_speed_ < 0) { @@ -881,12 +900,17 @@ void ViewerWidget::ReceivedAudioBufferForPlayback() // Convert to packed data for audio output AudioProcessor::Buffer buf; int r = audio_processor_.Convert(samples.to_raw_ptrs().data(), - samples.sample_count(), &buf); + samples.sample_count(), &buf); + qDebug() << "ViewerWidget::ReceivedAudioBufferForPlayback: Convert" + << "returned=" << r << "buf_size=" << buf.size(); // TempoProcessor may have emptied the array if (r >= 0) { if (!buf.empty()) { const QByteArray &pack = buf.at(0); + qDebug() << "ViewerWidget::ReceivedAudioBufferForPlayback: pushing" + << pack.size() << "bytes prequeuing=" + << prequeuing_audio_; if (prequeuing_audio_) { // Add to prequeued audio buffer prequeued_audio_.append(pack); @@ -1124,11 +1148,15 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) } AudioParams ap = GetConnectedNode()->GetAudioParams(); + qDebug() << "ViewerWidget::PlayInternal: audio params valid=" << ap.is_valid() + << "channel_count=" << ap.channel_count(); if (ap.is_valid() && ap.channel_count() != 0) { UpdateAudioProcessor(); // Verify audio processor output params are valid before using them AudioParams output_params = audio_processor_.to(); + qDebug() << "ViewerWidget::PlayInternal: audio processor output params valid=" + << output_params.is_valid(); if (!output_params.is_valid()) { qWarning() << "Audio processor output params are invalid, skipping audio playback"; @@ -1142,6 +1170,8 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only) prequeuing_audio_ = prequeue_count; // Queue two buffers ahead of time audio_playback_queue_time_ = GetConnectedNode()->GetPlayhead(); + qDebug() << "ViewerWidget::PlayInternal: prequeuing audio start time=" + << audio_playback_queue_time_.toDouble(); for (int i = 0; i < prequeue_count; i++) { QueueNextAudioBuffer(); }