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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user