Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-07-13 18:47:33 +08:00
5 changed files with 182 additions and 33 deletions
+52 -1
View File
@@ -93,6 +93,11 @@ int InputCallback(const void *input, void *output, unsigned long frameCount,
bool AudioManager::PushToOutput(const AudioParams &params,
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 &params,
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 &params,
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;
@@ -269,6 +280,39 @@ void AudioManager::StopRecording()
}
}
#ifdef Q_OS_LINUX
static PaDeviceIndex GetPreferredLinuxAudioDevice(bool is_output_device)
{
// Prefer PipeWire, then PulseAudio. Both provide mixing; plain ALSA/JACK
// defaults often fail to share the device on modern Linux desktops.
const QStringList preferred_host_apis = {
QStringLiteral("PipeWire"),
QStringLiteral("PulseAudio"),
};
for (const QString &preferred : preferred_host_apis) {
for (PaHostApiIndex i = 0, end = Pa_GetHostApiCount(); i < end; i++) {
const PaHostApiInfo *info = Pa_GetHostApiInfo(i);
if (!info) {
continue;
}
const QString name = QString::fromLatin1(info->name);
if (name.contains(preferred, Qt::CaseInsensitive)) {
PaDeviceIndex dev = is_output_device ? info->defaultOutputDevice :
info->defaultInputDevice;
if (dev != paNoDevice) {
return dev;
}
}
}
}
return is_output_device ? Pa_GetDefaultOutputDevice() :
Pa_GetDefaultInputDevice();
}
#endif
PaDeviceIndex AudioManager::FindConfigDeviceByName(bool is_output_device)
{
QString entry = is_output_device ? QStringLiteral("AudioOutput") :
@@ -293,8 +337,12 @@ PaDeviceIndex AudioManager::FindDeviceByName(const QString &s,
}
}
#ifdef Q_OS_LINUX
return GetPreferredLinuxAudioDevice(is_output_device);
#else
return is_output_device ? Pa_GetDefaultOutputDevice() :
Pa_GetDefaultInputDevice();
#endif
}
PaStreamParameters AudioManager::GetPortAudioParams(const AudioParams &params,
@@ -328,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);
+66 -15
View File
@@ -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 &params)
{
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;
}
-10
View File
@@ -579,14 +579,8 @@ bool RenderWorkerPool::PrepareJob(RenderTicketPtr ticket,
if (it != graph_cache_.end() && !project->is_modified()) {
graph_path = it->path;
AddGraphPathRefLocked(graph_path);
qDebug()
<< "RenderWorkerPool::PrepareJob: using cached graph snapshot"
<< graph_path;
} else {
if (it != graph_cache_.end()) {
qDebug()
<< "RenderWorkerPool::PrepareJob: graph stale, rewriting"
<< project->is_modified();
SetGraphPathCachedLocked(it->path, false);
graph_cache_.erase(it);
}
@@ -648,9 +642,6 @@ bool RenderWorkerPool::WriteGraphSnapshot(Project *project, QString *path)
return false;
}
qDebug() << "RenderWorkerPool wrote graph snapshot" << file.fileName()
<< "size" << QFileInfo(file.fileName()).size();
*path = file.fileName();
return true;
}
@@ -1242,7 +1233,6 @@ void RenderWorkerPool::FinishWithFrame(RenderTicketPtr ticket,
void RenderWorkerPool::CleanupGraphFile(const QString &path)
{
if (!path.isEmpty()) {
qDebug() << "RenderWorkerPool cleaning up graph file" << path;
QFile::remove(path);
}
}
+33 -6
View File
@@ -961,7 +961,7 @@ void TimelineWidget::SynchronizeSelectedClipsBySourceTime()
}
const QVector<SourceSyncClip> sync_clips =
GetSelectedSourceSyncClips(selected_blocks_);
GetSelectedSourceSyncClips(GetSelectedBlocks());
if (sync_clips.size() < 2) {
return;
}
@@ -1028,8 +1028,12 @@ void TimelineWidget::SynchronizeSelectedClipsByWaveform()
}
const QVector<WaveformSyncClip> sync_clips =
GetSelectedWaveformSyncClips(selected_blocks_);
GetSelectedWaveformSyncClips(GetSelectedBlocks());
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform:"
<< sync_clips.size() << "sync clip(s) selected";
if (sync_clips.size() < 2) {
Core::instance()->ShowStatusBarMessage(
tr("Select at least 2 clips with cached waveforms to sync by waveform"));
return;
}
@@ -1050,6 +1054,11 @@ void TimelineWidget::SynchronizeSelectedClipsByWaveform()
const QVector<double> reference_envelope =
ExtractWaveformCacheEnvelope(reference, sample_rate, window_samples);
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: sample_rate="
<< sample_rate << "window_samples=" << window_samples
<< "max_offset_windows=" << max_offset_windows
<< "reference_envelope_size=" << reference_envelope.size();
struct SyncPlacement {
ClipBlock *clip = nullptr;
rational timeline_in;
@@ -1066,9 +1075,14 @@ void TimelineWidget::SynchronizeSelectedClipsByWaveform()
sync_clip, sample_rate, window_samples);
const AudioWaveformSync::OffsetResult offset =
AudioWaveformSync::EstimateEnvelopeOffset(reference_envelope,
candidate_envelope,
window_samples,
max_offset_windows);
candidate_envelope,
window_samples,
max_offset_windows);
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: candidate"
<< sync_clip.clip << "envelope_size="
<< candidate_envelope.size() << "offset_valid="
<< offset.valid << "offset_samples=" << offset.offset_samples
<< "confidence=" << offset.confidence;
if (!offset.valid) {
continue;
}
@@ -1076,12 +1090,19 @@ void TimelineWidget::SynchronizeSelectedClipsByWaveform()
const AudioSynchronizer::Placement placement =
AudioSynchronizer::PlaceByWaveformOffset(
reference.clip->in(), offset.offset_samples, sample_rate);
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: placement"
<< "valid=" << placement.valid << "timeline_in="
<< placement.timeline_in.toDouble();
if (placement.valid && placement.timeline_in >= 0) {
placements.append({ sync_clip.clip, placement.timeline_in });
}
}
if (placements.size() < 2) {
qDebug() << "TimelineWidget::SynchronizeSelectedClipsByWaveform: no usable"
<< "offsets found";
Core::instance()->ShowStatusBarMessage(
tr("Could not find a usable waveform offset for the selected clips"));
return;
}
@@ -1107,7 +1128,9 @@ void TimelineWidget::SynchronizeSelectedClipsByWaveform()
new SetSelectionsCommand(this, new_selections, GetSelections()));
Core::instance()->undo_stack()->push(command,
tr("Synchronize Clips by Waveform"));
tr("Synchronize Clips by Waveform"));
Core::instance()->ShowStatusBarMessage(
tr("Synchronized %1 clip(s) by waveform").arg(placements.size()));
}
void TimelineWidget::GenerateProxiesForSelectedClips()
@@ -1686,6 +1709,10 @@ void TimelineWidget::ShowContextMenu()
menu.addAction(tr("Synchronize by Waveform"));
sync_by_waveform->setEnabled(
GetSelectedWaveformSyncClips(selected).size() >= 2);
sync_by_waveform->setShortcut(
QKeySequence(QStringLiteral("Ctrl+Shift+W")));
sync_by_waveform->setShortcutContext(Qt::WidgetShortcut);
this->addAction(sync_by_waveform);
connect(sync_by_waveform, &QAction::triggered, this,
&TimelineWidget::SynchronizeSelectedClipsByWaveform);
+31 -1
View File
@@ -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<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 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();
}