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.
This commit is contained in:
@@ -93,6 +93,11 @@ int InputCallback(const void *input, void *output, unsigned long frameCount,
|
|||||||
bool AudioManager::PushToOutput(const AudioParams ¶ms,
|
bool AudioManager::PushToOutput(const AudioParams ¶ms,
|
||||||
const QByteArray &samples, QString *error)
|
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 (output_device_ == paNoDevice) {
|
||||||
if (error)
|
if (error)
|
||||||
*error = tr("No output device is set");
|
*error = tr("No output device is set");
|
||||||
@@ -113,10 +118,14 @@ bool AudioManager::PushToOutput(const AudioParams ¶ms,
|
|||||||
if (r != paNoError) {
|
if (r != paNoError) {
|
||||||
// Unhandled error
|
// Unhandled error
|
||||||
//qCritical() << "Failed to open output stream:" << Pa_GetErrorText(r);
|
//qCritical() << "Failed to open output stream:" << Pa_GetErrorText(r);
|
||||||
|
qCritical() << "AudioManager::PushToOutput: Pa_OpenStream failed:"
|
||||||
|
<< Pa_GetErrorText(r);
|
||||||
if (error)
|
if (error)
|
||||||
*error = Pa_GetErrorText(r);
|
*error = Pa_GetErrorText(r);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
qDebug() << "AudioManager::PushToOutput: opened stream with"
|
||||||
|
<< params.channel_count() << "channels";
|
||||||
|
|
||||||
output_buffer_->set_bytes_per_frame(output_params_.samples_to_bytes(1));
|
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);
|
output_buffer_->write(samples);
|
||||||
|
|
||||||
if (!Pa_IsStreamActive(output_stream_)) {
|
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;
|
return true;
|
||||||
@@ -365,6 +376,9 @@ AudioManager::AudioManager()
|
|||||||
PaDeviceIndex output_device = FindConfigDeviceByName(true);
|
PaDeviceIndex output_device = FindConfigDeviceByName(true);
|
||||||
PaDeviceIndex input_device = FindConfigDeviceByName(false);
|
PaDeviceIndex input_device = FindConfigDeviceByName(false);
|
||||||
|
|
||||||
|
qDebug() << "AudioManager: selected output device index=" << output_device
|
||||||
|
<< "input device index=" << input_device;
|
||||||
|
|
||||||
SetOutputDevice(output_device);
|
SetOutputDevice(output_device);
|
||||||
SetInputDevice(input_device);
|
SetInputDevice(input_device);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,47 @@ extern "C" {
|
|||||||
namespace olive
|
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()
|
AudioProcessor::AudioProcessor()
|
||||||
{
|
{
|
||||||
filter_graph_ = nullptr;
|
filter_graph_ = nullptr;
|
||||||
@@ -59,16 +100,25 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
from_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(from.format());
|
AudioParams from_fixed = FixChannelLayout(from);
|
||||||
to_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(to.format());
|
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
|
// Set up audio buffer args
|
||||||
char filter_args[200];
|
char filter_args[200];
|
||||||
|
from_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(from_fixed.format());
|
||||||
|
to_fmt_ = FFmpegUtils::GetFFmpegSampleFormat(to_fixed.format());
|
||||||
snprintf(
|
snprintf(
|
||||||
filter_args, 200,
|
filter_args, 200,
|
||||||
"time_base=%d/%d:sample_rate=%d:sample_fmt=%d:channel_layout=0x%" PRIx64,
|
"time_base=%d/%d:sample_rate=%d:sample_fmt=%d:channel_layout=0x%" PRIx64,
|
||||||
1, from.sample_rate(), from.sample_rate(), from_fmt_,
|
1, from_fixed.sample_rate(), from_fixed.sample_rate(), from_fmt_,
|
||||||
from.channel_layout().u.mask);
|
from_fixed.channel_layout().u.mask);
|
||||||
|
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
@@ -120,18 +170,19 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create conversion filter
|
// Create conversion filter
|
||||||
auto ch1 = from.channel_layout();
|
auto ch1 = from_fixed.channel_layout();
|
||||||
auto ch2 = to.channel_layout();
|
auto ch2 = to_fixed.channel_layout();
|
||||||
if (from.sample_rate() != to.sample_rate() ||
|
if (from_fixed.sample_rate() != to_fixed.sample_rate() ||
|
||||||
av_channel_layout_compare(&ch1, &ch2) || from.format() != to.format() ||
|
av_channel_layout_compare(&ch1, &ch2) ||
|
||||||
(to.format().is_planar() &&
|
from_fixed.format() != to_fixed.format() ||
|
||||||
|
(to_fixed.format().is_planar() &&
|
||||||
create_tempo)) { // Tempo processor automatically converts to packed,
|
create_tempo)) { // Tempo processor automatically converts to packed,
|
||||||
// so if the desired output is planar, it'll need
|
// so if the desired output is planar, it'll need
|
||||||
// to be converted
|
// to be converted
|
||||||
snprintf(filter_args, 200,
|
snprintf(filter_args, 200,
|
||||||
"sample_fmts=%s:sample_rates=%d:channel_layouts=0x%" PRIx64,
|
"sample_fmts=%s:sample_rates=%d:channel_layouts=0x%" PRIx64,
|
||||||
av_get_sample_fmt_name(to_fmt_), to.sample_rate(),
|
av_get_sample_fmt_name(to_fmt_), to_fixed.sample_rate(),
|
||||||
to.channel_layout().u.mask);
|
to_fixed.channel_layout().u.mask);
|
||||||
|
|
||||||
AVFilterContext *c;
|
AVFilterContext *c;
|
||||||
r = avfilter_graph_create_filter(&c, avfilter_get_by_name("aformat"),
|
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();
|
in_frame_ = av_frame_alloc();
|
||||||
if (in_frame_) {
|
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_->format = from_fmt_;
|
||||||
in_frame_->ch_layout = from.channel_layout();
|
in_frame_->ch_layout = from_fixed.channel_layout();
|
||||||
in_frame_->pts = 0;
|
in_frame_->pts = 0;
|
||||||
} else {
|
} else {
|
||||||
qCritical() << "Failed to allocate input frame";
|
qCritical() << "Failed to allocate input frame";
|
||||||
@@ -199,8 +250,8 @@ bool AudioProcessor::Open(const AudioParams &from, const AudioParams &to,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
from_ = from;
|
from_ = from_fixed;
|
||||||
to_ = to;
|
to_ = to_fixed;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -601,6 +601,14 @@ void ViewerWidget::UpdateAudioProcessor()
|
|||||||
.toString()
|
.toString()
|
||||||
.toStdString()));
|
.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(
|
audio_processor_.Open(
|
||||||
ap, packed, (playback_speed_ == 0) ? 1 : std::abs(playback_speed_));
|
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
|
// Clamp queue end by zero and the audio length
|
||||||
queue_end = std::clamp(queue_end, rational(0),
|
queue_end = std::clamp(queue_end, rational(0),
|
||||||
GetConnectedNode()->GetAudioLength());
|
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_) ||
|
if ((playback_speed_ > 0 && queue_end <= audio_playback_queue_time_) ||
|
||||||
(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
|
// This will queue nothing, so stop the loop here
|
||||||
|
qDebug() << "ViewerWidget::QueueNextAudioBuffer: nothing to queue";
|
||||||
if (prequeuing_audio_) {
|
if (prequeuing_audio_) {
|
||||||
DecrementPrequeuedAudio();
|
DecrementPrequeuedAudio();
|
||||||
}
|
}
|
||||||
@@ -865,6 +878,8 @@ void ViewerWidget::QueueNextAudioBuffer()
|
|||||||
|
|
||||||
void ViewerWidget::ReceivedAudioBufferForPlayback()
|
void ViewerWidget::ReceivedAudioBufferForPlayback()
|
||||||
{
|
{
|
||||||
|
qDebug() << "ViewerWidget::ReceivedAudioBufferForPlayback: queue_size="
|
||||||
|
<< audio_playback_queue_.size();
|
||||||
while (!audio_playback_queue_.empty() &&
|
while (!audio_playback_queue_.empty() &&
|
||||||
audio_playback_queue_.front()->HasResult()) {
|
audio_playback_queue_.front()->HasResult()) {
|
||||||
RenderTicketWatcher *watcher = audio_playback_queue_.front();
|
RenderTicketWatcher *watcher = audio_playback_queue_.front();
|
||||||
@@ -872,6 +887,10 @@ void ViewerWidget::ReceivedAudioBufferForPlayback()
|
|||||||
|
|
||||||
if (watcher->HasResult()) {
|
if (watcher->HasResult()) {
|
||||||
SampleBuffer samples = watcher->Get().value<SampleBuffer>();
|
SampleBuffer samples = watcher->Get().value<SampleBuffer>();
|
||||||
|
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 (samples.is_allocated()) {
|
||||||
// If the samples must be reversed, reverse them now
|
// If the samples must be reversed, reverse them now
|
||||||
if (playback_speed_ < 0) {
|
if (playback_speed_ < 0) {
|
||||||
@@ -882,11 +901,16 @@ void ViewerWidget::ReceivedAudioBufferForPlayback()
|
|||||||
AudioProcessor::Buffer buf;
|
AudioProcessor::Buffer buf;
|
||||||
int r = audio_processor_.Convert(samples.to_raw_ptrs().data(),
|
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
|
// TempoProcessor may have emptied the array
|
||||||
if (r >= 0) {
|
if (r >= 0) {
|
||||||
if (!buf.empty()) {
|
if (!buf.empty()) {
|
||||||
const QByteArray &pack = buf.at(0);
|
const QByteArray &pack = buf.at(0);
|
||||||
|
qDebug() << "ViewerWidget::ReceivedAudioBufferForPlayback: pushing"
|
||||||
|
<< pack.size() << "bytes prequeuing="
|
||||||
|
<< prequeuing_audio_;
|
||||||
if (prequeuing_audio_) {
|
if (prequeuing_audio_) {
|
||||||
// Add to prequeued audio buffer
|
// Add to prequeued audio buffer
|
||||||
prequeued_audio_.append(pack);
|
prequeued_audio_.append(pack);
|
||||||
@@ -1124,11 +1148,15 @@ void ViewerWidget::PlayInternal(int speed, bool in_to_out_only)
|
|||||||
}
|
}
|
||||||
|
|
||||||
AudioParams ap = GetConnectedNode()->GetAudioParams();
|
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) {
|
if (ap.is_valid() && ap.channel_count() != 0) {
|
||||||
UpdateAudioProcessor();
|
UpdateAudioProcessor();
|
||||||
|
|
||||||
// Verify audio processor output params are valid before using them
|
// Verify audio processor output params are valid before using them
|
||||||
AudioParams output_params = audio_processor_.to();
|
AudioParams output_params = audio_processor_.to();
|
||||||
|
qDebug() << "ViewerWidget::PlayInternal: audio processor output params valid="
|
||||||
|
<< output_params.is_valid();
|
||||||
if (!output_params.is_valid()) {
|
if (!output_params.is_valid()) {
|
||||||
qWarning()
|
qWarning()
|
||||||
<< "Audio processor output params are invalid, skipping audio playback";
|
<< "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_ =
|
prequeuing_audio_ =
|
||||||
prequeue_count; // Queue two buffers ahead of time
|
prequeue_count; // Queue two buffers ahead of time
|
||||||
audio_playback_queue_time_ = GetConnectedNode()->GetPlayhead();
|
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++) {
|
for (int i = 0; i < prequeue_count; i++) {
|
||||||
QueueNextAudioBuffer();
|
QueueNextAudioBuffer();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user