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:
@@ -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
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¶ms,
|
||||
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 ¶ms,
|
||||
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 ¶ms,
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ¶ms, 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 ¶ms,
|
||||
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
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user