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:
2026-08-06 20:53:07 +08:00
parent 3d004c081b
commit 354df2e194
71 changed files with 8142 additions and 101 deletions
+24
View File
@@ -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
)
+49
View File
@@ -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);
}
+84
View File
@@ -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;
}
+280
View File
@@ -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));
}
+148
View File
@@ -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;
}
+124
View File
@@ -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
+206
View File
@@ -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;
}
+554
View File
@@ -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;
}