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
+59
View File
@@ -0,0 +1,59 @@
/***
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 OAK_EDITOR_AUDIO_ERROR_H
#define OAK_EDITOR_AUDIO_ERROR_H
/**
* @brief Current ABI version stamped into every oakaudio handle.
*
* Bump whenever a handle layout or the semantics of any exported function
* change incompatibly. Consumers should compare a handle's abi_version
* field against the value they were compiled with before dereferencing
* ctx.
*/
#define OAKAUDIO_ABI_VERSION 1
#if defined(_WIN32)
#if defined(OAKAUDIO_BUILD)
#define OAKAUDIO_API __declspec(dllexport)
#else
#define OAKAUDIO_API __declspec(dllimport)
#endif
#else
#define OAKAUDIO_API __attribute__((visibility("default")))
#endif
/**
* @brief Status and error codes shared by all oakaudio C API families.
*
* Return-code convention (mirrors engine/include/oakengine/init.h):
* 0 (OAKAUDIO_OK) on success, a negative OAKAUDIO_E_* error code on
* failure. String getters return the required buffer size in bytes
* (including the terminating NUL) as a non-negative value instead.
*/
#define OAKAUDIO_OK 0 /**< Success. */
#define OAKAUDIO_E_INVALID (-1) /**< NULL handle or invalid argument. */
#define OAKAUDIO_E_STATE (-2) /**< Call not valid in the current state. */
#define OAKAUDIO_E_FAILED (-3) /**< The underlying operation failed. */
#define OAKAUDIO_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
#define OAKAUDIO_E_NOMEM (-5) /**< Allocation failed. */
#endif //OAK_EDITOR_AUDIO_ERROR_H
+73
View File
@@ -0,0 +1,73 @@
/***
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 OAK_EDITOR_AUDIO_LEVELMETER_H
#define OAK_EDITOR_AUDIO_LEVELMETER_H
#include "error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file levelmeter.h
* @brief C ABI for the oakaudio level meter (olive::AudioLevelMeter):
* stateless peak/RMS/VU/LUFS analysis of planar float audio.
*/
/** Per-channel analysis results. dB fields floor at -200. */
typedef struct oakaudio_channel_stats {
double peak_linear;
double peak_db;
double rms_linear;
double rms_db;
double vu_db;
} oakaudio_channel_stats;
/** Buffer-wide summary. */
typedef struct oakaudio_meter_stats {
double max_peak_linear;
double integrated_lufs; /**< BS.1770-compatible unit (no K-weighting). */
int silence; /**< 1 when the buffer is (near-)silent. */
} oakaudio_meter_stats;
/**
* @brief Analyze a planar float buffer.
*
* @param planar Per-channel float planes.
* @param channel_count Number of channels (> 0).
* @param frame_count Frames per channel (>= 0).
* @param channels Receives per-channel stats; may be NULL.
* @param channels_capacity Capacity of `channels` (must be >=
* channel_count when channels is non-NULL).
* @param summary Receives the buffer-wide summary; may be NULL.
* @return OAKAUDIO_OK or OAKAUDIO_E_INVALID.
*/
OAKAUDIO_API 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);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_AUDIO_LEVELMETER_H
+176
View File
@@ -0,0 +1,176 @@
/***
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 OAK_EDITOR_AUDIO_MANAGER_H
#define OAK_EDITOR_AUDIO_MANAGER_H
#include <stdint.h>
#include "codec/encoder.h"
#include "error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file manager.h
* @brief C ABI for the oakaudio PortAudio output/input manager
* (olive::AudioManager singleton).
*
* OakAudioManager uses the standard handle layout (see oakcommon's
* common/handle.h) but with singleton semantics: ctx points to the
* process-wide instance created by oakaudio_manager_create_instance(), so
* addref() and release() are intentionally no-ops and never destroy
* anything (mirrors oakcommon's OakCurrent). abi_version is always
* OAKAUDIO_ABI_VERSION.
*
* Device indices are PortAudio PaDeviceIndex values (-1 = paNoDevice).
* Sample formats are olive::core::SampleFormat::Format values.
*/
typedef struct OakAudioManager {
void *ctx; /**< Opaque pointer to the singleton object. */
void (*addref)(void *ctx); /**< No-op (singleton). */
void (*release)(void *ctx); /**< No-op (singleton). */
uint32_t abi_version; /**< OAKAUDIO_ABI_VERSION. */
} OakAudioManager;
/**
* @brief Create the process-wide AudioManager (no-op when it exists).
*
* Initializes PortAudio and picks the configured/default devices.
*
* @return OAKAUDIO_OK or OAKAUDIO_E_NOMEM.
*/
OAKAUDIO_API int oakaudio_manager_create_instance(void);
/**
* @brief Destroy the process-wide AudioManager (no-op when absent).
*/
OAKAUDIO_API void oakaudio_manager_destroy_instance(void);
/**
* @brief Return a handle to the process-wide AudioManager.
*
* The returned handle is borrowed; addref/release are no-ops. When no
* instance exists the handle is empty (ctx == NULL) and all functions
* report OAKAUDIO_E_STATE.
*/
OAKAUDIO_API OakAudioManager oakaudio_manager_instance(void);
/**
* @brief Release a manager handle. No-op (singleton), safe on NULL/empty.
*/
OAKAUDIO_API void oakaudio_manager_free(OakAudioManager *self);
/**
* @brief Bytes between output-notify pulses (0 disables).
*/
OAKAUDIO_API int oakaudio_manager_set_output_notify_interval(
OakAudioManager self, int64_t bytes);
/**
* @brief Push a block of samples to the output device, opening/restarting
* the stream when the params changed.
*
* @param rate/layout/format Stream params (ffmpeg-style layout mask,
* SampleFormat::Format int).
* @param samples Packed samples in the given format.
* @param samples_size Byte count of `samples`.
* @param error_buf/error_buf_size Optional human-readable failure detail.
* @return OAKAUDIO_OK, OAKAUDIO_E_INVALID, OAKAUDIO_E_STATE (no output
* device), or OAKAUDIO_E_FAILED (PortAudio error, see error_buf).
*/
OAKAUDIO_API 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);
OAKAUDIO_API int oakaudio_manager_clear_buffered_output(OakAudioManager self);
OAKAUDIO_API int oakaudio_manager_stop_output(OakAudioManager self);
/**
* @brief Seconds of audio consumed by the output device since the last
* reset, compensated for output latency; negative when no stream
* is running.
*/
OAKAUDIO_API int oakaudio_manager_seconds(OakAudioManager self, double *out);
OAKAUDIO_API int oakaudio_manager_reset_output_clock(OakAudioManager self);
/**
* @brief Current output device index, paNoDevice (-1), or a negative
* OAKAUDIO_E_* code.
*/
OAKAUDIO_API int oakaudio_manager_get_output_device(OakAudioManager self);
OAKAUDIO_API int oakaudio_manager_set_output_device(OakAudioManager self,
int device);
OAKAUDIO_API int oakaudio_manager_get_input_device(OakAudioManager self);
OAKAUDIO_API int oakaudio_manager_set_input_device(OakAudioManager self,
int device);
/**
* @brief Close the output stream and re-initialize PortAudio.
*/
OAKAUDIO_API int oakaudio_manager_hard_reset(OakAudioManager self);
/**
* @brief Start recording the input device to a file via the oakcodec
* encoder C ABI.
*
* `params` must describe an audio-enabled encoding; the input stream is
* always captured as interleaved 32-bit float (the only format the
* oakcodec encoder write path accepts).
*
* @return OAKAUDIO_OK, OAKAUDIO_E_STATE (no input device), or
* OAKAUDIO_E_FAILED (see error_buf).
*/
OAKAUDIO_API int oakaudio_manager_start_recording(OakAudioManager self,
const oakcodec_encoding_params *params,
char *error_buf, int error_buf_size);
OAKAUDIO_API int oakaudio_manager_stop_recording(OakAudioManager self);
/**
* @brief Device index named by the configuration ("AudioOutput" /
* "AudioInput"), or the default device when unset/unmatched.
* Static: valid without an instance (PortAudio must be initialized
* by an instance first; returns paNoDevice otherwise).
*/
OAKAUDIO_API int oakaudio_manager_find_config_device_by_name_s(
int is_output_device);
/**
* @brief Device index whose name matches `name` exactly (empty name
* matches nothing, falls through to the default device).
*/
OAKAUDIO_API int oakaudio_manager_find_device_by_name_s(const char *name,
int is_output_device);
/**
* @brief Number of live oakaudio reference-counted objects (leak check).
*/
OAKAUDIO_API int oakaudio_debug_alive_count(void);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_AUDIO_MANAGER_H
+131
View File
@@ -0,0 +1,131 @@
/***
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 OAK_EDITOR_AUDIO_PROCESSOR_H
#define OAK_EDITOR_AUDIO_PROCESSOR_H
#include <stdint.h>
#include "error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file processor.h
* @brief C ABI for the oakaudio real-time resampler/format converter
* (olive::AudioProcessor).
*
* OakAudioProcessor follows the neutral by-value handle convention (see
* oakcommon's common/handle.h): oakaudio_processor_init() returns a handle
* whose underlying object has reference count 1, the addref and release
* function pointers adjust that count atomically (release destroys the
* object at zero), and abi_version is always OAKAUDIO_ABI_VERSION.
* Functions that only use a handle take it BY VALUE; an empty handle
* (ctx == NULL) is reported as OAKAUDIO_E_INVALID.
*
* Sample formats are passed as ints matching the
* olive::core::SampleFormat::Format enum values (invalid = -1, u8_p = 0,
* s16_p, s32_p, s64_p, f32_p, f64_p, u8, s16, s32, s64, f32, f64,
* count). Channel layouts are ffmpeg-style channel masks.
*/
typedef struct OakAudioProcessor {
void *ctx; /**< Opaque pointer to the reference-counted object. */
void (*addref)(void *ctx); /**< Atomically increments the count. */
void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */
uint32_t abi_version; /**< OAKAUDIO_ABI_VERSION. */
} OakAudioProcessor;
/** oakaudio_processor_convert() delivers planar 32-bit float output. */
#define OAKAUDIO_PROCESSOR_OUTPUT_FORMAT 4 /**< SampleFormat::f32_p. */
/**
* @brief Create a closed audio processor (count 1).
*
* @return Handle with reference count 1; ctx is NULL on allocation
* failure.
*/
OAKAUDIO_API OakAudioProcessor oakaudio_processor_init(void);
/**
* @brief Release one reference to a processor.
*
* Convenience wrapper around self->release(self->ctx); nulls self->ctx.
* No-op when self is NULL or self->ctx is NULL.
*/
OAKAUDIO_API void oakaudio_processor_free(OakAudioProcessor *self);
/**
* @brief Open the resampling/format-conversion graph.
*
* out_format is accepted for interface completeness but the conversion
* output is always planar 32-bit float (see
* OAKAUDIO_PROCESSOR_OUTPUT_FORMAT); passing any other format returns
* OAKAUDIO_E_INVALID. A channel layout mask of 0 falls back to the
* default layout for the channel count (stereo when unknown), matching
* the C++ implementation.
*
* @param speed Tempo factor (1.0 = unchanged).
* @return OAKAUDIO_OK, OAKAUDIO_E_STATE when already open,
* OAKAUDIO_E_INVALID for bad arguments, or OAKAUDIO_E_FAILED when
* the filter graph could not be created.
*/
OAKAUDIO_API 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);
/**
* @brief Close the graph (safe when closed; self must be non-empty).
*/
OAKAUDIO_API int oakaudio_processor_close(OakAudioProcessor self);
/**
* @brief 1 when open, 0 when closed, OAKAUDIO_E_INVALID for empty handle.
*/
OAKAUDIO_API int oakaudio_processor_is_open(OakAudioProcessor self);
/**
* @brief Push planar float input and pull converted output.
*
* @param in_planar Per-channel float input planes (in channel count);
* NULL with in_frame_count == 0 only pulls pending output.
* @param in_frame_count Frames per input channel.
* @param out_planar Per-channel float output planes (out channel count);
* NULL to discard/pull nothing (returns 0).
* @param out_capacity_frames Capacity of each output plane in frames.
* @return Number of output frames written (>= 0), or a negative
* OAKAUDIO_E_* code. Output is clamped to out_capacity_frames;
* remaining frames stay queued in the graph.
*/
OAKAUDIO_API int oakaudio_processor_convert(OakAudioProcessor self,
const float *const *in_planar, int in_frame_count,
float *const *out_planar, int out_capacity_frames);
/**
* @brief Signal end-of-input to the graph (flushes internal delay).
*/
OAKAUDIO_API int oakaudio_processor_flush(OakAudioProcessor self);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_AUDIO_PROCESSOR_H
+132
View File
@@ -0,0 +1,132 @@
/***
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 OAK_EDITOR_AUDIO_SYNC_H
#define OAK_EDITOR_AUDIO_SYNC_H
#include <stdint.h>
#include "error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file sync.h
* @brief C ABI for the oakaudio synchronization helpers
* (olive::AudioSynchronizer and olive::AudioWaveformSync):
* stateless source-time placement and envelope-correlation offset
* estimation.
*/
/** Result of an offset estimation. */
typedef struct oakaudio_offset_result {
int64_t offset_samples;
double confidence; /**< 0..1 correlation score. */
int valid; /**< 1 when an estimate was found. */
} oakaudio_offset_result;
/** Result of a stretch-plus-offset estimation. */
typedef struct oakaudio_stretch_offset_result {
double rate; /**< Playback rate aligning the candidate (> 1 = speed up). */
int64_t offset_samples;
double confidence;
int valid;
} oakaudio_stretch_offset_result;
/**
* @brief Per-window RMS envelope of a planar float buffer (static).
*
* @return Number of envelope windows (>= 0) or a negative OAKAUDIO_E_*
* code. When out is NULL or too small, the required window count
* is returned and nothing is written.
*/
OAKAUDIO_API int oakaudio_sync_extract_rms_envelope(
const float *const *planar, int channel_count, int frame_count,
uint64_t window_samples, double *out, int capacity);
/**
* @brief Estimate the candidate's offset against the reference by
* normalized cross-correlation of RMS envelopes.
*
* @param reference_valid/candidate_valid Optional per-window validity
* masks (NULL = all windows valid; when non-NULL the length must
* match the corresponding envelope length).
*/
OAKAUDIO_API 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);
/**
* @brief Estimate a playback-rate change plus offset aligning the
* candidate to the reference.
*
* The candidate envelope is resampled at each rate in
* [min_rate, max_rate] (step rate_step) and correlated against the
* reference. O(rates * lags * overlap); bound max_offset_windows.
*/
OAKAUDIO_API 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);
/** One clip's source-time metadata (rational seconds). */
typedef struct oakaudio_source_clip {
int64_t source_start_time_num;
int64_t source_start_time_den;
int64_t media_in_num;
int64_t media_in_den;
int has_source_start_time;
} oakaudio_source_clip;
/**
* @brief Place the candidate on the timeline so its source time aligns
* with the reference clip.
*
* @param reference_timeline_in_num/den Reference clip's timeline in point.
* @param out_num/out_den Receive the candidate's timeline in point.
* @param out_valid Receives 1 when placement succeeded.
*/
OAKAUDIO_API 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);
/**
* @brief Timeline placement from a measured waveform offset.
*/
OAKAUDIO_API 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);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_AUDIO_SYNC_H
+179
View File
@@ -0,0 +1,179 @@
/***
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 OAK_EDITOR_AUDIO_WAVEFORM_H
#define OAK_EDITOR_AUDIO_WAVEFORM_H
#include <stdint.h>
#include "error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file waveform.h
* @brief C ABI for the oakaudio visual waveform store
* (olive::AudioVisualWaveform) and whole-file waveform extraction.
*
* OakAudioWaveform follows the neutral by-value handle convention (see
* oakcommon's common/handle.h). Times are rationals as (num, den) pairs
* of int64_t in seconds; den must be non-zero.
*
* Summaries are stored as channel-interleaved min/max pairs: point p of
* channel c lives at pairs[p * channel_count + c]. This matches the
* on-disk/cache layout of the engine's waveform data (min/max float
* pairs), so the extraction output is drop-in compatible.
*/
/** One summarized waveform point of one channel. */
typedef struct oakaudio_min_max {
float min;
float max;
} oakaudio_min_max;
typedef struct OakAudioWaveform {
void *ctx; /**< Opaque pointer to the reference-counted object. */
void (*addref)(void *ctx); /**< Atomically increments the count. */
void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */
uint32_t abi_version; /**< OAKAUDIO_ABI_VERSION. */
} OakAudioWaveform;
/**
* @brief Create an empty waveform (count 1, channel count 0).
*/
OAKAUDIO_API OakAudioWaveform oakaudio_waveform_init(void);
/**
* @brief Release one reference. No-op on NULL/empty handle.
*/
OAKAUDIO_API void oakaudio_waveform_free(OakAudioWaveform *self);
/**
* @brief Channel count, or a negative OAKAUDIO_E_* code.
*/
OAKAUDIO_API int oakaudio_waveform_get_channel_count(OakAudioWaveform self);
OAKAUDIO_API int oakaudio_waveform_set_channel_count(OakAudioWaveform self,
int channels);
/**
* @brief Waveform length in seconds as a rational pair.
*/
OAKAUDIO_API int oakaudio_waveform_length(OakAudioWaveform self,
int64_t *num, int64_t *den);
/**
* @brief Write planar float samples into the waveform at `start` seconds,
* expanding it if necessary.
*
* @param planar Per-channel float planes; channel count is taken from the
* waveform (set it first with oakaudio_waveform_set_channel_count).
*/
OAKAUDIO_API 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);
/**
* @brief Copy summarized data from another waveform over this one.
*
* @param dest_num/dest_den Where in `self` the sums start being written.
* @param offset_num/offset_den Where in `src` reading starts.
* @param length_num/length_den Maximum amount to copy; 0/1 = all of src.
*/
OAKAUDIO_API 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);
OAKAUDIO_API int oakaudio_waveform_overwrite_silence(OakAudioWaveform self,
int64_t start_num, int64_t start_den,
int64_t length_num, int64_t length_den);
/**
* @brief Drop `length` seconds from the front (negative prepends silence).
*/
OAKAUDIO_API int oakaudio_waveform_trim_in(OakAudioWaveform self,
int64_t length_num, int64_t length_den);
OAKAUDIO_API int oakaudio_waveform_resize(OakAudioWaveform self,
int64_t length_num, int64_t length_den);
OAKAUDIO_API int oakaudio_waveform_trim_range(OakAudioWaveform self,
int64_t in_num, int64_t in_den,
int64_t length_num, int64_t length_den);
/**
* @brief Summarized min/max pairs covering [start, start+length).
*
* @param out_pairs Receives points * channel_count channel-interleaved
* pairs; may be NULL to query the point count.
* @param capacity_points Capacity of out_pairs in points.
* @return Number of points (>= 0), or a negative OAKAUDIO_E_* code.
* When out_pairs is NULL or too small the required count is
* returned and nothing is written.
*/
OAKAUDIO_API 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);
/**
* @brief Min/max of `length` samples starting at `start_index` for every
* channel (static, no handle).
*/
OAKAUDIO_API int oakaudio_waveform_sum_samples_s(const float *const *planar,
int channel_count, int start_index, int length,
oakaudio_min_max *out);
/**
* @brief Re-summarize channel-interleaved pairs into one point per
* channel (static, no handle).
*/
OAKAUDIO_API int oakaudio_waveform_re_sum_s(const oakaudio_min_max *in,
int nb_entries, int nb_channels, oakaudio_min_max *out);
/**
* @brief Extract a whole-file waveform summary from a media file through
* the oakcodec decoder C ABI.
*
* Decodes `filename`'s audio stream `stream_index` (index within the
* file's audio stream list) and reduces it to channel-interleaved
* min/max pairs, one point per `samples_per_point` source samples.
*
* @param out_pairs Receives the pairs; may be NULL to query the size.
* @param capacity_points Capacity of out_pairs in points.
* @param out_channel_count Receives the channel count (may be NULL).
* @return Number of points (>= 0); when out_pairs is NULL or too small,
* the required count is returned and nothing is written.
* Negative OAKAUDIO_E_* code on failure
* (OAKAUDIO_E_NOT_FOUND when the file/stream does not exist).
*/
OAKAUDIO_API 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);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_AUDIO_WAVEFORM_H
+189
View File
@@ -0,0 +1,189 @@
/***
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 OAK_EDITOR_COMMON_CONFIG_H
#define OAK_EDITOR_COMMON_CONFIG_H
#include <stdint.h>
#include "common/error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief De-Qt application configuration store, C ABI
* (M1-oakcommon.md §2.1, extended for the real consumer surface)
*
* oakcommon_config is a process-wide singleton key/value store (the de-Qt
* replacement for engine/config/config.h's QSettings/QVariant wrapper).
* Per the config-wave ruling it is NOT wrapped in the refcounted-handle
* convention of common/handle.h: there is exactly one store per process,
* so the family is a plain set of functions over that singleton (same
* singleton precedent as OakCurrent).
*
* Keys follow the frozen (group, key) convention of §2.1 and keep the
* QSettings INI shape: pass group == NULL (or "") for a top-level key,
* otherwise the entry is stored under an INI [group] section and
* addressed as "group/key" internally.
*
* Values are typed (string / int64 / double / bool). Rational settings
* are stored as strings in the "num/den" form used by
* oakcore_rational_to_string(). Typed getters take a fallback which is
* returned when the key is absent or has a different type (§2.1 special
* convention: they return values, not error codes).
*
* Persistence is an INI file at
* <FileFunctions::get_configuration_location()>/config.ini. The store
* starts up with compiled-in defaults; oakcommon_config_load() re-reads
* the file (a missing file is not an error) and oakcommon_config_save()
* writes it. The OAK_CONFIG_DIR environment override honored by
* get_configuration_location() also redirects this file (tests/tooling).
*
* NOTE (behavior change): the old Qt implementation persisted to
* config.xml (engine XML) — and on macOS QSettings used a plist — so
* previously saved settings do NOT carry over; the first run starts from
* the compiled-in defaults.
*/
typedef enum OakCommonConfigEntryType {
OAKCOMMON_CONFIG_ENTRY_NONE = 0, /**< No entry / null type. */
OAKCOMMON_CONFIG_ENTRY_STRING = 1,
OAKCOMMON_CONFIG_ENTRY_INT = 2,
OAKCOMMON_CONFIG_ENTRY_DOUBLE = 3,
OAKCOMMON_CONFIG_ENTRY_BOOL = 4
} OakCommonConfigEntryType;
/**
* @brief Handler for configuration errors that should be shown to the user
*
* The engine layer cannot show dialogs itself. The UI registers a handler
* (e.g. QMessageBox-based) at startup; without one, errors go to stderr.
* Same injection pattern as the codec task-submit callback.
*/
typedef void (*OakCommonConfigErrorHandler)(const char *title,
const char *message,
void *userdata);
/**
* @brief Resets the store to compiled-in defaults and loads config.ini
*
* A missing file leaves the defaults in place and returns OAKCOMMON_OK.
* Malformed lines are skipped. An unreadable existing file is reported
* through the error handler and returns OAKCOMMON_E_FAILED.
*/
int oakcommon_config_load(void);
/**
* @brief Writes the current store to config.ini (via a temp file + rename)
*
* On failure the error handler is invoked and OAKCOMMON_E_FAILED is
* returned.
*/
int oakcommon_config_save(void);
/**
* @brief Resets the store to compiled-in defaults (drops custom keys)
*/
int oakcommon_config_reset_defaults(void);
/**
* @brief Sets a string entry (§2.1)
*
* A new key is created as OAKCOMMON_CONFIG_ENTRY_STRING. Setting an
* existing typed (INT/DOUBLE/BOOL) entry parses the string into its
* declared type; an unparseable value returns OAKCOMMON_E_STATE and
* leaves the entry unchanged.
*/
void oakcommon_config_set(const char *group, const char *key,
const char *value_utf8);
/**
* @brief Reads an entry as a string, two-stage buffer (§2.1)
*
* Numeric/bool entries are formatted (bools as "true"/"false", doubles
* with %g).
*
* @return Required buffer size in bytes (including the terminating NUL),
* or a negative OAKCOMMON_E_* error code (OAKCOMMON_E_NOT_FOUND when the
* key is absent).
*/
int oakcommon_config_get(const char *group, const char *key, char *buf,
int buf_size);
/**
* @brief Reads an INT entry as int (§2.1)
*
* @return The stored value, or `fallback` when the key is absent or has
* a different type.
*/
int oakcommon_config_get_int(const char *group, const char *key,
int fallback);
/**
* @brief Reads a DOUBLE entry (§2.1), fallback semantics as get_int
*/
double oakcommon_config_get_double(const char *group, const char *key,
double fallback);
/**
* @brief Sets an INT entry (32-bit, §2.1)
*/
void oakcommon_config_set_int(const char *group, const char *key, int v);
/**
* @brief INT entry as int64 (extension for channel-layout style values)
*/
int64_t oakcommon_config_get_int64(const char *group, const char *key,
int64_t fallback);
void oakcommon_config_set_int64(const char *group, const char *key,
int64_t v);
/**
* @brief BOOL entry as int 0/1 (extension), fallback semantics as get_int
*/
int oakcommon_config_get_bool(const char *group, const char *key,
int fallback);
void oakcommon_config_set_bool(const char *group, const char *key, int v);
/**
* @brief Sets a DOUBLE entry (extension)
*/
void oakcommon_config_set_double(const char *group, const char *key,
double v);
/**
* @brief Returns the OakCommonConfigEntryType of a key, or a negative
* OAKCOMMON_E_* error (OAKCOMMON_E_NOT_FOUND when the key is absent)
*/
int oakcommon_config_entry_type(const char *group, const char *key);
/**
* @brief Registers (or clears, with NULL) the error handler
*/
int oakcommon_config_set_error_handler(OakCommonConfigErrorHandler handler,
void *userdata);
#ifdef __cplusplus
}
#endif
#endif // OAK_EDITOR_COMMON_CONFIG_H
+1
View File
@@ -3,3 +3,4 @@ add_subdirectory(undo)
add_subdirectory(node)
add_subdirectory(render)
add_subdirectory(codec)
add_subdirectory(audio)
+6
View File
@@ -0,0 +1,6 @@
add_subdirectory(src)
add_subdirectory(c_api)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
+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;
}
+71
View File
@@ -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}
)
+117
View File
@@ -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);
}
}
+57
View File
@@ -0,0 +1,57 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOLEVELMETER_H
#define OAK_AUDIOLEVELMETER_H
#include <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
+523
View File
@@ -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 &params,
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 &params,
std::string *error_str)
{
if (input_device_ == paNoDevice) {
return false;
}
input_encoder_ = oakcodec_encoder_init(&params);
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 &params,
PaDeviceIndex device)
{
PaStreamParameters p;
p.channelCount = params.channel_count();
p.device = device;
p.hostApiSpecificStreamInfo = nullptr;
p.sampleFormat = get_port_audio_sample_format(params.format());
if (device >= 0 && device < Pa_GetDeviceCount()) {
p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency;
} else {
p.suggestedLatency = 0;
}
return p;
}
AudioManager::AudioManager()
: output_stream_(nullptr)
, input_stream_(nullptr)
{
input_encoder_.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();
}
}
+142
View File
@@ -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 &params, 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 &params,
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
+218
View File
@@ -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 &params)
{
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);
}
}
}
+80
View File
@@ -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
+65
View File
@@ -0,0 +1,65 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "audiosynchronizer.h"
namespace olive
{
AudioSynchronizer::Placement AudioSynchronizer::place_by_source_time(
const SourceClip &reference, const SourceClip &candidate,
const core::Rational &reference_timeline_in)
{
Placement placement;
if (!reference.has_source_start_time || !candidate.has_source_start_time ||
reference.source_start_time.isNaN() ||
candidate.source_start_time.isNaN()) {
return placement;
}
const core::Rational reference_head_source =
reference.source_start_time + reference.media_in;
const core::Rational candidate_head_source =
candidate.source_start_time + candidate.media_in;
placement.timeline_in =
reference_timeline_in + candidate_head_source - reference_head_source;
placement.valid = !placement.timeline_in.isNaN();
return placement;
}
AudioSynchronizer::Placement AudioSynchronizer::place_by_waveform_offset(
const core::Rational &reference_timeline_in,
int64_t candidate_offset_samples, int sample_rate)
{
Placement placement;
if (sample_rate <= 0) {
return placement;
}
placement.timeline_in = reference_timeline_in +
core::Rational::from_double(
static_cast<double>(candidate_offset_samples) /
static_cast<double>(sample_rate));
placement.valid = !placement.timeline_in.isNaN();
return placement;
}
}
+55
View File
@@ -0,0 +1,55 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_AUDIOSYNCHRONIZER_H
#define OAK_AUDIOSYNCHRONIZER_H
#include <cstdint>
#include "olive/core/util/rational.h"
namespace olive
{
class AudioSynchronizer {
public:
struct SourceClip {
core::Rational source_start_time;
core::Rational media_in;
bool has_source_start_time = false;
};
struct Placement {
core::Rational timeline_in;
bool valid = false;
};
static Placement
place_by_source_time(const SourceClip &reference, const SourceClip &candidate,
const core::Rational &reference_timeline_in);
static Placement
place_by_waveform_offset(const core::Rational &reference_timeline_in,
int64_t candidate_offset_samples, int sample_rate);
};
}
#endif // OAK_AUDIOSYNCHRONIZER_H
+478
View File
@@ -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());
}
}
+160
View File
@@ -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
+257
View File
@@ -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;
}
}
+105
View File
@@ -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
+54
View File
@@ -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());
}
}
+47
View File
@@ -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
+96
View File
@@ -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 &params)
{
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);
}
}
+139
View File
@@ -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 &params);
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
+158
View File
@@ -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()
+83
View File
@@ -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)
+117
View File
@@ -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);
}
+207
View File
@@ -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(&params, 0, sizeof(params));
params.audio_enabled = 1;
EXPECT_EQ(oakaudio_manager_start_recording(none, &params, 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(&params, 0, sizeof(params));
EXPECT_EQ(oakaudio_manager_start_recording(m, &params, 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);
}
+254
View File
@@ -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);
}
+258
View File
@@ -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);
}
+353
View File
@@ -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);
}
+5 -3
View File
@@ -8,9 +8,11 @@
`k_proxy_missing`,不崩溃不阻塞。注册语义为同步提交(回调内完成或
排队后立即返回);`SubmitTask` 持锁调回调,回调内不可重入注册函数。
conform/proxy 任务的 working→finished 改名生命周期整体移交 M8 oaktask。
2. **Config**config 里程碑收口):`ProxyManager::proxy_params_from_config()`
返回编译期默认值(1280x720/div1/mp4/crf23/veryfast/含音频);未引入
内存态 stubffmpegencoder 当前版本已不读 Config)。
2. **Config**config 波次已收口):`ProxyManager::proxy_params_from_config()`
现经 `oakcommon_config_*` C ABI 读取 ProxyWidth/ProxyHeight/ProxyDivider/
ProxyCRF/ProxyPreset/ProxyIncludeAudioProxyParams 成员默认值兼作
getter fallback(与 oakcommon 编译期默认值一致:1280x720/div1/crf23/
veryfast/含音频)。
3. **纹理路径功能回退**oakrender 增补 shader-blit C API 后可恢复):
oakrender C API 无通用 shader-blitFFmpegDecoder 的 yuv2rgb GLSL 路径与
去隔行 shader 路径已删除;YUV 帧改在 CPU 上 swscale 转 RGBA 后
+25 -4
View File
@@ -24,6 +24,7 @@
#include <unistd.h>
#include "common/config.h"
#include "common/filefunctions.h"
#include "taskcallbacks.h"
@@ -188,10 +189,30 @@ bool ProxyManager::proxy_filename_has_audio(const std::string &proxy_filename)
ProxyManager::ProxyParams ProxyManager::proxy_params_from_config()
{
// Interim state: the Qt config store (OAK_CONFIG ProxyWidth/ProxyHeight/
// ProxyDivider/ProxyCRF/ProxyPreset/ProxyIncludeAudio) is not split yet,
// so the compiled-in defaults apply.
return ProxyParams();
// config 波次: reads go through the oakcommon_config_* C ABI. The
// ProxyParams member defaults double as the getter fallbacks (the same
// values are also registered as compiled-in config defaults).
ProxyParams params;
params.width =
oakcommon_config_get_int(nullptr, "ProxyWidth", params.width);
params.height =
oakcommon_config_get_int(nullptr, "ProxyHeight", params.height);
params.divider =
oakcommon_config_get_int(nullptr, "ProxyDivider", params.divider);
params.crf = oakcommon_config_get_int(nullptr, "ProxyCRF", params.crf);
params.include_audio =
oakcommon_config_get_bool(nullptr, "ProxyIncludeAudio",
params.include_audio ? 1 : 0) != 0;
const int preset_size =
oakcommon_config_get(nullptr, "ProxyPreset", nullptr, 0);
if (preset_size > 0) {
std::string preset(preset_size - 1, '\0');
oakcommon_config_get(nullptr, "ProxyPreset", preset.data(),
preset_size);
params.preset = preset;
}
return params;
}
std::string ProxyManager::find_f_fmpeg_executable(const std::string &configured_path)
+1
View File
@@ -1,5 +1,6 @@
target_sources(oakcommon PRIVATE
colortransform.cpp
config.cpp
commandlineparser.cpp
current.cpp
subtitleparams.cpp
+314
View File
@@ -0,0 +1,314 @@
/***
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 "common/config.h"
#include <cstring>
#include <mutex>
#include "../src/configstore.h"
namespace
{
std::mutex handler_mutex;
OakCommonConfigErrorHandler error_handler = nullptr;
void *error_handler_userdata = nullptr;
bool is_valid_key(const char *key)
{
return key != nullptr && key[0] != '\0';
}
bool is_valid_string_out(char *buf, int buf_size)
{
return buf_size >= 0 && (buf_size == 0 || buf != nullptr);
}
int write_string_result(const std::string &value, char *buf, int buf_size)
{
int required = static_cast<int>(value.size()) + 1;
if (buf != nullptr && buf_size >= required) {
memcpy(buf, value.c_str(), required);
}
return required;
}
int to_c_type(ConfigStore::Type type)
{
switch (type) {
case ConfigStore::Type::k_string:
return OAKCOMMON_CONFIG_ENTRY_STRING;
case ConfigStore::Type::k_int:
return OAKCOMMON_CONFIG_ENTRY_INT;
case ConfigStore::Type::k_double:
return OAKCOMMON_CONFIG_ENTRY_DOUBLE;
case ConfigStore::Type::k_bool:
return OAKCOMMON_CONFIG_ENTRY_BOOL;
default:
return OAKCOMMON_CONFIG_ENTRY_NONE;
}
}
} // namespace
int oakcommon_config_load(void)
{
try {
return ConfigStore::current().load() ? OAKCOMMON_OK
: OAKCOMMON_E_FAILED;
} catch (...) {
return OAKCOMMON_E_FAILED;
}
}
int oakcommon_config_save(void)
{
try {
return ConfigStore::current().save() ? OAKCOMMON_OK
: OAKCOMMON_E_FAILED;
} catch (...) {
return OAKCOMMON_E_FAILED;
}
}
int oakcommon_config_reset_defaults(void)
{
try {
ConfigStore::current().set_defaults();
return OAKCOMMON_OK;
} catch (...) {
return OAKCOMMON_E_FAILED;
}
}
void oakcommon_config_set(const char *group, const char *key,
const char *value_utf8)
{
if (!is_valid_key(key) || value_utf8 == nullptr) {
return;
}
try {
ConfigStore &store = ConfigStore::current();
const std::string joined = ConfigStore::join_key(group, key);
const ConfigStore::Entry *existing = store.get(joined);
if (existing == nullptr ||
existing->type == ConfigStore::Type::k_string) {
ConfigStore::Entry e;
e.type = ConfigStore::Type::k_string;
e.string_value = value_utf8;
store.set(joined, e);
return;
}
// Existing typed entry: parse the string into its declared type;
// an unparseable value leaves the entry unchanged.
ConfigStore::Entry parsed;
if (ConfigStore::string_to_value(value_utf8, existing->type,
&parsed)) {
store.set(joined, parsed);
}
} catch (...) {
// §2.1 setters return void; allocation failures are swallowed.
}
}
int oakcommon_config_get(const char *group, const char *key, char *buf,
int buf_size)
{
if (!is_valid_key(key) || !is_valid_string_out(buf, buf_size)) {
return OAKCOMMON_E_INVALID;
}
try {
const ConfigStore::Entry *entry =
ConfigStore::current().get(ConfigStore::join_key(group, key));
if (entry == nullptr) {
return OAKCOMMON_E_NOT_FOUND;
}
return write_string_result(ConfigStore::value_to_string(*entry), buf,
buf_size);
} catch (...) {
return OAKCOMMON_E_FAILED;
}
}
int oakcommon_config_get_int(const char *group, const char *key,
int fallback)
{
return static_cast<int>(
oakcommon_config_get_int64(group, key, fallback));
}
int64_t oakcommon_config_get_int64(const char *group, const char *key,
int64_t fallback)
{
if (!is_valid_key(key)) {
return fallback;
}
try {
const ConfigStore::Entry *entry =
ConfigStore::current().get(ConfigStore::join_key(group, key));
if (entry == nullptr || entry->type != ConfigStore::Type::k_int) {
return fallback;
}
return entry->int_value;
} catch (...) {
return fallback;
}
}
void oakcommon_config_set_int(const char *group, const char *key, int v)
{
oakcommon_config_set_int64(group, key, v);
}
void oakcommon_config_set_int64(const char *group, const char *key,
int64_t v)
{
if (!is_valid_key(key)) {
return;
}
try {
ConfigStore::Entry e;
e.type = ConfigStore::Type::k_int;
e.int_value = v;
ConfigStore::current().set(ConfigStore::join_key(group, key), e);
} catch (...) {
}
}
double oakcommon_config_get_double(const char *group, const char *key,
double fallback)
{
if (!is_valid_key(key)) {
return fallback;
}
try {
const ConfigStore::Entry *entry =
ConfigStore::current().get(ConfigStore::join_key(group, key));
if (entry == nullptr || entry->type != ConfigStore::Type::k_double) {
return fallback;
}
return entry->double_value;
} catch (...) {
return fallback;
}
}
void oakcommon_config_set_double(const char *group, const char *key,
double v)
{
if (!is_valid_key(key)) {
return;
}
try {
ConfigStore::Entry e;
e.type = ConfigStore::Type::k_double;
e.double_value = v;
ConfigStore::current().set(ConfigStore::join_key(group, key), e);
} catch (...) {
}
}
int oakcommon_config_get_bool(const char *group, const char *key,
int fallback)
{
if (!is_valid_key(key)) {
return fallback;
}
try {
const ConfigStore::Entry *entry =
ConfigStore::current().get(ConfigStore::join_key(group, key));
if (entry == nullptr || entry->type != ConfigStore::Type::k_bool) {
return fallback;
}
return entry->bool_value ? 1 : 0;
} catch (...) {
return fallback;
}
}
void oakcommon_config_set_bool(const char *group, const char *key, int v)
{
if (!is_valid_key(key)) {
return;
}
try {
ConfigStore::Entry e;
e.type = ConfigStore::Type::k_bool;
e.bool_value = v != 0;
ConfigStore::current().set(ConfigStore::join_key(group, key), e);
} catch (...) {
}
}
int oakcommon_config_entry_type(const char *group, const char *key)
{
if (!is_valid_key(key)) {
return OAKCOMMON_E_INVALID;
}
try {
const ConfigStore::Entry *entry =
ConfigStore::current().get(ConfigStore::join_key(group, key));
if (entry == nullptr) {
return OAKCOMMON_E_NOT_FOUND;
}
return to_c_type(entry->type);
} catch (...) {
return OAKCOMMON_E_FAILED;
}
}
int oakcommon_config_set_error_handler(OakCommonConfigErrorHandler handler,
void *userdata)
{
try {
{
std::lock_guard<std::mutex> lock(handler_mutex);
error_handler = handler;
error_handler_userdata = userdata;
}
if (handler != nullptr) {
ConfigStore::set_error_handler(
[](const std::string &title, const std::string &message) {
std::lock_guard<std::mutex> lock(handler_mutex);
if (error_handler != nullptr) {
error_handler(title.c_str(), message.c_str(),
error_handler_userdata);
}
});
} else {
ConfigStore::set_error_handler(nullptr);
}
return OAKCOMMON_OK;
} catch (...) {
return OAKCOMMON_E_FAILED;
}
}
+2
View File
@@ -19,6 +19,8 @@ add_library(oakcommon SHARED
commandlineparser.cpp
commandlineparser.h
colortransform.h
configstore.cpp
configstore.h
current.cpp
current.h
debug.cpp
+406
View File
@@ -0,0 +1,406 @@
/***
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 "configstore.h"
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <system_error>
#include "filefunctions.h"
namespace fs = std::filesystem;
ConfigStore::ErrorHandler ConfigStore::error_handler_ = nullptr;
ConfigStore &ConfigStore::current()
{
static ConfigStore store;
return store;
}
ConfigStore::ConfigStore()
{
set_defaults();
}
void ConfigStore::set_error_handler(ErrorHandler handler)
{
error_handler_ = std::move(handler);
}
void ConfigStore::report_error(const std::string &title,
const std::string &message)
{
if (error_handler_) {
error_handler_(title, message);
} else {
fprintf(stderr, "%s: %s\n", title.c_str(), message.c_str());
}
}
std::string ConfigStore::get_config_file_path()
{
return (fs::path(FileFunctions::get_configuration_location()) /
"config.ini")
.string();
}
std::string ConfigStore::join_key(const char *group, const char *key)
{
if (group != nullptr && group[0] != '\0') {
return std::string(group) + "/" + key;
}
return key;
}
void ConfigStore::set_defaults()
{
std::lock_guard<std::mutex> lock(mutex_);
config_map_.clear();
auto set_string = [this](const char *key, const char *value) {
Entry e;
e.type = Type::k_string;
e.string_value = value;
config_map_[key] = e;
};
auto set_int = [this](const char *key, int64_t value) {
Entry e;
e.type = Type::k_int;
e.int_value = value;
config_map_[key] = e;
};
auto set_bool = [this](const char *key, bool value) {
Entry e;
e.type = Type::k_bool;
e.bool_value = value;
config_map_[key] = e;
};
// Only the keys the de-Qt engine modules (oaknode/oakrender/oakcodec)
// actually read are registered here; the app-layer keys of the old Qt
// config arrive with the app/config wave. Enum-valued ints hardcode
// the numeric values of their (still Qt-based) defining headers:
//
// - Timeline::k_thumbnail_in_out / k_waveforms_enabled = 1
// (engine/timeline/timelinecommon.h)
// - PixelFormat::f32 = 4 (core/include/olive/core/render/pixelformat.h)
// - VideoParams::k_interlace_none = 0 (src/common/src/videoparams.h)
// - k_channel_layout_stereo = 3
// (core/include/olive/core/render/channellayout.h)
// - ColorCoding::k_red..k_navy = 0..11, k_lime = 6
// (engine/ui/colorcoding.h)
set_int("TimelineThumbnailMode", 1);
set_int("TimelineWaveformMode", 1);
set_int("DefaultSequenceWidth", 1920);
set_int("DefaultSequenceHeight", 1080);
// Rational settings are stored as strings in oakcore_rational
// "num/den" form; this mirrors the old default Rational(1001, 30000).
set_string("DefaultSequenceFrameRate", "1001/30000");
set_string("DefaultSequencePixelAspect", "1/1");
set_int("DefaultSequenceInterlacing", 0);
set_int("DefaultSequenceAudioFrequency", 48000);
set_int("DefaultSequenceAudioLayout", 3);
set_int("OfflinePixelFormat", 4);
set_bool("SplitClipsCopyNodes", true);
set_bool("UseProxyMedia", true);
set_bool("UseGLFinish", false);
set_bool("ReassocLinToNonLin", false);
set_string("GraphicsBackend", "opengl");
set_string("LUTLibraryPaths", "");
set_int("DiskCacheSaveInterval", 10000);
set_int("AutoCacheDelay", 1000);
set_string("DiskCacheBehind", "0/1");
set_string("DiskCacheAhead", "60/1");
set_int("ProxyWidth", 1280);
set_int("ProxyHeight", 720);
set_int("ProxyDivider", 1);
set_int("ProxyCRF", 23);
set_string("ProxyPreset", "veryfast");
set_bool("ProxyIncludeAudio", true);
set_int("MarkerColor", 6);
for (int i = 0; i <= 11; i++) {
set_int(("CatColor" + std::to_string(i)).c_str(), i);
}
}
std::string ConfigStore::value_to_string(const Entry &entry)
{
switch (entry.type) {
case Type::k_string:
return entry.string_value;
case Type::k_int:
return std::to_string(entry.int_value);
case Type::k_double: {
char buf[64];
snprintf(buf, sizeof(buf), "%g", entry.double_value);
return buf;
}
case Type::k_bool:
return entry.bool_value ? "true" : "false";
default:
return std::string();
}
}
bool ConfigStore::string_to_value(const std::string &text, Type type,
Entry *out)
{
Entry e;
e.type = type;
switch (type) {
case Type::k_string:
e.string_value = text;
break;
case Type::k_int: {
try {
size_t pos = 0;
e.int_value = std::stoll(text, &pos);
if (pos != text.size()) {
return false;
}
} catch (...) {
return false;
}
break;
}
case Type::k_double: {
try {
size_t pos = 0;
e.double_value = std::stod(text, &pos);
if (pos != text.size()) {
return false;
}
} catch (...) {
return false;
}
break;
}
case Type::k_bool:
if (text == "true" || text == "1") {
e.bool_value = true;
} else if (text == "false" || text == "0") {
e.bool_value = false;
} else {
return false;
}
break;
default:
return false;
}
*out = e;
return true;
}
namespace
{
std::string trim(const std::string &s)
{
const size_t first = s.find_first_not_of(" \t\r\n");
if (first == std::string::npos) {
return std::string();
}
const size_t last = s.find_last_not_of(" \t\r\n");
return s.substr(first, last - first + 1);
}
} // namespace
bool ConfigStore::load()
{
set_defaults();
const std::string path = get_config_file_path();
std::error_code ec;
if (!fs::exists(path, ec)) {
// No saved settings yet: defaults are fine, not an error.
return true;
}
// exists() also covers directories, which ifstream would happily
// "open" on POSIX; only a regular file is a readable config.
if (!fs::is_regular_file(path, ec)) {
report_error("Error loading settings",
"Failed to load application settings. This session will "
"use defaults.");
return false;
}
std::ifstream in(path);
if (!in.is_open()) {
report_error("Error loading settings",
"Failed to load application settings. This session will "
"use defaults.");
return false;
}
std::string group;
std::string line;
while (std::getline(in, line)) {
line = trim(line);
if (line.empty() || line.front() == ';' || line.front() == '#') {
continue;
}
if (line.front() == '[' && line.back() == ']') {
group = trim(line.substr(1, line.size() - 2));
continue;
}
const size_t eq = line.find('=');
if (eq == std::string::npos) {
// Malformed line: skip, keep going (matches QSettings' lax
// INI parsing).
continue;
}
std::string key = trim(line.substr(0, eq));
const std::string value = trim(line.substr(eq + 1));
if (key.empty()) {
continue;
}
if (!group.empty()) {
key = group + "/" + key;
}
Entry parsed;
const Entry *existing = get(key);
if (existing != nullptr) {
// Known key: honor its declared type. An unparseable value
// keeps the default.
if (string_to_value(value, existing->type, &parsed)) {
set(key, parsed);
}
} else {
// Unknown key: stored as a string.
parsed.type = Type::k_string;
parsed.string_value = value;
set(key, parsed);
}
}
return true;
}
bool ConfigStore::save()
{
const std::string real_filename = get_config_file_path();
const std::string temp_filename = real_filename + ".tmp";
// Flat keys are written at the top level; keys containing '/' become
// [group] sections (group = everything before the last '/'), keeping
// the QSettings INI key shape.
std::map<std::string, std::map<std::string, std::string>> sections;
{
std::lock_guard<std::mutex> lock(mutex_);
for (const auto &pair : config_map_) {
const std::string &key = pair.first;
const size_t slash = key.rfind('/');
std::string group = slash == std::string::npos
? std::string()
: key.substr(0, slash);
std::string sub = slash == std::string::npos
? key
: key.substr(slash + 1);
sections[group][sub] = value_to_string(pair.second);
}
}
{
std::ofstream out(temp_filename, std::ios::trunc);
if (!out.is_open()) {
report_error("Error saving settings",
"Failed to save application settings. The "
"application may lack write permissions for this "
"location.");
return false;
}
const auto flat = sections.find(std::string());
if (flat != sections.end()) {
for (const auto &pair : flat->second) {
out << pair.first << '=' << pair.second << '\n';
}
}
for (const auto &section : sections) {
if (section.first.empty()) {
continue;
}
out << '\n'
<< '[' << section.first << ']' << '\n';
for (const auto &pair : section.second) {
out << pair.first << '=' << pair.second << '\n';
}
}
out.flush();
if (!out.good()) {
report_error("Error saving settings",
"Failed to save application settings. The "
"application may lack write permissions for this "
"location.");
return false;
}
}
std::error_code ec;
fs::rename(temp_filename, real_filename, ec);
if (ec) {
fs::remove(real_filename, ec);
ec.clear();
fs::rename(temp_filename, real_filename, ec);
if (ec) {
report_error("Error saving settings",
"Failed to overwrite the application settings "
"file.");
return false;
}
}
return true;
}
const ConfigStore::Entry *ConfigStore::get(const std::string &key) const
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = config_map_.find(key);
return it == config_map_.end() ? nullptr : &it->second;
}
void ConfigStore::set(const std::string &key, const Entry &entry)
{
std::lock_guard<std::mutex> lock(mutex_);
config_map_[key] = entry;
}
+132
View File
@@ -0,0 +1,132 @@
/***
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 OAK_CONFIGSTORE_H
#define OAK_CONFIGSTORE_H
#include <cstdint>
#include <functional>
#include <map>
#include <mutex>
#include <string>
/**
* @brief De-Qt application configuration store (process singleton)
*
* Replacement for the Qt-based olive::Config (engine/config/config.h):
* QMap/QVariant became std::map + a small typed value, QSettings became a
* self-written INI file, and NodeValue::Type became ConfigStore::Type so
* config no longer depends on the node module.
*
* Keys keep the QSettings INI shape: "group/key" maps to an INI [group]
* section; flat keys stay at the top level. The file lives at
* <FileFunctions::get_configuration_location()>/config.ini (the
* OAK_CONFIG_DIR override applies, which is what tests use).
*
* All public methods are thread-safe (single mutex).
*/
class ConfigStore {
public:
enum class Type { k_none, k_string, k_int, k_double, k_bool };
struct Entry {
Type type = Type::k_none;
std::string string_value;
int64_t int_value = 0;
double double_value = 0.0;
bool bool_value = false;
};
using ErrorHandler =
std::function<void(const std::string &title, const std::string &message)>;
static ConfigStore &current();
/**
* @brief Resets the store to compiled-in defaults (drops custom keys)
*/
void set_defaults();
/**
* @brief Resets to defaults, then applies config.ini if it exists
*
* @return false when the file exists but could not be read (the error
* is also reported through the registered error handler).
*/
bool load();
/**
* @brief Writes the store to config.ini via temp file + rename
*
* @return false on failure (also reported through the error handler).
*/
bool save();
/**
* @brief Returns the entry for key, or nullptr when absent
*
* Keys are the joined "group/key" form (or the bare key when group is
* null/empty).
*/
const Entry *get(const std::string &key) const;
/**
* @brief Creates or replaces an entry
*/
void set(const std::string &key, const Entry &entry);
static void set_error_handler(ErrorHandler handler);
static void report_error(const std::string &title,
const std::string &message);
/**
* @brief <get_configuration_location()>/config.ini
*/
static std::string get_config_file_path();
/**
* @brief Joins group and key into the stored "group/key" form
*/
static std::string join_key(const char *group, const char *key);
/**
* @brief Serializes an entry for the INI file / string getter
*/
static std::string value_to_string(const Entry &entry);
/**
* @brief Parses text into an entry of the given type
*
* @return false when the text cannot be parsed as the requested type
* (strings always parse).
*/
static bool string_to_value(const std::string &text, Type type,
Entry *out);
private:
ConfigStore();
std::map<std::string, Entry> config_map_;
mutable std::mutex mutex_;
static ErrorHandler error_handler_;
};
#endif // OAK_CONFIGSTORE_H
+1
View File
@@ -21,6 +21,7 @@ include(GoogleTest)
add_executable(oakcommon-gtest
colortransform_test.cpp
commandlineparser_test.cpp
config_test.cpp
current_test.cpp
debug_test.cpp
dropworkflowbehavior_test.cpp
+384
View File
@@ -0,0 +1,384 @@
/***
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 "common/config.h"
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <gtest/gtest.h>
namespace
{
namespace fs = std::filesystem;
/**
* @brief Redirects the config file into a fresh per-test temp directory
*
* ConfigStore resolves its file through
* FileFunctions::get_configuration_location() on every load/save, and
* that honors OAK_CONFIG_DIR, so setting the env var per test isolates
* the on-disk state. The store itself is reset to defaults in SetUp.
*/
class ConfigTest : public testing::Test {
protected:
void SetUp() override
{
dir_ = fs::temp_directory_path() /
fs::path("oakconfig_test_" + std::to_string(
::testing::UnitTest::GetInstance()
->random_seed()) +
"_" + std::to_string(counter_++));
fs::create_directories(dir_);
setenv("OAK_CONFIG_DIR", dir_.string().c_str(), 1);
ASSERT_EQ(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
ASSERT_EQ(oakcommon_config_set_error_handler(nullptr, nullptr),
OAKCOMMON_OK);
}
void TearDown() override
{
unsetenv("OAK_CONFIG_DIR");
oakcommon_config_reset_defaults();
oakcommon_config_set_error_handler(nullptr, nullptr);
std::error_code ec;
fs::remove_all(dir_, ec);
}
std::string ini_contents()
{
std::ifstream in((dir_ / "config.ini").string());
return std::string(std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>());
}
fs::path dir_;
static int counter_;
};
int ConfigTest::counter_ = 0;
// --- compiled-in defaults -------------------------------------------------
TEST_F(ConfigTest, DefaultsAreRegistered)
{
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", -1),
1920);
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "SplitClipsCopyNodes", -1),
1);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "CatColor11", -1), 11);
char buf[64];
ASSERT_GT(oakcommon_config_get(nullptr, "GraphicsBackend", buf,
sizeof(buf)),
0);
EXPECT_STREQ(buf, "opengl");
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "DefaultSequenceWidth"),
OAKCOMMON_CONFIG_ENTRY_INT);
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "GraphicsBackend"),
OAKCOMMON_CONFIG_ENTRY_STRING);
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "UseProxyMedia"),
OAKCOMMON_CONFIG_ENTRY_BOOL);
}
// --- oakcommon_config_set / oakcommon_config_get (string) ------------------
TEST_F(ConfigTest, SetGetStringRoundtripTwoStage)
{
oakcommon_config_set(nullptr, "TestStringKey", "hello world");
// Stage 1: query the required size with a NULL buffer.
const int required =
oakcommon_config_get(nullptr, "TestStringKey", nullptr, 0);
ASSERT_EQ(required, int(strlen("hello world")) + 1);
// Stage 2: fetch into a sufficiently large buffer.
std::string buf(required, '\0');
ASSERT_EQ(oakcommon_config_get(nullptr, "TestStringKey", buf.data(),
required),
required);
EXPECT_STREQ(buf.c_str(), "hello world");
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestStringKey"),
OAKCOMMON_CONFIG_ENTRY_STRING);
}
TEST_F(ConfigTest, GetStringErrorPaths)
{
char buf[8];
// Missing key.
EXPECT_EQ(oakcommon_config_get(nullptr, "NoSuchKey", buf, sizeof(buf)),
OAKCOMMON_E_NOT_FOUND);
// NULL key.
EXPECT_EQ(oakcommon_config_get(nullptr, nullptr, buf, sizeof(buf)),
OAKCOMMON_E_INVALID);
// Negative buffer size.
EXPECT_EQ(oakcommon_config_get(nullptr, "GraphicsBackend", buf, -1),
OAKCOMMON_E_INVALID);
// set with NULL value is a no-op (void return, entry must not appear).
oakcommon_config_set(nullptr, "IgnoredKey", nullptr);
EXPECT_EQ(oakcommon_config_get(nullptr, "IgnoredKey", buf, sizeof(buf)),
OAKCOMMON_E_NOT_FOUND);
}
// --- group/key (§2.1 two-argument form) ------------------------------------
TEST_F(ConfigTest, GroupedKeyMapsToIniSection)
{
oakcommon_config_set("Audio", "Output", "coreaudio");
char buf[32];
ASSERT_GT(oakcommon_config_get("Audio", "Output", buf, sizeof(buf)), 0);
EXPECT_STREQ(buf, "coreaudio");
ASSERT_EQ(oakcommon_config_save(), OAKCOMMON_OK);
const std::string ini = ini_contents();
EXPECT_NE(ini.find("[Audio]"), std::string::npos);
EXPECT_NE(ini.find("Output=coreaudio"), std::string::npos);
// Empty group behaves like NULL group (top-level key).
oakcommon_config_set("", "FlatKey", "flat");
ASSERT_GT(oakcommon_config_get(nullptr, "FlatKey", buf, sizeof(buf)), 0);
EXPECT_STREQ(buf, "flat");
}
// --- int family -------------------------------------------------------------
TEST_F(ConfigTest, IntRoundtrip)
{
oakcommon_config_set_int(nullptr, "TestIntKey", -42);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "TestIntKey", 0), -42);
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestIntKey"),
OAKCOMMON_CONFIG_ENTRY_INT);
// Overrides a compiled-in default.
oakcommon_config_set_int(nullptr, "DefaultSequenceWidth", 3840);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", 0),
3840);
}
TEST_F(ConfigTest, IntFallbackOnMissingOrWrongType)
{
// Missing key -> fallback.
EXPECT_EQ(oakcommon_config_get_int(nullptr, "NoSuchKey", 7), 7);
// Wrong type (string entry) -> fallback.
EXPECT_EQ(oakcommon_config_get_int(nullptr, "GraphicsBackend", 9), 9);
}
TEST_F(ConfigTest, Int64RoundtripAndFallback)
{
oakcommon_config_set_int64(nullptr, "TestInt64Key",
INT64_C(5000000000));
EXPECT_EQ(oakcommon_config_get_int64(nullptr, "TestInt64Key", 0),
INT64_C(5000000000));
// Missing key -> fallback; NULL key -> fallback.
EXPECT_EQ(oakcommon_config_get_int64(nullptr, "NoSuchKey",
INT64_C(-1)),
INT64_C(-1));
EXPECT_EQ(oakcommon_config_get_int64(nullptr, nullptr, INT64_C(-2)),
INT64_C(-2));
}
// --- double family ----------------------------------------------------------
TEST_F(ConfigTest, DoubleRoundtrip)
{
oakcommon_config_set_double(nullptr, "TestDoubleKey", 2.5);
EXPECT_DOUBLE_EQ(oakcommon_config_get_double(nullptr, "TestDoubleKey", 0),
2.5);
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestDoubleKey"),
OAKCOMMON_CONFIG_ENTRY_DOUBLE);
}
TEST_F(ConfigTest, DoubleFallbackOnMissingOrWrongType)
{
EXPECT_DOUBLE_EQ(oakcommon_config_get_double(nullptr, "NoSuchKey", 1.5),
1.5);
// Wrong type (int entry) -> fallback.
EXPECT_DOUBLE_EQ(
oakcommon_config_get_double(nullptr, "DefaultSequenceWidth", 3.5),
3.5);
}
// --- bool family ------------------------------------------------------------
TEST_F(ConfigTest, BoolRoundtrip)
{
oakcommon_config_set_bool(nullptr, "TestBoolKey", 1);
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "TestBoolKey", 0), 1);
oakcommon_config_set_bool(nullptr, "TestBoolKey", 0);
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "TestBoolKey", 1), 0);
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "TestBoolKey"),
OAKCOMMON_CONFIG_ENTRY_BOOL);
}
TEST_F(ConfigTest, BoolFallbackOnMissingOrWrongType)
{
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "NoSuchKey", 1), 1);
// Wrong type (string entry) -> fallback.
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "GraphicsBackend", 1), 1);
}
// --- typed set through oakcommon_config_set ---------------------------------
TEST_F(ConfigTest, SetStringParsesIntoDeclaredType)
{
// Existing INT entry: a parseable string updates the value...
oakcommon_config_set(nullptr, "DefaultSequenceWidth", "2560");
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", 0),
2560);
// ...an unparseable one leaves the entry unchanged.
oakcommon_config_set(nullptr, "DefaultSequenceWidth", "not-a-number");
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", 0),
2560);
}
// --- load / save / reset_defaults -------------------------------------------
TEST_F(ConfigTest, SaveLoadRoundtrip)
{
oakcommon_config_set_int(nullptr, "DefaultSequenceHeight", 2160);
oakcommon_config_set("Session", "LastDir", "/tmp/media");
oakcommon_config_set_bool(nullptr, "UseGLFinish", 1);
ASSERT_EQ(oakcommon_config_save(), OAKCOMMON_OK);
ASSERT_TRUE(fs::exists(dir_ / "config.ini"));
// Wipe in-memory state; defaults must come back...
ASSERT_EQ(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceHeight", 0),
1080);
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "UseGLFinish", -1), 0);
char buf[64];
EXPECT_EQ(oakcommon_config_get("Session", "LastDir", buf, sizeof(buf)),
OAKCOMMON_E_NOT_FOUND);
// ...and load() restores everything that was saved.
ASSERT_EQ(oakcommon_config_load(), OAKCOMMON_OK);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceHeight", 0),
2160);
EXPECT_EQ(oakcommon_config_get_bool(nullptr, "UseGLFinish", -1), 1);
ASSERT_GT(oakcommon_config_get("Session", "LastDir", buf, sizeof(buf)),
0);
EXPECT_STREQ(buf, "/tmp/media");
EXPECT_EQ(oakcommon_config_entry_type("Session", "LastDir"),
OAKCOMMON_CONFIG_ENTRY_STRING);
}
TEST_F(ConfigTest, LoadMissingFileIsNotAnError)
{
// Fresh directory, no config.ini: defaults stay, OAKCOMMON_OK.
ASSERT_EQ(oakcommon_config_load(), OAKCOMMON_OK);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", -1),
1920);
}
TEST_F(ConfigTest, LoadErrorPathUnreadableFile)
{
// Make config.ini a directory: it exists but cannot be read.
fs::create_directories(dir_ / "config.ini");
EXPECT_EQ(oakcommon_config_load(), OAKCOMMON_E_FAILED);
}
TEST_F(ConfigTest, ResetDefaultsDropsCustomKeys)
{
oakcommon_config_set_int(nullptr, "TransientKey", 1);
ASSERT_EQ(oakcommon_config_get_int(nullptr, "TransientKey", -1), 1);
ASSERT_EQ(oakcommon_config_reset_defaults(), OAKCOMMON_OK);
EXPECT_EQ(oakcommon_config_get_int(nullptr, "TransientKey", -1), -1);
// Defaults survive the reset.
EXPECT_EQ(oakcommon_config_get_int(nullptr, "DefaultSequenceWidth", -1),
1920);
}
// --- error handler ----------------------------------------------------------
namespace
{
struct HandlerLog {
int calls = 0;
std::string title;
std::string message;
};
void recording_handler(const char *title, const char *message,
void *userdata)
{
auto *log = static_cast<HandlerLog *>(userdata);
log->calls++;
log->title = title;
log->message = message;
}
} // namespace
TEST_F(ConfigTest, ErrorHandlerFiresOnSaveFailure)
{
HandlerLog log;
ASSERT_EQ(
oakcommon_config_set_error_handler(recording_handler, &log),
OAKCOMMON_OK);
// Point the config dir at a path that exists as a *file*: the temp
// file cannot be created inside it, so save fails.
const fs::path blocker = fs::temp_directory_path() /
"oakconfig_test_blocker";
{
std::ofstream out(blocker.string());
out << "not a directory";
}
setenv("OAK_CONFIG_DIR", blocker.string().c_str(), 1);
EXPECT_EQ(oakcommon_config_save(), OAKCOMMON_E_FAILED);
EXPECT_EQ(log.calls, 1);
EXPECT_FALSE(log.title.empty());
EXPECT_FALSE(log.message.empty());
// Restore the per-test dir for TearDown.
setenv("OAK_CONFIG_DIR", dir_.string().c_str(), 1);
std::error_code ec;
fs::remove(blocker, ec);
}
TEST_F(ConfigTest, ClearErrorHandlerRestoresSilence)
{
// Registering and clearing must both succeed.
ASSERT_EQ(
oakcommon_config_set_error_handler(recording_handler, nullptr),
OAKCOMMON_OK);
EXPECT_EQ(oakcommon_config_set_error_handler(nullptr, nullptr),
OAKCOMMON_OK);
}
// --- entry_type error paths --------------------------------------------------
TEST_F(ConfigTest, EntryTypeErrorPaths)
{
EXPECT_EQ(oakcommon_config_entry_type(nullptr, "NoSuchKey"),
OAKCOMMON_E_NOT_FOUND);
EXPECT_EQ(oakcommon_config_entry_type(nullptr, nullptr),
OAKCOMMON_E_INVALID);
}
} // namespace
+3 -2
View File
@@ -257,8 +257,9 @@ cd src/node/src/node && c++ -std=c++17 -fsyntax-only -Wall \
add/remove_node 函数族)。
6. `factory.cpp` 的 `pluginSupport/` include、`traverser.h` 的 `"common/cancelableobject.h"`
原样保留(后者 oakcommon 没有,留 M 系列裁决)。
7. `node.h` 仍 include `config/config.h`、`ui/colorcoding.h`engine Qt 头,`color()` 用到
`OAK_CONFIG_STR(...)`)——config 波次处理。
7. ~~`node.h` 仍 include `config/config.h`~~ —— config 波次已处理:`configaccessor.h`
src/node/src)替代 engine Qt 头与 transition stub`OAK_CONFIG*` 宏在消费侧本地重定义为
`oakcommon_config_*` C 调用,调用点零改动;`ui/colorcoding.h` 仍走 stubUI 波次)。
8. `Node::gizmo_drag_move` 的 modifiers 参数为 `int`(原 `Qt::KeyboardModifiers`)。
9. timeformat 的 `format_date_time()` 不实现 `MMMM`(月名)/`dddd`(星期名)本地化 token
(原默认格式 `hh:mm:ss` 用不到);负 epoch 毫秒的 `zzz` 取模与 Qt 有边界差异(实际输入不会触发)。
+1 -1
View File
@@ -24,7 +24,7 @@
#include <algorithm>
#include <cmath>
#include "config/config.h"
#include "configaccessor.h"
#include "block/transition/transition.h"
#include "output/track/track.h"
#include "output/viewer/viewer.h"
@@ -28,7 +28,7 @@
#include "define.h"
#include "filefunctions.h"
#include "config/config.h"
#include "configaccessor.h"
#include "project.h"
namespace olive
+168
View File
@@ -0,0 +1,168 @@
/***
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 OAK_NODE_CONFIGACCESSOR_H
#define OAK_NODE_CONFIGACCESSOR_H
/**
* @brief Consumer-side shim over the oakcommon_config_* C ABI
*
* config 波次: the old engine/config/config.h (Qt) and the in-memory
* transition/config/config.h stub are gone. This header keeps the
* OAK_CONFIG(...)/Config::current()[...] call shape so call sites did
* not change; every accessor is a C call into liboakcommon. Per the
* cross-module rule only the oakcommon C API is used here, never the
* ConfigStore C++ class.
*/
#include <cstdint>
#include <string>
#include <type_traits>
#include "common/config.h"
#ifndef QStringLiteral
#define QStringLiteral(x) x
#endif
namespace olive
{
/**
* @brief Key-bound value proxy with the QVariant-subset accessors the
* call sites use
*/
class ConfigValue {
public:
ConfigValue() = default;
explicit ConfigValue(const std::string &key)
: key_(key)
{
}
bool to_bool() const
{
return oakcommon_config_get_bool(nullptr, key_.c_str(), 0) != 0;
}
bool toBool() const { return to_bool(); }
int to_int() const
{
return oakcommon_config_get_int(nullptr, key_.c_str(), 0);
}
int toInt() const { return to_int(); }
int64_t to_long_long() const
{
return oakcommon_config_get_int64(nullptr, key_.c_str(), 0);
}
uint64_t to_u_long_long() const
{
return static_cast<uint64_t>(to_long_long());
}
uint64_t toULongLong() const { return to_u_long_long(); }
double to_double() const
{
return oakcommon_config_get_double(nullptr, key_.c_str(), 0.0);
}
std::string to_string() const
{
const int size =
oakcommon_config_get(nullptr, key_.c_str(), nullptr, 0);
if (size <= 0) {
return std::string();
}
std::string s(size - 1, '\0');
oakcommon_config_get(nullptr, key_.c_str(), s.data(), size);
return s;
}
std::string toString() const { return to_string(); }
template <typename T> T value() const { return value_impl<T>(); }
ConfigValue &operator=(const std::string &s)
{
oakcommon_config_set(nullptr, key_.c_str(), s.c_str());
return *this;
}
ConfigValue &operator=(const char *s)
{
return *this = std::string(s);
}
private:
template <typename T>
typename std::enable_if<std::is_integral<T>::value ||
std::is_enum<T>::value,
T>::type
value_impl() const
{
return static_cast<T>(to_long_long());
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
value_impl() const
{
return static_cast<T>(to_double());
}
/**
* @brief Rational and friends: parsed from the stored "num/den"
* string via the type's own static from_string.
*/
template <typename T>
typename std::enable_if<!std::is_arithmetic<T>::value &&
!std::is_enum<T>::value,
T>::type
value_impl() const
{
return T::from_string(to_string());
}
std::string key_;
};
class Config {
public:
static Config &current()
{
static Config c;
return c;
}
ConfigValue operator[](const std::string &key) const
{
return ConfigValue(key);
}
};
} // namespace olive
#ifndef OAK_CONFIG
#define OAK_CONFIG(x) Config::current()[x]
#endif
#ifndef OAK_CONFIG_STR
#define OAK_CONFIG_STR(x) Config::current()[x]
#endif
#endif // OAK_NODE_CONFIGACCESSOR_H
+1 -1
View File
@@ -31,7 +31,7 @@
#include <cstdlib>
#include "lerp.h"
#include "config/config.h"
#include "configaccessor.h"
#include "group/group.h"
#include "project/serializer/typeserializer.h"
#include "nodeundo.h"
+1 -1
View File
@@ -21,7 +21,7 @@
#include "viewer.h"
#include "config/config.h"
#include "configaccessor.h"
#include "coreengine.h"
#include "traverser.h"
#include "olive/core/util/timecodefunctions.h"
+2 -2
View File
@@ -32,7 +32,7 @@
#include "filefunctions.h"
#include "qtutils.h"
#include "xmlutils.h"
#include "config/config.h"
#include "configaccessor.h"
#include "olive/core/util/stringutils.h"
#include "color/colormanager/colormanager.h"
#include "project.h"
@@ -392,7 +392,7 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
// Proxies can be globally disabled (Tools > Use Proxy Media) without
// losing each footage's individual proxy setting
// ADAPT(config 波次): engine config/config.h is still Qt-based; kept as-is
// config 波次: resolved via configaccessor.h -> oakcommon_config_* C ABI
const bool proxies_allowed =
Config::current()[QStringLiteral("UseProxyMedia")].toBool();
@@ -24,7 +24,7 @@
#include <cstdio>
#include <cstdlib>
#include "config/config.h"
#include "configaccessor.h"
#include "factory.h"
#include "group/group.h"
@@ -836,8 +836,8 @@ void ProjectSerializer210528::load_marker_list(XmlStreamReader *reader,
}
}
// ADAPT(config/M4): OAK_CONFIG is still Qt-based; TimelineMarker
// is de-Qt'd in M4 (must accept std::string name)
// MarkerColor resolves through configaccessor.h
// (oakcommon_config_* C ABI)
new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(),
TimeRange(in, out), name, markers);
}
@@ -24,7 +24,7 @@
#include <cstdio>
#include <cstdlib>
#include "config/config.h"
#include "configaccessor.h"
#include "factory.h"
#include "group/group.h"
@@ -825,8 +825,8 @@ void ProjectSerializer210907::load_marker_list(XmlStreamReader *reader,
}
}
// ADAPT(config/M4): OAK_CONFIG is still Qt-based; TimelineMarker
// is de-Qt'd in M4 (must accept std::string name)
// MarkerColor resolves through configaccessor.h
// (oakcommon_config_* C ABI)
new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(),
TimeRange(in, out), name, markers);
}
@@ -24,7 +24,7 @@
#include <cstdio>
#include <cstdlib>
#include "config/config.h"
#include "configaccessor.h"
#include "factory.h"
#include "group/group.h"
@@ -879,8 +879,8 @@ void ProjectSerializer211228::load_marker_list(XmlStreamReader *reader,
}
}
// ADAPT(config/M4): OAK_CONFIG is still Qt-based; TimelineMarker
// is de-Qt'd in M4 (must accept std::string name)
// MarkerColor resolves through configaccessor.h
// (oakcommon_config_* C ABI)
new TimelineMarker(OAK_CONFIG("MarkerColor").toInt(),
TimeRange(in, out), name, markers);
}
@@ -24,7 +24,7 @@
#include <cstdio>
#include <cstdlib>
#include "config/config.h"
#include "configaccessor.h"
#include "factory.h"
#include "block/clip/clip.h"
#include "group/group.h"
@@ -25,7 +25,7 @@
#include "serializer230220.h"
#include "config/config.h"
#include "configaccessor.h"
#include "factory.h"
#include "group/group.h"
#include "serializeddata.h"
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include <string>
#include "variant.h"
#ifndef QStringLiteral
#define QStringLiteral(x) x
#endif
namespace olive {
class ConfigValue : public olive::Variant {
public:
ConfigValue() = default;
bool toBool() const { return false; }
int toInt() const { return 0; }
};
class Config {
public:
static Config &current() { static Config c; return c; }
ConfigValue operator[](const std::string &) const { return ConfigValue(); }
};
}
#ifndef OAK_CONFIG
#define OAK_CONFIG(x) Config::current()[x]
#endif
#ifndef OAK_CONFIG_STR
#define OAK_CONFIG_STR(x) Config::current()[x]
#endif
+168
View File
@@ -0,0 +1,168 @@
/***
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 OAK_RENDER_CONFIGACCESSOR_H
#define OAK_RENDER_CONFIGACCESSOR_H
/**
* @brief Consumer-side shim over the oakcommon_config_* C ABI
*
* config : the old engine/config/config.h (Qt) and the in-memory
* transition/config/config.h stub are gone. This header keeps the
* OAK_CONFIG(...)/Config::current()[...] call shape so call sites did
* not change; every accessor is a C call into liboakcommon. Per the
* cross-module rule only the oakcommon C API is used here, never the
* ConfigStore C++ class.
*/
#include <cstdint>
#include <string>
#include <type_traits>
#include "common/config.h"
#ifndef QStringLiteral
#define QStringLiteral(x) x
#endif
namespace olive
{
/**
* @brief Key-bound value proxy with the QVariant-subset accessors the
* call sites use
*/
class ConfigValue {
public:
ConfigValue() = default;
explicit ConfigValue(const std::string &key)
: key_(key)
{
}
bool to_bool() const
{
return oakcommon_config_get_bool(nullptr, key_.c_str(), 0) != 0;
}
bool toBool() const { return to_bool(); }
int to_int() const
{
return oakcommon_config_get_int(nullptr, key_.c_str(), 0);
}
int toInt() const { return to_int(); }
int64_t to_long_long() const
{
return oakcommon_config_get_int64(nullptr, key_.c_str(), 0);
}
uint64_t to_u_long_long() const
{
return static_cast<uint64_t>(to_long_long());
}
uint64_t toULongLong() const { return to_u_long_long(); }
double to_double() const
{
return oakcommon_config_get_double(nullptr, key_.c_str(), 0.0);
}
std::string to_string() const
{
const int size =
oakcommon_config_get(nullptr, key_.c_str(), nullptr, 0);
if (size <= 0) {
return std::string();
}
std::string s(size - 1, '\0');
oakcommon_config_get(nullptr, key_.c_str(), s.data(), size);
return s;
}
std::string toString() const { return to_string(); }
template <typename T> T value() const { return value_impl<T>(); }
ConfigValue &operator=(const std::string &s)
{
oakcommon_config_set(nullptr, key_.c_str(), s.c_str());
return *this;
}
ConfigValue &operator=(const char *s)
{
return *this = std::string(s);
}
private:
template <typename T>
typename std::enable_if<std::is_integral<T>::value ||
std::is_enum<T>::value,
T>::type
value_impl() const
{
return static_cast<T>(to_long_long());
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
value_impl() const
{
return static_cast<T>(to_double());
}
/**
* @brief Rational and friends: parsed from the stored "num/den"
* string via the type's own static from_string.
*/
template <typename T>
typename std::enable_if<!std::is_arithmetic<T>::value &&
!std::is_enum<T>::value,
T>::type
value_impl() const
{
return T::from_string(to_string());
}
std::string key_;
};
class Config {
public:
static Config &current()
{
static Config c;
return c;
}
ConfigValue operator[](const std::string &key) const
{
return ConfigValue(key);
}
};
} // namespace olive
#ifndef OAK_CONFIG
#define OAK_CONFIG(x) Config::current()[x]
#endif
#ifndef OAK_CONFIG_STR
#define OAK_CONFIG_STR(x) Config::current()[x]
#endif
#endif // OAK_RENDER_CONFIGACCESSOR_H
+2 -2
View File
@@ -28,7 +28,7 @@
#include <fstream>
#include <sstream>
#include "config/config.h"
#include "configaccessor.h"
#include "coreengine.h"
#include "filefunctions.h"
@@ -287,7 +287,7 @@ DiskCacheFolder::DiskCacheFolder(const std::string &path)
// reached the folder through queued QMetaObject invocations.
int interval = OAK_CONFIG("DiskCacheSaveInterval").toInt();
if (interval <= 0) {
// Config default (10000 ms); the transition config stub returns 0
// Defensive fallback; the compiled-in config default is 10000 ms
interval = 10000;
}
save_thread_stop_ = false;
+1 -1
View File
@@ -24,7 +24,7 @@
#include <cctype>
#include <filesystem>
#include "config/config.h"
#include "configaccessor.h"
namespace olive
{
+1 -1
View File
@@ -30,7 +30,7 @@
#include "filefunctions.h"
#if !defined(OAK_RENDER_BACKEND_PLUGIN)
#include "config/config.h"
#include "configaccessor.h"
#endif
#include "render/job/shaderjob.h"
+1 -1
View File
@@ -29,7 +29,7 @@
#include <vector>
#include "color/colormanager/colormanager.h"
#include "config/config.h"
#include "configaccessor.h"
#include "group/group.h"
#include "node.h"
#include "output/viewer/viewer.h"
+1 -1
View File
@@ -26,7 +26,7 @@
#include <chrono>
#include <cstdio>
#include "config/config.h"
#include "configaccessor.h"
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
#include "backend/dynamicrenderer.h"
#endif
+1 -1
View File
@@ -29,7 +29,7 @@
#include <thread>
#include <vector>
#include "config/config.h"
#include "configaccessor.h"
#include "colorprocessorcache.h"
#include "output/viewer/viewer.h"
#include "project.h"
-43
View File
@@ -1,43 +0,0 @@
#pragma once
// Transitional stub for engine/config/config.h (still Qt-based, config 未拆分).
// Union of the src/node/transition stub and the render-facing surface:
// keeps the OAK_CONFIG* call shape; values are inert defaults until the
// config milestone wires the real store. operator[] returns a stored
// reference so writes (lutlibrary) compile; nothing is persisted. 只增不删。
#include <map>
#include <string>
#include "variant.h"
#ifndef QStringLiteral
#define QStringLiteral(x) x
#endif
namespace olive {
class ConfigValue : public olive::Variant {
public:
ConfigValue() = default;
ConfigValue(const std::string &s) : olive::Variant(s) {}
ConfigValue &operator=(const std::string &s)
{
olive::Variant::operator=(olive::Variant(s));
return *this;
}
bool toBool() const { return to_bool(); }
int toInt() const { return to_int(); }
uint64_t toULongLong() const { return to_u_long_long(); }
// rendermanager reads the graphics backend name through this
std::string toString() const { return to_string(); }
std::vector<std::string> toStringList() const { return to_string_list(); }
};
class Config {
public:
static Config &current() { static Config c; return c; }
ConfigValue &operator[](const std::string &key) { return values_[key]; }
private:
std::map<std::string, ConfigValue> values_;
};
}
#ifndef OAK_CONFIG
#define OAK_CONFIG(x) Config::current()[x]
#endif
#ifndef OAK_CONFIG_STR
#define OAK_CONFIG_STR(x) Config::current()[x]
#endif