build: split the engine into liboakengine.so; worker drops the UI entirely

Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).

- oak-render-worker now links liboakengine instead of the whole
  libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
  objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
  macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
  ../app but backends now live in engine/; a stale pre-split liboakgl
  in the build tree got dlopened instead, re-initialized and later
  destroyed the interposed engine statics (full-suite segfault at
  DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
2026-07-20 03:23:28 +08:00
parent 026ff94b5e
commit 28c4426236
604 changed files with 243 additions and 172 deletions
+32
View File
@@ -0,0 +1,32 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
audio/audiolevelmeter.cpp
audio/audiolevelmeter.h
audio/audiosynchronizer.cpp
audio/audiosynchronizer.h
audio/audiowaveformsync.cpp
audio/audiowaveformsync.h
audio/audiomanager.cpp
audio/audiomanager.h
audio/audioprocessor.cpp
audio/audioprocessor.h
audio/audiovisualwaveform.cpp
audio/audiovisualwaveform.h
PARENT_SCOPE
)
+104
View File
@@ -0,0 +1,104 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audiolevelmeter.h"
#include <algorithm>
#include <cmath>
#include "common/decibel.h"
namespace olive
{
AudioLevelMeter::Stats
AudioLevelMeter::analyze_sample_buffer(const core::SampleBuffer &samples)
{
Stats stats;
const int channel_count = samples.channel_count();
const size_t sample_count = samples.sample_count();
stats.channels.resize(channel_count);
if (!channel_count || !sample_count) {
return stats;
}
double total_square = 0.0;
size_t total_samples = 0;
for (int channel = 0; channel < channel_count; channel++) {
const float *channel_data = samples.data(channel);
double peak = 0.0;
double square_sum = 0.0;
for (size_t sample = 0; sample < sample_count; sample++) {
const double value = channel_data[sample];
const double abs_value = std::abs(value);
peak = std::max(peak, abs_value);
square_sum += value * value;
}
const double mean_square =
square_sum / static_cast<double>(sample_count);
const double rms = std::sqrt(mean_square);
ChannelStats channel_stats;
channel_stats.peak_linear = peak;
channel_stats.peak_db = linear_to_db(peak);
channel_stats.rms_linear = rms;
channel_stats.rms_db = linear_to_db(rms);
channel_stats.vu_db = channel_stats.rms_db;
stats.channels[channel] = channel_stats;
stats.max_peak_linear = std::max(stats.max_peak_linear, peak);
total_square += square_sum;
total_samples += sample_count;
}
stats.silence = qFuzzyIsNull(stats.max_peak_linear);
stats.integrated_lufs =
power_to_lufs(total_square / static_cast<double>(total_samples));
return stats;
}
double AudioLevelMeter::linear_to_db(double linear)
{
if (linear <= 0.0) {
return Decibel::minimum;
}
return Decibel::from_linear(linear);
}
double AudioLevelMeter::power_to_lufs(double mean_square)
{
if (mean_square <= 0.0) {
return Decibel::minimum;
}
// BS.1770 loudness uses K-weighted mean square. This first pass stores the
// compatible unit and can be extended with K-weighting without changing UI.
return -0.691 + 10.0 * std::log10(mean_square);
}
}
+57
View File
@@ -0,0 +1,57 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOLEVELMETER_H
#define OAK_AUDIOLEVELMETER_H
#include <QVector>
#include "olive/core/render/samplebuffer.h"
namespace olive
{
class AudioLevelMeter {
public:
struct ChannelStats {
double peak_linear = 0.0;
double peak_db = -200.0;
double rms_linear = 0.0;
double rms_db = -200.0;
double vu_db = -200.0;
};
struct Stats {
QVector<ChannelStats> channels;
double max_peak_linear = 0.0;
double integrated_lufs = -200.0;
bool silence = true;
};
static Stats analyze_sample_buffer(const core::SampleBuffer &samples);
private:
static double linear_to_db(double linear);
static double power_to_lufs(double mean_square);
};
}
#endif // OAK_AUDIOLEVELMETER_H
+486
View File
@@ -0,0 +1,486 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audiomanager.h"
#ifdef PA_HAS_JACK
#include <pa_jack.h>
#endif
#include "config/config.h"
namespace olive
{
AudioManager *AudioManager::instance_ = nullptr;
void AudioManager::create_instance()
{
if (instance_ == nullptr) {
instance_ = new AudioManager();
}
}
void AudioManager::destroy_instance()
{
delete instance_;
instance_ = nullptr;
}
AudioManager *AudioManager::instance()
{
return instance_;
}
void AudioManager::set_output_notify_interval(int n)
{
output_buffer_->set_notify_interval(n);
}
int output_callback(const void *input, void *output, unsigned long frame_count,
const PaStreamCallbackTimeInfo *time_info,
PaStreamCallbackFlags status_flags, void *user_data)
{
PreviewAudioDevice *device = static_cast<PreviewAudioDevice *>(user_data);
qint64 max_read = frame_count * device->bytes_per_frame();
qint64 read_count =
device->read(reinterpret_cast<char *>(output), max_read);
if (read_count < max_read) {
memset(reinterpret_cast<uint8_t *>(output) + read_count, 0,
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;
}
int input_callback(const void *input, void *output, unsigned long frame_count,
const PaStreamCallbackTimeInfo *time_info,
PaStreamCallbackFlags status_flags, void *user_data)
{
FFmpegEncoder *f = static_cast<FFmpegEncoder *>(user_data);
AudioParams our_params = f->params().audio_params();
our_params.set_format(
f->params().audio_params().format().to_packed_equivalent());
f->write_audio_data(our_params, reinterpret_cast<const uint8_t **>(&input),
frame_count);
return paContinue;
}
bool AudioManager::push_to_output(const AudioParams &params,
const QByteArray &samples, QString *error)
{
if (output_device_ == paNoDevice) {
if (error)
*error = tr("No output device is set");
return false;
}
if (output_params_ != params || output_stream_ == nullptr) {
output_params_ = params;
close_output_stream();
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(),
frames_per_buffer, paNoFlag, output_callback,
output_buffer_);
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));
}
output_buffer_->write(samples);
if (!Pa_IsStreamActive(output_stream_)) {
PaError r = Pa_StartStream(output_stream_);
qDebug() << "AudioManager::PushToOutput: Pa_StartStream returned"
<< r << Pa_GetErrorText(r);
}
return true;
}
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) {
case SampleFormat::u8:
case SampleFormat::u8_p:
return paUInt8;
case SampleFormat::s16:
case SampleFormat::s16_p:
return paInt16;
case SampleFormat::s32:
case SampleFormat::s32_p:
return paInt32;
case SampleFormat::f32:
case SampleFormat::f32_p:
return paFloat32;
case SampleFormat::s64:
case SampleFormat::s64_p:
case SampleFormat::f64:
case SampleFormat::f64_p:
case SampleFormat::invalid:
case SampleFormat::count:
break;
}
return 0;
}
void AudioManager::close_output_stream()
{
if (output_stream_) {
if (Pa_IsStreamActive(output_stream_)) {
stop_output();
}
Pa_CloseStream(output_stream_);
output_stream_ = nullptr;
}
}
void AudioManager::stop_output()
{
// Abort the stream so playback stops immediately
if (output_stream_) {
Pa_AbortStream(output_stream_);
clear_buffered_output();
}
}
void AudioManager::set_output_device(PaDeviceIndex device)
{
if (device == paNoDevice) {
qInfo() << "No output device found";
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
qWarning() << "Invalid output audio device index:" << device;
} else {
qInfo() << "Setting output audio device to"
<< Pa_GetDeviceInfo(device)->name;
}
output_device_ = device;
close_output_stream();
emit output_params_changed();
}
void AudioManager::set_input_device(PaDeviceIndex device)
{
if (device == paNoDevice) {
qInfo() << "No input device found";
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
qWarning() << "Invalid input audio device index:" << device;
} else {
qInfo() << "Setting input audio device to"
<< Pa_GetDeviceInfo(device)->name;
}
input_device_ = device;
}
void AudioManager::hard_reset()
{
close_output_stream();
Pa_Terminate();
Pa_Initialize();
}
bool AudioManager::start_recording(const EncodingParams &params,
QString *error_str)
{
if (input_device_ == paNoDevice) {
return false;
}
input_encoder_ = new FFmpegEncoder(params);
if (!input_encoder_->open()) {
qCritical() << "Failed to open encoder for recording";
return false;
}
PaStreamParameters p =
get_port_audio_params(params.audio_params(), input_device_);
PaError r = Pa_OpenStream(&input_stream_, &p, nullptr,
params.audio_params().sample_rate(),
paFramesPerBufferUnspecified, paNoFlag,
input_callback, input_encoder_);
if (r == paNoError) {
//const PaStreamInfo* info = Pa_GetStreamInfo(input_stream_);
r = Pa_StartStream(input_stream_);
if (r == paNoError) {
return true;
}
}
if (error_str) {
*error_str = Pa_GetErrorText(r);
}
stop_recording();
return false;
}
void AudioManager::stop_recording()
{
if (input_stream_) {
if (Pa_IsStreamActive(input_stream_)) {
Pa_StopStream(input_stream_);
}
Pa_CloseStream(input_stream_);
input_stream_ = nullptr;
}
if (input_encoder_) {
input_encoder_->close();
delete input_encoder_;
input_encoder_ = nullptr;
}
}
#ifdef Q_OS_LINUX
static bool is_preferred_linux_audio_host_api(const PaHostApiInfo *info)
{
if (!info) {
return false;
}
const QString name = QString::fromLatin1(info->name);
return name.contains(QStringLiteral("PipeWire"), Qt::CaseInsensitive) ||
name.contains(QStringLiteral("JACK"), Qt::CaseInsensitive) ||
name.contains(QStringLiteral("PulseAudio"), Qt::CaseInsensitive);
}
static PaDeviceIndex get_preferred_linux_audio_device(bool is_output_device)
{
// Prefer sound servers that provide mixing and desktop integration
// (PipeWire, JACK, PulseAudio) over plain ALSA defaults, which often
// fail to share the device on modern Linux desktops.
const QStringList preferred_host_apis = {
QStringLiteral("PipeWire"),
QStringLiteral("JACK"),
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::find_config_device_by_name(bool is_output_device)
{
QString entry = is_output_device ? QStringLiteral("AudioOutput") :
QStringLiteral("AudioInput");
return find_device_by_name(OAK_CONFIG_STR(entry).toString(),
is_output_device);
}
PaDeviceIndex AudioManager::find_device_by_name(const QString &s,
bool is_output_device)
{
PaDeviceIndex exact_match = paNoDevice;
if (!s.isEmpty()) {
for (PaDeviceIndex i = 0, end = Pa_GetDeviceCount(); i < end; i++) {
const PaDeviceInfo *device = Pa_GetDeviceInfo(i);
if (!device) {
continue;
}
if (((is_output_device && device->maxOutputChannels) ||
(!is_output_device && device->maxInputChannels)) &&
!s.compare(device->name)) {
exact_match = i;
break;
}
}
}
#ifdef Q_OS_LINUX
// Even if the user/config picked a device by name, upgrade to a preferred
// host API (PipeWire/JACK/PulseAudio) when one is available. This avoids
// getting stuck on an ALSA device that cannot share the hardware.
if (exact_match != paNoDevice) {
const PaDeviceInfo *matched_info = Pa_GetDeviceInfo(exact_match);
if (matched_info) {
const PaHostApiInfo *host_api =
Pa_GetHostApiInfo(matched_info->hostApi);
if (is_preferred_linux_audio_host_api(host_api)) {
// Keep an explicit choice that already uses a preferred API.
return exact_match;
}
// Upgrade a non-preferred (e.g. ALSA) match to a preferred backend
// when one is available.
PaDeviceIndex preferred =
get_preferred_linux_audio_device(is_output_device);
if (preferred != paNoDevice) {
qInfo() << "Overriding saved audio device" << s
<< "with preferred Linux audio device"
<< Pa_GetDeviceInfo(preferred)->name;
return preferred;
}
// No preferred backend available; keep the saved device.
return exact_match;
}
}
return get_preferred_linux_audio_device(is_output_device);
#else
if (exact_match != paNoDevice) {
return exact_match;
}
return is_output_device ? Pa_GetDefaultOutputDevice() :
Pa_GetDefaultInputDevice();
#endif
}
PaStreamParameters AudioManager::get_port_audio_params(const AudioParams &params,
PaDeviceIndex device)
{
PaStreamParameters p;
p.channelCount = params.channel_count();
p.device = device;
p.hostApiSpecificStreamInfo = nullptr;
p.sampleFormat = get_port_audio_sample_format(params.format());
if (device >= 0 && device < Pa_GetDeviceCount()) {
p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency;
} else {
p.suggestedLatency = 0;
}
return p;
}
AudioManager::AudioManager()
: output_stream_(nullptr)
, input_stream_(nullptr)
, input_encoder_(nullptr)
{
#ifdef PA_HAS_JACK
// PortAudio doesn't do a strcpy, so we need a const char that's readily accessible (i.e. not
// a QString converted to UTF-8)
PaJack_SetClientName("Oak Video Editor");
#endif
Pa_Initialize();
// Get device from config
PaDeviceIndex output_device = find_config_device_by_name(true);
PaDeviceIndex input_device = find_config_device_by_name(false);
qDebug() << "AudioManager: selected output device index=" << output_device
<< "input device index=" << input_device;
set_output_device(output_device);
set_input_device(input_device);
output_buffer_ = new PreviewAudioDevice(this);
output_buffer_->open(PreviewAudioDevice::ReadWrite);
connect(output_buffer_, &PreviewAudioDevice::notify, this,
&AudioManager::output_notify);
}
AudioManager::~AudioManager()
{
close_output_stream();
Pa_Terminate();
}
}
+134
View File
@@ -0,0 +1,134 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOMANAGER_H
#define OAK_AUDIOMANAGER_H
#include <memory>
#include <QtConcurrent/QtConcurrent>
#include <QThread>
#include <portaudio.h>
#include "audiovisualwaveform.h"
#include "audio/audioprocessor.h"
#include "common/define.h"
#include "common/playbackaudioclock.h"
#include "codec/ffmpeg/ffmpegencoder.h"
#include "render/audioplaybackcache.h"
#include "render/previewaudiodevice.h"
namespace olive
{
/**
* @brief Audio input and output management class
*
* Wraps around a QAudioOutput and AudioHybridDevice, connecting them together and exposing audio functionality to
* the rest of the system.
*/
class AudioManager : public QObject, public PlaybackAudioClock {
Q_OBJECT
public:
static void create_instance();
static void destroy_instance();
static AudioManager *instance();
void set_output_notify_interval(int n);
bool push_to_output(const AudioParams &params, const QByteArray &samples,
QString *error = nullptr);
void clear_buffered_output();
void stop_output();
/**
* @brief Seconds of audio consumed by the output device since the last reset
*
* Compensated for output latency so it represents what is actually
* audible. Returns a negative value when no output stream is running.
*/
virtual double seconds() const override;
/**
* @brief Restarts the output clock at zero for a new playback run
*/
void reset_output_clock();
PaDeviceIndex get_output_device() const
{
return output_device_;
}
PaDeviceIndex get_input_device() const
{
return input_device_;
}
void set_output_device(PaDeviceIndex device);
void set_input_device(PaDeviceIndex device);
void hard_reset();
bool start_recording(const EncodingParams &params,
QString *error_str = nullptr);
void stop_recording();
static PaDeviceIndex find_config_device_by_name(bool is_output_device);
static PaDeviceIndex find_device_by_name(const QString &s,
bool is_output_device);
static PaStreamParameters get_port_audio_params(const AudioParams &p,
PaDeviceIndex device);
signals:
void output_notify();
void output_params_changed();
private:
AudioManager();
virtual ~AudioManager() override;
static PaSampleFormat get_port_audio_sample_format(SampleFormat fmt);
void close_output_stream();
static AudioManager *instance_;
PaDeviceIndex output_device_;
PaStream *output_stream_;
AudioParams output_params_;
PreviewAudioDevice *output_buffer_;
PaDeviceIndex input_device_;
PaStream *input_stream_;
FFmpegEncoder *input_encoder_;
};
}
#endif // OAK_AUDIOMANAGER_H
+208
View File
@@ -0,0 +1,208 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audioprocessor.h"
#include <cstring>
#include <QDebug>
#include "common/ffmpegutils.h"
namespace olive
{
/**
* @brief Ensure an AudioParams has a usable channel layout mask.
*
* The bridge's abuffer/aformat filters reject a channel layout mask of 0
* (e.g. when the user config or a source stream reports a mask of 0).
* If the mask is zero, fall back to a default layout derived from the
* channel count (stereo when unknown).
*/
static AudioParams fix_channel_layout(const AudioParams &params)
{
AudioParams result = params;
if (params.channel_layout() == 0) {
int channels = params.channel_count();
if (channels <= 0) {
channels = 2;
}
qWarning() << "AudioProcessor: fixing unspecified channel layout"
<< "(channels=" << params.channel_count() << ") -> default"
<< channels << "channel layout";
result.set_channel_layout(fb_channel_layout_default(channels));
}
return result;
}
AudioProcessor::AudioProcessor()
{
graph_ = nullptr;
out_frame_ = nullptr;
}
AudioProcessor::~AudioProcessor()
{
close();
}
bool AudioProcessor::open(const AudioParams &from, const AudioParams &to,
double tempo)
{
if (graph_) {
qWarning() << "Tried to open a processor that was already open";
return false;
}
AudioParams from_fixed = fix_channel_layout(from);
AudioParams to_fixed = fix_channel_layout(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() << "to sample_rate="
<< to_fixed.sample_rate() << "channels=" << to_fixed.channel_count()
<< "layout_mask=0x" << to_fixed.channel_layout() << Qt::dec;
FBAudioGraphConfig config;
memset(&config, 0, sizeof(config));
config.in_sample_rate = from_fixed.sample_rate();
config.in_channel_layout_mask = from_fixed.channel_layout();
config.in_sample_format =
FFmpegUtils::get_f_fmpeg_sample_format(from_fixed.format());
config.in_channels = from_fixed.channel_count();
config.out_sample_rate = to_fixed.sample_rate();
config.out_channel_layout_mask = to_fixed.channel_layout();
config.out_sample_format =
FFmpegUtils::get_f_fmpeg_sample_format(to_fixed.format());
config.out_channels = to_fixed.channel_count();
config.out_is_planar = to_fixed.format().is_planar() ? 1 : 0;
config.tempo = tempo;
graph_ = fb_audio_graph_create(&config);
if (!graph_) {
qCritical() << "Failed to create audio filter graph";
return false;
}
out_frame_ = fb_frame_alloc();
if (!out_frame_) {
qCritical() << "Failed to allocate output frame";
close();
return false;
}
from_ = from_fixed;
to_ = to_fixed;
return true;
}
void AudioProcessor::close()
{
if (graph_) {
fb_audio_graph_free(&graph_);
}
if (out_frame_) {
fb_frame_free(&out_frame_);
}
}
int AudioProcessor::convert(float **in, int nb_in_samples,
AudioProcessor::Buffer *output)
{
if (!is_open()) {
qCritical() << "Tried to convert on closed processor";
return -1;
}
int r = 0;
if (in && nb_in_samples) {
r = fb_audio_graph_push(
graph_, reinterpret_cast<const uint8_t *const *>(in),
nb_in_samples);
if (r < 0) {
qCritical() << "Failed to add frame to buffersrc:" << r;
return r;
}
}
if (output) {
int nb_channels = to_.channel_count();
if (to_.format().is_packed()) {
nb_channels = 1;
}
AudioProcessor::Buffer &result = *output;
result.resize(nb_channels);
int byte_offset = 0;
while (true) {
r = fb_audio_graph_pull(graph_, out_frame_);
if (r <= 0) {
if (r == 0) {
// No more output available right now
r = 0;
} else {
// Handle unexpected error
qCritical() << "Failed to pull from buffersink:" << r;
}
break;
}
int nb_bytes = fb_frame_get_nb_samples(out_frame_) *
to_.bytes_per_sample_per_channel();
if (to_.format().is_packed()) {
nb_bytes *= to_.channel_count();
}
for (int i = 0; i < nb_channels; i++) {
result[i].resize(byte_offset + nb_bytes);
memcpy(result[i].data() + byte_offset,
fb_frame_get_data(out_frame_, i), nb_bytes);
}
byte_offset += nb_bytes;
}
}
return r;
}
void AudioProcessor::flush()
{
int r = fb_audio_graph_push(graph_, nullptr, 0);
if (r < 0) {
qCritical() << "Failed to flush:" << r;
}
}
}
+82
View File
@@ -0,0 +1,82 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOPROCESSOR_H
#define OAK_AUDIOPROCESSOR_H
#include <inttypes.h>
#include <olive/core/core.h>
#include <QByteArray>
#include <ffmpeg_bridge/ffmpeg_bridge.h>
#include "common/define.h"
namespace olive
{
using namespace core;
class AudioProcessor {
public:
AudioProcessor();
~AudioProcessor();
DISABLE_COPY_MOVE(AudioProcessor)
bool open(const AudioParams &from, const AudioParams &to,
double tempo = 1.0);
void close();
bool is_open() const
{
return graph_;
}
using Buffer = QVector<QByteArray>;
int convert(float **in, int nb_in_samples, AudioProcessor::Buffer *output);
void flush();
const AudioParams &from() const
{
return from_;
}
const AudioParams &to() const
{
return to_;
}
private:
FBAudioGraph *graph_;
AudioParams from_;
AudioParams to_;
FBFrame *out_frame_;
};
}
#endif // OAK_AUDIOPROCESSOR_H
+65
View File
@@ -0,0 +1,65 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audiosynchronizer.h"
namespace olive
{
AudioSynchronizer::Placement AudioSynchronizer::place_by_source_time(
const SourceClip &reference, const SourceClip &candidate,
const core::Rational &reference_timeline_in)
{
Placement placement;
if (!reference.has_source_start_time || !candidate.has_source_start_time ||
reference.source_start_time.isNaN() ||
candidate.source_start_time.isNaN()) {
return placement;
}
const core::Rational reference_head_source =
reference.source_start_time + reference.media_in;
const core::Rational candidate_head_source =
candidate.source_start_time + candidate.media_in;
placement.timeline_in =
reference_timeline_in + candidate_head_source - reference_head_source;
placement.valid = !placement.timeline_in.isNaN();
return placement;
}
AudioSynchronizer::Placement AudioSynchronizer::place_by_waveform_offset(
const core::Rational &reference_timeline_in,
int64_t candidate_offset_samples, int sample_rate)
{
Placement placement;
if (sample_rate <= 0) {
return placement;
}
placement.timeline_in = reference_timeline_in +
core::Rational::from_double(
static_cast<double>(candidate_offset_samples) /
static_cast<double>(sample_rate));
placement.valid = !placement.timeline_in.isNaN();
return placement;
}
}
+55
View File
@@ -0,0 +1,55 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOSYNCHRONIZER_H
#define OAK_AUDIOSYNCHRONIZER_H
#include <cstdint>
#include "olive/core/util/rational.h"
namespace olive
{
class AudioSynchronizer {
public:
struct SourceClip {
core::Rational source_start_time;
core::Rational media_in;
bool has_source_start_time = false;
};
struct Placement {
core::Rational timeline_in;
bool valid = false;
};
static Placement
place_by_source_time(const SourceClip &reference, const SourceClip &candidate,
const core::Rational &reference_timeline_in);
static Placement
place_by_waveform_offset(const core::Rational &reference_timeline_in,
int64_t candidate_offset_samples, int sample_rate);
};
}
#endif // OAK_AUDIOSYNCHRONIZER_H
+568
View File
@@ -0,0 +1,568 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audiovisualwaveform.h"
#include <QDebug>
#include <QtGlobal>
#include "config/config.h"
namespace olive
{
const Rational AudioVisualWaveform::k_minimum_sample_rate = Rational(1, 8);
const Rational AudioVisualWaveform::k_maximum_sample_rate = 1024;
AudioVisualWaveform::AudioVisualWaveform()
: channels_(0)
{
for (Rational i = k_minimum_sample_rate; i <= k_maximum_sample_rate; i *= 2) {
mipmapped_data_.insert({ i, Sample() });
}
}
void AudioVisualWaveform::overwrite_samples_from_buffer(
const SampleBuffer &samples, int sample_rate, const Rational &start,
double target_rate, Sample &data, size_t &start_index,
size_t &samples_length)
{
start_index = time_to_samples(start, target_rate);
samples_length =
time_to_samples(static_cast<double>(samples.sample_count()) /
static_cast<double>(sample_rate),
target_rate);
size_t end_index = start_index + samples_length;
if (data.size() < end_index) {
data.resize(end_index);
}
double chunk_size = double(sample_rate) / double(target_rate);
for (size_t i = 0; i < samples_length; i += channels_) {
size_t src_start = qRound((double(i) * chunk_size)) / channels_;
size_t src_end = qMin(
size_t(qRound64((double(i + channels_) * chunk_size))) / channels_,
samples.sample_count());
Sample summary = sum_samples(samples, src_start, src_end - src_start);
memcpy(&data.data()[i + start_index], summary.data(),
summary.size() * sizeof(SamplePerChannel));
}
}
void AudioVisualWaveform::overwrite_samples_from_mipmap(
const AudioVisualWaveform::Sample &input, double input_sample_rate,
size_t &input_start, size_t &input_length, const Rational &start,
double output_rate, AudioVisualWaveform::Sample &output_data)
{
size_t start_index = time_to_samples(start, output_rate);
size_t samples_length = time_to_samples(
static_cast<double>(input_length / channels_) / input_sample_rate,
output_rate);
size_t end_index = start_index + samples_length;
if (output_data.size() < end_index) {
output_data.resize(end_index);
}
// We guarantee mipmaps are powers of two so integer division should be perfectly accurate here
size_t chunk_size = input_sample_rate / output_rate;
for (size_t i = 0; i < samples_length; i += channels_) {
Sample summary =
re_sum_samples(&input.data()[input_start + (i * chunk_size)],
chunk_size * channels_, channels_);
memcpy(&output_data.data()[i + start_index], summary.data(),
summary.size() * sizeof(SamplePerChannel));
}
input_start = start_index;
input_length = samples_length;
}
void AudioVisualWaveform::validate_virtual_start(const Rational &new_start)
{
if (length_ == 0) {
virtual_start_ = new_start;
} else if (virtual_start_ > new_start) {
trim_in(new_start - virtual_start_);
}
}
void AudioVisualWaveform::overwrite_samples(const SampleBuffer &samples,
int sample_rate,
const Rational &start)
{
if (!channels_) {
qWarning() << "Failed to write samples - channel count is zero";
return;
}
validate_virtual_start(start);
// Process the largest mipmap directly for the samples
auto current_mipmap = mipmapped_data_.rbegin();
size_t input_start, input_length;
overwrite_samples_from_buffer(samples, sample_rate, start - virtual_start_,
current_mipmap->first.to_double(),
current_mipmap->second, input_start,
input_length);
while (true) {
// For each smaller mipmap, we just process from the mipmap before it, making each one
// exponentially faster to create
auto previous_mipmap = current_mipmap;
current_mipmap++;
if (current_mipmap == mipmapped_data_.rend()) {
break;
}
overwrite_samples_from_mipmap(
previous_mipmap->second, previous_mipmap->first.to_double(),
input_start, input_length, start - virtual_start_,
current_mipmap->first.to_double(), current_mipmap->second);
}
Rational sample_length(samples.sample_count(), sample_rate);
length_ = qMax(length_, start + sample_length);
}
void AudioVisualWaveform::overwrite_sums(const AudioVisualWaveform &sums,
const Rational &dest,
const Rational &offset,
const Rational &length)
{
validate_virtual_start(dest);
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
Rational rate = it->first;
Sample &our_arr = it->second;
const Sample &their_arr = sums.mipmapped_data_.at(rate);
double rate_dbl = rate.to_double();
// Get our destination sample
size_t our_start_index =
time_to_samples(dest - virtual_start_, rate_dbl);
// Get our source sample, indexing with the SOURCE's channel count
size_t their_start_index = std::floor(offset.to_double() * rate_dbl) *
sums.channel_count();
if (their_start_index >= their_arr.size()) {
continue;
}
// Determine how much we're copying
size_t copy_len = their_arr.size() - their_start_index;
if (!length.isNull()) {
copy_len = qMin(copy_len, time_to_samples(length, rate_dbl));
if (copy_len == 0) {
continue;
}
}
// Determine end index of our array
size_t end_index = our_start_index + copy_len;
if (our_arr.size() < end_index) {
our_arr.resize(end_index);
}
memcpy(reinterpret_cast<char *>(our_arr.data()) +
our_start_index * sizeof(SamplePerChannel),
reinterpret_cast<const char *>(their_arr.data()) +
their_start_index * sizeof(SamplePerChannel),
copy_len * sizeof(SamplePerChannel));
}
length_ = qMax(length_, dest + ((length.isNull()) ? sums.length() - offset :
length));
}
void AudioVisualWaveform::overwrite_silence(const Rational &start,
const Rational &length)
{
validate_virtual_start(start);
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
Rational rate = it->first;
Sample &our_arr = it->second;
double rate_dbl = rate.to_double();
// Get our destination sample
size_t our_start_index =
time_to_samples(start - virtual_start_, rate_dbl);
size_t our_length_index = time_to_samples(length, rate_dbl);
size_t our_end_index = our_start_index + our_length_index;
if (our_arr.size() < our_end_index) {
our_arr.resize(our_end_index);
}
memset(reinterpret_cast<char *>(our_arr.data()) +
our_start_index * sizeof(SamplePerChannel),
0, our_length_index * sizeof(SamplePerChannel));
}
length_ = qMax(length_, start + length);
}
void AudioVisualWaveform::trim_in(Rational length)
{
if (length == 0) {
return;
}
virtual_start_ += length;
bool negative = (length < 0);
if (negative) {
length = -length;
}
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
Rational rate = it->first;
double rate_dbl = rate.to_double();
Sample &data = it->second;
size_t chop_length = time_to_samples(length, rate_dbl);
if (chop_length == 0) {
continue;
}
if (!negative) {
data = Sample(data.begin() + chop_length, data.end());
} else {
data.insert(data.begin(), chop_length, SamplePerChannel());
}
}
if (!negative) {
length_ = qMax(Rational(0), length_ - length);
}
// Prepending grows the data before the existing start, so the absolute
// end (which length_ tracks) is unchanged
}
AudioVisualWaveform AudioVisualWaveform::mid(const Rational &offset) const
{
AudioVisualWaveform mid = *this;
mid.trim_in(offset - virtual_start_);
return mid;
}
AudioVisualWaveform AudioVisualWaveform::mid(const Rational &offset,
const Rational &length) const
{
AudioVisualWaveform mid = *this;
mid.trim_range(offset - virtual_start_, length);
return mid;
}
void AudioVisualWaveform::resize(const Rational &length)
{
if (length_ == length) {
return;
}
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
Rational rate = it->first;
double rate_dbl = rate.to_double();
Sample &data = it->second;
size_t chop_length = time_to_samples(length, rate_dbl);
data.resize(chop_length);
}
length_ = length;
}
void AudioVisualWaveform::trim_range(const Rational &in, const Rational &length)
{
trim_in(in);
resize(length);
}
AudioVisualWaveform::Sample
AudioVisualWaveform::get_summary_from_time(const Rational &start,
const Rational &length) const
{
// Find mipmap that requires
auto using_mipmap = get_mipmap_for_scale(length.flipped().to_double());
double rate_dbl = using_mipmap->first.to_double();
size_t start_sample = time_to_samples(start - virtual_start_, rate_dbl);
size_t sample_length = time_to_samples(length, rate_dbl);
const Sample &mipmap_data = using_mipmap->second;
// Determine if the array actually has this sample. Compare in signed
// arithmetic so a start past the end of the data doesn't underflow.
qint64 available = qint64(mipmap_data.size()) - qint64(start_sample);
if (available > 0) {
sample_length = qMin(sample_length, size_t(available));
if (sample_length > 0) {
return re_sum_samples(&mipmap_data.data()[start_sample],
sample_length, channels_);
}
}
// Return null samples
return AudioVisualWaveform::Sample(channel_count(), { 0, 0 });
}
void expand_min_max_channel(const float *a, size_t length, float &min_val,
float &max_val)
{
#if defined(Q_PROCESSOR_X86) || defined(Q_PROCESSOR_ARM)
// SSE optimized
// load the first 4 elements of 'a' into min and max (they are 4 * 32 = 128 bits)
__m128 max = _mm_loadu_ps(a);
__m128 min = _mm_loadu_ps(a);
// loop over 'a' and compare current elements with min and max 4 by 4.
// we need to make sure we don't read out of boundaries should 'a' length be not mod. 4
for (size_t i = 4; i < length - 4; i += 4) {
__m128 cur = _mm_loadu_ps(a + i);
max = _mm_max_ps(max, cur);
min = _mm_min_ps(min, cur);
}
// so we read the last 4 (or less) elements in a safe manner.
__m128 cur = _mm_loadu_ps(a + length - 4);
max = _mm_max_ps(max, cur);
min = _mm_min_ps(min, cur);
// this potentially overlaps up to the last 3 elements but it's not an issue.
// min and max will contain 4 min and max. To get the absolute min and max
// we need to compare the 4 values over themselves by shuffling each time.
for (size_t i = 0; i < 3; i++) {
max = _mm_max_ps(max, _mm_shuffle_ps(max, max, 0x93));
min = _mm_min_ps(min, _mm_shuffle_ps(min, min, 0x93));
}
// now min and max contain 4 identical items each representing min and max value respectively.
// and we store the first one into a float variable.
_mm_store_ss(&max_val, max);
_mm_store_ss(&min_val, min);
// I bet you don't find annotated low level code very often.
#else
// Standard unoptimized function
for (size_t i = 0; i < length; i++) {
min_val = std::min(min_val, a[i]);
max_val = std::max(max_val, a[i]);
}
#endif
}
AudioVisualWaveform::Sample
AudioVisualWaveform::sum_samples(const SampleBuffer &samples, size_t start_index,
size_t length)
{
int channels = samples.audio_params().channel_count();
AudioVisualWaveform::Sample summed_samples(channels);
for (int channel = 0; channel < samples.audio_params().channel_count();
channel++) {
expand_min_max_channel(samples.data(channel) + start_index, length,
summed_samples[channel].min,
summed_samples[channel].max);
}
// for reference: this approximation is n x faster (and less accurate) for a n-tracks clip
// for (size_t i=start_index; i<end_index; i++) {
// ExpandMinMax(summed_samples[i%channels], samples->data(i%channels)[i]);
// }
return summed_samples;
}
AudioVisualWaveform::Sample
AudioVisualWaveform::re_sum_samples(const SamplePerChannel *samples,
size_t nb_samples, int nb_channels)
{
AudioVisualWaveform::Sample summed_samples(nb_channels);
for (size_t i = 0; i < nb_samples; i += nb_channels) {
for (int j = 0; j < nb_channels; j++) {
const AudioVisualWaveform::SamplePerChannel &sample =
samples[i + j];
if (sample.min < summed_samples[j].min) {
summed_samples[j].min = sample.min;
}
if (sample.max > summed_samples[j].max) {
summed_samples[j].max = sample.max;
}
}
}
return summed_samples;
}
template <typename T> inline int round_away_from_zero(T t)
{
return (t < 0) ? std::floor(t) : std::ceil(t);
}
void AudioVisualWaveform::draw_sample(QPainter *painter, const Sample &sample,
int x, int y, int height, bool rectified)
{
if (sample.empty()) {
return;
}
int channel_height = height / sample.size();
int channel_half_height = channel_height / 2;
for (size_t i = 0; i < sample.size(); i++) {
float max = qMin(sample.at(i).max, 1.0f);
float min = qMax(sample.at(i).min, -1.0f);
if (rectified) {
int channel_bottom = y + channel_height * (i + 1);
int diff = round_away_from_zero((max - min) * channel_half_height);
painter->drawLine(x, channel_bottom - diff, x, channel_bottom);
} else {
int channel_mid = y + channel_height * i + channel_half_height;
// We subtract the sample so that positive Y values go up on the screen rather than down,
// which is how waveforms are usually rendered
painter->drawLine(
x,
channel_mid -
round_away_from_zero(
min * static_cast<float>(channel_half_height)),
x,
channel_mid -
round_away_from_zero(
max * static_cast<float>(channel_half_height)));
}
}
}
void AudioVisualWaveform::draw_waveform(QPainter *painter, const QRect &rect,
const double &scale,
const AudioVisualWaveform &samples,
const Rational &start_time)
{
if (samples.mipmapped_data_.empty()) {
return;
}
auto using_mipmap = samples.get_mipmap_for_scale(scale);
Rational rate = using_mipmap->first;
double rate_dbl = rate.to_double();
const Sample &arr = using_mipmap->second;
size_t start_sample_index =
samples.time_to_samples(start_time - samples.virtual_start_, rate_dbl);
if (start_sample_index >= arr.size()) {
return;
}
size_t next_sample_index = start_sample_index;
size_t sample_index;
Sample summary;
size_t summary_index = -1;
const QRect &viewport = painter->viewport();
QPoint top_left = painter->transform().map(viewport.topLeft());
size_t start = qMax(rect.x(), -top_left.x());
size_t end = qMin(rect.right(), -top_left.x() + viewport.width());
bool rectified = OAK_CONFIG("RectifiedWaveforms").toBool();
for (size_t i = start; i < end; i++) {
sample_index = next_sample_index;
if (sample_index == arr.size()) {
break;
}
next_sample_index = std::min(
arr.size(),
size_t(start_sample_index +
std::floor(rate_dbl * static_cast<double>(i - rect.x() + 1) /
scale) *
samples.channel_count()));
if (summary_index != sample_index) {
summary = AudioVisualWaveform::re_sum_samples(
&arr.at(sample_index),
qMax(size_t(samples.channel_count()),
next_sample_index - sample_index),
samples.channel_count());
summary_index = sample_index;
}
draw_sample(painter, summary, i, rect.y(), rect.height(), rectified);
}
}
size_t AudioVisualWaveform::time_to_samples(const Rational &time,
double sample_rate) const
{
return time_to_samples(time.to_double(), sample_rate);
}
size_t AudioVisualWaveform::time_to_samples(const double &time,
double sample_rate) const
{
return std::floor(time * sample_rate) * channels_;
}
std::map<Rational, AudioVisualWaveform::Sample>::const_iterator
AudioVisualWaveform::get_mipmap_for_scale(double scale) const
{
// Find largest mipmap for this scale (or the largest if we don't find one sufficient)
for (auto it = mipmapped_data_.cbegin(); it != mipmapped_data_.cend();
it++) {
if (it->first.to_double() >= scale) {
return it;
}
}
// We don't have a mipmap large enough for this scale, so just return the largest we have
return std::prev(mipmapped_data_.cend());
}
}
+163
View File
@@ -0,0 +1,163 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_SUMSAMPLES_H
#define OAK_SUMSAMPLES_H
#include <olive/core/core.h>
#include <QPainter>
#include <QVector>
namespace olive
{
using namespace core;
/**
* @brief A buffer of data used to store a visual representation of audio
*
* This differs from a SampleBuffer as the data in an AudioVisualWaveform has been reduced
* significantly and optimized for visual display.
*/
class AudioVisualWaveform {
public:
AudioVisualWaveform();
struct SamplePerChannel {
float min;
float max;
};
using Sample = std::vector<SamplePerChannel>;
int channel_count() const
{
return channels_;
}
void set_channel_count(int channels)
{
channels_ = channels;
}
const Rational &length() const
{
return length_;
}
/**
* @brief Writes samples into the visual waveform buffer
*
* Starting at `start`, writes samples over anything in the buffer, expanding it if necessary.
*/
void overwrite_samples(const SampleBuffer &samples, int sample_rate,
const Rational &start = 0);
/**
* @brief Replaces sums at a certain range in this visual waveform
*
* @param sums
*
* The sums to write over our current ones with.
*
* @param dest
*
* Where in this visual waveform these sums should START being written to.
*
* @param offset
*
* Where in the `sums` parameter this should start reading from. Defaults to 0.
*
* @param length
*
* Maximum length of `sums` to overwrite with.
*/
void overwrite_sums(const AudioVisualWaveform &sums, const Rational &dest,
const Rational &offset = 0, const Rational &length = 0);
void overwrite_silence(const Rational &start, const Rational &length);
void trim_in(Rational length);
AudioVisualWaveform mid(const Rational &offset) const;
AudioVisualWaveform mid(const Rational &offset,
const Rational &length) const;
void resize(const Rational &length);
void trim_range(const Rational &in, const Rational &length);
Sample get_summary_from_time(const Rational &start,
const Rational &length) const;
static Sample sum_samples(const SampleBuffer &samples, size_t start_index,
size_t length);
static Sample re_sum_samples(const SamplePerChannel *samples,
size_t nb_samples, int nb_channels);
static void draw_sample(QPainter *painter, const Sample &sample, int x,
int y, int height, bool rectified);
static void draw_waveform(QPainter *painter, const QRect &rect,
const double &scale,
const AudioVisualWaveform &samples,
const Rational &start_time);
// Must be a power of 2
static const Rational k_minimum_sample_rate;
static const Rational k_maximum_sample_rate;
private:
void overwrite_samples_from_buffer(const SampleBuffer &samples,
int sample_rate, const Rational &start,
double target_rate, Sample &data,
size_t &start_index,
size_t &samples_length);
void overwrite_samples_from_mipmap(const Sample &input,
double input_sample_rate,
size_t &input_start, size_t &input_length,
const Rational &start, double output_rate,
Sample &output_data);
size_t time_to_samples(const Rational &time, double sample_rate) const;
size_t time_to_samples(const double &time, double sample_rate) const;
std::map<Rational, Sample>::const_iterator
get_mipmap_for_scale(double scale) const;
void validate_virtual_start(const Rational &new_start);
Rational virtual_start_;
int channels_;
std::map<Rational, Sample> mipmapped_data_;
Rational length_;
};
}
Q_DECLARE_METATYPE(olive::AudioVisualWaveform)
#endif // OAK_SUMSAMPLES_H
+245
View File
@@ -0,0 +1,245 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audiowaveformsync.h"
#include <algorithm>
#include <cmath>
namespace olive
{
QVector<double>
AudioWaveformSync::extract_rms_envelope(const core::SampleBuffer &samples,
size_t window_samples)
{
QVector<double> envelope;
const int channel_count = samples.channel_count();
const size_t sample_count = samples.sample_count();
if (!channel_count || !sample_count || !window_samples) {
return envelope;
}
const size_t window_count =
(sample_count + window_samples - 1) / window_samples;
envelope.resize(static_cast<int>(window_count));
for (size_t window = 0; window < window_count; window++) {
const size_t start = window * window_samples;
const size_t end = std::min(start + window_samples, sample_count);
double square_sum = 0.0;
size_t total = 0;
for (int channel = 0; channel < channel_count; channel++) {
const float *data = samples.data(channel);
for (size_t sample = start; sample < end; sample++) {
const double value = data[sample];
square_sum += value * value;
total++;
}
}
envelope[static_cast<int>(window)] =
total ? std::sqrt(square_sum / static_cast<double>(total)) : 0.0;
}
return envelope;
}
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_offset(
const core::SampleBuffer &reference, const core::SampleBuffer &candidate,
size_t window_samples, int64_t max_offset_samples)
{
if (!window_samples) {
return OffsetResult();
}
const QVector<double> reference_envelope =
extract_rms_envelope(reference, window_samples);
const QVector<double> candidate_envelope =
extract_rms_envelope(candidate, window_samples);
const int64_t max_offset_windows =
max_offset_samples / static_cast<int64_t>(window_samples);
return estimate_envelope_offset(reference_envelope, candidate_envelope,
window_samples, max_offset_windows);
}
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_envelope_offset(
const QVector<double> &reference, const QVector<double> &candidate,
size_t window_samples, int64_t max_offset_windows)
{
return estimate_envelope_offset(reference, candidate, QVector<bool>(),
QVector<bool>(), window_samples,
max_offset_windows);
}
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_envelope_offset(
const QVector<double> &reference, const QVector<double> &candidate,
const QVector<bool> &reference_valid, const QVector<bool> &candidate_valid,
size_t window_samples, int64_t max_offset_windows)
{
OffsetResult result;
if (reference.isEmpty() || candidate.isEmpty() || !window_samples) {
return result;
}
const auto is_valid = [](const QVector<bool> &mask, int size, int index) {
return mask.size() != size || mask.at(index);
};
double best_score = -2.0;
int64_t best_lag = 0;
for (int64_t lag = -max_offset_windows; lag <= max_offset_windows; lag++) {
const int reference_start =
static_cast<int>(std::max<int64_t>(0, -lag));
const int candidate_start = static_cast<int>(std::max<int64_t>(0, lag));
const int overlap = std::min(reference.size() - reference_start,
candidate.size() - candidate_start);
if (overlap < 2) {
continue;
}
// Only windows marked valid on both sides participate in the score
double reference_mean = 0.0;
double candidate_mean = 0.0;
int valid_count = 0;
for (int i = 0; i < overlap; i++) {
const int reference_index = reference_start + i;
const int candidate_index = candidate_start + i;
if (!is_valid(reference_valid, reference.size(),
reference_index) ||
!is_valid(candidate_valid, candidate.size(), candidate_index)) {
continue;
}
reference_mean += reference.at(reference_index);
candidate_mean += candidate.at(candidate_index);
valid_count++;
}
if (valid_count < 2) {
continue;
}
reference_mean /= static_cast<double>(valid_count);
candidate_mean /= static_cast<double>(valid_count);
double numerator = 0.0;
double reference_energy = 0.0;
double candidate_energy = 0.0;
for (int i = 0; i < overlap; i++) {
const int reference_index = reference_start + i;
const int candidate_index = candidate_start + i;
if (!is_valid(reference_valid, reference.size(),
reference_index) ||
!is_valid(candidate_valid, candidate.size(), candidate_index)) {
continue;
}
const double reference_value =
reference.at(reference_index) - reference_mean;
const double candidate_value =
candidate.at(candidate_index) - candidate_mean;
numerator += reference_value * candidate_value;
reference_energy += reference_value * reference_value;
candidate_energy += candidate_value * candidate_value;
}
if (qFuzzyIsNull(reference_energy) || qFuzzyIsNull(candidate_energy)) {
continue;
}
const double score =
numerator / std::sqrt(reference_energy * candidate_energy);
if (score > best_score) {
best_score = score;
best_lag = lag;
}
}
if (best_score > -2.0) {
result.valid = true;
result.confidence = std::max(0.0, best_score);
result.offset_samples = best_lag * static_cast<int64_t>(window_samples);
}
return result;
}
AudioWaveformSync::StretchOffsetResult AudioWaveformSync::estimate_stretch_and_offset(
const QVector<double> &reference, const QVector<double> &candidate,
const QVector<bool> &reference_valid, const QVector<bool> &candidate_valid,
size_t window_samples, int64_t max_offset_windows, double min_rate,
double max_rate, double rate_step)
{
StretchOffsetResult result;
if (reference.isEmpty() || candidate.isEmpty() || !window_samples ||
min_rate <= 0.0 || max_rate < min_rate || rate_step <= 0.0) {
return result;
}
double best_confidence = -2.0;
for (double rate = min_rate; rate <= max_rate + rate_step * 0.5;
rate += rate_step) {
// Resample the candidate envelope so that window i of the resampled
// envelope corresponds to window i*rate of the original
const int resampled_size =
static_cast<int>(candidate.size() / rate);
if (resampled_size < 2) {
continue;
}
QVector<double> resampled(resampled_size);
QVector<bool> resampled_valid(resampled_size);
for (int i = 0; i < resampled_size; i++) {
const double position = i * rate;
const int lower = static_cast<int>(position);
const int upper =
std::min(lower + 1, static_cast<int>(candidate.size()) - 1);
const double fraction = position - lower;
resampled[i] = candidate.at(lower) * (1.0 - fraction) +
candidate.at(upper) * fraction;
resampled_valid[i] =
(candidate_valid.size() != candidate.size() ||
(candidate_valid.at(lower) && candidate_valid.at(upper)));
}
const OffsetResult offset = estimate_envelope_offset(
reference, resampled, reference_valid, resampled_valid,
window_samples, max_offset_windows);
if (offset.valid && offset.confidence > best_confidence) {
best_confidence = offset.confidence;
result.valid = true;
result.rate = rate;
result.confidence = offset.confidence;
result.offset_samples = offset.offset_samples;
}
}
return result;
}
}
+102
View File
@@ -0,0 +1,102 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOWAVEFORMSYNC_H
#define OAK_AUDIOWAVEFORMSYNC_H
#include <cstdint>
#include <QVector>
#include "olive/core/render/samplebuffer.h"
namespace olive
{
class AudioWaveformSync {
public:
struct OffsetResult {
int64_t offset_samples = 0;
double confidence = 0.0;
bool valid = false;
};
struct StretchOffsetResult {
// Playback rate the candidate must be played at to align with the
// reference (e.g. 2.0 = candidate runs at half speed and needs to be
// sped up 2x)
double rate = 1.0;
int64_t offset_samples = 0;
double confidence = 0.0;
bool valid = false;
};
static QVector<double> extract_rms_envelope(const core::SampleBuffer &samples,
size_t window_samples);
static OffsetResult estimate_offset(const core::SampleBuffer &reference,
const core::SampleBuffer &candidate,
size_t window_samples,
int64_t max_offset_samples);
static OffsetResult estimate_envelope_offset(const QVector<double> &reference,
const QVector<double> &candidate,
size_t window_samples,
int64_t max_offset_windows);
/**
* @brief Offset estimation that ignores windows flagged as invalid
*
* @p reference_valid and @p candidate_valid mark which envelope windows
* contain real data (e.g. actually cached waveform regions). Windows
* flagged false on either side are excluded from the correlation instead
* of being treated as silence, which improves accuracy when parts of the
* waveform cache have not been generated yet. Empty masks are treated as
* "all windows valid".
*/
static OffsetResult estimate_envelope_offset(const QVector<double> &reference,
const QVector<double> &candidate,
const QVector<bool> &reference_valid,
const QVector<bool> &candidate_valid,
size_t window_samples,
int64_t max_offset_windows);
/**
* @brief Estimates a playback-rate change plus offset aligning the
* candidate to the reference
*
* The candidate envelope is resampled at each candidate rate in
* [min_rate, max_rate] (step rate_step) and correlated against the
* reference. rate > 1 means the candidate runs slower than the reference
* and must be sped up. The search is O(rates * lags * overlap), so
* callers should bound max_offset_windows to a sensible range.
*/
static StretchOffsetResult
estimate_stretch_and_offset(const QVector<double> &reference,
const QVector<double> &candidate,
const QVector<bool> &reference_valid,
const QVector<bool> &candidate_valid, size_t window_samples,
int64_t max_offset_windows, double min_rate,
double max_rate, double rate_step);
};
}
#endif // OAK_AUDIOWAVEFORMSYNC_H