refactor(config,audio): merge config into oakcommon, split oakaudio
- config moves into oakcommon as ConfigStore + oakcommon_config_* C API (INI storage, typed entries, error-handler injection); node and render call sites keep OAK_CONFIG() macro shape via a local shim that forwards to the C API; transition config stubs removed - oakaudio: de-Qt all six classes, C ABI in include/audio with refcounted handles (processor/manager/waveform/levelmeter/sync, 48 functions); PreviewAudioDevice moved in from render; recording goes through oakcodec encoder; waveform extract uses probe + ffmpeg_bridge decode (decode_audio needs M8 task system) - fix re_sum_samples min/max init bug (values clamped to 0 for same-sign ranges) - every C API function has positive + error-path tests; suites: oakcommon 193, oakaudio 36, oaknode 96, oakrender 42, oakcodec 18
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(c_api)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,24 @@
|
||||
# Oak Video Editor - 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/>.
|
||||
|
||||
target_sources(oakaudio PRIVATE
|
||||
alive.cpp
|
||||
levelmeter.cpp
|
||||
manager.cpp
|
||||
processor.cpp
|
||||
sync.cpp
|
||||
waveform.cpp
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 <atomic>
|
||||
|
||||
#include "audio/error.h"
|
||||
#include "audio/manager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic<int> g_alive{ 0 };
|
||||
}
|
||||
|
||||
namespace oakaudio
|
||||
{
|
||||
|
||||
void alive_inc()
|
||||
{
|
||||
g_alive.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void alive_dec()
|
||||
{
|
||||
g_alive.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_debug_alive_count(void)
|
||||
{
|
||||
return g_alive.load(std::memory_order_relaxed);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 "audio/levelmeter.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "audiolevelmeter.h"
|
||||
#include "ffmpeg_bridge/ffmpeg_bridge.h"
|
||||
|
||||
using olive::AudioLevelMeter;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::Rational;
|
||||
using olive::core::SampleBuffer;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
extern "C" int oakaudio_levelmeter_analyze(const float *const *planar,
|
||||
int channel_count, int frame_count,
|
||||
oakaudio_channel_stats *channels, int channels_capacity,
|
||||
oakaudio_meter_stats *summary)
|
||||
{
|
||||
if (!planar || channel_count <= 0 || frame_count < 0 ||
|
||||
(channels && channels_capacity < channel_count)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!channels && !summary) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// Repack into a SampleBuffer (planar f32) for the C++ implementation.
|
||||
AudioParams params(48000, fb_channel_layout_default(channel_count),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(frame_count, 48000));
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (frame_count > 0) {
|
||||
memcpy(buffer.data(ch), planar[ch],
|
||||
size_t(frame_count) * sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
const AudioLevelMeter::Stats stats =
|
||||
AudioLevelMeter::analyze_sample_buffer(buffer);
|
||||
|
||||
if (channels) {
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
const AudioLevelMeter::ChannelStats &s =
|
||||
stats.channels[size_t(ch)];
|
||||
oakaudio_channel_stats &dst = channels[ch];
|
||||
dst.peak_linear = s.peak_linear;
|
||||
dst.peak_db = s.peak_db;
|
||||
dst.rms_linear = s.rms_linear;
|
||||
dst.rms_db = s.rms_db;
|
||||
dst.vu_db = s.vu_db;
|
||||
}
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
summary->max_peak_linear = stats.max_peak_linear;
|
||||
summary->integrated_lufs = stats.integrated_lufs;
|
||||
summary->silence = stats.silence ? 1 : 0;
|
||||
}
|
||||
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 "audio/manager.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "audiomanager.h"
|
||||
|
||||
using olive::AudioManager;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Singleton semantics (mirrors oakcommon's OakCurrent): the ctx points to
|
||||
// the process-wide instance, so addref/release never destroy anything.
|
||||
void singleton_addref(void *ctx)
|
||||
{
|
||||
(void) ctx;
|
||||
}
|
||||
|
||||
void singleton_release(void *ctx)
|
||||
{
|
||||
(void) ctx;
|
||||
}
|
||||
|
||||
OakAudioManager wrap(AudioManager *m)
|
||||
{
|
||||
OakAudioManager h = {};
|
||||
h.ctx = m;
|
||||
h.addref = &singleton_addref;
|
||||
h.release = &singleton_release;
|
||||
h.abi_version = OAKAUDIO_ABI_VERSION;
|
||||
return h;
|
||||
}
|
||||
|
||||
AudioManager *impl(OakAudioManager self)
|
||||
{
|
||||
return static_cast<AudioManager *>(self.ctx);
|
||||
}
|
||||
|
||||
int write_error(const std::string &s, char *buf, int buf_size)
|
||||
{
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = std::min(int(s.size()), buf_size - 1);
|
||||
std::memcpy(buf, s.data(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return int(s.size()) + 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakaudio_manager_create_instance(void)
|
||||
{
|
||||
if (!AudioManager::instance()) {
|
||||
try {
|
||||
AudioManager::create_instance();
|
||||
} catch (...) {
|
||||
return OAKAUDIO_E_NOMEM;
|
||||
}
|
||||
}
|
||||
return AudioManager::instance() ? OAKAUDIO_OK : OAKAUDIO_E_NOMEM;
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_manager_destroy_instance(void)
|
||||
{
|
||||
AudioManager::destroy_instance();
|
||||
}
|
||||
|
||||
extern "C" OakAudioManager oakaudio_manager_instance(void)
|
||||
{
|
||||
return wrap(AudioManager::instance());
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_manager_free(OakAudioManager *self)
|
||||
{
|
||||
// Singleton: releasing never destroys; just clear the caller's copy.
|
||||
if (self) {
|
||||
self->ctx = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_set_output_notify_interval(
|
||||
OakAudioManager self, int64_t bytes)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (bytes < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
m->set_output_notify_interval(bytes);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_push_to_output(OakAudioManager self,
|
||||
int rate, uint64_t layout, int format,
|
||||
const char *samples, int64_t samples_size,
|
||||
char *error_buf, int error_buf_size)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (rate <= 0 || !samples || samples_size < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const AudioParams params(rate, layout,
|
||||
SampleFormat(SampleFormat::Format(format)));
|
||||
std::string error;
|
||||
if (!m->push_to_output(params, samples, samples_size, &error)) {
|
||||
if (error_buf && error_buf_size > 0) {
|
||||
write_error(error, error_buf, error_buf_size);
|
||||
}
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_clear_buffered_output(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->clear_buffered_output();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_stop_output(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->stop_output();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_seconds(OakAudioManager self, double *out)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (!out) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
*out = m->seconds();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_reset_output_clock(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->reset_output_clock();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_get_output_device(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
return int(m->get_output_device());
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_set_output_device(OakAudioManager self,
|
||||
int device)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->set_output_device(PaDeviceIndex(device));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_get_input_device(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
return int(m->get_input_device());
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_set_input_device(OakAudioManager self,
|
||||
int device)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->set_input_device(PaDeviceIndex(device));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_hard_reset(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->hard_reset();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_start_recording(OakAudioManager self,
|
||||
const oakcodec_encoding_params *params,
|
||||
char *error_buf, int error_buf_size)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (!params || !params->audio_enabled) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (!m->start_recording(*params, &error)) {
|
||||
if (error_buf && error_buf_size > 0) {
|
||||
write_error(error, error_buf, error_buf_size);
|
||||
}
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_stop_recording(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->stop_recording();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_find_config_device_by_name_s(
|
||||
int is_output_device)
|
||||
{
|
||||
return int(AudioManager::find_config_device_by_name(is_output_device != 0));
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_find_device_by_name_s(const char *name,
|
||||
int is_output_device)
|
||||
{
|
||||
if (!name) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
return int(AudioManager::find_device_by_name(name, is_output_device != 0));
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 "audio/processor.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "audioprocessor.h"
|
||||
#include "refcounted.h"
|
||||
|
||||
using olive::AudioProcessor;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
extern "C" OakAudioProcessor oakaudio_processor_init(void)
|
||||
{
|
||||
return oakaudio::make_handle_in_place<OakAudioProcessor, AudioProcessor>();
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_processor_free(OakAudioProcessor *self)
|
||||
{
|
||||
oakaudio::free_handle(self);
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_open(OakAudioProcessor self,
|
||||
int in_rate, uint64_t in_layout, int in_format,
|
||||
int out_rate, uint64_t out_layout, int out_format, double speed)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (p->is_open()) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (in_rate <= 0 || out_rate <= 0 || speed <= 0.0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// The C ABI delivers planar float output only; force the output format
|
||||
// stage to f32p (see OAKAUDIO_PROCESSOR_OUTPUT_FORMAT).
|
||||
if (out_format != OAKAUDIO_PROCESSOR_OUTPUT_FORMAT) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const AudioParams from(in_rate, in_layout,
|
||||
SampleFormat(SampleFormat::Format(in_format)));
|
||||
const AudioParams to(out_rate, out_layout,
|
||||
SampleFormat(SampleFormat::Format(out_format)));
|
||||
|
||||
return p->open(from, to, speed) ? OAKAUDIO_OK : OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_close(OakAudioProcessor self)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
p->close();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_is_open(OakAudioProcessor self)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
return p->is_open() ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_convert(OakAudioProcessor self,
|
||||
const float *const *in_planar, int in_frame_count,
|
||||
float *const *out_planar, int out_capacity_frames)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!p->is_open()) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (in_frame_count < 0 || out_capacity_frames < 0 ||
|
||||
(in_frame_count > 0 && !in_planar)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const int channels = p->to().channel_count();
|
||||
if (channels <= 0) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
|
||||
AudioProcessor::Buffer buf;
|
||||
int r = p->convert(const_cast<float **>(in_planar), in_frame_count,
|
||||
out_planar ? &buf : nullptr);
|
||||
if (r < 0) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
if (!out_planar) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Output is planar f32 (enforced by open()); each buffer entry is one
|
||||
// channel's float plane.
|
||||
const int out_frames = buf.empty() ? 0 :
|
||||
int(buf[0].size() / sizeof(float));
|
||||
const int frames = std::min(out_frames, out_capacity_frames);
|
||||
for (int ch = 0; ch < channels && ch < int(buf.size()); ch++) {
|
||||
if (out_planar[ch]) {
|
||||
memcpy(out_planar[ch], buf[size_t(ch)].data(),
|
||||
size_t(frames) * sizeof(float));
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_flush(OakAudioProcessor self)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!p->is_open()) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
p->flush();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 OAKAUDIO_C_API_REFCOUNTED_H
|
||||
#define OAKAUDIO_C_API_REFCOUNTED_H
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "audio/error.h"
|
||||
|
||||
namespace oakaudio
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Heap box behind every handle's ctx pointer.
|
||||
*
|
||||
* Same pattern as oakcodec's c_api/refcounted.h: holds the wrapped
|
||||
* object plus its atomic reference count. addref and release are emitted
|
||||
* per boxed type so that the function pointers stored in a handle always
|
||||
* run code from the DLL that created the object. Every box also
|
||||
* participates in the oakaudio_debug_alive_count() ledger.
|
||||
*/
|
||||
template <typename T> struct RefCounted {
|
||||
T impl;
|
||||
std::atomic<uint32_t> refs;
|
||||
|
||||
template <typename... Args>
|
||||
explicit RefCounted(Args &&...args)
|
||||
: impl(std::forward<Args>(args)...)
|
||||
, refs(1)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T> void ref_counted_addref(void *ctx)
|
||||
{
|
||||
auto *box = static_cast<RefCounted<T> *>(ctx);
|
||||
if (box)
|
||||
box->refs.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void alive_inc();
|
||||
void alive_dec();
|
||||
|
||||
template <typename T> void ref_counted_release(void *ctx)
|
||||
{
|
||||
auto *box = static_cast<RefCounted<T> *>(ctx);
|
||||
if (box && box->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
delete box;
|
||||
alive_dec();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a by-value handle owning a freshly boxed object (count 1).
|
||||
*
|
||||
* On allocation failure the returned handle has ctx == NULL (all C API
|
||||
* functions treat that as OAKAUDIO_E_INVALID and free() as a no-op).
|
||||
*/
|
||||
template <typename Handle, typename T, typename... Args>
|
||||
Handle make_handle_in_place(Args &&...args)
|
||||
{
|
||||
Handle h = {};
|
||||
try {
|
||||
h.ctx = new RefCounted<T>(std::forward<Args>(args)...);
|
||||
alive_inc();
|
||||
} catch (...) {
|
||||
h.ctx = nullptr;
|
||||
}
|
||||
h.addref = &ref_counted_addref<T>;
|
||||
h.release = &ref_counted_release<T>;
|
||||
h.abi_version = OAKAUDIO_ABI_VERSION;
|
||||
return h;
|
||||
}
|
||||
|
||||
template <typename Handle, typename T> Handle make_handle(T &&value)
|
||||
{
|
||||
return make_handle_in_place<Handle, typename std::decay<T>::type>(
|
||||
std::forward<T>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Recover the boxed object from a handle ctx (NULL-safe).
|
||||
*/
|
||||
template <typename T> T *handle_impl(void *ctx)
|
||||
{
|
||||
auto *box = static_cast<RefCounted<T> *>(ctx);
|
||||
return box ? &box->impl : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shared free() body: release the ctx, no-op on NULL/empty handle.
|
||||
*/
|
||||
template <typename Handle> void free_handle(Handle *h)
|
||||
{
|
||||
if (!h || !h->ctx || !h->release)
|
||||
return;
|
||||
h->release(h->ctx);
|
||||
h->ctx = nullptr;
|
||||
}
|
||||
|
||||
} // namespace oakaudio
|
||||
|
||||
#endif // OAKAUDIO_C_API_REFCOUNTED_H
|
||||
@@ -0,0 +1,206 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 "audio/sync.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "audiosynchronizer.h"
|
||||
#include "audiowaveformsync.h"
|
||||
#include "ffmpeg_bridge/ffmpeg_bridge.h"
|
||||
|
||||
using olive::AudioSynchronizer;
|
||||
using olive::AudioWaveformSync;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::Rational;
|
||||
using olive::core::SampleBuffer;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<char> to_mask(const uint8_t *valid, int len)
|
||||
{
|
||||
std::vector<char> mask;
|
||||
if (valid) {
|
||||
mask.resize(size_t(len));
|
||||
for (int i = 0; i < len; i++) {
|
||||
mask[size_t(i)] = valid[i] ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakaudio_sync_extract_rms_envelope(
|
||||
const float *const *planar, int channel_count, int frame_count,
|
||||
uint64_t window_samples, double *out, int capacity)
|
||||
{
|
||||
if (!planar || channel_count <= 0 || frame_count < 0 ||
|
||||
!window_samples || capacity < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioParams params(48000, fb_channel_layout_default(channel_count),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(frame_count, 48000));
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (frame_count > 0) {
|
||||
memcpy(buffer.data(ch), planar[ch],
|
||||
size_t(frame_count) * sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<double> envelope =
|
||||
AudioWaveformSync::extract_rms_envelope(buffer, window_samples);
|
||||
const int windows = int(envelope.size());
|
||||
if (!out || capacity < windows) {
|
||||
return windows;
|
||||
}
|
||||
memcpy(out, envelope.data(), size_t(windows) * sizeof(double));
|
||||
return windows;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_estimate_envelope_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const uint8_t *reference_valid, const uint8_t *candidate_valid,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
oakaudio_offset_result *out)
|
||||
{
|
||||
if (!out || !reference || !candidate || reference_len <= 0 ||
|
||||
candidate_len <= 0 || !window_samples || max_offset_windows < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const std::vector<double> ref(reference, reference + reference_len);
|
||||
const std::vector<double> cand(candidate, candidate + candidate_len);
|
||||
const std::vector<char> ref_valid = to_mask(reference_valid, reference_len);
|
||||
const std::vector<char> cand_valid =
|
||||
to_mask(candidate_valid, candidate_len);
|
||||
|
||||
const AudioWaveformSync::OffsetResult r =
|
||||
AudioWaveformSync::estimate_envelope_offset(
|
||||
ref, cand, ref_valid, cand_valid, window_samples,
|
||||
max_offset_windows);
|
||||
|
||||
out->offset_samples = r.offset_samples;
|
||||
out->confidence = r.confidence;
|
||||
out->valid = r.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_estimate_stretch_and_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const uint8_t *reference_valid, const uint8_t *candidate_valid,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
double min_rate, double max_rate, double rate_step,
|
||||
oakaudio_stretch_offset_result *out)
|
||||
{
|
||||
if (!out || !reference || !candidate || reference_len <= 0 ||
|
||||
candidate_len <= 0 || !window_samples || max_offset_windows < 0 ||
|
||||
min_rate <= 0.0 || max_rate < min_rate || rate_step <= 0.0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const std::vector<double> ref(reference, reference + reference_len);
|
||||
const std::vector<double> cand(candidate, candidate + candidate_len);
|
||||
const std::vector<char> ref_valid = to_mask(reference_valid, reference_len);
|
||||
const std::vector<char> cand_valid =
|
||||
to_mask(candidate_valid, candidate_len);
|
||||
|
||||
const AudioWaveformSync::StretchOffsetResult r =
|
||||
AudioWaveformSync::estimate_stretch_and_offset(
|
||||
ref, cand, ref_valid, cand_valid, window_samples,
|
||||
max_offset_windows, min_rate, max_rate, rate_step);
|
||||
|
||||
out->rate = r.rate;
|
||||
out->offset_samples = r.offset_samples;
|
||||
out->confidence = r.confidence;
|
||||
out->valid = r.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_place_by_source_time(
|
||||
const oakaudio_source_clip *reference,
|
||||
const oakaudio_source_clip *candidate,
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
int64_t *out_num, int64_t *out_den, int *out_valid)
|
||||
{
|
||||
if (!reference || !candidate || !out_num || !out_den || !out_valid ||
|
||||
reference->source_start_time_den == 0 ||
|
||||
reference->media_in_den == 0 ||
|
||||
candidate->source_start_time_den == 0 ||
|
||||
candidate->media_in_den == 0 || reference_timeline_in_den == 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioSynchronizer::SourceClip ref;
|
||||
ref.source_start_time = Rational(int(reference->source_start_time_num),
|
||||
int(reference->source_start_time_den));
|
||||
ref.media_in = Rational(int(reference->media_in_num),
|
||||
int(reference->media_in_den));
|
||||
ref.has_source_start_time = reference->has_source_start_time != 0;
|
||||
|
||||
AudioSynchronizer::SourceClip cand;
|
||||
cand.source_start_time = Rational(int(candidate->source_start_time_num),
|
||||
int(candidate->source_start_time_den));
|
||||
cand.media_in = Rational(int(candidate->media_in_num),
|
||||
int(candidate->media_in_den));
|
||||
cand.has_source_start_time = candidate->has_source_start_time != 0;
|
||||
|
||||
const AudioSynchronizer::Placement p = AudioSynchronizer::place_by_source_time(
|
||||
ref, cand,
|
||||
Rational(int(reference_timeline_in_num),
|
||||
int(reference_timeline_in_den)));
|
||||
|
||||
*out_num = p.timeline_in.numerator();
|
||||
*out_den = p.timeline_in.denominator();
|
||||
*out_valid = p.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_place_by_waveform_offset(
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
int64_t candidate_offset_samples, int sample_rate,
|
||||
int64_t *out_num, int64_t *out_den, int *out_valid)
|
||||
{
|
||||
if (!out_num || !out_den || !out_valid ||
|
||||
reference_timeline_in_den == 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const AudioSynchronizer::Placement p =
|
||||
AudioSynchronizer::place_by_waveform_offset(
|
||||
Rational(int(reference_timeline_in_num),
|
||||
int(reference_timeline_in_den)),
|
||||
candidate_offset_samples, sample_rate);
|
||||
|
||||
*out_num = p.timeline_in.numerator();
|
||||
*out_den = p.timeline_in.denominator();
|
||||
*out_valid = p.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 "audio/waveform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "audiovisualwaveform.h"
|
||||
#include "codec/decoder.h"
|
||||
#include "ffmpeg_bridge/ffmpeg_bridge.h"
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
#include "refcounted.h"
|
||||
|
||||
using olive::AudioVisualWaveform;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::Rational;
|
||||
using olive::core::SampleBuffer;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
AudioVisualWaveform::SamplePerChannel *as_pairs(oakaudio_min_max *p)
|
||||
{
|
||||
static_assert(sizeof(oakaudio_min_max) ==
|
||||
sizeof(AudioVisualWaveform::SamplePerChannel),
|
||||
"POD layout mismatch");
|
||||
return reinterpret_cast<AudioVisualWaveform::SamplePerChannel *>(p);
|
||||
}
|
||||
|
||||
const AudioVisualWaveform::SamplePerChannel *
|
||||
as_pairs_const(const oakaudio_min_max *p)
|
||||
{
|
||||
return reinterpret_cast<const AudioVisualWaveform::SamplePerChannel *>(p);
|
||||
}
|
||||
|
||||
bool make_rational(int64_t num, int64_t den, Rational *out)
|
||||
{
|
||||
if (den == 0) {
|
||||
return false;
|
||||
}
|
||||
*out = Rational(int(num), int(den));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- oakaudio_waveform_extract() helpers --------------------------------- */
|
||||
|
||||
#define OAKAUDIO_EXTRACT_MAX_CHANNELS 64
|
||||
|
||||
using PendingPlanes = std::vector<std::vector<float>>;
|
||||
|
||||
void append_pending(PendingPlanes &pending, FBFrame *frame, int channels,
|
||||
int nb)
|
||||
{
|
||||
if (pending.empty()) {
|
||||
pending.resize(size_t(channels));
|
||||
}
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
const float *data =
|
||||
reinterpret_cast<const float *>(fb_frame_get_data(frame, ch));
|
||||
std::vector<float> &plane = pending[size_t(ch)];
|
||||
plane.insert(plane.end(), data, data + nb);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit one point per samples_per_point pending samples. With `flush`, a
|
||||
// trailing partial point is emitted too.
|
||||
void emit_points(int channels, int samples_per_point, PendingPlanes &pending,
|
||||
std::vector<oakaudio_min_max> &points, bool flush)
|
||||
{
|
||||
if (pending.empty()) {
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
const size_t available = pending[0].size();
|
||||
if (available == 0 ||
|
||||
(!flush && available < size_t(samples_per_point))) {
|
||||
return;
|
||||
}
|
||||
const size_t n = std::min(available, size_t(samples_per_point));
|
||||
|
||||
const size_t point = points.size() / size_t(channels);
|
||||
points.resize(points.size() + size_t(channels));
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
std::vector<float> &plane = pending[size_t(ch)];
|
||||
float mn = plane[0];
|
||||
float mx = mn;
|
||||
for (size_t i = 1; i < n; i++) {
|
||||
mn = std::min(mn, plane[i]);
|
||||
mx = std::max(mx, plane[i]);
|
||||
}
|
||||
oakaudio_min_max &dst =
|
||||
points[point * size_t(channels) + size_t(ch)];
|
||||
dst.min = mn;
|
||||
dst.max = mx;
|
||||
plane.erase(plane.begin(), plane.begin() + ptrdiff_t(n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int drain_graph(FBAudioGraph *graph, FBFrame *converted, int channels,
|
||||
int samples_per_point, PendingPlanes &pending,
|
||||
std::vector<oakaudio_min_max> &points)
|
||||
{
|
||||
while (true) {
|
||||
const int pull = fb_audio_graph_pull(graph, converted);
|
||||
if (pull < 0) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
if (pull == 0) {
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
append_pending(pending, converted, channels,
|
||||
fb_frame_get_nb_samples(converted));
|
||||
emit_points(channels, samples_per_point, pending, points, false);
|
||||
}
|
||||
}
|
||||
|
||||
void flush_points(int channels, int samples_per_point, PendingPlanes &pending,
|
||||
std::vector<oakaudio_min_max> &points)
|
||||
{
|
||||
emit_points(channels, samples_per_point, pending, points, true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" OakAudioWaveform oakaudio_waveform_init(void)
|
||||
{
|
||||
return oakaudio::make_handle_in_place<OakAudioWaveform,
|
||||
AudioVisualWaveform>();
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_waveform_free(OakAudioWaveform *self)
|
||||
{
|
||||
oakaudio::free_handle(self);
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_get_channel_count(OakAudioWaveform self)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
return w->channel_count();
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_set_channel_count(OakAudioWaveform self,
|
||||
int channels)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (channels < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->set_channel_count(channels);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_length(OakAudioWaveform self,
|
||||
int64_t *num, int64_t *den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!num || !den) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
*num = w->length().numerator();
|
||||
*den = w->length().denominator();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_overwrite_samples(OakAudioWaveform self,
|
||||
const float *const *planar, int frame_count, int sample_rate,
|
||||
int64_t start_num, int64_t start_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational start;
|
||||
if (!planar || frame_count <= 0 || sample_rate <= 0 ||
|
||||
!make_rational(start_num, start_den, &start)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const int channels = w->channel_count();
|
||||
if (channels <= 0) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
|
||||
// Repack the caller's planes into a SampleBuffer (planar f32).
|
||||
AudioParams params(sample_rate, fb_channel_layout_default(channels),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(frame_count, sample_rate));
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
}
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
memcpy(buffer.data(ch), planar[ch],
|
||||
size_t(frame_count) * sizeof(float));
|
||||
}
|
||||
|
||||
w->overwrite_samples(buffer, sample_rate, start);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_overwrite_sums(OakAudioWaveform self,
|
||||
OakAudioWaveform src,
|
||||
int64_t dest_num, int64_t dest_den,
|
||||
int64_t offset_num, int64_t offset_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
AudioVisualWaveform *other =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(src.ctx);
|
||||
if (!w || !other) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational dest, offset, length;
|
||||
if (!make_rational(dest_num, dest_den, &dest) ||
|
||||
!make_rational(offset_num, offset_den, &offset) ||
|
||||
!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->overwrite_sums(*other, dest, offset, length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_overwrite_silence(OakAudioWaveform self,
|
||||
int64_t start_num, int64_t start_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational start, length;
|
||||
if (!make_rational(start_num, start_den, &start) ||
|
||||
!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->overwrite_silence(start, length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_trim_in(OakAudioWaveform self,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational length;
|
||||
if (!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->trim_in(length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_resize(OakAudioWaveform self,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational length;
|
||||
if (!make_rational(length_num, length_den, &length) || length < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->resize(length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_trim_range(OakAudioWaveform self,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational in, length;
|
||||
if (!make_rational(in_num, in_den, &in) ||
|
||||
!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->trim_range(in, length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_get_summary(OakAudioWaveform self,
|
||||
int64_t start_num, int64_t start_den,
|
||||
int64_t length_num, int64_t length_den,
|
||||
oakaudio_min_max *out_pairs, int capacity_points)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational start, length;
|
||||
if (!make_rational(start_num, start_den, &start) ||
|
||||
!make_rational(length_num, length_den, &length) || length <= 0 ||
|
||||
capacity_points < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// Points are produced at the length scale: one point per channel per
|
||||
// `length`-sized window covering [start, start+length) — i.e. exactly
|
||||
// one point, matching AudioVisualWaveform::get_summary_from_time().
|
||||
AudioVisualWaveform::Sample summary =
|
||||
w->get_summary_from_time(start, length);
|
||||
const int points = int(summary.size()) /
|
||||
std::max(1, w->channel_count());
|
||||
|
||||
if (!out_pairs || capacity_points < points) {
|
||||
return points;
|
||||
}
|
||||
memcpy(out_pairs, summary.data(),
|
||||
summary.size() * sizeof(oakaudio_min_max));
|
||||
return points;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_sum_samples_s(const float *const *planar,
|
||||
int channel_count, int start_index, int length,
|
||||
oakaudio_min_max *out)
|
||||
{
|
||||
if (!planar || !out || channel_count <= 0 || start_index < 0 ||
|
||||
length <= 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioParams params(48000, fb_channel_layout_default(channel_count),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(length + start_index, 48000));
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
memcpy(buffer.data(ch) + start_index, planar[ch],
|
||||
size_t(length) * sizeof(float));
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample summary = AudioVisualWaveform::sum_samples(
|
||||
buffer, size_t(start_index), size_t(length));
|
||||
if (int(summary.size()) < channel_count) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
memcpy(out, summary.data(),
|
||||
size_t(channel_count) * sizeof(oakaudio_min_max));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_re_sum_s(const oakaudio_min_max *in,
|
||||
int nb_entries, int nb_channels, oakaudio_min_max *out)
|
||||
{
|
||||
if (!in || !out || nb_entries <= 0 || nb_channels <= 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample summary = AudioVisualWaveform::re_sum_samples(
|
||||
as_pairs_const(in), size_t(nb_entries), nb_channels);
|
||||
memcpy(out, summary.data(),
|
||||
size_t(nb_channels) * sizeof(oakaudio_min_max));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_extract(const char *filename,
|
||||
int stream_index, int samples_per_point,
|
||||
oakaudio_min_max *out_pairs, int capacity_points,
|
||||
int *out_channel_count)
|
||||
{
|
||||
if (!filename || stream_index < 0 || samples_per_point <= 0 ||
|
||||
capacity_points < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// Probe for the stream's native rate/layout (oakcodec probe is
|
||||
// stateless and does not need a conform)
|
||||
OakDecoder probe = oakcodec_decoder_probe(filename);
|
||||
if (!probe.ctx) {
|
||||
return OAKAUDIO_E_NOT_FOUND;
|
||||
}
|
||||
oakcodec_audio_stream_info info;
|
||||
int r = oakcodec_decoder_probe_get_audio_stream(probe, stream_index,
|
||||
&info);
|
||||
oakcodec_decoder_free(&probe);
|
||||
if (r != OAKCODEC_OK) {
|
||||
return OAKAUDIO_E_NOT_FOUND;
|
||||
}
|
||||
if (info.sample_rate <= 0 || info.channel_count <= 0) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
// Decode the whole stream through ffmpeg_bridge (fb_decoder +
|
||||
// fb_audio_graph) rather than oakcodec_decoder_decode_audio: the
|
||||
// oakcodec decode path is conform-cache based and cannot decode media
|
||||
// without an existing pcm conform until the task system lands (M8).
|
||||
// The stream is reduced to channel-interleaved min/max points at the
|
||||
// native rate/layout.
|
||||
FBDecoder *decoder = fb_decoder_create();
|
||||
if (!decoder) {
|
||||
return OAKAUDIO_E_NOMEM;
|
||||
}
|
||||
r = fb_decoder_open(decoder, filename, info.stream_index);
|
||||
if (r < 0) {
|
||||
fb_decoder_free(&decoder);
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
const int channels = info.channel_count;
|
||||
std::vector<oakaudio_min_max> points;
|
||||
PendingPlanes pending; // per-channel planar backlog
|
||||
|
||||
// The graph converts the stream's native format to planar float; the
|
||||
// stream info carries the validated sample format/rate/layout (audio
|
||||
// frames do not report a sample format through fb_frame_get_format).
|
||||
FBStreamInfo sinfo;
|
||||
if (fb_decoder_get_stream_info(decoder, &sinfo) < 0 ||
|
||||
sinfo.sample_rate <= 0) {
|
||||
fb_decoder_close(decoder);
|
||||
fb_decoder_free(&decoder);
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
FBAudioGraphConfig config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.in_sample_rate = sinfo.sample_rate;
|
||||
config.in_channel_layout_mask = sinfo.channel_layout_mask;
|
||||
config.in_sample_format = sinfo.sample_format;
|
||||
config.in_channels = channels;
|
||||
config.out_sample_rate = config.in_sample_rate;
|
||||
config.out_channel_layout_mask = config.in_channel_layout_mask;
|
||||
config.out_sample_format = fb_sample_fmt_fltp;
|
||||
config.out_channels = channels;
|
||||
config.out_is_planar = 1;
|
||||
config.tempo = 1.0;
|
||||
|
||||
FBPacket *packet = fb_packet_alloc();
|
||||
FBFrame *frame = fb_frame_alloc();
|
||||
FBFrame *converted = fb_frame_alloc();
|
||||
FBAudioGraph *graph = fb_audio_graph_create(&config);
|
||||
int result = OAKAUDIO_OK;
|
||||
|
||||
if (!packet || !frame || !converted) {
|
||||
result = OAKAUDIO_E_NOMEM;
|
||||
goto done;
|
||||
}
|
||||
if (!graph) {
|
||||
result = OAKAUDIO_E_FAILED;
|
||||
goto done;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (fb_decoder_get_frame(decoder, packet, frame) < 0) {
|
||||
break; // EOF or error: stop decoding
|
||||
}
|
||||
|
||||
// Push the decoded frame (planar pointer array; a packed source is
|
||||
// read from plane 0 by the buffersrc)
|
||||
const uint8_t *planes[OAKAUDIO_EXTRACT_MAX_CHANNELS];
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
planes[ch] = fb_frame_get_data(frame, ch);
|
||||
}
|
||||
if (fb_audio_graph_push(graph, planes,
|
||||
fb_frame_get_nb_samples(frame)) < 0) {
|
||||
result = OAKAUDIO_E_FAILED;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (drain_graph(graph, converted, channels, samples_per_point,
|
||||
pending, points) != OAKAUDIO_OK) {
|
||||
result = OAKAUDIO_E_FAILED;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the resampler delay
|
||||
if (graph) {
|
||||
fb_audio_graph_push(graph, nullptr, 0);
|
||||
while (fb_audio_graph_pull(graph, converted) == 1) {
|
||||
append_pending(pending, converted, channels,
|
||||
fb_frame_get_nb_samples(converted));
|
||||
}
|
||||
flush_points(channels, samples_per_point, pending, points);
|
||||
}
|
||||
|
||||
done:
|
||||
if (graph) {
|
||||
fb_audio_graph_free(&graph);
|
||||
}
|
||||
if (converted) {
|
||||
fb_frame_free(&converted);
|
||||
}
|
||||
if (frame) {
|
||||
fb_frame_free(&frame);
|
||||
}
|
||||
if (packet) {
|
||||
fb_packet_free(&packet);
|
||||
}
|
||||
fb_decoder_close(decoder);
|
||||
fb_decoder_free(&decoder);
|
||||
if (result != OAKAUDIO_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (out_channel_count) {
|
||||
*out_channel_count = channels;
|
||||
}
|
||||
|
||||
const int point_count = int(points.size()) / channels;
|
||||
if (!out_pairs || capacity_points < point_count) {
|
||||
return point_count;
|
||||
}
|
||||
memcpy(out_pairs, points.data(),
|
||||
points.size() * sizeof(oakaudio_min_max));
|
||||
return point_count;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# Oak Video Editor - 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/>.
|
||||
|
||||
add_library(oakaudio SHARED
|
||||
audiolevelmeter.cpp
|
||||
audiolevelmeter.h
|
||||
audiomanager.cpp
|
||||
audiomanager.h
|
||||
audioprocessor.cpp
|
||||
audioprocessor.h
|
||||
audiosynchronizer.cpp
|
||||
audiosynchronizer.h
|
||||
audiovisualwaveform.cpp
|
||||
audiovisualwaveform.h
|
||||
audiowaveformsync.cpp
|
||||
audiowaveformsync.h
|
||||
configbridge.cpp
|
||||
configbridge.h
|
||||
previewaudiodevice.cpp
|
||||
previewaudiodevice.h
|
||||
)
|
||||
|
||||
# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
|
||||
# build (see src/audio/standalone) sets OAK_REPO_ROOT explicitly.
|
||||
if(NOT DEFINED OAK_REPO_ROOT)
|
||||
set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
find_package(PortAudio REQUIRED)
|
||||
|
||||
target_include_directories(oakaudio PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${OAK_REPO_ROOT}/include
|
||||
${OAK_REPO_ROOT}/core/include
|
||||
${OAK_REPO_ROOT}/ffmpeg_bridge/include
|
||||
${PORTAUDIO_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# 01 §1 rule 5: only the OAKAUDIO_API-marked C functions are exported;
|
||||
# audio-internal C++ classes must not leak into the global symbol
|
||||
# namespace.
|
||||
target_compile_options(oakaudio PRIVATE
|
||||
-fvisibility=hidden
|
||||
-fvisibility-inlines-hidden
|
||||
)
|
||||
|
||||
# Cross-module access goes through C ABIs only: oakcommon (config,
|
||||
# ffmpegutils), oakcodec (encoder for recording, decoder for waveform
|
||||
# extraction), olivecore (Rational/AudioParams/SampleBuffer wrappers),
|
||||
# ffmpeg_bridge (fb_audio_graph resampler infra, same precedent as
|
||||
# oakcommon/oakcodec), PortAudio (output device).
|
||||
target_link_libraries(oakaudio PUBLIC
|
||||
oakcommon
|
||||
oakcodec
|
||||
olivecore
|
||||
ffmpeg_bridge
|
||||
${PORTAUDIO_LIBRARIES}
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
/***
|
||||
|
||||
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>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// De-Qt note: engine/common/decibel.h pulls in QtGlobal, so the two
|
||||
// constants/functions used here are inlined (same math: minimum = -200,
|
||||
// from_linear = 20*log10 clamped to minimum on -inf).
|
||||
static constexpr double k_decibel_minimum = -200.0;
|
||||
|
||||
static double decibel_from_linear(double linear)
|
||||
{
|
||||
double v = 20.0 * std::log10(linear);
|
||||
if (std::isinf(v)) {
|
||||
return k_decibel_minimum;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// qFuzzyIsNull(double): |x| < 1e-12
|
||||
stats.silence = std::abs(stats.max_peak_linear) < 1e-12;
|
||||
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 k_decibel_minimum;
|
||||
}
|
||||
|
||||
return decibel_from_linear(linear);
|
||||
}
|
||||
|
||||
double AudioLevelMeter::power_to_lufs(double mean_square)
|
||||
{
|
||||
if (mean_square <= 0.0) {
|
||||
return k_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 <vector>
|
||||
|
||||
#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 {
|
||||
std::vector<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,523 @@
|
||||
/***
|
||||
|
||||
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"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef PA_HAS_JACK
|
||||
#include <pa_jack.h>
|
||||
#endif
|
||||
|
||||
#include "configbridge.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(int64_t n)
|
||||
{
|
||||
output_buffer_->set_notify_interval(n);
|
||||
}
|
||||
|
||||
void AudioManager::set_output_notify_callback(std::function<void()> callback)
|
||||
{
|
||||
output_buffer_->set_notify_callback(std::move(callback));
|
||||
}
|
||||
|
||||
int output_callback(const void *input, void *output, unsigned long frame_count,
|
||||
const PaStreamCallbackTimeInfo *time_info,
|
||||
PaStreamCallbackFlags status_flags, void *user_data)
|
||||
{
|
||||
(void) input;
|
||||
(void) time_info;
|
||||
(void) status_flags;
|
||||
|
||||
PreviewAudioDevice *device = static_cast<PreviewAudioDevice *>(user_data);
|
||||
|
||||
int64_t max_read = int64_t(frame_count) * device->bytes_per_frame();
|
||||
int64_t read_count =
|
||||
device->read(reinterpret_cast<char *>(output), max_read);
|
||||
if (read_count < max_read) {
|
||||
memset(reinterpret_cast<uint8_t *>(output) + read_count, 0,
|
||||
size_t(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)
|
||||
{
|
||||
(void) output;
|
||||
(void) time_info;
|
||||
(void) status_flags;
|
||||
|
||||
// The oakcodec encoder write path accepts interleaved float32 only; the
|
||||
// input stream is opened with paFloat32 (see start_recording()).
|
||||
OakEncoder *encoder = static_cast<OakEncoder *>(user_data);
|
||||
|
||||
oakcodec_encoder_write_audio(*encoder,
|
||||
reinterpret_cast<const float *>(input),
|
||||
int(frame_count));
|
||||
|
||||
return paContinue;
|
||||
}
|
||||
|
||||
bool AudioManager::push_to_output(const core::AudioParams ¶ms,
|
||||
const char *samples, int64_t samples_size,
|
||||
std::string *error)
|
||||
{
|
||||
if (output_device_ == paNoDevice) {
|
||||
if (error)
|
||||
*error = "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 =
|
||||
(unsigned long) audio_config::output_buffer_size();
|
||||
|
||||
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
|
||||
fprintf(stderr,
|
||||
"AudioManager::push_to_output: Pa_OpenStream failed: %s\n",
|
||||
Pa_GetErrorText(r));
|
||||
if (error)
|
||||
*error = Pa_GetErrorText(r);
|
||||
return false;
|
||||
}
|
||||
|
||||
output_buffer_->set_bytes_per_frame(output_params_.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
output_buffer_->write(samples, samples_size);
|
||||
|
||||
if (!Pa_IsStreamActive(output_stream_)) {
|
||||
PaError r = Pa_StartStream(output_stream_);
|
||||
if (r != paNoError) {
|
||||
fprintf(stderr,
|
||||
"AudioManager::push_to_output: Pa_StartStream returned "
|
||||
"%d %s\n",
|
||||
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 std::max(0.0, seconds);
|
||||
}
|
||||
|
||||
void AudioManager::reset_output_clock()
|
||||
{
|
||||
output_buffer_->reset_output_frames();
|
||||
}
|
||||
|
||||
PaSampleFormat AudioManager::get_port_audio_sample_format(core::SampleFormat fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case core::SampleFormat::u8:
|
||||
case core::SampleFormat::u8_p:
|
||||
return paUInt8;
|
||||
case core::SampleFormat::s16:
|
||||
case core::SampleFormat::s16_p:
|
||||
return paInt16;
|
||||
case core::SampleFormat::s32:
|
||||
case core::SampleFormat::s32_p:
|
||||
return paInt32;
|
||||
case core::SampleFormat::f32:
|
||||
case core::SampleFormat::f32_p:
|
||||
return paFloat32;
|
||||
case core::SampleFormat::s64:
|
||||
case core::SampleFormat::s64_p:
|
||||
case core::SampleFormat::f64:
|
||||
case core::SampleFormat::f64_p:
|
||||
case core::SampleFormat::invalid:
|
||||
case core::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) {
|
||||
fprintf(stderr, "AudioManager: no output device found\n");
|
||||
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
|
||||
fprintf(stderr, "AudioManager: invalid output audio device index: "
|
||||
"%d\n",
|
||||
device);
|
||||
} else {
|
||||
fprintf(stderr, "AudioManager: setting output audio device to %s\n",
|
||||
Pa_GetDeviceInfo(device)->name);
|
||||
}
|
||||
|
||||
output_device_ = device;
|
||||
|
||||
close_output_stream();
|
||||
}
|
||||
|
||||
void AudioManager::set_input_device(PaDeviceIndex device)
|
||||
{
|
||||
if (device == paNoDevice) {
|
||||
fprintf(stderr, "AudioManager: no input device found\n");
|
||||
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
|
||||
fprintf(stderr, "AudioManager: invalid input audio device index: %d\n",
|
||||
device);
|
||||
} else {
|
||||
fprintf(stderr, "AudioManager: setting input audio device to %s\n",
|
||||
Pa_GetDeviceInfo(device)->name);
|
||||
}
|
||||
|
||||
input_device_ = device;
|
||||
}
|
||||
|
||||
void AudioManager::hard_reset()
|
||||
{
|
||||
close_output_stream();
|
||||
Pa_Terminate();
|
||||
Pa_Initialize();
|
||||
}
|
||||
|
||||
bool AudioManager::start_recording(const oakcodec_encoding_params ¶ms,
|
||||
std::string *error_str)
|
||||
{
|
||||
if (input_device_ == paNoDevice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
input_encoder_ = oakcodec_encoder_init(¶ms);
|
||||
if (!input_encoder_.ctx || oakcodec_encoder_open(input_encoder_) != 0) {
|
||||
fprintf(stderr,
|
||||
"AudioManager: failed to open encoder for recording\n");
|
||||
if (input_encoder_.ctx) {
|
||||
char buf[512];
|
||||
if (oakcodec_encoder_last_error(input_encoder_, buf,
|
||||
int(sizeof(buf))) > 0 &&
|
||||
error_str) {
|
||||
*error_str = buf;
|
||||
}
|
||||
oakcodec_encoder_free(&input_encoder_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The oakcodec encoder write path takes interleaved float32; capture in
|
||||
// that format regardless of the target encoding sample format.
|
||||
core::AudioParams stream_params(params.audio_sample_rate,
|
||||
params.audio_channel_layout,
|
||||
core::SampleFormat::f32);
|
||||
PaStreamParameters p =
|
||||
get_port_audio_params(stream_params, input_device_);
|
||||
|
||||
PaError r = Pa_OpenStream(&input_stream_, &p, nullptr,
|
||||
params.audio_sample_rate,
|
||||
paFramesPerBufferUnspecified, paNoFlag,
|
||||
input_callback, &input_encoder_);
|
||||
if (r == paNoError) {
|
||||
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_.ctx) {
|
||||
oakcodec_encoder_flush(input_encoder_);
|
||||
oakcodec_encoder_free(&input_encoder_);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
static bool str_contains_ci(const char *haystack, const char *needle)
|
||||
{
|
||||
const size_t needle_len = strlen(needle);
|
||||
if (!needle_len) {
|
||||
return true;
|
||||
}
|
||||
for (const char *p = haystack; *p; p++) {
|
||||
if (strncasecmp(p, needle, needle_len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_preferred_linux_audio_host_api(const PaHostApiInfo *info)
|
||||
{
|
||||
if (!info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains_ci(info->name, "PipeWire") ||
|
||||
str_contains_ci(info->name, "JACK") ||
|
||||
str_contains_ci(info->name, "PulseAudio");
|
||||
}
|
||||
|
||||
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.
|
||||
static const char *const preferred_host_apis[] = {
|
||||
"PipeWire",
|
||||
"JACK",
|
||||
"PulseAudio",
|
||||
};
|
||||
|
||||
for (const char *preferred : preferred_host_apis) {
|
||||
for (PaHostApiIndex i = 0, end = Pa_GetHostApiCount(); i < end; i++) {
|
||||
const PaHostApiInfo *info = Pa_GetHostApiInfo(i);
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains_ci(info->name, preferred)) {
|
||||
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)
|
||||
{
|
||||
return find_device_by_name(
|
||||
audio_config::device_name(is_output_device), is_output_device);
|
||||
}
|
||||
|
||||
PaDeviceIndex AudioManager::find_device_by_name(const std::string &s,
|
||||
bool is_output_device)
|
||||
{
|
||||
PaDeviceIndex exact_match = paNoDevice;
|
||||
|
||||
if (!s.empty()) {
|
||||
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 == device->name) {
|
||||
exact_match = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __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) {
|
||||
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 core::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_.ctx = nullptr;
|
||||
input_encoder_.addref = nullptr;
|
||||
input_encoder_.release = nullptr;
|
||||
input_encoder_.abi_version = 0;
|
||||
|
||||
#ifdef PA_HAS_JACK
|
||||
// PortAudio doesn't do a strcpy, so we need a const char that's readily accessible
|
||||
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);
|
||||
|
||||
set_output_device(output_device);
|
||||
set_input_device(input_device);
|
||||
|
||||
output_buffer_ = new PreviewAudioDevice();
|
||||
}
|
||||
|
||||
AudioManager::~AudioManager()
|
||||
{
|
||||
close_output_stream();
|
||||
|
||||
delete output_buffer_;
|
||||
|
||||
Pa_Terminate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/***
|
||||
|
||||
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 <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <portaudio.h>
|
||||
|
||||
#include "codec/encoder.h"
|
||||
#include "olive/core/render/audioparams.h"
|
||||
#include "previewaudiodevice.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Audio input and output management class
|
||||
*
|
||||
* Wraps a PortAudio output stream and a PreviewAudioDevice pull buffer,
|
||||
* exposing audio functionality to the rest of the system.
|
||||
*
|
||||
* De-Qt notes:
|
||||
* - No longer a QObject and no longer inherits PlaybackAudioClock (the
|
||||
* clock interface lives in engine/common, which is not split); the
|
||||
* seconds() method is kept with the same semantics.
|
||||
* - The output_params_changed / output_notify signals are gone; the
|
||||
* notify-interval pulse is delivered through an optional
|
||||
* std::function (set_output_notify_callback) instead.
|
||||
* - Recording goes through the oakcodec encoder C ABI (OakEncoder)
|
||||
* instead of the FFmpegEncoder C++ class; the input stream is always
|
||||
* captured as interleaved float32.
|
||||
*/
|
||||
class AudioManager {
|
||||
public:
|
||||
static void create_instance();
|
||||
static void destroy_instance();
|
||||
|
||||
static AudioManager *instance();
|
||||
|
||||
void set_output_notify_interval(int64_t n);
|
||||
|
||||
/**
|
||||
* @brief Optional callback fired when a notify interval boundary is
|
||||
* crossed (called from the PortAudio callback thread)
|
||||
*/
|
||||
void set_output_notify_callback(std::function<void()> callback);
|
||||
|
||||
bool push_to_output(const core::AudioParams ¶ms, const char *samples,
|
||||
int64_t samples_size, std::string *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.
|
||||
*/
|
||||
double seconds() const;
|
||||
|
||||
/**
|
||||
* @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 oakcodec_encoding_params ¶ms,
|
||||
std::string *error_str = nullptr);
|
||||
|
||||
void stop_recording();
|
||||
|
||||
static PaDeviceIndex find_config_device_by_name(bool is_output_device);
|
||||
static PaDeviceIndex find_device_by_name(const std::string &s,
|
||||
bool is_output_device);
|
||||
|
||||
static PaStreamParameters get_port_audio_params(const core::AudioParams &p,
|
||||
PaDeviceIndex device);
|
||||
|
||||
private:
|
||||
AudioManager();
|
||||
|
||||
~AudioManager();
|
||||
|
||||
static PaSampleFormat get_port_audio_sample_format(core::SampleFormat fmt);
|
||||
|
||||
void close_output_stream();
|
||||
|
||||
static AudioManager *instance_;
|
||||
|
||||
PaDeviceIndex output_device_;
|
||||
PaStream *output_stream_;
|
||||
core::AudioParams output_params_;
|
||||
PreviewAudioDevice *output_buffer_;
|
||||
|
||||
PaDeviceIndex input_device_;
|
||||
PaStream *input_stream_;
|
||||
|
||||
OakEncoder input_encoder_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOMANAGER_H
|
||||
@@ -0,0 +1,218 @@
|
||||
/***
|
||||
|
||||
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 <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/ffmpegutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Bridge sample format for a native format via the oakcommon C ABI
|
||||
*/
|
||||
static int to_bridge_sample_format(core::SampleFormat fmt)
|
||||
{
|
||||
int out = -1; /* fb_sample_fmt_none */
|
||||
oakcommon_ffmpegutils_get_ffmpeg_sample_format(int(fmt), &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 core::AudioParams fix_channel_layout(const core::AudioParams ¶ms)
|
||||
{
|
||||
core::AudioParams result = params;
|
||||
|
||||
if (params.channel_layout() == 0) {
|
||||
int channels = params.channel_count();
|
||||
if (channels <= 0) {
|
||||
channels = 2;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: fixing unspecified channel layout "
|
||||
"(channels=%d) -> default %d channel layout\n",
|
||||
params.channel_count(), channels);
|
||||
|
||||
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 core::AudioParams &from,
|
||||
const core::AudioParams &to, double tempo)
|
||||
{
|
||||
if (graph_) {
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: tried to open a processor that was "
|
||||
"already open\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
core::AudioParams from_fixed = fix_channel_layout(from);
|
||||
core::AudioParams to_fixed = fix_channel_layout(to);
|
||||
|
||||
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 = to_bridge_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 = to_bridge_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_) {
|
||||
fprintf(stderr, "AudioProcessor: failed to create audio filter "
|
||||
"graph\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
out_frame_ = fb_frame_alloc();
|
||||
if (!out_frame_) {
|
||||
fprintf(stderr, "AudioProcessor: failed to allocate output frame\n");
|
||||
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()) {
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: tried to convert on closed processor\n");
|
||||
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) {
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: failed to add frame to buffersrc: %d\n",
|
||||
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(size_t(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
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: failed to pull from "
|
||||
"buffersink: %d\n",
|
||||
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[size_t(i)].resize(size_t(byte_offset + nb_bytes));
|
||||
memcpy(result[size_t(i)].data() + byte_offset,
|
||||
fb_frame_get_data(out_frame_, i), size_t(nb_bytes));
|
||||
}
|
||||
byte_offset += nb_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void AudioProcessor::flush()
|
||||
{
|
||||
int r = fb_audio_graph_push(graph_, nullptr, 0);
|
||||
if (r < 0) {
|
||||
fprintf(stderr, "AudioProcessor: failed to flush: %d\n", r);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/***
|
||||
|
||||
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 <vector>
|
||||
|
||||
#include <ffmpeg_bridge/ffmpeg_bridge.h>
|
||||
|
||||
#include "olive/core/render/audioparams.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioProcessor {
|
||||
public:
|
||||
AudioProcessor();
|
||||
|
||||
~AudioProcessor();
|
||||
|
||||
AudioProcessor(const AudioProcessor &) = delete;
|
||||
AudioProcessor &operator=(const AudioProcessor &) = delete;
|
||||
|
||||
bool open(const core::AudioParams &from, const core::AudioParams &to,
|
||||
double tempo = 1.0);
|
||||
|
||||
void close();
|
||||
|
||||
bool is_open() const
|
||||
{
|
||||
return graph_;
|
||||
}
|
||||
|
||||
using Buffer = std::vector<std::vector<char>>;
|
||||
int convert(float **in, int nb_in_samples, AudioProcessor::Buffer *output);
|
||||
|
||||
void flush();
|
||||
|
||||
const core::AudioParams &from() const
|
||||
{
|
||||
return from_;
|
||||
}
|
||||
const core::AudioParams &to() const
|
||||
{
|
||||
return to_;
|
||||
}
|
||||
|
||||
private:
|
||||
FBAudioGraph *graph_;
|
||||
|
||||
core::AudioParams from_;
|
||||
|
||||
core::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,478 @@
|
||||
/***
|
||||
|
||||
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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "olive/core/util/cpuoptimize.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using core::Rational;
|
||||
|
||||
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 core::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 = size_t(std::llround(double(i) * chunk_size)) / channels_;
|
||||
size_t src_end = std::min(
|
||||
size_t(std::llround(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 = size_t(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 core::SampleBuffer &samples,
|
||||
int sample_rate,
|
||||
const Rational &start)
|
||||
{
|
||||
if (!channels_) {
|
||||
fprintf(stderr,
|
||||
"AudioVisualWaveform: failed to write samples - channel "
|
||||
"count is zero\n");
|
||||
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(int64_t(samples.sample_count()), sample_rate);
|
||||
length_ = std::max(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 = size_t(std::floor(offset.to_double() * rate_dbl)) *
|
||||
size_t(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 = std::min(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_ = std::max(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_ = std::max(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_ = std::max(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.
|
||||
int64_t available = int64_t(mipmap_data.size()) - int64_t(start_sample);
|
||||
if (available > 0) {
|
||||
sample_length = std::min(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(size_t(channel_count()), { 0, 0 });
|
||||
}
|
||||
|
||||
void expand_min_max_channel(const float *a, size_t length, float &min_val,
|
||||
float &max_val)
|
||||
{
|
||||
#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_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 core::SampleBuffer &samples,
|
||||
size_t start_index, size_t length)
|
||||
{
|
||||
int channels = samples.audio_params().channel_count();
|
||||
const size_t channel_count = size_t(channels);
|
||||
AudioVisualWaveform::Sample summed_samples(channel_count);
|
||||
|
||||
for (int channel = 0; channel < channels; channel++) {
|
||||
expand_min_max_channel(samples.data(channel) + start_index, length,
|
||||
summed_samples[size_t(channel)].min,
|
||||
summed_samples[size_t(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)
|
||||
{
|
||||
const size_t channel_count = size_t(nb_channels);
|
||||
AudioVisualWaveform::Sample summed_samples(channel_count);
|
||||
|
||||
// Initialize from the first point instead of {0,0}: the engine version
|
||||
// started from zero-initialized pairs, which clamped all-positive
|
||||
// (resp. all-negative) ranges to a zero min (max). Fixed in oakaudio.
|
||||
if (nb_samples >= channel_count) {
|
||||
for (size_t j = 0; j < channel_count; j++) {
|
||||
summed_samples[j] = samples[j];
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < nb_samples; i += size_t(nb_channels)) {
|
||||
for (int j = 0; j < nb_channels; j++) {
|
||||
const AudioVisualWaveform::SamplePerChannel &sample =
|
||||
samples[i + size_t(j)];
|
||||
|
||||
if (sample.min < summed_samples[size_t(j)].min) {
|
||||
summed_samples[size_t(j)].min = sample.min;
|
||||
}
|
||||
|
||||
if (sample.max > summed_samples[size_t(j)].max) {
|
||||
summed_samples[size_t(j)].max = sample.max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summed_samples;
|
||||
}
|
||||
|
||||
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 size_t(std::floor(time * sample_rate)) * size_t(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,160 @@
|
||||
/***
|
||||
|
||||
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 <map>
|
||||
#include <vector>
|
||||
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
#include "olive/core/util/rational.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* De-Qt note: the QPainter-based draw_sample()/draw_waveform() functions
|
||||
* live in the app layer now; this class only stores and summarizes data.
|
||||
*/
|
||||
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 core::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 core::SampleBuffer &samples, int sample_rate,
|
||||
const core::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 core::Rational &dest,
|
||||
const core::Rational &offset = 0,
|
||||
const core::Rational &length = 0);
|
||||
|
||||
void overwrite_silence(const core::Rational &start,
|
||||
const core::Rational &length);
|
||||
|
||||
void trim_in(core::Rational length);
|
||||
|
||||
AudioVisualWaveform mid(const core::Rational &offset) const;
|
||||
AudioVisualWaveform mid(const core::Rational &offset,
|
||||
const core::Rational &length) const;
|
||||
|
||||
void resize(const core::Rational &length);
|
||||
|
||||
void trim_range(const core::Rational &in, const core::Rational &length);
|
||||
|
||||
Sample get_summary_from_time(const core::Rational &start,
|
||||
const core::Rational &length) const;
|
||||
|
||||
static Sample sum_samples(const core::SampleBuffer &samples,
|
||||
size_t start_index, size_t length);
|
||||
|
||||
static Sample re_sum_samples(const SamplePerChannel *samples,
|
||||
size_t nb_samples, int nb_channels);
|
||||
|
||||
// Must be a power of 2
|
||||
static const core::Rational k_minimum_sample_rate;
|
||||
static const core::Rational k_maximum_sample_rate;
|
||||
|
||||
private:
|
||||
void overwrite_samples_from_buffer(const core::SampleBuffer &samples,
|
||||
int sample_rate,
|
||||
const core::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 core::Rational &start,
|
||||
double output_rate, Sample &output_data);
|
||||
|
||||
size_t time_to_samples(const core::Rational &time, double sample_rate) const;
|
||||
size_t time_to_samples(const double &time, double sample_rate) const;
|
||||
|
||||
std::map<core::Rational, Sample>::const_iterator
|
||||
get_mipmap_for_scale(double scale) const;
|
||||
|
||||
void validate_virtual_start(const core::Rational &new_start);
|
||||
|
||||
core::Rational virtual_start_;
|
||||
|
||||
int channels_;
|
||||
|
||||
std::map<core::Rational, Sample> mipmapped_data_;
|
||||
|
||||
core::Rational length_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SUMSAMPLES_H
|
||||
@@ -0,0 +1,257 @@
|
||||
/***
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
std::vector<double>
|
||||
AudioWaveformSync::extract_rms_envelope(const core::SampleBuffer &samples,
|
||||
size_t window_samples)
|
||||
{
|
||||
std::vector<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(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[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 std::vector<double> reference_envelope =
|
||||
extract_rms_envelope(reference, window_samples);
|
||||
const std::vector<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 std::vector<double> &reference, const std::vector<double> &candidate,
|
||||
size_t window_samples, int64_t max_offset_windows)
|
||||
{
|
||||
return estimate_envelope_offset(reference, candidate, std::vector<char>(),
|
||||
std::vector<char>(), window_samples,
|
||||
max_offset_windows);
|
||||
}
|
||||
|
||||
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_envelope_offset(
|
||||
const std::vector<double> &reference, const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples, int64_t max_offset_windows)
|
||||
{
|
||||
OffsetResult result;
|
||||
if (reference.empty() || candidate.empty() || !window_samples) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto is_valid = [](const std::vector<char> &mask, size_t size,
|
||||
size_t index) {
|
||||
return mask.size() != size || mask.at(index);
|
||||
};
|
||||
|
||||
double best_score = -2.0;
|
||||
int64_t best_lag = 0;
|
||||
|
||||
const int reference_size = static_cast<int>(reference.size());
|
||||
const int candidate_size = static_cast<int>(candidate.size());
|
||||
|
||||
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(),
|
||||
size_t(reference_index)) ||
|
||||
!is_valid(candidate_valid, candidate.size(),
|
||||
size_t(candidate_index))) {
|
||||
continue;
|
||||
}
|
||||
reference_mean += reference.at(size_t(reference_index));
|
||||
candidate_mean += candidate.at(size_t(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(),
|
||||
size_t(reference_index)) ||
|
||||
!is_valid(candidate_valid, candidate.size(),
|
||||
size_t(candidate_index))) {
|
||||
continue;
|
||||
}
|
||||
const double reference_value =
|
||||
reference.at(size_t(reference_index)) - reference_mean;
|
||||
const double candidate_value =
|
||||
candidate.at(size_t(candidate_index)) - candidate_mean;
|
||||
numerator += reference_value * candidate_value;
|
||||
reference_energy += reference_value * reference_value;
|
||||
candidate_energy += candidate_value * candidate_value;
|
||||
}
|
||||
|
||||
// qFuzzyIsNull(double): |x| < 1e-12
|
||||
if (std::abs(reference_energy) < 1e-12 ||
|
||||
std::abs(candidate_energy) < 1e-12) {
|
||||
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 std::vector<double> &reference, const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples, int64_t max_offset_windows, double min_rate,
|
||||
double max_rate, double rate_step)
|
||||
{
|
||||
StretchOffsetResult result;
|
||||
if (reference.empty() || candidate.empty() || !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;
|
||||
}
|
||||
|
||||
const size_t resampled_len = size_t(resampled_size);
|
||||
std::vector<double> resampled(resampled_len);
|
||||
std::vector<char> resampled_valid(resampled_len);
|
||||
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[size_t(i)] = candidate.at(size_t(lower)) * (1.0 - fraction) +
|
||||
candidate.at(size_t(upper)) * fraction;
|
||||
|
||||
resampled_valid[size_t(i)] =
|
||||
(candidate_valid.size() != candidate.size() ||
|
||||
(candidate_valid.at(size_t(lower)) &&
|
||||
candidate_valid.at(size_t(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,105 @@
|
||||
/***
|
||||
|
||||
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 <vector>
|
||||
|
||||
#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 std::vector<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 std::vector<double> &reference,
|
||||
const std::vector<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 std::vector<double> &reference,
|
||||
const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &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 std::vector<double> &reference,
|
||||
const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples,
|
||||
int64_t max_offset_windows, double min_rate,
|
||||
double max_rate, double rate_step);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOWAVEFORMSYNC_H
|
||||
@@ -0,0 +1,54 @@
|
||||
/***
|
||||
|
||||
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 "configbridge.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "common/config.h"
|
||||
|
||||
namespace olive::audio_config
|
||||
{
|
||||
|
||||
int output_buffer_size()
|
||||
{
|
||||
// 0 = let PortAudio choose the buffer size (old default)
|
||||
return oakcommon_config_get_int(nullptr, "AudioOutputBufferSize", 0);
|
||||
}
|
||||
|
||||
std::string device_name(bool is_output_device)
|
||||
{
|
||||
const char *key = is_output_device ? "AudioOutput" : "AudioInput";
|
||||
|
||||
int size = oakcommon_config_get(nullptr, key, nullptr, 0);
|
||||
if (size <= 1) {
|
||||
// Absent (OAKCOMMON_E_NOT_FOUND) or empty
|
||||
return std::string();
|
||||
}
|
||||
|
||||
const size_t buf_len = size_t(size);
|
||||
std::vector<char> buf(buf_len);
|
||||
if (oakcommon_config_get(nullptr, key, buf.data(), size) < 0) {
|
||||
return std::string();
|
||||
}
|
||||
return std::string(buf.data());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/***
|
||||
|
||||
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_AUDIO_CONFIGBRIDGE_H
|
||||
#define OAK_AUDIO_CONFIGBRIDGE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace olive::audio_config
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Thin wrappers over the oakcommon config C ABI
|
||||
* (include/common/config.h)
|
||||
*
|
||||
* The old engine code read these keys through OAK_CONFIG/OAK_CONFIG_STR;
|
||||
* oakaudio reaches the same store through oakcommon_config_*. Typed
|
||||
* getters fall back when the key is absent (the compiled-in defaults do
|
||||
* not carry the audio device keys).
|
||||
*/
|
||||
|
||||
/** "AudioOutputBufferSize": PortAudio framesPerBuffer (0 = auto). */
|
||||
int output_buffer_size();
|
||||
|
||||
/** "AudioOutput" / "AudioInput": saved device name ("" when unset). */
|
||||
std::string device_name(bool is_output_device);
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIO_CONFIGBRIDGE_H
|
||||
@@ -0,0 +1,96 @@
|
||||
/***
|
||||
|
||||
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 "previewaudiodevice.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
PreviewAudioDevice::PreviewAudioDevice()
|
||||
: bytes_per_frame_(0)
|
||||
, notify_interval_(0)
|
||||
, bytes_read_(0)
|
||||
{
|
||||
}
|
||||
|
||||
PreviewAudioDevice::~PreviewAudioDevice() = default;
|
||||
|
||||
void PreviewAudioDevice::set_params(const core::AudioParams ¶ms)
|
||||
{
|
||||
set_bytes_per_frame(params.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
int64_t PreviewAudioDevice::read(char *data, int64_t max_size)
|
||||
{
|
||||
bool notify = false;
|
||||
int64_t copy_length;
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
copy_length = std::min(max_size, int64_t(buffer_.size()));
|
||||
|
||||
if (copy_length) {
|
||||
int64_t new_bytes_read = bytes_read_ + copy_length;
|
||||
|
||||
if (notify_interval_ > 0 && notify_callback_) {
|
||||
if ((bytes_read_ / notify_interval_) !=
|
||||
(new_bytes_read / notify_interval_)) {
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
bytes_read_ = new_bytes_read;
|
||||
|
||||
memcpy(data, buffer_.data(), copy_length);
|
||||
buffer_.erase(buffer_.begin(), buffer_.begin() + copy_length);
|
||||
}
|
||||
}
|
||||
|
||||
// Fired outside the lock (see set_notify_callback())
|
||||
if (notify) {
|
||||
notify_callback_();
|
||||
}
|
||||
|
||||
return copy_length;
|
||||
}
|
||||
|
||||
int64_t PreviewAudioDevice::write(const char *data, int64_t length)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
buffer_.insert(buffer_.end(), data, data + length);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
void PreviewAudioDevice::clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
buffer_.clear();
|
||||
bytes_read_ = 0;
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/***
|
||||
|
||||
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_PREVIEWAUDIODEVICE_H
|
||||
#define OAK_PREVIEWAUDIODEVICE_H
|
||||
|
||||
#include "olive/core/render/audioparams.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Pull-style sample buffer fed to the audio output callback
|
||||
*
|
||||
* Formerly a QIODevice subclass consumed by QAudioOutput. Now a plain class:
|
||||
* the audio backend (PortAudio, see engine/audio AudioManager) pulls samples
|
||||
* through read() from its stream callback and the render side pushes samples
|
||||
* through write(). The callback-driven pull semantics are unchanged.
|
||||
*/
|
||||
class PreviewAudioDevice {
|
||||
public:
|
||||
PreviewAudioDevice();
|
||||
|
||||
virtual ~PreviewAudioDevice();
|
||||
|
||||
/**
|
||||
* @brief Read up to `max_size` bytes from the queued buffer
|
||||
*
|
||||
* Called from the audio output callback. Returns the number of bytes
|
||||
* actually copied (0 when the buffer is empty, i.e. underrun).
|
||||
*/
|
||||
int64_t read(char *data, int64_t max_size);
|
||||
|
||||
/**
|
||||
* @brief Append `length` bytes to the queued buffer
|
||||
*/
|
||||
int64_t write(const char *data, int64_t length);
|
||||
|
||||
// Derives the frame size from the audio format (bytes per sample per
|
||||
// channel * channel count). Until params are set, bytes_per_frame()
|
||||
// reports 0, i.e. "unknown".
|
||||
void set_params(const core::AudioParams ¶ms);
|
||||
|
||||
int bytes_per_frame() const
|
||||
{
|
||||
return bytes_per_frame_;
|
||||
}
|
||||
|
||||
void set_bytes_per_frame(int b)
|
||||
{
|
||||
bytes_per_frame_ = b;
|
||||
}
|
||||
|
||||
void set_notify_interval(int64_t i)
|
||||
{
|
||||
notify_interval_ = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the callback fired when a notify interval boundary is crossed
|
||||
*
|
||||
* Replaces the former `notify` signal. The callback is invoked from read(),
|
||||
* i.e. from the audio output callback thread, AFTER the internal lock has
|
||||
* been released (the Qt version emitted while holding the lock; receivers
|
||||
* lived on another thread so it was effectively queued). The callback must
|
||||
* therefore be thread-safe and must not call back into this device.
|
||||
*/
|
||||
void set_notify_callback(std::function<void()> callback)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
notify_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Frames consumed by the audio output callback
|
||||
*
|
||||
* Counted in the callback itself so underrun (zero-filled) frames are
|
||||
* included, making the value usable as a playback clock.
|
||||
*/
|
||||
void add_output_frames(int64_t frame_count)
|
||||
{
|
||||
output_frames_consumed_.fetch_add(frame_count);
|
||||
}
|
||||
|
||||
int64_t output_frames_consumed() const
|
||||
{
|
||||
return output_frames_consumed_.load();
|
||||
}
|
||||
|
||||
void reset_output_frames()
|
||||
{
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex lock_;
|
||||
|
||||
std::vector<char> buffer_;
|
||||
|
||||
int bytes_per_frame_;
|
||||
|
||||
int64_t notify_interval_;
|
||||
|
||||
int64_t bytes_read_;
|
||||
|
||||
std::function<void()> notify_callback_;
|
||||
|
||||
std::atomic<int64_t> output_frames_consumed_{0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PREVIEWAUDIODEVICE_H
|
||||
@@ -0,0 +1,158 @@
|
||||
# Oak Video Editor - 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/>.
|
||||
|
||||
# Standalone build driver for the oakaudio module (M6). Mirrors
|
||||
# src/codec/standalone: oakaudio links oakcodec (encoder for recording,
|
||||
# decoder for waveform extraction), oakcommon (config + ffmpegutils C
|
||||
# ABI), olivecore and ffmpeg_bridge, plus PortAudio for the output
|
||||
# device. oakcodec's own dependency stack (oakrender/oaknode/...) is
|
||||
# assembled the same way src/codec/standalone does it.
|
||||
#
|
||||
# Usage (macOS/Homebrew):
|
||||
# cmake -S src/audio/standalone -B build-audio
|
||||
# cmake --build build-audio -j
|
||||
# ctest --test-dir build-audio
|
||||
|
||||
cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
|
||||
|
||||
project(oakaudio-standalone LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
get_filename_component(OAK_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake")
|
||||
if(EXISTS "/opt/homebrew")
|
||||
list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew")
|
||||
endif()
|
||||
|
||||
find_package(EXPAT REQUIRED)
|
||||
find_package(OpenColorIO CONFIG REQUIRED)
|
||||
find_package(OpenImageIO CONFIG REQUIRED)
|
||||
|
||||
set(OCIO_LIBRARIES OpenColorIO::OpenColorIO)
|
||||
set(OCIO_INCLUDE_DIRS "")
|
||||
set(OIIO_LIBRARIES OpenImageIO::OpenImageIO)
|
||||
set(OIIO_INCLUDE_DIRS "")
|
||||
|
||||
# In-repo libraries, built from source (same set src/codec/standalone
|
||||
# assembles, because oakaudio links oakcodec):
|
||||
# - olivecore (core/): oakcore_* C ABI and olive::core C++ wrappers
|
||||
# - ffmpeg_bridge: fb_* C ABI (resampler infra + codec's FFmpeg access)
|
||||
# - oakundo / oakcommon / oaknode / oakrender: oakcodec's own dependencies
|
||||
# - oakcodec: encoder (recording) + decoder (waveform extract) C ABI
|
||||
set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON)
|
||||
|
||||
add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core)
|
||||
target_include_directories(olivecore PUBLIC ${OAK_REPO_ROOT}/third_party/openfx/include)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge)
|
||||
|
||||
set(BUILD_TESTS OFF)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
|
||||
set(BUILD_TESTS ON)
|
||||
|
||||
# oaknode needs its transition stubs when built in this tree (see
|
||||
# src/render/standalone/CMakeLists.txt).
|
||||
target_include_directories(oaknode BEFORE PUBLIC
|
||||
${OAK_REPO_ROOT}/src/render/transition
|
||||
${OAK_REPO_ROOT}/src/node/transition
|
||||
${OAK_REPO_ROOT}/src/render/src
|
||||
)
|
||||
target_include_directories(oaknode PUBLIC
|
||||
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
|
||||
/opt/homebrew/include
|
||||
/opt/homebrew/include/Imath
|
||||
)
|
||||
target_link_options(oaknode PRIVATE
|
||||
"-undefined" "dynamic_lookup"
|
||||
)
|
||||
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/render/c_api ${CMAKE_BINARY_DIR}/render_c_api)
|
||||
|
||||
# Transition stub dirs must precede everything else: src/render/transition
|
||||
# first, then src/node/transition (shared stubs).
|
||||
target_include_directories(oakrender BEFORE PUBLIC
|
||||
${OAK_REPO_ROOT}/src/render/transition
|
||||
${OAK_REPO_ROOT}/src/node/transition
|
||||
)
|
||||
|
||||
target_include_directories(oakrender PUBLIC
|
||||
${OAK_REPO_ROOT}/engine/include
|
||||
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
|
||||
/opt/homebrew/include
|
||||
/opt/homebrew/include/Imath
|
||||
/opt/homebrew/include/OpenEXR
|
||||
)
|
||||
|
||||
# Vulkan headers (Homebrew keg-only vulkan-headers).
|
||||
if(NOT EXISTS "/opt/homebrew/include/vulkan/vulkan.h")
|
||||
execute_process(COMMAND brew --prefix vulkan-headers
|
||||
OUTPUT_VARIABLE VULKAN_HEADERS_PREFIX
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET)
|
||||
if(VULKAN_HEADERS_PREFIX AND EXISTS "${VULKAN_HEADERS_PREFIX}/include/vulkan/vulkan.h")
|
||||
target_include_directories(oakrender PUBLIC "${VULKAN_HEADERS_PREFIX}/include")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Symbols of the not-yet-split engine modules dangle by design. The
|
||||
# backend libraries resolve most symbols from liboakrender at load time
|
||||
# and dangle the same way.
|
||||
foreach(t oakrender oakgl oakgl2 oakvulkan)
|
||||
if(TARGET ${t})
|
||||
target_link_options(${t} PRIVATE
|
||||
"-undefined" "dynamic_lookup"
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
target_link_libraries(oakrender PRIVATE
|
||||
oaknode
|
||||
oakcommon
|
||||
oakundo
|
||||
olivecore
|
||||
ffmpeg_bridge
|
||||
${OCIO_LIBRARIES}
|
||||
${OIIO_LIBRARIES}
|
||||
"-framework OpenGL"
|
||||
"-framework CoreVideo"
|
||||
"-framework Metal"
|
||||
"-framework QuartzCore"
|
||||
)
|
||||
|
||||
# oakcodec (oakaudio's only codec access goes through its C ABI).
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/codec/src ${CMAKE_BINARY_DIR}/codec)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/codec/c_api ${CMAKE_BINARY_DIR}/codec_c_api)
|
||||
|
||||
target_link_options(oakcodec PRIVATE
|
||||
"-undefined" "dynamic_lookup"
|
||||
)
|
||||
|
||||
# oakaudio itself.
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/audio/src ${CMAKE_BINARY_DIR}/audio)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/audio/c_api ${CMAKE_BINARY_DIR}/audio_c_api)
|
||||
|
||||
# Tests (oakaudio-gtest).
|
||||
if(BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/audio/tests ${CMAKE_BINARY_DIR}/audio_tests)
|
||||
endif()
|
||||
@@ -0,0 +1,83 @@
|
||||
# Oak Video Editor - 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/>.
|
||||
|
||||
find_package(GTest REQUIRED)
|
||||
include(GoogleTest)
|
||||
|
||||
# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
|
||||
# build (see src/audio/standalone) sets OAK_REPO_ROOT explicitly.
|
||||
if(NOT DEFINED OAK_REPO_ROOT)
|
||||
set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
add_executable(oakaudio-gtest
|
||||
levelmeter_test.cpp
|
||||
manager_test.cpp
|
||||
processor_test.cpp
|
||||
sync_test.cpp
|
||||
waveform_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(oakaudio-gtest PRIVATE
|
||||
oakaudio
|
||||
oakcodec
|
||||
oakrender
|
||||
oaknode
|
||||
oakcommon
|
||||
oakundo
|
||||
olivecore
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
# liboakrender/liboaknode dangle OFX host symbols (-undefined
|
||||
# dynamic_lookup); force-load the host support archive into the test
|
||||
# process so dyld finds them in the flat namespace at startup. Mirrors
|
||||
# src/codec/tests/CMakeLists.txt.
|
||||
if(NOT DEFINED OAKRENDER_OFX_HOST_ARCHIVE)
|
||||
find_library(OAKRENDER_OFX_HOST_ARCHIVE NAMES OfxHost
|
||||
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport)
|
||||
endif()
|
||||
if(NOT OAKRENDER_OFX_HOST_ARCHIVE)
|
||||
message(FATAL_ERROR
|
||||
"libOfxHost.a not found; run the full-tree build once or set "
|
||||
"OAKRENDER_OFX_HOST_ARCHIVE")
|
||||
endif()
|
||||
target_link_options(oakaudio-gtest PRIVATE
|
||||
"-Wl,-force_load,${OAKRENDER_OFX_HOST_ARCHIVE}")
|
||||
|
||||
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
|
||||
# dynamic_lookup; the test binary links the inert shim from
|
||||
# src/node/standalone instead.
|
||||
target_sources(oakaudio-gtest PRIVATE
|
||||
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp)
|
||||
target_include_directories(oakaudio-gtest PRIVATE
|
||||
${OAK_REPO_ROOT}/engine/include
|
||||
)
|
||||
|
||||
# include/ must win over the render/node transition dirs that leak in
|
||||
# through oaknode's PUBLIC includes: they carry codec/*.h stubs that would
|
||||
# otherwise shadow the real oakcodec public headers. -iquote is searched
|
||||
# before every -I for quoted includes.
|
||||
target_compile_options(oakaudio-gtest PRIVATE
|
||||
"-iquote" "${OAK_REPO_ROOT}/include"
|
||||
)
|
||||
|
||||
# tests/demo.mp4 lives at the repo's shared tests directory.
|
||||
target_compile_definitions(oakaudio-gtest PRIVATE
|
||||
OAKAUDIO_TEST_DATA_DIR="${OAK_REPO_ROOT}/tests")
|
||||
|
||||
gtest_discover_tests(oakaudio-gtest)
|
||||
@@ -0,0 +1,117 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/levelmeter.h"
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzeConstantSignal)
|
||||
{
|
||||
// Constant 0.5 on both channels: peak = rms = 0.5, dB = 20*log10(0.5)
|
||||
std::vector<float> ch(1024, 0.5f);
|
||||
const float *planes[2] = { ch.data(), ch.data() };
|
||||
oakaudio_channel_stats channels[2];
|
||||
oakaudio_meter_stats summary;
|
||||
|
||||
ASSERT_EQ(oakaudio_levelmeter_analyze(planes, 2, 1024, channels, 2,
|
||||
&summary),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
for (int c = 0; c < 2; c++) {
|
||||
EXPECT_DOUBLE_EQ(channels[c].peak_linear, 0.5);
|
||||
EXPECT_DOUBLE_EQ(channels[c].rms_linear, 0.5);
|
||||
EXPECT_NEAR(channels[c].peak_db, 20.0 * std::log10(0.5), 1e-9);
|
||||
EXPECT_NEAR(channels[c].rms_db, 20.0 * std::log10(0.5), 1e-9);
|
||||
EXPECT_DOUBLE_EQ(channels[c].vu_db, channels[c].rms_db);
|
||||
}
|
||||
|
||||
EXPECT_DOUBLE_EQ(summary.max_peak_linear, 0.5);
|
||||
EXPECT_EQ(summary.silence, 0);
|
||||
// LUFS = -0.691 + 10*log10(mean square) = -0.691 + 10*log10(0.25)
|
||||
EXPECT_NEAR(summary.integrated_lufs, -0.691 + 10.0 * std::log10(0.25),
|
||||
1e-9);
|
||||
}
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzeSilence)
|
||||
{
|
||||
std::vector<float> ch(512, 0.0f);
|
||||
const float *planes[1] = { ch.data() };
|
||||
oakaudio_meter_stats summary;
|
||||
|
||||
ASSERT_EQ(oakaudio_levelmeter_analyze(planes, 1, 512, nullptr, 0,
|
||||
&summary),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(summary.silence, 1);
|
||||
EXPECT_DOUBLE_EQ(summary.max_peak_linear, 0.0);
|
||||
EXPECT_DOUBLE_EQ(summary.integrated_lufs, -200.0);
|
||||
}
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzePeakPerChannel)
|
||||
{
|
||||
std::vector<float> quiet(256, 0.1f);
|
||||
std::vector<float> loud(256, 0.0f);
|
||||
loud[7] = -0.8f; // single peak
|
||||
const float *planes[2] = { quiet.data(), loud.data() };
|
||||
oakaudio_channel_stats channels[2];
|
||||
|
||||
ASSERT_EQ(oakaudio_levelmeter_analyze(planes, 2, 256, channels, 2,
|
||||
nullptr),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_NEAR(channels[0].peak_linear, 0.1, 1e-6);
|
||||
EXPECT_NEAR(channels[1].peak_linear, 0.8, 1e-6);
|
||||
// dB floor: the zero samples dominate, but the peak channel has signal
|
||||
EXPECT_GT(channels[1].peak_db, channels[0].peak_db);
|
||||
}
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzeErrorPaths)
|
||||
{
|
||||
std::vector<float> ch(64, 0.5f);
|
||||
const float *planes[1] = { ch.data() };
|
||||
oakaudio_channel_stats channels[1];
|
||||
oakaudio_meter_stats summary;
|
||||
|
||||
// NULL planes
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(nullptr, 1, 64, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Zero channels
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 0, 64, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Negative frame count
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 1, -1, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Insufficient channel capacity
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 1, 64, channels, 0,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Both outs NULL
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 1, 64, nullptr, 0, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL plane inside array
|
||||
const float *bad_planes[1] = { nullptr };
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(bad_planes, 1, 64, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/manager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kSampleFmtF32 = 10; // olive::core::SampleFormat::f32
|
||||
constexpr uint64_t kLayoutStereo = 0x3;
|
||||
|
||||
// Creates the singleton for the duration of the test; skips when no
|
||||
// audio device environment is available.
|
||||
struct ManagerFixture {
|
||||
ManagerFixture()
|
||||
{
|
||||
created = (oakaudio_manager_create_instance() == OAKAUDIO_OK) &&
|
||||
oakaudio_manager_instance().ctx != nullptr;
|
||||
}
|
||||
~ManagerFixture()
|
||||
{
|
||||
if (created) {
|
||||
oakaudio_manager_destroy_instance();
|
||||
}
|
||||
}
|
||||
bool created = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioManager, InstanceLifecycle)
|
||||
{
|
||||
// Without an instance all calls report E_STATE and instance() is empty
|
||||
OakAudioManager none = oakaudio_manager_instance();
|
||||
EXPECT_EQ(none.ctx, nullptr);
|
||||
EXPECT_EQ(none.abi_version, OAKAUDIO_ABI_VERSION);
|
||||
|
||||
double secs;
|
||||
EXPECT_EQ(oakaudio_manager_seconds(none, &secs), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_get_output_device(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_get_input_device(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_device(none, 0), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_set_input_device(none, 0), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_notify_interval(none, 1024),
|
||||
OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_clear_buffered_output(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_stop_output(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_reset_output_clock(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_hard_reset(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_stop_recording(none), OAKAUDIO_E_STATE);
|
||||
|
||||
float samples[2] = { 0.0f, 0.0f };
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(none, 48000, kLayoutStereo,
|
||||
kSampleFmtF32,
|
||||
reinterpret_cast<char *>(samples),
|
||||
sizeof(samples), nullptr, 0),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
oakcodec_encoding_params params;
|
||||
std::memset(¶ms, 0, sizeof(params));
|
||||
params.audio_enabled = 1;
|
||||
EXPECT_EQ(oakaudio_manager_start_recording(none, ¶ms, nullptr, 0),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
// free is a no-op and safe on NULL/empty
|
||||
oakaudio_manager_free(nullptr);
|
||||
oakaudio_manager_free(&none);
|
||||
EXPECT_EQ(none.ctx, nullptr);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, DeviceRoundTrip)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
OakAudioManager m = oakaudio_manager_instance();
|
||||
ASSERT_NE(m.ctx, nullptr);
|
||||
// Singleton: addref/release never destroy
|
||||
m.addref(m.ctx);
|
||||
m.release(m.ctx);
|
||||
EXPECT_EQ(oakaudio_manager_instance().ctx, m.ctx);
|
||||
|
||||
// Devices: whatever was detected, get/set round-trips
|
||||
const int out_device = oakaudio_manager_get_output_device(m);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_device(m, out_device), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_get_output_device(m), out_device);
|
||||
|
||||
const int in_device = oakaudio_manager_get_input_device(m);
|
||||
EXPECT_EQ(oakaudio_manager_set_input_device(m, in_device), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_get_input_device(m), in_device);
|
||||
|
||||
// Notify interval set/get-free command
|
||||
EXPECT_EQ(oakaudio_manager_set_output_notify_interval(m, 4096),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_notify_interval(m, -1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
// No stream running: seconds() is negative, clock commands are valid
|
||||
double secs = 1.0;
|
||||
EXPECT_EQ(oakaudio_manager_seconds(m, &secs), OAKAUDIO_OK);
|
||||
EXPECT_LT(secs, 0.0);
|
||||
EXPECT_EQ(oakaudio_manager_seconds(m, nullptr), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_manager_reset_output_clock(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_clear_buffered_output(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_stop_output(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_hard_reset(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_stop_recording(m), OAKAUDIO_OK);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, PushToOutput)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
OakAudioManager m = oakaudio_manager_instance();
|
||||
if (oakaudio_manager_get_output_device(m) < 0) {
|
||||
GTEST_SKIP() << "no output device";
|
||||
}
|
||||
|
||||
// One second of silence, packed f32 stereo
|
||||
std::vector<float> silence(48000 * 2, 0.0f);
|
||||
char error[256];
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(
|
||||
m, 48000, kLayoutStereo, kSampleFmtF32,
|
||||
reinterpret_cast<char *>(silence.data()),
|
||||
int64_t(silence.size() * sizeof(float)), error,
|
||||
int(sizeof(error))),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(
|
||||
m, 0, kLayoutStereo, kSampleFmtF32,
|
||||
reinterpret_cast<char *>(silence.data()), 16, nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(m, 48000, kLayoutStereo,
|
||||
kSampleFmtF32, nullptr, 16,
|
||||
nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
oakaudio_manager_stop_output(m);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, StartRecordingErrorPaths)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
OakAudioManager m = oakaudio_manager_instance();
|
||||
|
||||
// NULL params / audio disabled
|
||||
EXPECT_EQ(oakaudio_manager_start_recording(m, nullptr, nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
oakcodec_encoding_params params;
|
||||
std::memset(¶ms, 0, sizeof(params));
|
||||
EXPECT_EQ(oakaudio_manager_start_recording(m, ¶ms, nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, FindDeviceByName)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
// A name that matches nothing falls back to the default device (or
|
||||
// paNoDevice on device-less systems); either way no crash and a valid
|
||||
// index or -1.
|
||||
const int out = oakaudio_manager_find_device_by_name_s(
|
||||
"definitely-not-a-real-device-name-oakaudio-test", 1);
|
||||
EXPECT_GE(out, -1);
|
||||
|
||||
const int cfg = oakaudio_manager_find_config_device_by_name_s(1);
|
||||
EXPECT_GE(cfg, -1);
|
||||
|
||||
// Error path: NULL name
|
||||
EXPECT_EQ(oakaudio_manager_find_device_by_name_s(nullptr, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/manager.h"
|
||||
#include "audio/processor.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kSampleFmtF32P = 4; // olive::core::SampleFormat::f32_p
|
||||
constexpr uint64_t kLayoutStereo = 0x3;
|
||||
|
||||
struct ProcessorHandle {
|
||||
OakAudioProcessor h = oakaudio_processor_init();
|
||||
~ProcessorHandle() { oakaudio_processor_free(&h); }
|
||||
};
|
||||
|
||||
// Feed a full buffer through the processor and return the total number of
|
||||
// output frames produced (input drained + flushed).
|
||||
int convert_all(OakAudioProcessor p, const std::vector<std::vector<float>> &in,
|
||||
int chunk)
|
||||
{
|
||||
const int channels = int(in.size());
|
||||
const size_t nch = size_t(channels);
|
||||
std::vector<const float *> in_planes(nch);
|
||||
std::vector<std::vector<float>> out_store(nch);
|
||||
std::vector<float *> out_planes(nch);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
in_planes[size_t(ch)] = in[size_t(ch)].data();
|
||||
out_store[size_t(ch)].resize(size_t(chunk) * 4 + 4096);
|
||||
out_planes[size_t(ch)] = out_store[size_t(ch)].data();
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
const int frames = int(in[0].size());
|
||||
for (int pos = 0; pos < frames; pos += chunk) {
|
||||
const int n = std::min(chunk, frames - pos);
|
||||
std::vector<const float *> window(nch);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
window[size_t(ch)] = in[size_t(ch)].data() + pos;
|
||||
}
|
||||
const int produced = oakaudio_processor_convert(
|
||||
p, window.data(), n, out_planes.data(), int(out_store[0].size()));
|
||||
if (produced < 0) {
|
||||
return produced;
|
||||
}
|
||||
total += produced;
|
||||
}
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_flush(p), OAKAUDIO_OK);
|
||||
// Drain the resampler's internal delay
|
||||
for (int guard = 0; guard < 64; guard++) {
|
||||
const int produced = oakaudio_processor_convert(
|
||||
p, nullptr, 0, out_planes.data(), int(out_store[0].size()));
|
||||
if (produced <= 0) {
|
||||
break;
|
||||
}
|
||||
total += produced;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
std::vector<std::vector<float>> make_sine(int channels, int frames, int rate)
|
||||
{
|
||||
const size_t nch = size_t(channels);
|
||||
std::vector<std::vector<float>> data(nch);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
data[size_t(ch)].resize(size_t(frames));
|
||||
for (int i = 0; i < frames; i++) {
|
||||
data[size_t(ch)][size_t(i)] =
|
||||
0.5f * std::sin(2.0 * M_PI * 440.0 * i / rate);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioProcessor, InitFree)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
{
|
||||
ProcessorHandle p;
|
||||
ASSERT_NE(p.h.ctx, nullptr);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before + 1);
|
||||
}
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
|
||||
// free is a no-op on NULL / empty handles
|
||||
oakaudio_processor_free(nullptr);
|
||||
OakAudioProcessor empty = {};
|
||||
oakaudio_processor_free(&empty);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, OpenCloseIsOpen)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 0);
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 1);
|
||||
|
||||
// Error path: opening an open processor
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_close(p.h), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 0);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, OpenInvalidArgs)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
// Unsupported output format (only f32p is delivered)
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, 1, 1.0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Bad sample rate
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 0, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 0);
|
||||
|
||||
// Empty handle
|
||||
OakAudioProcessor empty = {};
|
||||
EXPECT_EQ(oakaudio_processor_open(empty, 44100, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_close(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_flush(empty), OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertResample441To48)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
ProcessorHandle p;
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
const int in_frames = 44100; // one second
|
||||
const auto sine = make_sine(2, in_frames, 44100);
|
||||
const int produced = convert_all(p.h, sine, 4096);
|
||||
ASSERT_GE(produced, 0);
|
||||
|
||||
// One second at 44.1k must become (within resampler tolerance) one
|
||||
// second at 48k.
|
||||
EXPECT_NEAR(produced, 48000, 200);
|
||||
|
||||
// Re-open check for leaks
|
||||
oakaudio_processor_close(p.h);
|
||||
oakaudio_processor_free(&p.h);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertSilenceStaysSilent)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
std::vector<std::vector<float>> silence(2, std::vector<float>(4096, 0.0f));
|
||||
std::vector<std::vector<float>> out(2, std::vector<float>(8192, -1.0f));
|
||||
std::vector<const float *> in_planes = { silence[0].data(),
|
||||
silence[1].data() };
|
||||
std::vector<float *> out_planes = { out[0].data(), out[1].data() };
|
||||
|
||||
const int produced = oakaudio_processor_convert(
|
||||
p.h, in_planes.data(), 4096, out_planes.data(), 8192);
|
||||
ASSERT_GT(produced, 0);
|
||||
EXPECT_EQ(produced, 4096); // same rate in/out: 1:1 frames
|
||||
for (int i = 0; i < produced; i++) {
|
||||
EXPECT_FLOAT_EQ(out[0][size_t(i)], 0.0f);
|
||||
EXPECT_FLOAT_EQ(out[1][size_t(i)], 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertTempo)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.5),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
const auto sine = make_sine(2, 48000, 48000);
|
||||
const int produced = convert_all(p.h, sine, 4096);
|
||||
ASSERT_GE(produced, 0);
|
||||
|
||||
// 1.5x tempo: one second of input becomes roughly 2/3 second of
|
||||
// output (atempo works on correlated windows, so allow slack)
|
||||
EXPECT_NEAR(produced, int(48000 / 1.5), 3000);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertErrorPaths)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
|
||||
// Convert on a closed processor
|
||||
float dummy = 0.0f;
|
||||
float *out_planes[1] = { &dummy };
|
||||
const float *in_planes[1] = { &dummy };
|
||||
EXPECT_EQ(oakaudio_processor_convert(p.h, in_planes, 1, out_planes, 1),
|
||||
OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_processor_flush(p.h), OAKAUDIO_E_STATE);
|
||||
|
||||
// Empty handle
|
||||
OakAudioProcessor empty = {};
|
||||
EXPECT_EQ(oakaudio_processor_convert(empty, in_planes, 1, out_planes, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
// NULL input planes with frames
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_processor_convert(p.h, nullptr, 10, out_planes, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Negative counts
|
||||
EXPECT_EQ(oakaudio_processor_convert(p.h, in_planes, -1, out_planes, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_flush(p.h), OAKAUDIO_OK);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/sync.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// 8-window synthetic envelope with a distinctive shape
|
||||
std::vector<double> make_envelope()
|
||||
{
|
||||
return { 0.1, 0.5, 0.9, 0.3, 0.2, 0.8, 0.4, 0.1 };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioSync, ExtractRmsEnvelope)
|
||||
{
|
||||
// Mono, window = 4 frames: window 0 constant 0.5 -> RMS 0.5
|
||||
std::vector<float> ch(8, 0.0f);
|
||||
for (int i = 0; i < 4; i++)
|
||||
ch[size_t(i)] = 0.5f;
|
||||
const float *planes[1] = { ch.data() };
|
||||
|
||||
// Query mode
|
||||
const int windows =
|
||||
oakaudio_sync_extract_rms_envelope(planes, 1, 8, 4, nullptr, 0);
|
||||
ASSERT_EQ(windows, 2);
|
||||
|
||||
double envelope[2];
|
||||
ASSERT_EQ(oakaudio_sync_extract_rms_envelope(planes, 1, 8, 4, envelope, 2),
|
||||
2);
|
||||
EXPECT_DOUBLE_EQ(envelope[0], 0.5);
|
||||
EXPECT_DOUBLE_EQ(envelope[1], 0.0);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, ExtractRmsEnvelopeErrorPaths)
|
||||
{
|
||||
float v = 0.0f;
|
||||
const float *planes[1] = { &v };
|
||||
double out[1];
|
||||
|
||||
EXPECT_EQ(oakaudio_sync_extract_rms_envelope(nullptr, 1, 8, 4, out, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_extract_rms_envelope(planes, 0, 8, 4, out, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_extract_rms_envelope(planes, 1, 8, 0, out, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateEnvelopeOffset)
|
||||
{
|
||||
const std::vector<double> ref = make_envelope();
|
||||
// Candidate = reference shifted right by 2 windows
|
||||
std::vector<double> cand = { 0.0, 0.0 };
|
||||
cand.insert(cand.end(), ref.begin(), ref.end());
|
||||
|
||||
oakaudio_offset_result out;
|
||||
ASSERT_EQ(oakaudio_sync_estimate_envelope_offset(
|
||||
ref.data(), int(ref.size()), cand.data(), int(cand.size()),
|
||||
nullptr, nullptr, 100, 8, &out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(out.valid, 1);
|
||||
EXPECT_EQ(out.offset_samples, 2 * 100);
|
||||
EXPECT_GT(out.confidence, 0.9);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateEnvelopeOffsetWithMasks)
|
||||
{
|
||||
const std::vector<double> ref = make_envelope();
|
||||
std::vector<double> cand = { 0.0, 0.0 };
|
||||
cand.insert(cand.end(), ref.begin(), ref.end());
|
||||
|
||||
std::vector<uint8_t> all_valid_ref(ref.size(), 1);
|
||||
std::vector<uint8_t> all_valid_cand(cand.size(), 1);
|
||||
|
||||
oakaudio_offset_result out;
|
||||
ASSERT_EQ(oakaudio_sync_estimate_envelope_offset(
|
||||
ref.data(), int(ref.size()), cand.data(), int(cand.size()),
|
||||
all_valid_ref.data(), all_valid_cand.data(), 100, 8, &out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(out.valid, 1);
|
||||
EXPECT_EQ(out.offset_samples, 200);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateEnvelopeOffsetErrorPaths)
|
||||
{
|
||||
double env[4] = { 0.1, 0.2, 0.3, 0.4 };
|
||||
oakaudio_offset_result out;
|
||||
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(nullptr, 4, env, 4,
|
||||
nullptr, nullptr, 100, 4,
|
||||
&out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(env, 0, env, 4, nullptr,
|
||||
nullptr, 100, 4, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(env, 4, env, 4, nullptr,
|
||||
nullptr, 0, 4, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(env, 4, env, 4, nullptr,
|
||||
nullptr, 100, 4, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateStretchAndOffset)
|
||||
{
|
||||
const std::vector<double> ref = make_envelope();
|
||||
// Candidate runs at half speed: each ref window duplicated
|
||||
std::vector<double> cand;
|
||||
for (double v : ref) {
|
||||
cand.push_back(v);
|
||||
cand.push_back(v);
|
||||
}
|
||||
|
||||
oakaudio_stretch_offset_result out;
|
||||
ASSERT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
ref.data(), int(ref.size()), cand.data(), int(cand.size()),
|
||||
nullptr, nullptr, 100, 4, 1.0, 2.0, 0.25, &out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(out.valid, 1);
|
||||
EXPECT_NEAR(out.rate, 2.0, 0.13);
|
||||
EXPECT_GT(out.confidence, 0.9);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateStretchAndOffsetErrorPaths)
|
||||
{
|
||||
double env[4] = { 0.1, 0.2, 0.3, 0.4 };
|
||||
oakaudio_stretch_offset_result out;
|
||||
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
nullptr, 4, env, 4, nullptr, nullptr, 100, 4, 1.0, 2.0, 0.5,
|
||||
&out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// min_rate <= 0
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
env, 4, env, 4, nullptr, nullptr, 100, 4, 0.0, 2.0, 0.5, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// max < min
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
env, 4, env, 4, nullptr, nullptr, 100, 4, 2.0, 1.0, 0.5, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL out
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
env, 4, env, 4, nullptr, nullptr, 100, 4, 1.0, 2.0, 0.5,
|
||||
nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, PlaceBySourceTime)
|
||||
{
|
||||
oakaudio_source_clip ref = {};
|
||||
ref.source_start_time_num = 10; // source clock at 10s
|
||||
ref.source_start_time_den = 1;
|
||||
ref.media_in_num = 2; // clip head is 2s into the media
|
||||
ref.media_in_den = 1;
|
||||
ref.has_source_start_time = 1;
|
||||
|
||||
oakaudio_source_clip cand = {};
|
||||
cand.source_start_time_num = 14; // 4s later on the same source clock
|
||||
cand.source_start_time_den = 1;
|
||||
cand.media_in_num = 0;
|
||||
cand.media_in_den = 1;
|
||||
cand.has_source_start_time = 1;
|
||||
|
||||
int64_t num, den;
|
||||
int valid;
|
||||
ASSERT_EQ(oakaudio_sync_place_by_source_time(&ref, &cand, 5, 1, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 1);
|
||||
// candidate head source = 14+0, reference head source = 10+2 = 12;
|
||||
// timeline_in = 5 + 14 - 12 = 7
|
||||
EXPECT_EQ(num, 7);
|
||||
EXPECT_EQ(den, 1);
|
||||
|
||||
// Missing source start time -> invalid placement, still OAKAUDIO_OK
|
||||
cand.has_source_start_time = 0;
|
||||
ASSERT_EQ(oakaudio_sync_place_by_source_time(&ref, &cand, 5, 1, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 0);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, PlaceBySourceTimeErrorPaths)
|
||||
{
|
||||
oakaudio_source_clip clip = {};
|
||||
clip.source_start_time_den = 1;
|
||||
clip.media_in_den = 1;
|
||||
clip.has_source_start_time = 1;
|
||||
|
||||
int64_t num, den;
|
||||
int valid;
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(nullptr, &clip, 5, 1, &num,
|
||||
&den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Zero denominators
|
||||
oakaudio_source_clip bad = clip;
|
||||
bad.media_in_den = 0;
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(&clip, &bad, 5, 1, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(&clip, &clip, 5, 0, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(&clip, &clip, 5, 1, nullptr,
|
||||
&den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, PlaceByWaveformOffset)
|
||||
{
|
||||
int64_t num, den;
|
||||
int valid;
|
||||
|
||||
// Reference at 5s, candidate is 24000 samples late at 48k -> 5.5s
|
||||
ASSERT_EQ(oakaudio_sync_place_by_waveform_offset(5, 1, 24000, 48000, &num,
|
||||
&den, &valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 1);
|
||||
EXPECT_NEAR(double(num) / double(den), 5.5, 1e-6);
|
||||
|
||||
// Bad sample rate -> invalid placement
|
||||
ASSERT_EQ(oakaudio_sync_place_by_waveform_offset(5, 1, 24000, 0, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 0);
|
||||
|
||||
// Error path: NULL outs / zero denominator
|
||||
EXPECT_EQ(oakaudio_sync_place_by_waveform_offset(5, 0, 24000, 48000, &num,
|
||||
&den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_place_by_waveform_offset(5, 1, 24000, 48000,
|
||||
nullptr, &den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - 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 <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/levelmeter.h"
|
||||
#include "audio/manager.h"
|
||||
#include "audio/waveform.h"
|
||||
#include "codec/decoder.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct WaveformHandle {
|
||||
OakAudioWaveform h = oakaudio_waveform_init();
|
||||
~WaveformHandle() { oakaudio_waveform_free(&h); }
|
||||
};
|
||||
|
||||
std::string demo_file()
|
||||
{
|
||||
return std::string(OAKAUDIO_TEST_DATA_DIR) + "/demo.mp4";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioWaveform, InitFree)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_NE(w.h.ctx, nullptr);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before + 1);
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(w.h), 0);
|
||||
}
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
|
||||
oakaudio_waveform_free(nullptr);
|
||||
OakAudioWaveform empty = {};
|
||||
oakaudio_waveform_free(&empty);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ChannelCountAndLength)
|
||||
{
|
||||
WaveformHandle w;
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(w.h, 2), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(w.h), 2);
|
||||
|
||||
int64_t num = -1, den = -1;
|
||||
EXPECT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_EQ(num, 0);
|
||||
|
||||
// Error paths
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(empty, 2), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(w.h, -1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_length(empty, &num, &den), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_length(w.h, nullptr, &den), OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSamplesAndSummary)
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
|
||||
// One second at 48k: first half +0.5, second half -0.5
|
||||
std::vector<float> data(48000);
|
||||
for (int i = 0; i < 48000; i++) {
|
||||
data[size_t(i)] = (i < 24000) ? 0.5f : -0.5f;
|
||||
}
|
||||
const float *planes[1] = { data.data() };
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 48000, 48000, 0,
|
||||
1),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Summary of the whole second: min -0.5, max +0.5
|
||||
oakaudio_min_max pairs[2];
|
||||
const int points =
|
||||
oakaudio_waveform_get_summary(w.h, 0, 1, 1, 1, pairs, 2);
|
||||
ASSERT_EQ(points, 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, -0.5f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.5f);
|
||||
|
||||
// Summary of the first half only: all +0.5
|
||||
const int first_half =
|
||||
oakaudio_waveform_get_summary(w.h, 0, 1, 1, 2, pairs, 2);
|
||||
ASSERT_EQ(first_half, 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, 0.5f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.5f);
|
||||
|
||||
// Query mode: NULL out returns the point count
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 1, 1, nullptr, 0), 1);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSamplesErrorPaths)
|
||||
{
|
||||
WaveformHandle w;
|
||||
float v = 0.0f;
|
||||
const float *planes[1] = { &v };
|
||||
|
||||
// Channel count not set
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 16, 48000, 0, 1),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
// Zero denominator
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 16, 48000, 0, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL planes / bad counts
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, nullptr, 16, 48000, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 0, 48000, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(empty, planes, 16, 48000, 0,
|
||||
1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSilenceAndTrim)
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
|
||||
// 2 seconds of silence
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(w.h, 0, 1, 2, 1), OAKAUDIO_OK);
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 2.0, 1e-9);
|
||||
|
||||
oakaudio_min_max pairs[1];
|
||||
ASSERT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 2, 1, pairs, 1), 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, 0.0f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.0f);
|
||||
|
||||
// Trim away the first second
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(w.h, 1, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Resize to half a second
|
||||
EXPECT_EQ(oakaudio_waveform_resize(w.h, 1, 2), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 0.5, 1e-9);
|
||||
|
||||
// trim_range: in 0, length 1s
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(w.h, 0, 1, 1, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(w.h, 0, 0, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(w.h, 1, 0), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_resize(w.h, 1, 0), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(w.h, 0, 1, 1, 0), OAKAUDIO_E_INVALID);
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(empty, 1, 1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_resize(empty, 1, 1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(empty, 0, 1, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(empty, 0, 1, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(empty, 0, 1, 1, 1, pairs, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 1, 0, pairs, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSums)
|
||||
{
|
||||
WaveformHandle src, dst;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(src.h, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(dst.h, 1), OAKAUDIO_OK);
|
||||
|
||||
std::vector<float> data(48000, 0.25f);
|
||||
const float *planes[1] = { data.data() };
|
||||
ASSERT_EQ(oakaudio_waveform_overwrite_samples(src.h, planes, 48000, 48000,
|
||||
0, 1),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
// Copy all of src into dst at t=0
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, src.h, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_OK);
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(dst.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
oakaudio_min_max pairs[1];
|
||||
ASSERT_EQ(oakaudio_waveform_get_summary(dst.h, 0, 1, 1, 1, pairs, 1), 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.25f);
|
||||
|
||||
// Error paths
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, empty, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(empty, src.h, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, src.h, 0, 0, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, SumSamplesStatic)
|
||||
{
|
||||
std::vector<float> ch0 = { 0.1f, -0.4f, 0.3f, 0.2f };
|
||||
std::vector<float> ch1 = { -0.9f, 0.5f, 0.1f, 0.0f };
|
||||
const float *planes[2] = { ch0.data(), ch1.data() };
|
||||
oakaudio_min_max out[2];
|
||||
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 4, out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_FLOAT_EQ(out[0].min, -0.4f);
|
||||
EXPECT_FLOAT_EQ(out[0].max, 0.3f);
|
||||
EXPECT_FLOAT_EQ(out[1].min, -0.9f);
|
||||
EXPECT_FLOAT_EQ(out[1].max, 0.5f);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(nullptr, 2, 0, 4, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 0, 0, 4, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 0, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 4, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ReSumStatic)
|
||||
{
|
||||
oakaudio_min_max in[4] = { { -0.5f, 0.4f }, { -0.2f, 0.9f },
|
||||
{ -0.7f, 0.1f }, { 0.0f, 0.3f } };
|
||||
oakaudio_min_max out[2];
|
||||
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 2, out), OAKAUDIO_OK);
|
||||
EXPECT_FLOAT_EQ(out[0].min, -0.7f);
|
||||
EXPECT_FLOAT_EQ(out[0].max, 0.4f);
|
||||
EXPECT_FLOAT_EQ(out[1].min, -0.2f);
|
||||
EXPECT_FLOAT_EQ(out[1].max, 0.9f);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(nullptr, 4, 2, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 0, 2, out), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 0, out), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 2, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ExtractFromMediaFile)
|
||||
{
|
||||
const std::string file = demo_file();
|
||||
|
||||
// Probe the expected duration independently
|
||||
OakDecoder probe = oakcodec_decoder_probe(file.c_str());
|
||||
ASSERT_NE(probe.ctx, nullptr) << "demo.mp4 not decodable";
|
||||
oakcodec_audio_stream_info info;
|
||||
ASSERT_EQ(oakcodec_decoder_probe_get_audio_stream(probe, 0, &info),
|
||||
OAKCODEC_OK);
|
||||
// The audio stream's duration_ts is not populated for this file; the
|
||||
// video stream carries the clip duration.
|
||||
oakcodec_video_stream_info vinfo;
|
||||
double duration = 0.0;
|
||||
if (oakcodec_decoder_probe_get_video_stream(probe, 0, &vinfo) ==
|
||||
OAKCODEC_OK &&
|
||||
vinfo.time_base_den > 0) {
|
||||
duration = double(vinfo.duration_ts) * vinfo.time_base_num /
|
||||
vinfo.time_base_den;
|
||||
}
|
||||
oakcodec_decoder_free(&probe);
|
||||
ASSERT_GT(duration, 0.0);
|
||||
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
|
||||
constexpr int kSamplesPerPoint = 1024;
|
||||
// Two-stage sizing: NULL out returns the required point count
|
||||
const int required = oakaudio_waveform_extract(
|
||||
file.c_str(), 0, kSamplesPerPoint, nullptr, 0, nullptr);
|
||||
ASSERT_GT(required, 0);
|
||||
|
||||
std::vector<oakaudio_min_max> pairs(size_t(required) * 4);
|
||||
int channels = 0;
|
||||
const int points =
|
||||
oakaudio_waveform_extract(file.c_str(), 0, kSamplesPerPoint,
|
||||
pairs.data(), int(pairs.size()), &channels);
|
||||
ASSERT_EQ(points, required);
|
||||
EXPECT_EQ(channels, info.channel_count);
|
||||
|
||||
// Length consistency with the stream duration (generous tolerance for
|
||||
// container/decoder rounding)
|
||||
const double covered =
|
||||
double(points) * kSamplesPerPoint / info.sample_rate;
|
||||
EXPECT_NEAR(covered, duration, std::max(0.5, duration * 0.1));
|
||||
|
||||
// Non-trivial content: at least one point carries signal
|
||||
bool any_signal = false;
|
||||
for (int i = 0; i < points * channels; i++) {
|
||||
if (pairs[size_t(i)].max > 0.0f || pairs[size_t(i)].min < 0.0f) {
|
||||
any_signal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(any_signal);
|
||||
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ExtractErrorPaths)
|
||||
{
|
||||
int channels = 0;
|
||||
oakaudio_min_max pairs[8];
|
||||
|
||||
// Nonexistent file
|
||||
EXPECT_EQ(oakaudio_waveform_extract("/nonexistent/file.mp4", 0, 1024,
|
||||
pairs, 8, &channels),
|
||||
OAKAUDIO_E_NOT_FOUND);
|
||||
// NULL filename
|
||||
EXPECT_EQ(oakaudio_waveform_extract(nullptr, 0, 1024, pairs, 8, &channels),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Bad stream index
|
||||
EXPECT_EQ(oakaudio_waveform_extract(demo_file().c_str(), 99, 1024, pairs, 8,
|
||||
&channels),
|
||||
OAKAUDIO_E_NOT_FOUND);
|
||||
// Bad samples-per-point
|
||||
EXPECT_EQ(oakaudio_waveform_extract(demo_file().c_str(), 0, 0, pairs, 8,
|
||||
&channels),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
Reference in New Issue
Block a user