refactor(config,audio): merge config into oakcommon, split oakaudio
- config moves into oakcommon as ConfigStore + oakcommon_config_* C API (INI storage, typed entries, error-handler injection); node and render call sites keep OAK_CONFIG() macro shape via a local shim that forwards to the C API; transition config stubs removed - oakaudio: de-Qt all six classes, C ABI in include/audio with refcounted handles (processor/manager/waveform/levelmeter/sync, 48 functions); PreviewAudioDevice moved in from render; recording goes through oakcodec encoder; waveform extract uses probe + ffmpeg_bridge decode (decode_audio needs M8 task system) - fix re_sum_samples min/max init bug (values clamped to 0 for same-sign ranges) - every C API function has positive + error-path tests; suites: oakcommon 193, oakaudio 36, oaknode 96, oakrender 42, oakcodec 18
This commit is contained in:
@@ -0,0 +1,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -3,3 +3,4 @@ add_subdirectory(undo)
|
||||
add_subdirectory(node)
|
||||
add_subdirectory(render)
|
||||
add_subdirectory(codec)
|
||||
add_subdirectory(audio)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(c_api)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,24 @@
|
||||
# Oak Video Editor - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
target_sources(oakaudio PRIVATE
|
||||
alive.cpp
|
||||
levelmeter.cpp
|
||||
manager.cpp
|
||||
processor.cpp
|
||||
sync.cpp
|
||||
waveform.cpp
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include "audio/error.h"
|
||||
#include "audio/manager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::atomic<int> g_alive{ 0 };
|
||||
}
|
||||
|
||||
namespace oakaudio
|
||||
{
|
||||
|
||||
void alive_inc()
|
||||
{
|
||||
g_alive.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void alive_dec()
|
||||
{
|
||||
g_alive.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_debug_alive_count(void)
|
||||
{
|
||||
return g_alive.load(std::memory_order_relaxed);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audio/levelmeter.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "audiolevelmeter.h"
|
||||
#include "ffmpeg_bridge/ffmpeg_bridge.h"
|
||||
|
||||
using olive::AudioLevelMeter;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::Rational;
|
||||
using olive::core::SampleBuffer;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
extern "C" int oakaudio_levelmeter_analyze(const float *const *planar,
|
||||
int channel_count, int frame_count,
|
||||
oakaudio_channel_stats *channels, int channels_capacity,
|
||||
oakaudio_meter_stats *summary)
|
||||
{
|
||||
if (!planar || channel_count <= 0 || frame_count < 0 ||
|
||||
(channels && channels_capacity < channel_count)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!channels && !summary) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// Repack into a SampleBuffer (planar f32) for the C++ implementation.
|
||||
AudioParams params(48000, fb_channel_layout_default(channel_count),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(frame_count, 48000));
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (frame_count > 0) {
|
||||
memcpy(buffer.data(ch), planar[ch],
|
||||
size_t(frame_count) * sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
const AudioLevelMeter::Stats stats =
|
||||
AudioLevelMeter::analyze_sample_buffer(buffer);
|
||||
|
||||
if (channels) {
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
const AudioLevelMeter::ChannelStats &s =
|
||||
stats.channels[size_t(ch)];
|
||||
oakaudio_channel_stats &dst = channels[ch];
|
||||
dst.peak_linear = s.peak_linear;
|
||||
dst.peak_db = s.peak_db;
|
||||
dst.rms_linear = s.rms_linear;
|
||||
dst.rms_db = s.rms_db;
|
||||
dst.vu_db = s.vu_db;
|
||||
}
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
summary->max_peak_linear = stats.max_peak_linear;
|
||||
summary->integrated_lufs = stats.integrated_lufs;
|
||||
summary->silence = stats.silence ? 1 : 0;
|
||||
}
|
||||
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audio/manager.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "audiomanager.h"
|
||||
|
||||
using olive::AudioManager;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Singleton semantics (mirrors oakcommon's OakCurrent): the ctx points to
|
||||
// the process-wide instance, so addref/release never destroy anything.
|
||||
void singleton_addref(void *ctx)
|
||||
{
|
||||
(void) ctx;
|
||||
}
|
||||
|
||||
void singleton_release(void *ctx)
|
||||
{
|
||||
(void) ctx;
|
||||
}
|
||||
|
||||
OakAudioManager wrap(AudioManager *m)
|
||||
{
|
||||
OakAudioManager h = {};
|
||||
h.ctx = m;
|
||||
h.addref = &singleton_addref;
|
||||
h.release = &singleton_release;
|
||||
h.abi_version = OAKAUDIO_ABI_VERSION;
|
||||
return h;
|
||||
}
|
||||
|
||||
AudioManager *impl(OakAudioManager self)
|
||||
{
|
||||
return static_cast<AudioManager *>(self.ctx);
|
||||
}
|
||||
|
||||
int write_error(const std::string &s, char *buf, int buf_size)
|
||||
{
|
||||
if (buf && buf_size > 0) {
|
||||
const int n = std::min(int(s.size()), buf_size - 1);
|
||||
std::memcpy(buf, s.data(), size_t(n));
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return int(s.size()) + 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakaudio_manager_create_instance(void)
|
||||
{
|
||||
if (!AudioManager::instance()) {
|
||||
try {
|
||||
AudioManager::create_instance();
|
||||
} catch (...) {
|
||||
return OAKAUDIO_E_NOMEM;
|
||||
}
|
||||
}
|
||||
return AudioManager::instance() ? OAKAUDIO_OK : OAKAUDIO_E_NOMEM;
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_manager_destroy_instance(void)
|
||||
{
|
||||
AudioManager::destroy_instance();
|
||||
}
|
||||
|
||||
extern "C" OakAudioManager oakaudio_manager_instance(void)
|
||||
{
|
||||
return wrap(AudioManager::instance());
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_manager_free(OakAudioManager *self)
|
||||
{
|
||||
// Singleton: releasing never destroys; just clear the caller's copy.
|
||||
if (self) {
|
||||
self->ctx = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_set_output_notify_interval(
|
||||
OakAudioManager self, int64_t bytes)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (bytes < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
m->set_output_notify_interval(bytes);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_push_to_output(OakAudioManager self,
|
||||
int rate, uint64_t layout, int format,
|
||||
const char *samples, int64_t samples_size,
|
||||
char *error_buf, int error_buf_size)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (rate <= 0 || !samples || samples_size < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const AudioParams params(rate, layout,
|
||||
SampleFormat(SampleFormat::Format(format)));
|
||||
std::string error;
|
||||
if (!m->push_to_output(params, samples, samples_size, &error)) {
|
||||
if (error_buf && error_buf_size > 0) {
|
||||
write_error(error, error_buf, error_buf_size);
|
||||
}
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_clear_buffered_output(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->clear_buffered_output();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_stop_output(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->stop_output();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_seconds(OakAudioManager self, double *out)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (!out) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
*out = m->seconds();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_reset_output_clock(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->reset_output_clock();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_get_output_device(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
return int(m->get_output_device());
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_set_output_device(OakAudioManager self,
|
||||
int device)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->set_output_device(PaDeviceIndex(device));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_get_input_device(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
return int(m->get_input_device());
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_set_input_device(OakAudioManager self,
|
||||
int device)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->set_input_device(PaDeviceIndex(device));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_hard_reset(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->hard_reset();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_start_recording(OakAudioManager self,
|
||||
const oakcodec_encoding_params *params,
|
||||
char *error_buf, int error_buf_size)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (!params || !params->audio_enabled) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (!m->start_recording(*params, &error)) {
|
||||
if (error_buf && error_buf_size > 0) {
|
||||
write_error(error, error_buf, error_buf_size);
|
||||
}
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_stop_recording(OakAudioManager self)
|
||||
{
|
||||
AudioManager *m = impl(self);
|
||||
if (!m) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
m->stop_recording();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_find_config_device_by_name_s(
|
||||
int is_output_device)
|
||||
{
|
||||
return int(AudioManager::find_config_device_by_name(is_output_device != 0));
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_manager_find_device_by_name_s(const char *name,
|
||||
int is_output_device)
|
||||
{
|
||||
if (!name) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
return int(AudioManager::find_device_by_name(name, is_output_device != 0));
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audio/processor.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "audioprocessor.h"
|
||||
#include "refcounted.h"
|
||||
|
||||
using olive::AudioProcessor;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
extern "C" OakAudioProcessor oakaudio_processor_init(void)
|
||||
{
|
||||
return oakaudio::make_handle_in_place<OakAudioProcessor, AudioProcessor>();
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_processor_free(OakAudioProcessor *self)
|
||||
{
|
||||
oakaudio::free_handle(self);
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_open(OakAudioProcessor self,
|
||||
int in_rate, uint64_t in_layout, int in_format,
|
||||
int out_rate, uint64_t out_layout, int out_format, double speed)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (p->is_open()) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (in_rate <= 0 || out_rate <= 0 || speed <= 0.0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// The C ABI delivers planar float output only; force the output format
|
||||
// stage to f32p (see OAKAUDIO_PROCESSOR_OUTPUT_FORMAT).
|
||||
if (out_format != OAKAUDIO_PROCESSOR_OUTPUT_FORMAT) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const AudioParams from(in_rate, in_layout,
|
||||
SampleFormat(SampleFormat::Format(in_format)));
|
||||
const AudioParams to(out_rate, out_layout,
|
||||
SampleFormat(SampleFormat::Format(out_format)));
|
||||
|
||||
return p->open(from, to, speed) ? OAKAUDIO_OK : OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_close(OakAudioProcessor self)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
p->close();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_is_open(OakAudioProcessor self)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
return p->is_open() ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_convert(OakAudioProcessor self,
|
||||
const float *const *in_planar, int in_frame_count,
|
||||
float *const *out_planar, int out_capacity_frames)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!p->is_open()) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
if (in_frame_count < 0 || out_capacity_frames < 0 ||
|
||||
(in_frame_count > 0 && !in_planar)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const int channels = p->to().channel_count();
|
||||
if (channels <= 0) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
|
||||
AudioProcessor::Buffer buf;
|
||||
int r = p->convert(const_cast<float **>(in_planar), in_frame_count,
|
||||
out_planar ? &buf : nullptr);
|
||||
if (r < 0) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
if (!out_planar) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Output is planar f32 (enforced by open()); each buffer entry is one
|
||||
// channel's float plane.
|
||||
const int out_frames = buf.empty() ? 0 :
|
||||
int(buf[0].size() / sizeof(float));
|
||||
const int frames = std::min(out_frames, out_capacity_frames);
|
||||
for (int ch = 0; ch < channels && ch < int(buf.size()); ch++) {
|
||||
if (out_planar[ch]) {
|
||||
memcpy(out_planar[ch], buf[size_t(ch)].data(),
|
||||
size_t(frames) * sizeof(float));
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_processor_flush(OakAudioProcessor self)
|
||||
{
|
||||
AudioProcessor *p = oakaudio::handle_impl<AudioProcessor>(self.ctx);
|
||||
if (!p) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!p->is_open()) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
p->flush();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAKAUDIO_C_API_REFCOUNTED_H
|
||||
#define OAKAUDIO_C_API_REFCOUNTED_H
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "audio/error.h"
|
||||
|
||||
namespace oakaudio
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Heap box behind every handle's ctx pointer.
|
||||
*
|
||||
* Same pattern as oakcodec's c_api/refcounted.h: holds the wrapped
|
||||
* object plus its atomic reference count. addref and release are emitted
|
||||
* per boxed type so that the function pointers stored in a handle always
|
||||
* run code from the DLL that created the object. Every box also
|
||||
* participates in the oakaudio_debug_alive_count() ledger.
|
||||
*/
|
||||
template <typename T> struct RefCounted {
|
||||
T impl;
|
||||
std::atomic<uint32_t> refs;
|
||||
|
||||
template <typename... Args>
|
||||
explicit RefCounted(Args &&...args)
|
||||
: impl(std::forward<Args>(args)...)
|
||||
, refs(1)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T> void ref_counted_addref(void *ctx)
|
||||
{
|
||||
auto *box = static_cast<RefCounted<T> *>(ctx);
|
||||
if (box)
|
||||
box->refs.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void alive_inc();
|
||||
void alive_dec();
|
||||
|
||||
template <typename T> void ref_counted_release(void *ctx)
|
||||
{
|
||||
auto *box = static_cast<RefCounted<T> *>(ctx);
|
||||
if (box && box->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
delete box;
|
||||
alive_dec();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a by-value handle owning a freshly boxed object (count 1).
|
||||
*
|
||||
* On allocation failure the returned handle has ctx == NULL (all C API
|
||||
* functions treat that as OAKAUDIO_E_INVALID and free() as a no-op).
|
||||
*/
|
||||
template <typename Handle, typename T, typename... Args>
|
||||
Handle make_handle_in_place(Args &&...args)
|
||||
{
|
||||
Handle h = {};
|
||||
try {
|
||||
h.ctx = new RefCounted<T>(std::forward<Args>(args)...);
|
||||
alive_inc();
|
||||
} catch (...) {
|
||||
h.ctx = nullptr;
|
||||
}
|
||||
h.addref = &ref_counted_addref<T>;
|
||||
h.release = &ref_counted_release<T>;
|
||||
h.abi_version = OAKAUDIO_ABI_VERSION;
|
||||
return h;
|
||||
}
|
||||
|
||||
template <typename Handle, typename T> Handle make_handle(T &&value)
|
||||
{
|
||||
return make_handle_in_place<Handle, typename std::decay<T>::type>(
|
||||
std::forward<T>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Recover the boxed object from a handle ctx (NULL-safe).
|
||||
*/
|
||||
template <typename T> T *handle_impl(void *ctx)
|
||||
{
|
||||
auto *box = static_cast<RefCounted<T> *>(ctx);
|
||||
return box ? &box->impl : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shared free() body: release the ctx, no-op on NULL/empty handle.
|
||||
*/
|
||||
template <typename Handle> void free_handle(Handle *h)
|
||||
{
|
||||
if (!h || !h->ctx || !h->release)
|
||||
return;
|
||||
h->release(h->ctx);
|
||||
h->ctx = nullptr;
|
||||
}
|
||||
|
||||
} // namespace oakaudio
|
||||
|
||||
#endif // OAKAUDIO_C_API_REFCOUNTED_H
|
||||
@@ -0,0 +1,206 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audio/sync.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "audiosynchronizer.h"
|
||||
#include "audiowaveformsync.h"
|
||||
#include "ffmpeg_bridge/ffmpeg_bridge.h"
|
||||
|
||||
using olive::AudioSynchronizer;
|
||||
using olive::AudioWaveformSync;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::Rational;
|
||||
using olive::core::SampleBuffer;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<char> to_mask(const uint8_t *valid, int len)
|
||||
{
|
||||
std::vector<char> mask;
|
||||
if (valid) {
|
||||
mask.resize(size_t(len));
|
||||
for (int i = 0; i < len; i++) {
|
||||
mask[size_t(i)] = valid[i] ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int oakaudio_sync_extract_rms_envelope(
|
||||
const float *const *planar, int channel_count, int frame_count,
|
||||
uint64_t window_samples, double *out, int capacity)
|
||||
{
|
||||
if (!planar || channel_count <= 0 || frame_count < 0 ||
|
||||
!window_samples || capacity < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioParams params(48000, fb_channel_layout_default(channel_count),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(frame_count, 48000));
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (frame_count > 0) {
|
||||
memcpy(buffer.data(ch), planar[ch],
|
||||
size_t(frame_count) * sizeof(float));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<double> envelope =
|
||||
AudioWaveformSync::extract_rms_envelope(buffer, window_samples);
|
||||
const int windows = int(envelope.size());
|
||||
if (!out || capacity < windows) {
|
||||
return windows;
|
||||
}
|
||||
memcpy(out, envelope.data(), size_t(windows) * sizeof(double));
|
||||
return windows;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_estimate_envelope_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const uint8_t *reference_valid, const uint8_t *candidate_valid,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
oakaudio_offset_result *out)
|
||||
{
|
||||
if (!out || !reference || !candidate || reference_len <= 0 ||
|
||||
candidate_len <= 0 || !window_samples || max_offset_windows < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const std::vector<double> ref(reference, reference + reference_len);
|
||||
const std::vector<double> cand(candidate, candidate + candidate_len);
|
||||
const std::vector<char> ref_valid = to_mask(reference_valid, reference_len);
|
||||
const std::vector<char> cand_valid =
|
||||
to_mask(candidate_valid, candidate_len);
|
||||
|
||||
const AudioWaveformSync::OffsetResult r =
|
||||
AudioWaveformSync::estimate_envelope_offset(
|
||||
ref, cand, ref_valid, cand_valid, window_samples,
|
||||
max_offset_windows);
|
||||
|
||||
out->offset_samples = r.offset_samples;
|
||||
out->confidence = r.confidence;
|
||||
out->valid = r.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_estimate_stretch_and_offset(
|
||||
const double *reference, int reference_len,
|
||||
const double *candidate, int candidate_len,
|
||||
const uint8_t *reference_valid, const uint8_t *candidate_valid,
|
||||
uint64_t window_samples, int64_t max_offset_windows,
|
||||
double min_rate, double max_rate, double rate_step,
|
||||
oakaudio_stretch_offset_result *out)
|
||||
{
|
||||
if (!out || !reference || !candidate || reference_len <= 0 ||
|
||||
candidate_len <= 0 || !window_samples || max_offset_windows < 0 ||
|
||||
min_rate <= 0.0 || max_rate < min_rate || rate_step <= 0.0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const std::vector<double> ref(reference, reference + reference_len);
|
||||
const std::vector<double> cand(candidate, candidate + candidate_len);
|
||||
const std::vector<char> ref_valid = to_mask(reference_valid, reference_len);
|
||||
const std::vector<char> cand_valid =
|
||||
to_mask(candidate_valid, candidate_len);
|
||||
|
||||
const AudioWaveformSync::StretchOffsetResult r =
|
||||
AudioWaveformSync::estimate_stretch_and_offset(
|
||||
ref, cand, ref_valid, cand_valid, window_samples,
|
||||
max_offset_windows, min_rate, max_rate, rate_step);
|
||||
|
||||
out->rate = r.rate;
|
||||
out->offset_samples = r.offset_samples;
|
||||
out->confidence = r.confidence;
|
||||
out->valid = r.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_place_by_source_time(
|
||||
const oakaudio_source_clip *reference,
|
||||
const oakaudio_source_clip *candidate,
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
int64_t *out_num, int64_t *out_den, int *out_valid)
|
||||
{
|
||||
if (!reference || !candidate || !out_num || !out_den || !out_valid ||
|
||||
reference->source_start_time_den == 0 ||
|
||||
reference->media_in_den == 0 ||
|
||||
candidate->source_start_time_den == 0 ||
|
||||
candidate->media_in_den == 0 || reference_timeline_in_den == 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioSynchronizer::SourceClip ref;
|
||||
ref.source_start_time = Rational(int(reference->source_start_time_num),
|
||||
int(reference->source_start_time_den));
|
||||
ref.media_in = Rational(int(reference->media_in_num),
|
||||
int(reference->media_in_den));
|
||||
ref.has_source_start_time = reference->has_source_start_time != 0;
|
||||
|
||||
AudioSynchronizer::SourceClip cand;
|
||||
cand.source_start_time = Rational(int(candidate->source_start_time_num),
|
||||
int(candidate->source_start_time_den));
|
||||
cand.media_in = Rational(int(candidate->media_in_num),
|
||||
int(candidate->media_in_den));
|
||||
cand.has_source_start_time = candidate->has_source_start_time != 0;
|
||||
|
||||
const AudioSynchronizer::Placement p = AudioSynchronizer::place_by_source_time(
|
||||
ref, cand,
|
||||
Rational(int(reference_timeline_in_num),
|
||||
int(reference_timeline_in_den)));
|
||||
|
||||
*out_num = p.timeline_in.numerator();
|
||||
*out_den = p.timeline_in.denominator();
|
||||
*out_valid = p.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_sync_place_by_waveform_offset(
|
||||
int64_t reference_timeline_in_num, int64_t reference_timeline_in_den,
|
||||
int64_t candidate_offset_samples, int sample_rate,
|
||||
int64_t *out_num, int64_t *out_den, int *out_valid)
|
||||
{
|
||||
if (!out_num || !out_den || !out_valid ||
|
||||
reference_timeline_in_den == 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const AudioSynchronizer::Placement p =
|
||||
AudioSynchronizer::place_by_waveform_offset(
|
||||
Rational(int(reference_timeline_in_num),
|
||||
int(reference_timeline_in_den)),
|
||||
candidate_offset_samples, sample_rate);
|
||||
|
||||
*out_num = p.timeline_in.numerator();
|
||||
*out_den = p.timeline_in.denominator();
|
||||
*out_valid = p.valid ? 1 : 0;
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audio/waveform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "audiovisualwaveform.h"
|
||||
#include "codec/decoder.h"
|
||||
#include "ffmpeg_bridge/ffmpeg_bridge.h"
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
#include "refcounted.h"
|
||||
|
||||
using olive::AudioVisualWaveform;
|
||||
using olive::core::AudioParams;
|
||||
using olive::core::Rational;
|
||||
using olive::core::SampleBuffer;
|
||||
using olive::core::SampleFormat;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
AudioVisualWaveform::SamplePerChannel *as_pairs(oakaudio_min_max *p)
|
||||
{
|
||||
static_assert(sizeof(oakaudio_min_max) ==
|
||||
sizeof(AudioVisualWaveform::SamplePerChannel),
|
||||
"POD layout mismatch");
|
||||
return reinterpret_cast<AudioVisualWaveform::SamplePerChannel *>(p);
|
||||
}
|
||||
|
||||
const AudioVisualWaveform::SamplePerChannel *
|
||||
as_pairs_const(const oakaudio_min_max *p)
|
||||
{
|
||||
return reinterpret_cast<const AudioVisualWaveform::SamplePerChannel *>(p);
|
||||
}
|
||||
|
||||
bool make_rational(int64_t num, int64_t den, Rational *out)
|
||||
{
|
||||
if (den == 0) {
|
||||
return false;
|
||||
}
|
||||
*out = Rational(int(num), int(den));
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- oakaudio_waveform_extract() helpers --------------------------------- */
|
||||
|
||||
#define OAKAUDIO_EXTRACT_MAX_CHANNELS 64
|
||||
|
||||
using PendingPlanes = std::vector<std::vector<float>>;
|
||||
|
||||
void append_pending(PendingPlanes &pending, FBFrame *frame, int channels,
|
||||
int nb)
|
||||
{
|
||||
if (pending.empty()) {
|
||||
pending.resize(size_t(channels));
|
||||
}
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
const float *data =
|
||||
reinterpret_cast<const float *>(fb_frame_get_data(frame, ch));
|
||||
std::vector<float> &plane = pending[size_t(ch)];
|
||||
plane.insert(plane.end(), data, data + nb);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit one point per samples_per_point pending samples. With `flush`, a
|
||||
// trailing partial point is emitted too.
|
||||
void emit_points(int channels, int samples_per_point, PendingPlanes &pending,
|
||||
std::vector<oakaudio_min_max> &points, bool flush)
|
||||
{
|
||||
if (pending.empty()) {
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
const size_t available = pending[0].size();
|
||||
if (available == 0 ||
|
||||
(!flush && available < size_t(samples_per_point))) {
|
||||
return;
|
||||
}
|
||||
const size_t n = std::min(available, size_t(samples_per_point));
|
||||
|
||||
const size_t point = points.size() / size_t(channels);
|
||||
points.resize(points.size() + size_t(channels));
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
std::vector<float> &plane = pending[size_t(ch)];
|
||||
float mn = plane[0];
|
||||
float mx = mn;
|
||||
for (size_t i = 1; i < n; i++) {
|
||||
mn = std::min(mn, plane[i]);
|
||||
mx = std::max(mx, plane[i]);
|
||||
}
|
||||
oakaudio_min_max &dst =
|
||||
points[point * size_t(channels) + size_t(ch)];
|
||||
dst.min = mn;
|
||||
dst.max = mx;
|
||||
plane.erase(plane.begin(), plane.begin() + ptrdiff_t(n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int drain_graph(FBAudioGraph *graph, FBFrame *converted, int channels,
|
||||
int samples_per_point, PendingPlanes &pending,
|
||||
std::vector<oakaudio_min_max> &points)
|
||||
{
|
||||
while (true) {
|
||||
const int pull = fb_audio_graph_pull(graph, converted);
|
||||
if (pull < 0) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
if (pull == 0) {
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
append_pending(pending, converted, channels,
|
||||
fb_frame_get_nb_samples(converted));
|
||||
emit_points(channels, samples_per_point, pending, points, false);
|
||||
}
|
||||
}
|
||||
|
||||
void flush_points(int channels, int samples_per_point, PendingPlanes &pending,
|
||||
std::vector<oakaudio_min_max> &points)
|
||||
{
|
||||
emit_points(channels, samples_per_point, pending, points, true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" OakAudioWaveform oakaudio_waveform_init(void)
|
||||
{
|
||||
return oakaudio::make_handle_in_place<OakAudioWaveform,
|
||||
AudioVisualWaveform>();
|
||||
}
|
||||
|
||||
extern "C" void oakaudio_waveform_free(OakAudioWaveform *self)
|
||||
{
|
||||
oakaudio::free_handle(self);
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_get_channel_count(OakAudioWaveform self)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
return w->channel_count();
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_set_channel_count(OakAudioWaveform self,
|
||||
int channels)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (channels < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->set_channel_count(channels);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_length(OakAudioWaveform self,
|
||||
int64_t *num, int64_t *den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
if (!num || !den) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
*num = w->length().numerator();
|
||||
*den = w->length().denominator();
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_overwrite_samples(OakAudioWaveform self,
|
||||
const float *const *planar, int frame_count, int sample_rate,
|
||||
int64_t start_num, int64_t start_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational start;
|
||||
if (!planar || frame_count <= 0 || sample_rate <= 0 ||
|
||||
!make_rational(start_num, start_den, &start)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
const int channels = w->channel_count();
|
||||
if (channels <= 0) {
|
||||
return OAKAUDIO_E_STATE;
|
||||
}
|
||||
|
||||
// Repack the caller's planes into a SampleBuffer (planar f32).
|
||||
AudioParams params(sample_rate, fb_channel_layout_default(channels),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(frame_count, sample_rate));
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
}
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
memcpy(buffer.data(ch), planar[ch],
|
||||
size_t(frame_count) * sizeof(float));
|
||||
}
|
||||
|
||||
w->overwrite_samples(buffer, sample_rate, start);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_overwrite_sums(OakAudioWaveform self,
|
||||
OakAudioWaveform src,
|
||||
int64_t dest_num, int64_t dest_den,
|
||||
int64_t offset_num, int64_t offset_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
AudioVisualWaveform *other =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(src.ctx);
|
||||
if (!w || !other) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational dest, offset, length;
|
||||
if (!make_rational(dest_num, dest_den, &dest) ||
|
||||
!make_rational(offset_num, offset_den, &offset) ||
|
||||
!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->overwrite_sums(*other, dest, offset, length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_overwrite_silence(OakAudioWaveform self,
|
||||
int64_t start_num, int64_t start_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational start, length;
|
||||
if (!make_rational(start_num, start_den, &start) ||
|
||||
!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->overwrite_silence(start, length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_trim_in(OakAudioWaveform self,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational length;
|
||||
if (!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->trim_in(length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_resize(OakAudioWaveform self,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational length;
|
||||
if (!make_rational(length_num, length_den, &length) || length < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->resize(length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_trim_range(OakAudioWaveform self,
|
||||
int64_t in_num, int64_t in_den,
|
||||
int64_t length_num, int64_t length_den)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational in, length;
|
||||
if (!make_rational(in_num, in_den, &in) ||
|
||||
!make_rational(length_num, length_den, &length)) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
w->trim_range(in, length);
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_get_summary(OakAudioWaveform self,
|
||||
int64_t start_num, int64_t start_den,
|
||||
int64_t length_num, int64_t length_den,
|
||||
oakaudio_min_max *out_pairs, int capacity_points)
|
||||
{
|
||||
AudioVisualWaveform *w =
|
||||
oakaudio::handle_impl<AudioVisualWaveform>(self.ctx);
|
||||
if (!w) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
Rational start, length;
|
||||
if (!make_rational(start_num, start_den, &start) ||
|
||||
!make_rational(length_num, length_den, &length) || length <= 0 ||
|
||||
capacity_points < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// Points are produced at the length scale: one point per channel per
|
||||
// `length`-sized window covering [start, start+length) — i.e. exactly
|
||||
// one point, matching AudioVisualWaveform::get_summary_from_time().
|
||||
AudioVisualWaveform::Sample summary =
|
||||
w->get_summary_from_time(start, length);
|
||||
const int points = int(summary.size()) /
|
||||
std::max(1, w->channel_count());
|
||||
|
||||
if (!out_pairs || capacity_points < points) {
|
||||
return points;
|
||||
}
|
||||
memcpy(out_pairs, summary.data(),
|
||||
summary.size() * sizeof(oakaudio_min_max));
|
||||
return points;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_sum_samples_s(const float *const *planar,
|
||||
int channel_count, int start_index, int length,
|
||||
oakaudio_min_max *out)
|
||||
{
|
||||
if (!planar || !out || channel_count <= 0 || start_index < 0 ||
|
||||
length <= 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioParams params(48000, fb_channel_layout_default(channel_count),
|
||||
SampleFormat(SampleFormat::f32_p));
|
||||
SampleBuffer buffer(params, Rational(length + start_index, 48000));
|
||||
for (int ch = 0; ch < channel_count; ch++) {
|
||||
if (!planar[ch]) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
memcpy(buffer.data(ch) + start_index, planar[ch],
|
||||
size_t(length) * sizeof(float));
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample summary = AudioVisualWaveform::sum_samples(
|
||||
buffer, size_t(start_index), size_t(length));
|
||||
if (int(summary.size()) < channel_count) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
memcpy(out, summary.data(),
|
||||
size_t(channel_count) * sizeof(oakaudio_min_max));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_re_sum_s(const oakaudio_min_max *in,
|
||||
int nb_entries, int nb_channels, oakaudio_min_max *out)
|
||||
{
|
||||
if (!in || !out || nb_entries <= 0 || nb_channels <= 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample summary = AudioVisualWaveform::re_sum_samples(
|
||||
as_pairs_const(in), size_t(nb_entries), nb_channels);
|
||||
memcpy(out, summary.data(),
|
||||
size_t(nb_channels) * sizeof(oakaudio_min_max));
|
||||
return OAKAUDIO_OK;
|
||||
}
|
||||
|
||||
extern "C" int oakaudio_waveform_extract(const char *filename,
|
||||
int stream_index, int samples_per_point,
|
||||
oakaudio_min_max *out_pairs, int capacity_points,
|
||||
int *out_channel_count)
|
||||
{
|
||||
if (!filename || stream_index < 0 || samples_per_point <= 0 ||
|
||||
capacity_points < 0) {
|
||||
return OAKAUDIO_E_INVALID;
|
||||
}
|
||||
|
||||
// Probe for the stream's native rate/layout (oakcodec probe is
|
||||
// stateless and does not need a conform)
|
||||
OakDecoder probe = oakcodec_decoder_probe(filename);
|
||||
if (!probe.ctx) {
|
||||
return OAKAUDIO_E_NOT_FOUND;
|
||||
}
|
||||
oakcodec_audio_stream_info info;
|
||||
int r = oakcodec_decoder_probe_get_audio_stream(probe, stream_index,
|
||||
&info);
|
||||
oakcodec_decoder_free(&probe);
|
||||
if (r != OAKCODEC_OK) {
|
||||
return OAKAUDIO_E_NOT_FOUND;
|
||||
}
|
||||
if (info.sample_rate <= 0 || info.channel_count <= 0) {
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
// Decode the whole stream through ffmpeg_bridge (fb_decoder +
|
||||
// fb_audio_graph) rather than oakcodec_decoder_decode_audio: the
|
||||
// oakcodec decode path is conform-cache based and cannot decode media
|
||||
// without an existing pcm conform until the task system lands (M8).
|
||||
// The stream is reduced to channel-interleaved min/max points at the
|
||||
// native rate/layout.
|
||||
FBDecoder *decoder = fb_decoder_create();
|
||||
if (!decoder) {
|
||||
return OAKAUDIO_E_NOMEM;
|
||||
}
|
||||
r = fb_decoder_open(decoder, filename, info.stream_index);
|
||||
if (r < 0) {
|
||||
fb_decoder_free(&decoder);
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
const int channels = info.channel_count;
|
||||
std::vector<oakaudio_min_max> points;
|
||||
PendingPlanes pending; // per-channel planar backlog
|
||||
|
||||
// The graph converts the stream's native format to planar float; the
|
||||
// stream info carries the validated sample format/rate/layout (audio
|
||||
// frames do not report a sample format through fb_frame_get_format).
|
||||
FBStreamInfo sinfo;
|
||||
if (fb_decoder_get_stream_info(decoder, &sinfo) < 0 ||
|
||||
sinfo.sample_rate <= 0) {
|
||||
fb_decoder_close(decoder);
|
||||
fb_decoder_free(&decoder);
|
||||
return OAKAUDIO_E_FAILED;
|
||||
}
|
||||
|
||||
FBAudioGraphConfig config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.in_sample_rate = sinfo.sample_rate;
|
||||
config.in_channel_layout_mask = sinfo.channel_layout_mask;
|
||||
config.in_sample_format = sinfo.sample_format;
|
||||
config.in_channels = channels;
|
||||
config.out_sample_rate = config.in_sample_rate;
|
||||
config.out_channel_layout_mask = config.in_channel_layout_mask;
|
||||
config.out_sample_format = fb_sample_fmt_fltp;
|
||||
config.out_channels = channels;
|
||||
config.out_is_planar = 1;
|
||||
config.tempo = 1.0;
|
||||
|
||||
FBPacket *packet = fb_packet_alloc();
|
||||
FBFrame *frame = fb_frame_alloc();
|
||||
FBFrame *converted = fb_frame_alloc();
|
||||
FBAudioGraph *graph = fb_audio_graph_create(&config);
|
||||
int result = OAKAUDIO_OK;
|
||||
|
||||
if (!packet || !frame || !converted) {
|
||||
result = OAKAUDIO_E_NOMEM;
|
||||
goto done;
|
||||
}
|
||||
if (!graph) {
|
||||
result = OAKAUDIO_E_FAILED;
|
||||
goto done;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (fb_decoder_get_frame(decoder, packet, frame) < 0) {
|
||||
break; // EOF or error: stop decoding
|
||||
}
|
||||
|
||||
// Push the decoded frame (planar pointer array; a packed source is
|
||||
// read from plane 0 by the buffersrc)
|
||||
const uint8_t *planes[OAKAUDIO_EXTRACT_MAX_CHANNELS];
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
planes[ch] = fb_frame_get_data(frame, ch);
|
||||
}
|
||||
if (fb_audio_graph_push(graph, planes,
|
||||
fb_frame_get_nb_samples(frame)) < 0) {
|
||||
result = OAKAUDIO_E_FAILED;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (drain_graph(graph, converted, channels, samples_per_point,
|
||||
pending, points) != OAKAUDIO_OK) {
|
||||
result = OAKAUDIO_E_FAILED;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the resampler delay
|
||||
if (graph) {
|
||||
fb_audio_graph_push(graph, nullptr, 0);
|
||||
while (fb_audio_graph_pull(graph, converted) == 1) {
|
||||
append_pending(pending, converted, channels,
|
||||
fb_frame_get_nb_samples(converted));
|
||||
}
|
||||
flush_points(channels, samples_per_point, pending, points);
|
||||
}
|
||||
|
||||
done:
|
||||
if (graph) {
|
||||
fb_audio_graph_free(&graph);
|
||||
}
|
||||
if (converted) {
|
||||
fb_frame_free(&converted);
|
||||
}
|
||||
if (frame) {
|
||||
fb_frame_free(&frame);
|
||||
}
|
||||
if (packet) {
|
||||
fb_packet_free(&packet);
|
||||
}
|
||||
fb_decoder_close(decoder);
|
||||
fb_decoder_free(&decoder);
|
||||
if (result != OAKAUDIO_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (out_channel_count) {
|
||||
*out_channel_count = channels;
|
||||
}
|
||||
|
||||
const int point_count = int(points.size()) / channels;
|
||||
if (!out_pairs || capacity_points < point_count) {
|
||||
return point_count;
|
||||
}
|
||||
memcpy(out_pairs, points.data(),
|
||||
points.size() * sizeof(oakaudio_min_max));
|
||||
return point_count;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# Oak Video Editor - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
add_library(oakaudio SHARED
|
||||
audiolevelmeter.cpp
|
||||
audiolevelmeter.h
|
||||
audiomanager.cpp
|
||||
audiomanager.h
|
||||
audioprocessor.cpp
|
||||
audioprocessor.h
|
||||
audiosynchronizer.cpp
|
||||
audiosynchronizer.h
|
||||
audiovisualwaveform.cpp
|
||||
audiovisualwaveform.h
|
||||
audiowaveformsync.cpp
|
||||
audiowaveformsync.h
|
||||
configbridge.cpp
|
||||
configbridge.h
|
||||
previewaudiodevice.cpp
|
||||
previewaudiodevice.h
|
||||
)
|
||||
|
||||
# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
|
||||
# build (see src/audio/standalone) sets OAK_REPO_ROOT explicitly.
|
||||
if(NOT DEFINED OAK_REPO_ROOT)
|
||||
set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
find_package(PortAudio REQUIRED)
|
||||
|
||||
target_include_directories(oakaudio PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${OAK_REPO_ROOT}/include
|
||||
${OAK_REPO_ROOT}/core/include
|
||||
${OAK_REPO_ROOT}/ffmpeg_bridge/include
|
||||
${PORTAUDIO_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# 01 §1 rule 5: only the OAKAUDIO_API-marked C functions are exported;
|
||||
# audio-internal C++ classes must not leak into the global symbol
|
||||
# namespace.
|
||||
target_compile_options(oakaudio PRIVATE
|
||||
-fvisibility=hidden
|
||||
-fvisibility-inlines-hidden
|
||||
)
|
||||
|
||||
# Cross-module access goes through C ABIs only: oakcommon (config,
|
||||
# ffmpegutils), oakcodec (encoder for recording, decoder for waveform
|
||||
# extraction), olivecore (Rational/AudioParams/SampleBuffer wrappers),
|
||||
# ffmpeg_bridge (fb_audio_graph resampler infra, same precedent as
|
||||
# oakcommon/oakcodec), PortAudio (output device).
|
||||
target_link_libraries(oakaudio PUBLIC
|
||||
oakcommon
|
||||
oakcodec
|
||||
olivecore
|
||||
ffmpeg_bridge
|
||||
${PORTAUDIO_LIBRARIES}
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiolevelmeter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// De-Qt note: engine/common/decibel.h pulls in QtGlobal, so the two
|
||||
// constants/functions used here are inlined (same math: minimum = -200,
|
||||
// from_linear = 20*log10 clamped to minimum on -inf).
|
||||
static constexpr double k_decibel_minimum = -200.0;
|
||||
|
||||
static double decibel_from_linear(double linear)
|
||||
{
|
||||
double v = 20.0 * std::log10(linear);
|
||||
if (std::isinf(v)) {
|
||||
return k_decibel_minimum;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
AudioLevelMeter::Stats
|
||||
AudioLevelMeter::analyze_sample_buffer(const core::SampleBuffer &samples)
|
||||
{
|
||||
Stats stats;
|
||||
|
||||
const int channel_count = samples.channel_count();
|
||||
const size_t sample_count = samples.sample_count();
|
||||
stats.channels.resize(channel_count);
|
||||
|
||||
if (!channel_count || !sample_count) {
|
||||
return stats;
|
||||
}
|
||||
|
||||
double total_square = 0.0;
|
||||
size_t total_samples = 0;
|
||||
|
||||
for (int channel = 0; channel < channel_count; channel++) {
|
||||
const float *channel_data = samples.data(channel);
|
||||
double peak = 0.0;
|
||||
double square_sum = 0.0;
|
||||
|
||||
for (size_t sample = 0; sample < sample_count; sample++) {
|
||||
const double value = channel_data[sample];
|
||||
const double abs_value = std::abs(value);
|
||||
|
||||
peak = std::max(peak, abs_value);
|
||||
square_sum += value * value;
|
||||
}
|
||||
|
||||
const double mean_square =
|
||||
square_sum / static_cast<double>(sample_count);
|
||||
const double rms = std::sqrt(mean_square);
|
||||
|
||||
ChannelStats channel_stats;
|
||||
channel_stats.peak_linear = peak;
|
||||
channel_stats.peak_db = linear_to_db(peak);
|
||||
channel_stats.rms_linear = rms;
|
||||
channel_stats.rms_db = linear_to_db(rms);
|
||||
channel_stats.vu_db = channel_stats.rms_db;
|
||||
stats.channels[channel] = channel_stats;
|
||||
|
||||
stats.max_peak_linear = std::max(stats.max_peak_linear, peak);
|
||||
total_square += square_sum;
|
||||
total_samples += sample_count;
|
||||
}
|
||||
|
||||
// qFuzzyIsNull(double): |x| < 1e-12
|
||||
stats.silence = std::abs(stats.max_peak_linear) < 1e-12;
|
||||
stats.integrated_lufs =
|
||||
power_to_lufs(total_square / static_cast<double>(total_samples));
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
double AudioLevelMeter::linear_to_db(double linear)
|
||||
{
|
||||
if (linear <= 0.0) {
|
||||
return k_decibel_minimum;
|
||||
}
|
||||
|
||||
return decibel_from_linear(linear);
|
||||
}
|
||||
|
||||
double AudioLevelMeter::power_to_lufs(double mean_square)
|
||||
{
|
||||
if (mean_square <= 0.0) {
|
||||
return k_decibel_minimum;
|
||||
}
|
||||
|
||||
// BS.1770 loudness uses K-weighted mean square. This first pass stores the
|
||||
// compatible unit and can be extended with K-weighting without changing UI.
|
||||
return -0.691 + 10.0 * std::log10(mean_square);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOLEVELMETER_H
|
||||
#define OAK_AUDIOLEVELMETER_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioLevelMeter {
|
||||
public:
|
||||
struct ChannelStats {
|
||||
double peak_linear = 0.0;
|
||||
double peak_db = -200.0;
|
||||
double rms_linear = 0.0;
|
||||
double rms_db = -200.0;
|
||||
double vu_db = -200.0;
|
||||
};
|
||||
|
||||
struct Stats {
|
||||
std::vector<ChannelStats> channels;
|
||||
double max_peak_linear = 0.0;
|
||||
double integrated_lufs = -200.0;
|
||||
bool silence = true;
|
||||
};
|
||||
|
||||
static Stats analyze_sample_buffer(const core::SampleBuffer &samples);
|
||||
|
||||
private:
|
||||
static double linear_to_db(double linear);
|
||||
static double power_to_lufs(double mean_square);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOLEVELMETER_H
|
||||
@@ -0,0 +1,523 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiomanager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef PA_HAS_JACK
|
||||
#include <pa_jack.h>
|
||||
#endif
|
||||
|
||||
#include "configbridge.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
AudioManager *AudioManager::instance_ = nullptr;
|
||||
|
||||
void AudioManager::create_instance()
|
||||
{
|
||||
if (instance_ == nullptr) {
|
||||
instance_ = new AudioManager();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioManager::destroy_instance()
|
||||
{
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
}
|
||||
|
||||
AudioManager *AudioManager::instance()
|
||||
{
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void AudioManager::set_output_notify_interval(int64_t n)
|
||||
{
|
||||
output_buffer_->set_notify_interval(n);
|
||||
}
|
||||
|
||||
void AudioManager::set_output_notify_callback(std::function<void()> callback)
|
||||
{
|
||||
output_buffer_->set_notify_callback(std::move(callback));
|
||||
}
|
||||
|
||||
int output_callback(const void *input, void *output, unsigned long frame_count,
|
||||
const PaStreamCallbackTimeInfo *time_info,
|
||||
PaStreamCallbackFlags status_flags, void *user_data)
|
||||
{
|
||||
(void) input;
|
||||
(void) time_info;
|
||||
(void) status_flags;
|
||||
|
||||
PreviewAudioDevice *device = static_cast<PreviewAudioDevice *>(user_data);
|
||||
|
||||
int64_t max_read = int64_t(frame_count) * device->bytes_per_frame();
|
||||
int64_t read_count =
|
||||
device->read(reinterpret_cast<char *>(output), max_read);
|
||||
if (read_count < max_read) {
|
||||
memset(reinterpret_cast<uint8_t *>(output) + read_count, 0,
|
||||
size_t(max_read - read_count));
|
||||
}
|
||||
|
||||
// Count all frames leaving the device (including zero-filled underrun
|
||||
// frames) so this can serve as the playback master clock
|
||||
device->add_output_frames(frame_count);
|
||||
|
||||
return paContinue;
|
||||
}
|
||||
|
||||
int input_callback(const void *input, void *output, unsigned long frame_count,
|
||||
const PaStreamCallbackTimeInfo *time_info,
|
||||
PaStreamCallbackFlags status_flags, void *user_data)
|
||||
{
|
||||
(void) output;
|
||||
(void) time_info;
|
||||
(void) status_flags;
|
||||
|
||||
// The oakcodec encoder write path accepts interleaved float32 only; the
|
||||
// input stream is opened with paFloat32 (see start_recording()).
|
||||
OakEncoder *encoder = static_cast<OakEncoder *>(user_data);
|
||||
|
||||
oakcodec_encoder_write_audio(*encoder,
|
||||
reinterpret_cast<const float *>(input),
|
||||
int(frame_count));
|
||||
|
||||
return paContinue;
|
||||
}
|
||||
|
||||
bool AudioManager::push_to_output(const core::AudioParams ¶ms,
|
||||
const char *samples, int64_t samples_size,
|
||||
std::string *error)
|
||||
{
|
||||
if (output_device_ == paNoDevice) {
|
||||
if (error)
|
||||
*error = "No output device is set";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (output_params_ != params || output_stream_ == nullptr) {
|
||||
output_params_ = params;
|
||||
|
||||
close_output_stream();
|
||||
|
||||
PaStreamParameters p = get_port_audio_params(params, output_device_);
|
||||
|
||||
// 0 = let PortAudio choose the buffer size
|
||||
const unsigned long frames_per_buffer =
|
||||
(unsigned long) audio_config::output_buffer_size();
|
||||
|
||||
PaError r = Pa_OpenStream(&output_stream_, nullptr, &p,
|
||||
output_params_.sample_rate(),
|
||||
frames_per_buffer, paNoFlag, output_callback,
|
||||
output_buffer_);
|
||||
if (r != paNoError) {
|
||||
// Unhandled error
|
||||
fprintf(stderr,
|
||||
"AudioManager::push_to_output: Pa_OpenStream failed: %s\n",
|
||||
Pa_GetErrorText(r));
|
||||
if (error)
|
||||
*error = Pa_GetErrorText(r);
|
||||
return false;
|
||||
}
|
||||
|
||||
output_buffer_->set_bytes_per_frame(output_params_.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
output_buffer_->write(samples, samples_size);
|
||||
|
||||
if (!Pa_IsStreamActive(output_stream_)) {
|
||||
PaError r = Pa_StartStream(output_stream_);
|
||||
if (r != paNoError) {
|
||||
fprintf(stderr,
|
||||
"AudioManager::push_to_output: Pa_StartStream returned "
|
||||
"%d %s\n",
|
||||
r, Pa_GetErrorText(r));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioManager::clear_buffered_output()
|
||||
{
|
||||
output_buffer_->clear();
|
||||
}
|
||||
|
||||
double AudioManager::seconds() const
|
||||
{
|
||||
if (!output_stream_ || !Pa_IsStreamActive(output_stream_)) {
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
double seconds = double(output_buffer_->output_frames_consumed()) /
|
||||
double(output_params_.sample_rate());
|
||||
|
||||
// Compensate for output latency so the clock reflects what is audible
|
||||
if (const PaStreamInfo *info = Pa_GetStreamInfo(output_stream_)) {
|
||||
seconds -= info->outputLatency;
|
||||
}
|
||||
|
||||
return std::max(0.0, seconds);
|
||||
}
|
||||
|
||||
void AudioManager::reset_output_clock()
|
||||
{
|
||||
output_buffer_->reset_output_frames();
|
||||
}
|
||||
|
||||
PaSampleFormat AudioManager::get_port_audio_sample_format(core::SampleFormat fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case core::SampleFormat::u8:
|
||||
case core::SampleFormat::u8_p:
|
||||
return paUInt8;
|
||||
case core::SampleFormat::s16:
|
||||
case core::SampleFormat::s16_p:
|
||||
return paInt16;
|
||||
case core::SampleFormat::s32:
|
||||
case core::SampleFormat::s32_p:
|
||||
return paInt32;
|
||||
case core::SampleFormat::f32:
|
||||
case core::SampleFormat::f32_p:
|
||||
return paFloat32;
|
||||
case core::SampleFormat::s64:
|
||||
case core::SampleFormat::s64_p:
|
||||
case core::SampleFormat::f64:
|
||||
case core::SampleFormat::f64_p:
|
||||
case core::SampleFormat::invalid:
|
||||
case core::SampleFormat::count:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AudioManager::close_output_stream()
|
||||
{
|
||||
if (output_stream_) {
|
||||
if (Pa_IsStreamActive(output_stream_)) {
|
||||
stop_output();
|
||||
}
|
||||
Pa_CloseStream(output_stream_);
|
||||
output_stream_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioManager::stop_output()
|
||||
{
|
||||
// Abort the stream so playback stops immediately
|
||||
if (output_stream_) {
|
||||
Pa_AbortStream(output_stream_);
|
||||
clear_buffered_output();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioManager::set_output_device(PaDeviceIndex device)
|
||||
{
|
||||
if (device == paNoDevice) {
|
||||
fprintf(stderr, "AudioManager: no output device found\n");
|
||||
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
|
||||
fprintf(stderr, "AudioManager: invalid output audio device index: "
|
||||
"%d\n",
|
||||
device);
|
||||
} else {
|
||||
fprintf(stderr, "AudioManager: setting output audio device to %s\n",
|
||||
Pa_GetDeviceInfo(device)->name);
|
||||
}
|
||||
|
||||
output_device_ = device;
|
||||
|
||||
close_output_stream();
|
||||
}
|
||||
|
||||
void AudioManager::set_input_device(PaDeviceIndex device)
|
||||
{
|
||||
if (device == paNoDevice) {
|
||||
fprintf(stderr, "AudioManager: no input device found\n");
|
||||
} else if (device < 0 || device >= Pa_GetDeviceCount()) {
|
||||
fprintf(stderr, "AudioManager: invalid input audio device index: %d\n",
|
||||
device);
|
||||
} else {
|
||||
fprintf(stderr, "AudioManager: setting input audio device to %s\n",
|
||||
Pa_GetDeviceInfo(device)->name);
|
||||
}
|
||||
|
||||
input_device_ = device;
|
||||
}
|
||||
|
||||
void AudioManager::hard_reset()
|
||||
{
|
||||
close_output_stream();
|
||||
Pa_Terminate();
|
||||
Pa_Initialize();
|
||||
}
|
||||
|
||||
bool AudioManager::start_recording(const oakcodec_encoding_params ¶ms,
|
||||
std::string *error_str)
|
||||
{
|
||||
if (input_device_ == paNoDevice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
input_encoder_ = oakcodec_encoder_init(¶ms);
|
||||
if (!input_encoder_.ctx || oakcodec_encoder_open(input_encoder_) != 0) {
|
||||
fprintf(stderr,
|
||||
"AudioManager: failed to open encoder for recording\n");
|
||||
if (input_encoder_.ctx) {
|
||||
char buf[512];
|
||||
if (oakcodec_encoder_last_error(input_encoder_, buf,
|
||||
int(sizeof(buf))) > 0 &&
|
||||
error_str) {
|
||||
*error_str = buf;
|
||||
}
|
||||
oakcodec_encoder_free(&input_encoder_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The oakcodec encoder write path takes interleaved float32; capture in
|
||||
// that format regardless of the target encoding sample format.
|
||||
core::AudioParams stream_params(params.audio_sample_rate,
|
||||
params.audio_channel_layout,
|
||||
core::SampleFormat::f32);
|
||||
PaStreamParameters p =
|
||||
get_port_audio_params(stream_params, input_device_);
|
||||
|
||||
PaError r = Pa_OpenStream(&input_stream_, &p, nullptr,
|
||||
params.audio_sample_rate,
|
||||
paFramesPerBufferUnspecified, paNoFlag,
|
||||
input_callback, &input_encoder_);
|
||||
if (r == paNoError) {
|
||||
r = Pa_StartStream(input_stream_);
|
||||
if (r == paNoError) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (error_str) {
|
||||
*error_str = Pa_GetErrorText(r);
|
||||
}
|
||||
|
||||
stop_recording();
|
||||
return false;
|
||||
}
|
||||
|
||||
void AudioManager::stop_recording()
|
||||
{
|
||||
if (input_stream_) {
|
||||
if (Pa_IsStreamActive(input_stream_)) {
|
||||
Pa_StopStream(input_stream_);
|
||||
}
|
||||
Pa_CloseStream(input_stream_);
|
||||
|
||||
input_stream_ = nullptr;
|
||||
}
|
||||
|
||||
if (input_encoder_.ctx) {
|
||||
oakcodec_encoder_flush(input_encoder_);
|
||||
oakcodec_encoder_free(&input_encoder_);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
static bool str_contains_ci(const char *haystack, const char *needle)
|
||||
{
|
||||
const size_t needle_len = strlen(needle);
|
||||
if (!needle_len) {
|
||||
return true;
|
||||
}
|
||||
for (const char *p = haystack; *p; p++) {
|
||||
if (strncasecmp(p, needle, needle_len) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_preferred_linux_audio_host_api(const PaHostApiInfo *info)
|
||||
{
|
||||
if (!info) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return str_contains_ci(info->name, "PipeWire") ||
|
||||
str_contains_ci(info->name, "JACK") ||
|
||||
str_contains_ci(info->name, "PulseAudio");
|
||||
}
|
||||
|
||||
static PaDeviceIndex get_preferred_linux_audio_device(bool is_output_device)
|
||||
{
|
||||
// Prefer sound servers that provide mixing and desktop integration
|
||||
// (PipeWire, JACK, PulseAudio) over plain ALSA defaults, which often
|
||||
// fail to share the device on modern Linux desktops.
|
||||
static const char *const preferred_host_apis[] = {
|
||||
"PipeWire",
|
||||
"JACK",
|
||||
"PulseAudio",
|
||||
};
|
||||
|
||||
for (const char *preferred : preferred_host_apis) {
|
||||
for (PaHostApiIndex i = 0, end = Pa_GetHostApiCount(); i < end; i++) {
|
||||
const PaHostApiInfo *info = Pa_GetHostApiInfo(i);
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains_ci(info->name, preferred)) {
|
||||
PaDeviceIndex dev = is_output_device ? info->defaultOutputDevice :
|
||||
info->defaultInputDevice;
|
||||
if (dev != paNoDevice) {
|
||||
return dev;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return is_output_device ? Pa_GetDefaultOutputDevice() :
|
||||
Pa_GetDefaultInputDevice();
|
||||
}
|
||||
#endif
|
||||
|
||||
PaDeviceIndex AudioManager::find_config_device_by_name(bool is_output_device)
|
||||
{
|
||||
return find_device_by_name(
|
||||
audio_config::device_name(is_output_device), is_output_device);
|
||||
}
|
||||
|
||||
PaDeviceIndex AudioManager::find_device_by_name(const std::string &s,
|
||||
bool is_output_device)
|
||||
{
|
||||
PaDeviceIndex exact_match = paNoDevice;
|
||||
|
||||
if (!s.empty()) {
|
||||
for (PaDeviceIndex i = 0, end = Pa_GetDeviceCount(); i < end; i++) {
|
||||
const PaDeviceInfo *device = Pa_GetDeviceInfo(i);
|
||||
if (!device) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (((is_output_device && device->maxOutputChannels) ||
|
||||
(!is_output_device && device->maxInputChannels)) &&
|
||||
s == device->name) {
|
||||
exact_match = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
// Even if the user/config picked a device by name, upgrade to a preferred
|
||||
// host API (PipeWire/JACK/PulseAudio) when one is available. This avoids
|
||||
// getting stuck on an ALSA device that cannot share the hardware.
|
||||
if (exact_match != paNoDevice) {
|
||||
const PaDeviceInfo *matched_info = Pa_GetDeviceInfo(exact_match);
|
||||
if (matched_info) {
|
||||
const PaHostApiInfo *host_api =
|
||||
Pa_GetHostApiInfo(matched_info->hostApi);
|
||||
if (is_preferred_linux_audio_host_api(host_api)) {
|
||||
// Keep an explicit choice that already uses a preferred API.
|
||||
return exact_match;
|
||||
}
|
||||
|
||||
// Upgrade a non-preferred (e.g. ALSA) match to a preferred backend
|
||||
// when one is available.
|
||||
PaDeviceIndex preferred =
|
||||
get_preferred_linux_audio_device(is_output_device);
|
||||
if (preferred != paNoDevice) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
// No preferred backend available; keep the saved device.
|
||||
return exact_match;
|
||||
}
|
||||
}
|
||||
|
||||
return get_preferred_linux_audio_device(is_output_device);
|
||||
#else
|
||||
if (exact_match != paNoDevice) {
|
||||
return exact_match;
|
||||
}
|
||||
|
||||
return is_output_device ? Pa_GetDefaultOutputDevice() :
|
||||
Pa_GetDefaultInputDevice();
|
||||
#endif
|
||||
}
|
||||
|
||||
PaStreamParameters AudioManager::get_port_audio_params(const core::AudioParams ¶ms,
|
||||
PaDeviceIndex device)
|
||||
{
|
||||
PaStreamParameters p;
|
||||
|
||||
p.channelCount = params.channel_count();
|
||||
p.device = device;
|
||||
p.hostApiSpecificStreamInfo = nullptr;
|
||||
p.sampleFormat = get_port_audio_sample_format(params.format());
|
||||
|
||||
if (device >= 0 && device < Pa_GetDeviceCount()) {
|
||||
p.suggestedLatency = Pa_GetDeviceInfo(device)->defaultLowOutputLatency;
|
||||
} else {
|
||||
p.suggestedLatency = 0;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
AudioManager::AudioManager()
|
||||
: output_stream_(nullptr)
|
||||
, input_stream_(nullptr)
|
||||
{
|
||||
input_encoder_.ctx = nullptr;
|
||||
input_encoder_.addref = nullptr;
|
||||
input_encoder_.release = nullptr;
|
||||
input_encoder_.abi_version = 0;
|
||||
|
||||
#ifdef PA_HAS_JACK
|
||||
// PortAudio doesn't do a strcpy, so we need a const char that's readily accessible
|
||||
PaJack_SetClientName("Oak Video Editor");
|
||||
#endif
|
||||
|
||||
Pa_Initialize();
|
||||
|
||||
// Get device from config
|
||||
PaDeviceIndex output_device = find_config_device_by_name(true);
|
||||
PaDeviceIndex input_device = find_config_device_by_name(false);
|
||||
|
||||
set_output_device(output_device);
|
||||
set_input_device(input_device);
|
||||
|
||||
output_buffer_ = new PreviewAudioDevice();
|
||||
}
|
||||
|
||||
AudioManager::~AudioManager()
|
||||
{
|
||||
close_output_stream();
|
||||
|
||||
delete output_buffer_;
|
||||
|
||||
Pa_Terminate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOMANAGER_H
|
||||
#define OAK_AUDIOMANAGER_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <portaudio.h>
|
||||
|
||||
#include "codec/encoder.h"
|
||||
#include "olive/core/render/audioparams.h"
|
||||
#include "previewaudiodevice.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Audio input and output management class
|
||||
*
|
||||
* Wraps a PortAudio output stream and a PreviewAudioDevice pull buffer,
|
||||
* exposing audio functionality to the rest of the system.
|
||||
*
|
||||
* De-Qt notes:
|
||||
* - No longer a QObject and no longer inherits PlaybackAudioClock (the
|
||||
* clock interface lives in engine/common, which is not split); the
|
||||
* seconds() method is kept with the same semantics.
|
||||
* - The output_params_changed / output_notify signals are gone; the
|
||||
* notify-interval pulse is delivered through an optional
|
||||
* std::function (set_output_notify_callback) instead.
|
||||
* - Recording goes through the oakcodec encoder C ABI (OakEncoder)
|
||||
* instead of the FFmpegEncoder C++ class; the input stream is always
|
||||
* captured as interleaved float32.
|
||||
*/
|
||||
class AudioManager {
|
||||
public:
|
||||
static void create_instance();
|
||||
static void destroy_instance();
|
||||
|
||||
static AudioManager *instance();
|
||||
|
||||
void set_output_notify_interval(int64_t n);
|
||||
|
||||
/**
|
||||
* @brief Optional callback fired when a notify interval boundary is
|
||||
* crossed (called from the PortAudio callback thread)
|
||||
*/
|
||||
void set_output_notify_callback(std::function<void()> callback);
|
||||
|
||||
bool push_to_output(const core::AudioParams ¶ms, const char *samples,
|
||||
int64_t samples_size, std::string *error = nullptr);
|
||||
|
||||
void clear_buffered_output();
|
||||
|
||||
void stop_output();
|
||||
|
||||
/**
|
||||
* @brief Seconds of audio consumed by the output device since the last reset
|
||||
*
|
||||
* Compensated for output latency so it represents what is actually
|
||||
* audible. Returns a negative value when no output stream is running.
|
||||
*/
|
||||
double seconds() const;
|
||||
|
||||
/**
|
||||
* @brief Restarts the output clock at zero for a new playback run
|
||||
*/
|
||||
void reset_output_clock();
|
||||
|
||||
PaDeviceIndex get_output_device() const
|
||||
{
|
||||
return output_device_;
|
||||
}
|
||||
|
||||
PaDeviceIndex get_input_device() const
|
||||
{
|
||||
return input_device_;
|
||||
}
|
||||
|
||||
void set_output_device(PaDeviceIndex device);
|
||||
|
||||
void set_input_device(PaDeviceIndex device);
|
||||
|
||||
void hard_reset();
|
||||
|
||||
bool start_recording(const oakcodec_encoding_params ¶ms,
|
||||
std::string *error_str = nullptr);
|
||||
|
||||
void stop_recording();
|
||||
|
||||
static PaDeviceIndex find_config_device_by_name(bool is_output_device);
|
||||
static PaDeviceIndex find_device_by_name(const std::string &s,
|
||||
bool is_output_device);
|
||||
|
||||
static PaStreamParameters get_port_audio_params(const core::AudioParams &p,
|
||||
PaDeviceIndex device);
|
||||
|
||||
private:
|
||||
AudioManager();
|
||||
|
||||
~AudioManager();
|
||||
|
||||
static PaSampleFormat get_port_audio_sample_format(core::SampleFormat fmt);
|
||||
|
||||
void close_output_stream();
|
||||
|
||||
static AudioManager *instance_;
|
||||
|
||||
PaDeviceIndex output_device_;
|
||||
PaStream *output_stream_;
|
||||
core::AudioParams output_params_;
|
||||
PreviewAudioDevice *output_buffer_;
|
||||
|
||||
PaDeviceIndex input_device_;
|
||||
PaStream *input_stream_;
|
||||
|
||||
OakEncoder input_encoder_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOMANAGER_H
|
||||
@@ -0,0 +1,218 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audioprocessor.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "common/ffmpegutils.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Bridge sample format for a native format via the oakcommon C ABI
|
||||
*/
|
||||
static int to_bridge_sample_format(core::SampleFormat fmt)
|
||||
{
|
||||
int out = -1; /* fb_sample_fmt_none */
|
||||
oakcommon_ffmpegutils_get_ffmpeg_sample_format(int(fmt), &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Ensure an AudioParams has a usable channel layout mask.
|
||||
*
|
||||
* The bridge's abuffer/aformat filters reject a channel layout mask of 0
|
||||
* (e.g. when the user config or a source stream reports a mask of 0).
|
||||
* If the mask is zero, fall back to a default layout derived from the
|
||||
* channel count (stereo when unknown).
|
||||
*/
|
||||
static core::AudioParams fix_channel_layout(const core::AudioParams ¶ms)
|
||||
{
|
||||
core::AudioParams result = params;
|
||||
|
||||
if (params.channel_layout() == 0) {
|
||||
int channels = params.channel_count();
|
||||
if (channels <= 0) {
|
||||
channels = 2;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: fixing unspecified channel layout "
|
||||
"(channels=%d) -> default %d channel layout\n",
|
||||
params.channel_count(), channels);
|
||||
|
||||
result.set_channel_layout(fb_channel_layout_default(channels));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AudioProcessor::AudioProcessor()
|
||||
{
|
||||
graph_ = nullptr;
|
||||
out_frame_ = nullptr;
|
||||
}
|
||||
|
||||
AudioProcessor::~AudioProcessor()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
bool AudioProcessor::open(const core::AudioParams &from,
|
||||
const core::AudioParams &to, double tempo)
|
||||
{
|
||||
if (graph_) {
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: tried to open a processor that was "
|
||||
"already open\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
core::AudioParams from_fixed = fix_channel_layout(from);
|
||||
core::AudioParams to_fixed = fix_channel_layout(to);
|
||||
|
||||
FBAudioGraphConfig config;
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.in_sample_rate = from_fixed.sample_rate();
|
||||
config.in_channel_layout_mask = from_fixed.channel_layout();
|
||||
config.in_sample_format = to_bridge_sample_format(from_fixed.format());
|
||||
config.in_channels = from_fixed.channel_count();
|
||||
|
||||
config.out_sample_rate = to_fixed.sample_rate();
|
||||
config.out_channel_layout_mask = to_fixed.channel_layout();
|
||||
config.out_sample_format = to_bridge_sample_format(to_fixed.format());
|
||||
config.out_channels = to_fixed.channel_count();
|
||||
config.out_is_planar = to_fixed.format().is_planar() ? 1 : 0;
|
||||
|
||||
config.tempo = tempo;
|
||||
|
||||
graph_ = fb_audio_graph_create(&config);
|
||||
if (!graph_) {
|
||||
fprintf(stderr, "AudioProcessor: failed to create audio filter "
|
||||
"graph\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
out_frame_ = fb_frame_alloc();
|
||||
if (!out_frame_) {
|
||||
fprintf(stderr, "AudioProcessor: failed to allocate output frame\n");
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
from_ = from_fixed;
|
||||
to_ = to_fixed;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioProcessor::close()
|
||||
{
|
||||
if (graph_) {
|
||||
fb_audio_graph_free(&graph_);
|
||||
}
|
||||
|
||||
if (out_frame_) {
|
||||
fb_frame_free(&out_frame_);
|
||||
}
|
||||
}
|
||||
|
||||
int AudioProcessor::convert(float **in, int nb_in_samples,
|
||||
AudioProcessor::Buffer *output)
|
||||
{
|
||||
if (!is_open()) {
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: tried to convert on closed processor\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int r = 0;
|
||||
|
||||
if (in && nb_in_samples) {
|
||||
r = fb_audio_graph_push(
|
||||
graph_, reinterpret_cast<const uint8_t *const *>(in),
|
||||
nb_in_samples);
|
||||
if (r < 0) {
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: failed to add frame to buffersrc: %d\n",
|
||||
r);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
if (output) {
|
||||
int nb_channels = to_.channel_count();
|
||||
|
||||
if (to_.format().is_packed()) {
|
||||
nb_channels = 1;
|
||||
}
|
||||
|
||||
AudioProcessor::Buffer &result = *output;
|
||||
result.resize(size_t(nb_channels));
|
||||
|
||||
int byte_offset = 0;
|
||||
|
||||
while (true) {
|
||||
r = fb_audio_graph_pull(graph_, out_frame_);
|
||||
if (r <= 0) {
|
||||
if (r == 0) {
|
||||
// No more output available right now
|
||||
r = 0;
|
||||
} else {
|
||||
// Handle unexpected error
|
||||
fprintf(stderr,
|
||||
"AudioProcessor: failed to pull from "
|
||||
"buffersink: %d\n",
|
||||
r);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
int nb_bytes = fb_frame_get_nb_samples(out_frame_) *
|
||||
to_.bytes_per_sample_per_channel();
|
||||
if (to_.format().is_packed()) {
|
||||
nb_bytes *= to_.channel_count();
|
||||
}
|
||||
|
||||
for (int i = 0; i < nb_channels; i++) {
|
||||
result[size_t(i)].resize(size_t(byte_offset + nb_bytes));
|
||||
memcpy(result[size_t(i)].data() + byte_offset,
|
||||
fb_frame_get_data(out_frame_, i), size_t(nb_bytes));
|
||||
}
|
||||
byte_offset += nb_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void AudioProcessor::flush()
|
||||
{
|
||||
int r = fb_audio_graph_push(graph_, nullptr, 0);
|
||||
if (r < 0) {
|
||||
fprintf(stderr, "AudioProcessor: failed to flush: %d\n", r);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOPROCESSOR_H
|
||||
#define OAK_AUDIOPROCESSOR_H
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <vector>
|
||||
|
||||
#include <ffmpeg_bridge/ffmpeg_bridge.h>
|
||||
|
||||
#include "olive/core/render/audioparams.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioProcessor {
|
||||
public:
|
||||
AudioProcessor();
|
||||
|
||||
~AudioProcessor();
|
||||
|
||||
AudioProcessor(const AudioProcessor &) = delete;
|
||||
AudioProcessor &operator=(const AudioProcessor &) = delete;
|
||||
|
||||
bool open(const core::AudioParams &from, const core::AudioParams &to,
|
||||
double tempo = 1.0);
|
||||
|
||||
void close();
|
||||
|
||||
bool is_open() const
|
||||
{
|
||||
return graph_;
|
||||
}
|
||||
|
||||
using Buffer = std::vector<std::vector<char>>;
|
||||
int convert(float **in, int nb_in_samples, AudioProcessor::Buffer *output);
|
||||
|
||||
void flush();
|
||||
|
||||
const core::AudioParams &from() const
|
||||
{
|
||||
return from_;
|
||||
}
|
||||
const core::AudioParams &to() const
|
||||
{
|
||||
return to_;
|
||||
}
|
||||
|
||||
private:
|
||||
FBAudioGraph *graph_;
|
||||
|
||||
core::AudioParams from_;
|
||||
|
||||
core::AudioParams to_;
|
||||
|
||||
FBFrame *out_frame_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOPROCESSOR_H
|
||||
@@ -0,0 +1,65 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiosynchronizer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
AudioSynchronizer::Placement AudioSynchronizer::place_by_source_time(
|
||||
const SourceClip &reference, const SourceClip &candidate,
|
||||
const core::Rational &reference_timeline_in)
|
||||
{
|
||||
Placement placement;
|
||||
if (!reference.has_source_start_time || !candidate.has_source_start_time ||
|
||||
reference.source_start_time.isNaN() ||
|
||||
candidate.source_start_time.isNaN()) {
|
||||
return placement;
|
||||
}
|
||||
|
||||
const core::Rational reference_head_source =
|
||||
reference.source_start_time + reference.media_in;
|
||||
const core::Rational candidate_head_source =
|
||||
candidate.source_start_time + candidate.media_in;
|
||||
|
||||
placement.timeline_in =
|
||||
reference_timeline_in + candidate_head_source - reference_head_source;
|
||||
placement.valid = !placement.timeline_in.isNaN();
|
||||
return placement;
|
||||
}
|
||||
|
||||
AudioSynchronizer::Placement AudioSynchronizer::place_by_waveform_offset(
|
||||
const core::Rational &reference_timeline_in,
|
||||
int64_t candidate_offset_samples, int sample_rate)
|
||||
{
|
||||
Placement placement;
|
||||
if (sample_rate <= 0) {
|
||||
return placement;
|
||||
}
|
||||
|
||||
placement.timeline_in = reference_timeline_in +
|
||||
core::Rational::from_double(
|
||||
static_cast<double>(candidate_offset_samples) /
|
||||
static_cast<double>(sample_rate));
|
||||
placement.valid = !placement.timeline_in.isNaN();
|
||||
return placement;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOSYNCHRONIZER_H
|
||||
#define OAK_AUDIOSYNCHRONIZER_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "olive/core/util/rational.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioSynchronizer {
|
||||
public:
|
||||
struct SourceClip {
|
||||
core::Rational source_start_time;
|
||||
core::Rational media_in;
|
||||
bool has_source_start_time = false;
|
||||
};
|
||||
|
||||
struct Placement {
|
||||
core::Rational timeline_in;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
static Placement
|
||||
place_by_source_time(const SourceClip &reference, const SourceClip &candidate,
|
||||
const core::Rational &reference_timeline_in);
|
||||
|
||||
static Placement
|
||||
place_by_waveform_offset(const core::Rational &reference_timeline_in,
|
||||
int64_t candidate_offset_samples, int sample_rate);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOSYNCHRONIZER_H
|
||||
@@ -0,0 +1,478 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiovisualwaveform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "olive/core/util/cpuoptimize.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
using core::Rational;
|
||||
|
||||
const Rational AudioVisualWaveform::k_minimum_sample_rate = Rational(1, 8);
|
||||
const Rational AudioVisualWaveform::k_maximum_sample_rate = 1024;
|
||||
|
||||
AudioVisualWaveform::AudioVisualWaveform()
|
||||
: channels_(0)
|
||||
{
|
||||
for (Rational i = k_minimum_sample_rate; i <= k_maximum_sample_rate; i *= 2) {
|
||||
mipmapped_data_.insert({ i, Sample() });
|
||||
}
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::overwrite_samples_from_buffer(
|
||||
const core::SampleBuffer &samples, int sample_rate, const Rational &start,
|
||||
double target_rate, Sample &data, size_t &start_index,
|
||||
size_t &samples_length)
|
||||
{
|
||||
start_index = time_to_samples(start, target_rate);
|
||||
samples_length =
|
||||
time_to_samples(static_cast<double>(samples.sample_count()) /
|
||||
static_cast<double>(sample_rate),
|
||||
target_rate);
|
||||
|
||||
size_t end_index = start_index + samples_length;
|
||||
if (data.size() < end_index) {
|
||||
data.resize(end_index);
|
||||
}
|
||||
|
||||
double chunk_size = double(sample_rate) / double(target_rate);
|
||||
|
||||
for (size_t i = 0; i < samples_length; i += channels_) {
|
||||
size_t src_start = size_t(std::llround(double(i) * chunk_size)) / channels_;
|
||||
size_t src_end = std::min(
|
||||
size_t(std::llround(double(i + channels_) * chunk_size)) / channels_,
|
||||
samples.sample_count());
|
||||
|
||||
Sample summary = sum_samples(samples, src_start, src_end - src_start);
|
||||
|
||||
memcpy(&data.data()[i + start_index], summary.data(),
|
||||
summary.size() * sizeof(SamplePerChannel));
|
||||
}
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::overwrite_samples_from_mipmap(
|
||||
const AudioVisualWaveform::Sample &input, double input_sample_rate,
|
||||
size_t &input_start, size_t &input_length, const Rational &start,
|
||||
double output_rate, AudioVisualWaveform::Sample &output_data)
|
||||
{
|
||||
size_t start_index = time_to_samples(start, output_rate);
|
||||
size_t samples_length = time_to_samples(
|
||||
static_cast<double>(input_length / channels_) / input_sample_rate,
|
||||
output_rate);
|
||||
|
||||
size_t end_index = start_index + samples_length;
|
||||
if (output_data.size() < end_index) {
|
||||
output_data.resize(end_index);
|
||||
}
|
||||
|
||||
// We guarantee mipmaps are powers of two so integer division should be perfectly accurate here
|
||||
size_t chunk_size = size_t(input_sample_rate / output_rate);
|
||||
|
||||
for (size_t i = 0; i < samples_length; i += channels_) {
|
||||
Sample summary =
|
||||
re_sum_samples(&input.data()[input_start + (i * chunk_size)],
|
||||
chunk_size * channels_, channels_);
|
||||
|
||||
memcpy(&output_data.data()[i + start_index], summary.data(),
|
||||
summary.size() * sizeof(SamplePerChannel));
|
||||
}
|
||||
|
||||
input_start = start_index;
|
||||
input_length = samples_length;
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::validate_virtual_start(const Rational &new_start)
|
||||
{
|
||||
if (length_ == 0) {
|
||||
virtual_start_ = new_start;
|
||||
} else if (virtual_start_ > new_start) {
|
||||
trim_in(new_start - virtual_start_);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::overwrite_samples(const core::SampleBuffer &samples,
|
||||
int sample_rate,
|
||||
const Rational &start)
|
||||
{
|
||||
if (!channels_) {
|
||||
fprintf(stderr,
|
||||
"AudioVisualWaveform: failed to write samples - channel "
|
||||
"count is zero\n");
|
||||
return;
|
||||
}
|
||||
|
||||
validate_virtual_start(start);
|
||||
|
||||
// Process the largest mipmap directly for the samples
|
||||
auto current_mipmap = mipmapped_data_.rbegin();
|
||||
size_t input_start, input_length;
|
||||
overwrite_samples_from_buffer(samples, sample_rate, start - virtual_start_,
|
||||
current_mipmap->first.to_double(),
|
||||
current_mipmap->second, input_start,
|
||||
input_length);
|
||||
|
||||
while (true) {
|
||||
// For each smaller mipmap, we just process from the mipmap before it, making each one
|
||||
// exponentially faster to create
|
||||
auto previous_mipmap = current_mipmap;
|
||||
current_mipmap++;
|
||||
if (current_mipmap == mipmapped_data_.rend()) {
|
||||
break;
|
||||
}
|
||||
|
||||
overwrite_samples_from_mipmap(
|
||||
previous_mipmap->second, previous_mipmap->first.to_double(),
|
||||
input_start, input_length, start - virtual_start_,
|
||||
current_mipmap->first.to_double(), current_mipmap->second);
|
||||
}
|
||||
|
||||
Rational sample_length(int64_t(samples.sample_count()), sample_rate);
|
||||
length_ = std::max(length_, start + sample_length);
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::overwrite_sums(const AudioVisualWaveform &sums,
|
||||
const Rational &dest,
|
||||
const Rational &offset,
|
||||
const Rational &length)
|
||||
{
|
||||
validate_virtual_start(dest);
|
||||
|
||||
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
|
||||
Rational rate = it->first;
|
||||
|
||||
Sample &our_arr = it->second;
|
||||
const Sample &their_arr = sums.mipmapped_data_.at(rate);
|
||||
|
||||
double rate_dbl = rate.to_double();
|
||||
|
||||
// Get our destination sample
|
||||
size_t our_start_index =
|
||||
time_to_samples(dest - virtual_start_, rate_dbl);
|
||||
|
||||
// Get our source sample, indexing with the SOURCE's channel count
|
||||
size_t their_start_index = size_t(std::floor(offset.to_double() * rate_dbl)) *
|
||||
size_t(sums.channel_count());
|
||||
if (their_start_index >= their_arr.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine how much we're copying
|
||||
size_t copy_len = their_arr.size() - their_start_index;
|
||||
if (!length.isNull()) {
|
||||
copy_len = std::min(copy_len, time_to_samples(length, rate_dbl));
|
||||
if (copy_len == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine end index of our array
|
||||
size_t end_index = our_start_index + copy_len;
|
||||
if (our_arr.size() < end_index) {
|
||||
our_arr.resize(end_index);
|
||||
}
|
||||
|
||||
memcpy(reinterpret_cast<char *>(our_arr.data()) +
|
||||
our_start_index * sizeof(SamplePerChannel),
|
||||
reinterpret_cast<const char *>(their_arr.data()) +
|
||||
their_start_index * sizeof(SamplePerChannel),
|
||||
copy_len * sizeof(SamplePerChannel));
|
||||
}
|
||||
|
||||
length_ = std::max(length_, dest + ((length.isNull()) ? sums.length() - offset :
|
||||
length));
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::overwrite_silence(const Rational &start,
|
||||
const Rational &length)
|
||||
{
|
||||
validate_virtual_start(start);
|
||||
|
||||
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
|
||||
Rational rate = it->first;
|
||||
|
||||
Sample &our_arr = it->second;
|
||||
|
||||
double rate_dbl = rate.to_double();
|
||||
|
||||
// Get our destination sample
|
||||
size_t our_start_index =
|
||||
time_to_samples(start - virtual_start_, rate_dbl);
|
||||
size_t our_length_index = time_to_samples(length, rate_dbl);
|
||||
size_t our_end_index = our_start_index + our_length_index;
|
||||
|
||||
if (our_arr.size() < our_end_index) {
|
||||
our_arr.resize(our_end_index);
|
||||
}
|
||||
|
||||
memset(reinterpret_cast<char *>(our_arr.data()) +
|
||||
our_start_index * sizeof(SamplePerChannel),
|
||||
0, our_length_index * sizeof(SamplePerChannel));
|
||||
}
|
||||
|
||||
length_ = std::max(length_, start + length);
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::trim_in(Rational length)
|
||||
{
|
||||
if (length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
virtual_start_ += length;
|
||||
|
||||
bool negative = (length < 0);
|
||||
if (negative) {
|
||||
length = -length;
|
||||
}
|
||||
|
||||
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
|
||||
Rational rate = it->first;
|
||||
double rate_dbl = rate.to_double();
|
||||
Sample &data = it->second;
|
||||
|
||||
size_t chop_length = time_to_samples(length, rate_dbl);
|
||||
if (chop_length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!negative) {
|
||||
data = Sample(data.begin() + chop_length, data.end());
|
||||
} else {
|
||||
data.insert(data.begin(), chop_length, SamplePerChannel());
|
||||
}
|
||||
}
|
||||
|
||||
if (!negative) {
|
||||
length_ = std::max(Rational(0), length_ - length);
|
||||
}
|
||||
// Prepending grows the data before the existing start, so the absolute
|
||||
// end (which length_ tracks) is unchanged
|
||||
}
|
||||
|
||||
AudioVisualWaveform AudioVisualWaveform::mid(const Rational &offset) const
|
||||
{
|
||||
AudioVisualWaveform mid = *this;
|
||||
|
||||
mid.trim_in(offset - virtual_start_);
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
AudioVisualWaveform AudioVisualWaveform::mid(const Rational &offset,
|
||||
const Rational &length) const
|
||||
{
|
||||
AudioVisualWaveform mid = *this;
|
||||
|
||||
mid.trim_range(offset - virtual_start_, length);
|
||||
|
||||
return mid;
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::resize(const Rational &length)
|
||||
{
|
||||
if (length_ == length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto it = mipmapped_data_.begin(); it != mipmapped_data_.end(); it++) {
|
||||
Rational rate = it->first;
|
||||
double rate_dbl = rate.to_double();
|
||||
Sample &data = it->second;
|
||||
|
||||
size_t chop_length = time_to_samples(length, rate_dbl);
|
||||
|
||||
data.resize(chop_length);
|
||||
}
|
||||
|
||||
length_ = length;
|
||||
}
|
||||
|
||||
void AudioVisualWaveform::trim_range(const Rational &in, const Rational &length)
|
||||
{
|
||||
trim_in(in);
|
||||
resize(length);
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
AudioVisualWaveform::get_summary_from_time(const Rational &start,
|
||||
const Rational &length) const
|
||||
{
|
||||
// Find mipmap that requires
|
||||
auto using_mipmap = get_mipmap_for_scale(length.flipped().to_double());
|
||||
|
||||
double rate_dbl = using_mipmap->first.to_double();
|
||||
|
||||
size_t start_sample = time_to_samples(start - virtual_start_, rate_dbl);
|
||||
size_t sample_length = time_to_samples(length, rate_dbl);
|
||||
|
||||
const Sample &mipmap_data = using_mipmap->second;
|
||||
|
||||
// Determine if the array actually has this sample. Compare in signed
|
||||
// arithmetic so a start past the end of the data doesn't underflow.
|
||||
int64_t available = int64_t(mipmap_data.size()) - int64_t(start_sample);
|
||||
if (available > 0) {
|
||||
sample_length = std::min(sample_length, size_t(available));
|
||||
|
||||
if (sample_length > 0) {
|
||||
return re_sum_samples(&mipmap_data.data()[start_sample],
|
||||
sample_length, channels_);
|
||||
}
|
||||
}
|
||||
|
||||
// Return null samples
|
||||
return AudioVisualWaveform::Sample(size_t(channel_count()), { 0, 0 });
|
||||
}
|
||||
|
||||
void expand_min_max_channel(const float *a, size_t length, float &min_val,
|
||||
float &max_val)
|
||||
{
|
||||
#if defined(OLIVE_PROCESSOR_X86) || defined(OLIVE_PROCESSOR_ARM)
|
||||
// SSE optimized
|
||||
|
||||
// load the first 4 elements of 'a' into min and max (they are 4 * 32 = 128 bits)
|
||||
__m128 max = _mm_loadu_ps(a);
|
||||
__m128 min = _mm_loadu_ps(a);
|
||||
|
||||
// loop over 'a' and compare current elements with min and max 4 by 4.
|
||||
// we need to make sure we don't read out of boundaries should 'a' length be not mod. 4
|
||||
for (size_t i = 4; i < length - 4; i += 4) {
|
||||
__m128 cur = _mm_loadu_ps(a + i);
|
||||
max = _mm_max_ps(max, cur);
|
||||
min = _mm_min_ps(min, cur);
|
||||
}
|
||||
// so we read the last 4 (or less) elements in a safe manner.
|
||||
__m128 cur = _mm_loadu_ps(a + length - 4);
|
||||
max = _mm_max_ps(max, cur);
|
||||
min = _mm_min_ps(min, cur);
|
||||
// this potentially overlaps up to the last 3 elements but it's not an issue.
|
||||
|
||||
// min and max will contain 4 min and max. To get the absolute min and max
|
||||
// we need to compare the 4 values over themselves by shuffling each time.
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
max = _mm_max_ps(max, _mm_shuffle_ps(max, max, 0x93));
|
||||
min = _mm_min_ps(min, _mm_shuffle_ps(min, min, 0x93));
|
||||
}
|
||||
// now min and max contain 4 identical items each representing min and max value respectively.
|
||||
|
||||
// and we store the first one into a float variable.
|
||||
_mm_store_ss(&max_val, max);
|
||||
_mm_store_ss(&min_val, min);
|
||||
// I bet you don't find annotated low level code very often.
|
||||
#else
|
||||
// Standard unoptimized function
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
min_val = std::min(min_val, a[i]);
|
||||
max_val = std::max(max_val, a[i]);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
AudioVisualWaveform::sum_samples(const core::SampleBuffer &samples,
|
||||
size_t start_index, size_t length)
|
||||
{
|
||||
int channels = samples.audio_params().channel_count();
|
||||
const size_t channel_count = size_t(channels);
|
||||
AudioVisualWaveform::Sample summed_samples(channel_count);
|
||||
|
||||
for (int channel = 0; channel < channels; channel++) {
|
||||
expand_min_max_channel(samples.data(channel) + start_index, length,
|
||||
summed_samples[size_t(channel)].min,
|
||||
summed_samples[size_t(channel)].max);
|
||||
}
|
||||
|
||||
// for reference: this approximation is n x faster (and less accurate) for a n-tracks clip
|
||||
// for (size_t i=start_index; i<end_index; i++) {
|
||||
// ExpandMinMax(summed_samples[i%channels], samples->data(i%channels)[i]);
|
||||
// }
|
||||
|
||||
return summed_samples;
|
||||
}
|
||||
|
||||
AudioVisualWaveform::Sample
|
||||
AudioVisualWaveform::re_sum_samples(const SamplePerChannel *samples,
|
||||
size_t nb_samples, int nb_channels)
|
||||
{
|
||||
const size_t channel_count = size_t(nb_channels);
|
||||
AudioVisualWaveform::Sample summed_samples(channel_count);
|
||||
|
||||
// Initialize from the first point instead of {0,0}: the engine version
|
||||
// started from zero-initialized pairs, which clamped all-positive
|
||||
// (resp. all-negative) ranges to a zero min (max). Fixed in oakaudio.
|
||||
if (nb_samples >= channel_count) {
|
||||
for (size_t j = 0; j < channel_count; j++) {
|
||||
summed_samples[j] = samples[j];
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < nb_samples; i += size_t(nb_channels)) {
|
||||
for (int j = 0; j < nb_channels; j++) {
|
||||
const AudioVisualWaveform::SamplePerChannel &sample =
|
||||
samples[i + size_t(j)];
|
||||
|
||||
if (sample.min < summed_samples[size_t(j)].min) {
|
||||
summed_samples[size_t(j)].min = sample.min;
|
||||
}
|
||||
|
||||
if (sample.max > summed_samples[size_t(j)].max) {
|
||||
summed_samples[size_t(j)].max = sample.max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summed_samples;
|
||||
}
|
||||
|
||||
size_t AudioVisualWaveform::time_to_samples(const Rational &time,
|
||||
double sample_rate) const
|
||||
{
|
||||
return time_to_samples(time.to_double(), sample_rate);
|
||||
}
|
||||
|
||||
size_t AudioVisualWaveform::time_to_samples(const double &time,
|
||||
double sample_rate) const
|
||||
{
|
||||
return size_t(std::floor(time * sample_rate)) * size_t(channels_);
|
||||
}
|
||||
|
||||
std::map<Rational, AudioVisualWaveform::Sample>::const_iterator
|
||||
AudioVisualWaveform::get_mipmap_for_scale(double scale) const
|
||||
{
|
||||
// Find largest mipmap for this scale (or the largest if we don't find one sufficient)
|
||||
for (auto it = mipmapped_data_.cbegin(); it != mipmapped_data_.cend();
|
||||
it++) {
|
||||
if (it->first.to_double() >= scale) {
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have a mipmap large enough for this scale, so just return the largest we have
|
||||
return std::prev(mipmapped_data_.cend());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_SUMSAMPLES_H
|
||||
#define OAK_SUMSAMPLES_H
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
#include "olive/core/util/rational.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A buffer of data used to store a visual representation of audio
|
||||
*
|
||||
* This differs from a SampleBuffer as the data in an AudioVisualWaveform has been reduced
|
||||
* significantly and optimized for visual display.
|
||||
*
|
||||
* De-Qt note: the QPainter-based draw_sample()/draw_waveform() functions
|
||||
* live in the app layer now; this class only stores and summarizes data.
|
||||
*/
|
||||
class AudioVisualWaveform {
|
||||
public:
|
||||
AudioVisualWaveform();
|
||||
|
||||
struct SamplePerChannel {
|
||||
float min;
|
||||
float max;
|
||||
};
|
||||
|
||||
using Sample = std::vector<SamplePerChannel>;
|
||||
|
||||
int channel_count() const
|
||||
{
|
||||
return channels_;
|
||||
}
|
||||
|
||||
void set_channel_count(int channels)
|
||||
{
|
||||
channels_ = channels;
|
||||
}
|
||||
|
||||
const core::Rational &length() const
|
||||
{
|
||||
return length_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Writes samples into the visual waveform buffer
|
||||
*
|
||||
* Starting at `start`, writes samples over anything in the buffer, expanding it if necessary.
|
||||
*/
|
||||
void overwrite_samples(const core::SampleBuffer &samples, int sample_rate,
|
||||
const core::Rational &start = 0);
|
||||
|
||||
/**
|
||||
* @brief Replaces sums at a certain range in this visual waveform
|
||||
*
|
||||
* @param sums
|
||||
*
|
||||
* The sums to write over our current ones with.
|
||||
*
|
||||
* @param dest
|
||||
*
|
||||
* Where in this visual waveform these sums should START being written to.
|
||||
*
|
||||
* @param offset
|
||||
*
|
||||
* Where in the `sums` parameter this should start reading from. Defaults to 0.
|
||||
*
|
||||
* @param length
|
||||
*
|
||||
* Maximum length of `sums` to overwrite with.
|
||||
*/
|
||||
void overwrite_sums(const AudioVisualWaveform &sums,
|
||||
const core::Rational &dest,
|
||||
const core::Rational &offset = 0,
|
||||
const core::Rational &length = 0);
|
||||
|
||||
void overwrite_silence(const core::Rational &start,
|
||||
const core::Rational &length);
|
||||
|
||||
void trim_in(core::Rational length);
|
||||
|
||||
AudioVisualWaveform mid(const core::Rational &offset) const;
|
||||
AudioVisualWaveform mid(const core::Rational &offset,
|
||||
const core::Rational &length) const;
|
||||
|
||||
void resize(const core::Rational &length);
|
||||
|
||||
void trim_range(const core::Rational &in, const core::Rational &length);
|
||||
|
||||
Sample get_summary_from_time(const core::Rational &start,
|
||||
const core::Rational &length) const;
|
||||
|
||||
static Sample sum_samples(const core::SampleBuffer &samples,
|
||||
size_t start_index, size_t length);
|
||||
|
||||
static Sample re_sum_samples(const SamplePerChannel *samples,
|
||||
size_t nb_samples, int nb_channels);
|
||||
|
||||
// Must be a power of 2
|
||||
static const core::Rational k_minimum_sample_rate;
|
||||
static const core::Rational k_maximum_sample_rate;
|
||||
|
||||
private:
|
||||
void overwrite_samples_from_buffer(const core::SampleBuffer &samples,
|
||||
int sample_rate,
|
||||
const core::Rational &start,
|
||||
double target_rate, Sample &data,
|
||||
size_t &start_index,
|
||||
size_t &samples_length);
|
||||
|
||||
void overwrite_samples_from_mipmap(const Sample &input,
|
||||
double input_sample_rate,
|
||||
size_t &input_start, size_t &input_length,
|
||||
const core::Rational &start,
|
||||
double output_rate, Sample &output_data);
|
||||
|
||||
size_t time_to_samples(const core::Rational &time, double sample_rate) const;
|
||||
size_t time_to_samples(const double &time, double sample_rate) const;
|
||||
|
||||
std::map<core::Rational, Sample>::const_iterator
|
||||
get_mipmap_for_scale(double scale) const;
|
||||
|
||||
void validate_virtual_start(const core::Rational &new_start);
|
||||
|
||||
core::Rational virtual_start_;
|
||||
|
||||
int channels_;
|
||||
|
||||
std::map<core::Rational, Sample> mipmapped_data_;
|
||||
|
||||
core::Rational length_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_SUMSAMPLES_H
|
||||
@@ -0,0 +1,257 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "audiowaveformsync.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
std::vector<double>
|
||||
AudioWaveformSync::extract_rms_envelope(const core::SampleBuffer &samples,
|
||||
size_t window_samples)
|
||||
{
|
||||
std::vector<double> envelope;
|
||||
|
||||
const int channel_count = samples.channel_count();
|
||||
const size_t sample_count = samples.sample_count();
|
||||
if (!channel_count || !sample_count || !window_samples) {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
const size_t window_count =
|
||||
(sample_count + window_samples - 1) / window_samples;
|
||||
envelope.resize(window_count);
|
||||
|
||||
for (size_t window = 0; window < window_count; window++) {
|
||||
const size_t start = window * window_samples;
|
||||
const size_t end = std::min(start + window_samples, sample_count);
|
||||
double square_sum = 0.0;
|
||||
size_t total = 0;
|
||||
|
||||
for (int channel = 0; channel < channel_count; channel++) {
|
||||
const float *data = samples.data(channel);
|
||||
for (size_t sample = start; sample < end; sample++) {
|
||||
const double value = data[sample];
|
||||
square_sum += value * value;
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
envelope[window] =
|
||||
total ? std::sqrt(square_sum / static_cast<double>(total)) : 0.0;
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_offset(
|
||||
const core::SampleBuffer &reference, const core::SampleBuffer &candidate,
|
||||
size_t window_samples, int64_t max_offset_samples)
|
||||
{
|
||||
if (!window_samples) {
|
||||
return OffsetResult();
|
||||
}
|
||||
|
||||
const std::vector<double> reference_envelope =
|
||||
extract_rms_envelope(reference, window_samples);
|
||||
const std::vector<double> candidate_envelope =
|
||||
extract_rms_envelope(candidate, window_samples);
|
||||
const int64_t max_offset_windows =
|
||||
max_offset_samples / static_cast<int64_t>(window_samples);
|
||||
|
||||
return estimate_envelope_offset(reference_envelope, candidate_envelope,
|
||||
window_samples, max_offset_windows);
|
||||
}
|
||||
|
||||
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_envelope_offset(
|
||||
const std::vector<double> &reference, const std::vector<double> &candidate,
|
||||
size_t window_samples, int64_t max_offset_windows)
|
||||
{
|
||||
return estimate_envelope_offset(reference, candidate, std::vector<char>(),
|
||||
std::vector<char>(), window_samples,
|
||||
max_offset_windows);
|
||||
}
|
||||
|
||||
AudioWaveformSync::OffsetResult AudioWaveformSync::estimate_envelope_offset(
|
||||
const std::vector<double> &reference, const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples, int64_t max_offset_windows)
|
||||
{
|
||||
OffsetResult result;
|
||||
if (reference.empty() || candidate.empty() || !window_samples) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto is_valid = [](const std::vector<char> &mask, size_t size,
|
||||
size_t index) {
|
||||
return mask.size() != size || mask.at(index);
|
||||
};
|
||||
|
||||
double best_score = -2.0;
|
||||
int64_t best_lag = 0;
|
||||
|
||||
const int reference_size = static_cast<int>(reference.size());
|
||||
const int candidate_size = static_cast<int>(candidate.size());
|
||||
|
||||
for (int64_t lag = -max_offset_windows; lag <= max_offset_windows; lag++) {
|
||||
const int reference_start =
|
||||
static_cast<int>(std::max<int64_t>(0, -lag));
|
||||
const int candidate_start = static_cast<int>(std::max<int64_t>(0, lag));
|
||||
const int overlap = std::min(reference_size - reference_start,
|
||||
candidate_size - candidate_start);
|
||||
|
||||
if (overlap < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only windows marked valid on both sides participate in the score
|
||||
double reference_mean = 0.0;
|
||||
double candidate_mean = 0.0;
|
||||
int valid_count = 0;
|
||||
for (int i = 0; i < overlap; i++) {
|
||||
const int reference_index = reference_start + i;
|
||||
const int candidate_index = candidate_start + i;
|
||||
if (!is_valid(reference_valid, reference.size(),
|
||||
size_t(reference_index)) ||
|
||||
!is_valid(candidate_valid, candidate.size(),
|
||||
size_t(candidate_index))) {
|
||||
continue;
|
||||
}
|
||||
reference_mean += reference.at(size_t(reference_index));
|
||||
candidate_mean += candidate.at(size_t(candidate_index));
|
||||
valid_count++;
|
||||
}
|
||||
|
||||
if (valid_count < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
reference_mean /= static_cast<double>(valid_count);
|
||||
candidate_mean /= static_cast<double>(valid_count);
|
||||
|
||||
double numerator = 0.0;
|
||||
double reference_energy = 0.0;
|
||||
double candidate_energy = 0.0;
|
||||
for (int i = 0; i < overlap; i++) {
|
||||
const int reference_index = reference_start + i;
|
||||
const int candidate_index = candidate_start + i;
|
||||
if (!is_valid(reference_valid, reference.size(),
|
||||
size_t(reference_index)) ||
|
||||
!is_valid(candidate_valid, candidate.size(),
|
||||
size_t(candidate_index))) {
|
||||
continue;
|
||||
}
|
||||
const double reference_value =
|
||||
reference.at(size_t(reference_index)) - reference_mean;
|
||||
const double candidate_value =
|
||||
candidate.at(size_t(candidate_index)) - candidate_mean;
|
||||
numerator += reference_value * candidate_value;
|
||||
reference_energy += reference_value * reference_value;
|
||||
candidate_energy += candidate_value * candidate_value;
|
||||
}
|
||||
|
||||
// qFuzzyIsNull(double): |x| < 1e-12
|
||||
if (std::abs(reference_energy) < 1e-12 ||
|
||||
std::abs(candidate_energy) < 1e-12) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const double score =
|
||||
numerator / std::sqrt(reference_energy * candidate_energy);
|
||||
if (score > best_score) {
|
||||
best_score = score;
|
||||
best_lag = lag;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_score > -2.0) {
|
||||
result.valid = true;
|
||||
result.confidence = std::max(0.0, best_score);
|
||||
result.offset_samples = best_lag * static_cast<int64_t>(window_samples);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AudioWaveformSync::StretchOffsetResult AudioWaveformSync::estimate_stretch_and_offset(
|
||||
const std::vector<double> &reference, const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples, int64_t max_offset_windows, double min_rate,
|
||||
double max_rate, double rate_step)
|
||||
{
|
||||
StretchOffsetResult result;
|
||||
if (reference.empty() || candidate.empty() || !window_samples ||
|
||||
min_rate <= 0.0 || max_rate < min_rate || rate_step <= 0.0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
double best_confidence = -2.0;
|
||||
|
||||
for (double rate = min_rate; rate <= max_rate + rate_step * 0.5;
|
||||
rate += rate_step) {
|
||||
// Resample the candidate envelope so that window i of the resampled
|
||||
// envelope corresponds to window i*rate of the original
|
||||
const int resampled_size =
|
||||
static_cast<int>(candidate.size() / rate);
|
||||
if (resampled_size < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t resampled_len = size_t(resampled_size);
|
||||
std::vector<double> resampled(resampled_len);
|
||||
std::vector<char> resampled_valid(resampled_len);
|
||||
for (int i = 0; i < resampled_size; i++) {
|
||||
const double position = i * rate;
|
||||
const int lower = static_cast<int>(position);
|
||||
const int upper =
|
||||
std::min(lower + 1, static_cast<int>(candidate.size()) - 1);
|
||||
const double fraction = position - lower;
|
||||
|
||||
resampled[size_t(i)] = candidate.at(size_t(lower)) * (1.0 - fraction) +
|
||||
candidate.at(size_t(upper)) * fraction;
|
||||
|
||||
resampled_valid[size_t(i)] =
|
||||
(candidate_valid.size() != candidate.size() ||
|
||||
(candidate_valid.at(size_t(lower)) &&
|
||||
candidate_valid.at(size_t(upper))));
|
||||
}
|
||||
|
||||
const OffsetResult offset = estimate_envelope_offset(
|
||||
reference, resampled, reference_valid, resampled_valid,
|
||||
window_samples, max_offset_windows);
|
||||
|
||||
if (offset.valid && offset.confidence > best_confidence) {
|
||||
best_confidence = offset.confidence;
|
||||
result.valid = true;
|
||||
result.rate = rate;
|
||||
result.confidence = offset.confidence;
|
||||
result.offset_samples = offset.offset_samples;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIOWAVEFORMSYNC_H
|
||||
#define OAK_AUDIOWAVEFORMSYNC_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "olive/core/render/samplebuffer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
class AudioWaveformSync {
|
||||
public:
|
||||
struct OffsetResult {
|
||||
int64_t offset_samples = 0;
|
||||
double confidence = 0.0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct StretchOffsetResult {
|
||||
// Playback rate the candidate must be played at to align with the
|
||||
// reference (e.g. 2.0 = candidate runs at half speed and needs to be
|
||||
// sped up 2x)
|
||||
double rate = 1.0;
|
||||
int64_t offset_samples = 0;
|
||||
double confidence = 0.0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
static std::vector<double>
|
||||
extract_rms_envelope(const core::SampleBuffer &samples,
|
||||
size_t window_samples);
|
||||
|
||||
static OffsetResult estimate_offset(const core::SampleBuffer &reference,
|
||||
const core::SampleBuffer &candidate,
|
||||
size_t window_samples,
|
||||
int64_t max_offset_samples);
|
||||
|
||||
static OffsetResult
|
||||
estimate_envelope_offset(const std::vector<double> &reference,
|
||||
const std::vector<double> &candidate,
|
||||
size_t window_samples,
|
||||
int64_t max_offset_windows);
|
||||
|
||||
/**
|
||||
* @brief Offset estimation that ignores windows flagged as invalid
|
||||
*
|
||||
* @p reference_valid and @p candidate_valid mark which envelope windows
|
||||
* contain real data (e.g. actually cached waveform regions). Windows
|
||||
* flagged false on either side are excluded from the correlation instead
|
||||
* of being treated as silence, which improves accuracy when parts of the
|
||||
* waveform cache have not been generated yet. Empty masks are treated as
|
||||
* "all windows valid".
|
||||
*/
|
||||
static OffsetResult
|
||||
estimate_envelope_offset(const std::vector<double> &reference,
|
||||
const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples,
|
||||
int64_t max_offset_windows);
|
||||
|
||||
/**
|
||||
* @brief Estimates a playback-rate change plus offset aligning the
|
||||
* candidate to the reference
|
||||
*
|
||||
* The candidate envelope is resampled at each candidate rate in
|
||||
* [min_rate, max_rate] (step rate_step) and correlated against the
|
||||
* reference. rate > 1 means the candidate runs slower than the reference
|
||||
* and must be sped up. The search is O(rates * lags * overlap), so
|
||||
* callers should bound max_offset_windows to a sensible range.
|
||||
*/
|
||||
static StretchOffsetResult
|
||||
estimate_stretch_and_offset(const std::vector<double> &reference,
|
||||
const std::vector<double> &candidate,
|
||||
const std::vector<char> &reference_valid,
|
||||
const std::vector<char> &candidate_valid,
|
||||
size_t window_samples,
|
||||
int64_t max_offset_windows, double min_rate,
|
||||
double max_rate, double rate_step);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIOWAVEFORMSYNC_H
|
||||
@@ -0,0 +1,54 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "configbridge.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "common/config.h"
|
||||
|
||||
namespace olive::audio_config
|
||||
{
|
||||
|
||||
int output_buffer_size()
|
||||
{
|
||||
// 0 = let PortAudio choose the buffer size (old default)
|
||||
return oakcommon_config_get_int(nullptr, "AudioOutputBufferSize", 0);
|
||||
}
|
||||
|
||||
std::string device_name(bool is_output_device)
|
||||
{
|
||||
const char *key = is_output_device ? "AudioOutput" : "AudioInput";
|
||||
|
||||
int size = oakcommon_config_get(nullptr, key, nullptr, 0);
|
||||
if (size <= 1) {
|
||||
// Absent (OAKCOMMON_E_NOT_FOUND) or empty
|
||||
return std::string();
|
||||
}
|
||||
|
||||
const size_t buf_len = size_t(size);
|
||||
std::vector<char> buf(buf_len);
|
||||
if (oakcommon_config_get(nullptr, key, buf.data(), size) < 0) {
|
||||
return std::string();
|
||||
}
|
||||
return std::string(buf.data());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/***
|
||||
|
||||
Oak - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_AUDIO_CONFIGBRIDGE_H
|
||||
#define OAK_AUDIO_CONFIGBRIDGE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace olive::audio_config
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Thin wrappers over the oakcommon config C ABI
|
||||
* (include/common/config.h)
|
||||
*
|
||||
* The old engine code read these keys through OAK_CONFIG/OAK_CONFIG_STR;
|
||||
* oakaudio reaches the same store through oakcommon_config_*. Typed
|
||||
* getters fall back when the key is absent (the compiled-in defaults do
|
||||
* not carry the audio device keys).
|
||||
*/
|
||||
|
||||
/** "AudioOutputBufferSize": PortAudio framesPerBuffer (0 = auto). */
|
||||
int output_buffer_size();
|
||||
|
||||
/** "AudioOutput" / "AudioInput": saved device name ("" when unset). */
|
||||
std::string device_name(bool is_output_device);
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_AUDIO_CONFIGBRIDGE_H
|
||||
@@ -0,0 +1,96 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "previewaudiodevice.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
PreviewAudioDevice::PreviewAudioDevice()
|
||||
: bytes_per_frame_(0)
|
||||
, notify_interval_(0)
|
||||
, bytes_read_(0)
|
||||
{
|
||||
}
|
||||
|
||||
PreviewAudioDevice::~PreviewAudioDevice() = default;
|
||||
|
||||
void PreviewAudioDevice::set_params(const core::AudioParams ¶ms)
|
||||
{
|
||||
set_bytes_per_frame(params.samples_to_bytes(1));
|
||||
}
|
||||
|
||||
int64_t PreviewAudioDevice::read(char *data, int64_t max_size)
|
||||
{
|
||||
bool notify = false;
|
||||
int64_t copy_length;
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
copy_length = std::min(max_size, int64_t(buffer_.size()));
|
||||
|
||||
if (copy_length) {
|
||||
int64_t new_bytes_read = bytes_read_ + copy_length;
|
||||
|
||||
if (notify_interval_ > 0 && notify_callback_) {
|
||||
if ((bytes_read_ / notify_interval_) !=
|
||||
(new_bytes_read / notify_interval_)) {
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
bytes_read_ = new_bytes_read;
|
||||
|
||||
memcpy(data, buffer_.data(), copy_length);
|
||||
buffer_.erase(buffer_.begin(), buffer_.begin() + copy_length);
|
||||
}
|
||||
}
|
||||
|
||||
// Fired outside the lock (see set_notify_callback())
|
||||
if (notify) {
|
||||
notify_callback_();
|
||||
}
|
||||
|
||||
return copy_length;
|
||||
}
|
||||
|
||||
int64_t PreviewAudioDevice::write(const char *data, int64_t length)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
buffer_.insert(buffer_.end(), data, data + length);
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
void PreviewAudioDevice::clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
|
||||
buffer_.clear();
|
||||
bytes_read_ = 0;
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/***
|
||||
|
||||
Olive - Non-Linear Video Editor
|
||||
Copyright (C) 2022 Olive Team
|
||||
Modifications Copyright (C) 2025 mikesolar
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAK_PREVIEWAUDIODEVICE_H
|
||||
#define OAK_PREVIEWAUDIODEVICE_H
|
||||
|
||||
#include "olive/core/render/audioparams.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Pull-style sample buffer fed to the audio output callback
|
||||
*
|
||||
* Formerly a QIODevice subclass consumed by QAudioOutput. Now a plain class:
|
||||
* the audio backend (PortAudio, see engine/audio AudioManager) pulls samples
|
||||
* through read() from its stream callback and the render side pushes samples
|
||||
* through write(). The callback-driven pull semantics are unchanged.
|
||||
*/
|
||||
class PreviewAudioDevice {
|
||||
public:
|
||||
PreviewAudioDevice();
|
||||
|
||||
virtual ~PreviewAudioDevice();
|
||||
|
||||
/**
|
||||
* @brief Read up to `max_size` bytes from the queued buffer
|
||||
*
|
||||
* Called from the audio output callback. Returns the number of bytes
|
||||
* actually copied (0 when the buffer is empty, i.e. underrun).
|
||||
*/
|
||||
int64_t read(char *data, int64_t max_size);
|
||||
|
||||
/**
|
||||
* @brief Append `length` bytes to the queued buffer
|
||||
*/
|
||||
int64_t write(const char *data, int64_t length);
|
||||
|
||||
// Derives the frame size from the audio format (bytes per sample per
|
||||
// channel * channel count). Until params are set, bytes_per_frame()
|
||||
// reports 0, i.e. "unknown".
|
||||
void set_params(const core::AudioParams ¶ms);
|
||||
|
||||
int bytes_per_frame() const
|
||||
{
|
||||
return bytes_per_frame_;
|
||||
}
|
||||
|
||||
void set_bytes_per_frame(int b)
|
||||
{
|
||||
bytes_per_frame_ = b;
|
||||
}
|
||||
|
||||
void set_notify_interval(int64_t i)
|
||||
{
|
||||
notify_interval_ = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Install the callback fired when a notify interval boundary is crossed
|
||||
*
|
||||
* Replaces the former `notify` signal. The callback is invoked from read(),
|
||||
* i.e. from the audio output callback thread, AFTER the internal lock has
|
||||
* been released (the Qt version emitted while holding the lock; receivers
|
||||
* lived on another thread so it was effectively queued). The callback must
|
||||
* therefore be thread-safe and must not call back into this device.
|
||||
*/
|
||||
void set_notify_callback(std::function<void()> callback)
|
||||
{
|
||||
std::lock_guard<std::mutex> locker(lock_);
|
||||
notify_callback_ = std::move(callback);
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* @brief Frames consumed by the audio output callback
|
||||
*
|
||||
* Counted in the callback itself so underrun (zero-filled) frames are
|
||||
* included, making the value usable as a playback clock.
|
||||
*/
|
||||
void add_output_frames(int64_t frame_count)
|
||||
{
|
||||
output_frames_consumed_.fetch_add(frame_count);
|
||||
}
|
||||
|
||||
int64_t output_frames_consumed() const
|
||||
{
|
||||
return output_frames_consumed_.load();
|
||||
}
|
||||
|
||||
void reset_output_frames()
|
||||
{
|
||||
output_frames_consumed_.store(0);
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex lock_;
|
||||
|
||||
std::vector<char> buffer_;
|
||||
|
||||
int bytes_per_frame_;
|
||||
|
||||
int64_t notify_interval_;
|
||||
|
||||
int64_t bytes_read_;
|
||||
|
||||
std::function<void()> notify_callback_;
|
||||
|
||||
std::atomic<int64_t> output_frames_consumed_{0};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PREVIEWAUDIODEVICE_H
|
||||
@@ -0,0 +1,158 @@
|
||||
# Oak Video Editor - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Standalone build driver for the oakaudio module (M6). Mirrors
|
||||
# src/codec/standalone: oakaudio links oakcodec (encoder for recording,
|
||||
# decoder for waveform extraction), oakcommon (config + ffmpegutils C
|
||||
# ABI), olivecore and ffmpeg_bridge, plus PortAudio for the output
|
||||
# device. oakcodec's own dependency stack (oakrender/oaknode/...) is
|
||||
# assembled the same way src/codec/standalone does it.
|
||||
#
|
||||
# Usage (macOS/Homebrew):
|
||||
# cmake -S src/audio/standalone -B build-audio
|
||||
# cmake --build build-audio -j
|
||||
# ctest --test-dir build-audio
|
||||
|
||||
cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
|
||||
|
||||
project(oakaudio-standalone LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
get_filename_component(OAK_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake")
|
||||
if(EXISTS "/opt/homebrew")
|
||||
list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew")
|
||||
endif()
|
||||
|
||||
find_package(EXPAT REQUIRED)
|
||||
find_package(OpenColorIO CONFIG REQUIRED)
|
||||
find_package(OpenImageIO CONFIG REQUIRED)
|
||||
|
||||
set(OCIO_LIBRARIES OpenColorIO::OpenColorIO)
|
||||
set(OCIO_INCLUDE_DIRS "")
|
||||
set(OIIO_LIBRARIES OpenImageIO::OpenImageIO)
|
||||
set(OIIO_INCLUDE_DIRS "")
|
||||
|
||||
# In-repo libraries, built from source (same set src/codec/standalone
|
||||
# assembles, because oakaudio links oakcodec):
|
||||
# - olivecore (core/): oakcore_* C ABI and olive::core C++ wrappers
|
||||
# - ffmpeg_bridge: fb_* C ABI (resampler infra + codec's FFmpeg access)
|
||||
# - oakundo / oakcommon / oaknode / oakrender: oakcodec's own dependencies
|
||||
# - oakcodec: encoder (recording) + decoder (waveform extract) C ABI
|
||||
set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON)
|
||||
|
||||
add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core)
|
||||
target_include_directories(olivecore PUBLIC ${OAK_REPO_ROOT}/third_party/openfx/include)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge)
|
||||
|
||||
set(BUILD_TESTS OFF)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
|
||||
set(BUILD_TESTS ON)
|
||||
|
||||
# oaknode needs its transition stubs when built in this tree (see
|
||||
# src/render/standalone/CMakeLists.txt).
|
||||
target_include_directories(oaknode BEFORE PUBLIC
|
||||
${OAK_REPO_ROOT}/src/render/transition
|
||||
${OAK_REPO_ROOT}/src/node/transition
|
||||
${OAK_REPO_ROOT}/src/render/src
|
||||
)
|
||||
target_include_directories(oaknode PUBLIC
|
||||
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
|
||||
/opt/homebrew/include
|
||||
/opt/homebrew/include/Imath
|
||||
)
|
||||
target_link_options(oaknode PRIVATE
|
||||
"-undefined" "dynamic_lookup"
|
||||
)
|
||||
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/render/c_api ${CMAKE_BINARY_DIR}/render_c_api)
|
||||
|
||||
# Transition stub dirs must precede everything else: src/render/transition
|
||||
# first, then src/node/transition (shared stubs).
|
||||
target_include_directories(oakrender BEFORE PUBLIC
|
||||
${OAK_REPO_ROOT}/src/render/transition
|
||||
${OAK_REPO_ROOT}/src/node/transition
|
||||
)
|
||||
|
||||
target_include_directories(oakrender PUBLIC
|
||||
${OAK_REPO_ROOT}/engine/include
|
||||
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
|
||||
/opt/homebrew/include
|
||||
/opt/homebrew/include/Imath
|
||||
/opt/homebrew/include/OpenEXR
|
||||
)
|
||||
|
||||
# Vulkan headers (Homebrew keg-only vulkan-headers).
|
||||
if(NOT EXISTS "/opt/homebrew/include/vulkan/vulkan.h")
|
||||
execute_process(COMMAND brew --prefix vulkan-headers
|
||||
OUTPUT_VARIABLE VULKAN_HEADERS_PREFIX
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET)
|
||||
if(VULKAN_HEADERS_PREFIX AND EXISTS "${VULKAN_HEADERS_PREFIX}/include/vulkan/vulkan.h")
|
||||
target_include_directories(oakrender PUBLIC "${VULKAN_HEADERS_PREFIX}/include")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Symbols of the not-yet-split engine modules dangle by design. The
|
||||
# backend libraries resolve most symbols from liboakrender at load time
|
||||
# and dangle the same way.
|
||||
foreach(t oakrender oakgl oakgl2 oakvulkan)
|
||||
if(TARGET ${t})
|
||||
target_link_options(${t} PRIVATE
|
||||
"-undefined" "dynamic_lookup"
|
||||
)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
target_link_libraries(oakrender PRIVATE
|
||||
oaknode
|
||||
oakcommon
|
||||
oakundo
|
||||
olivecore
|
||||
ffmpeg_bridge
|
||||
${OCIO_LIBRARIES}
|
||||
${OIIO_LIBRARIES}
|
||||
"-framework OpenGL"
|
||||
"-framework CoreVideo"
|
||||
"-framework Metal"
|
||||
"-framework QuartzCore"
|
||||
)
|
||||
|
||||
# oakcodec (oakaudio's only codec access goes through its C ABI).
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/codec/src ${CMAKE_BINARY_DIR}/codec)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/codec/c_api ${CMAKE_BINARY_DIR}/codec_c_api)
|
||||
|
||||
target_link_options(oakcodec PRIVATE
|
||||
"-undefined" "dynamic_lookup"
|
||||
)
|
||||
|
||||
# oakaudio itself.
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/audio/src ${CMAKE_BINARY_DIR}/audio)
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/audio/c_api ${CMAKE_BINARY_DIR}/audio_c_api)
|
||||
|
||||
# Tests (oakaudio-gtest).
|
||||
if(BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_subdirectory(${OAK_REPO_ROOT}/src/audio/tests ${CMAKE_BINARY_DIR}/audio_tests)
|
||||
endif()
|
||||
@@ -0,0 +1,83 @@
|
||||
# Oak Video Editor - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
find_package(GTest REQUIRED)
|
||||
include(GoogleTest)
|
||||
|
||||
# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
|
||||
# build (see src/audio/standalone) sets OAK_REPO_ROOT explicitly.
|
||||
if(NOT DEFINED OAK_REPO_ROOT)
|
||||
set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
add_executable(oakaudio-gtest
|
||||
levelmeter_test.cpp
|
||||
manager_test.cpp
|
||||
processor_test.cpp
|
||||
sync_test.cpp
|
||||
waveform_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(oakaudio-gtest PRIVATE
|
||||
oakaudio
|
||||
oakcodec
|
||||
oakrender
|
||||
oaknode
|
||||
oakcommon
|
||||
oakundo
|
||||
olivecore
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
# liboakrender/liboaknode dangle OFX host symbols (-undefined
|
||||
# dynamic_lookup); force-load the host support archive into the test
|
||||
# process so dyld finds them in the flat namespace at startup. Mirrors
|
||||
# src/codec/tests/CMakeLists.txt.
|
||||
if(NOT DEFINED OAKRENDER_OFX_HOST_ARCHIVE)
|
||||
find_library(OAKRENDER_OFX_HOST_ARCHIVE NAMES OfxHost
|
||||
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport)
|
||||
endif()
|
||||
if(NOT OAKRENDER_OFX_HOST_ARCHIVE)
|
||||
message(FATAL_ERROR
|
||||
"libOfxHost.a not found; run the full-tree build once or set "
|
||||
"OAKRENDER_OFX_HOST_ARCHIVE")
|
||||
endif()
|
||||
target_link_options(oakaudio-gtest PRIVATE
|
||||
"-Wl,-force_load,${OAKRENDER_OFX_HOST_ARCHIVE}")
|
||||
|
||||
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
|
||||
# dynamic_lookup; the test binary links the inert shim from
|
||||
# src/node/standalone instead.
|
||||
target_sources(oakaudio-gtest PRIVATE
|
||||
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp)
|
||||
target_include_directories(oakaudio-gtest PRIVATE
|
||||
${OAK_REPO_ROOT}/engine/include
|
||||
)
|
||||
|
||||
# include/ must win over the render/node transition dirs that leak in
|
||||
# through oaknode's PUBLIC includes: they carry codec/*.h stubs that would
|
||||
# otherwise shadow the real oakcodec public headers. -iquote is searched
|
||||
# before every -I for quoted includes.
|
||||
target_compile_options(oakaudio-gtest PRIVATE
|
||||
"-iquote" "${OAK_REPO_ROOT}/include"
|
||||
)
|
||||
|
||||
# tests/demo.mp4 lives at the repo's shared tests directory.
|
||||
target_compile_definitions(oakaudio-gtest PRIVATE
|
||||
OAKAUDIO_TEST_DATA_DIR="${OAK_REPO_ROOT}/tests")
|
||||
|
||||
gtest_discover_tests(oakaudio-gtest)
|
||||
@@ -0,0 +1,117 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/levelmeter.h"
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzeConstantSignal)
|
||||
{
|
||||
// Constant 0.5 on both channels: peak = rms = 0.5, dB = 20*log10(0.5)
|
||||
std::vector<float> ch(1024, 0.5f);
|
||||
const float *planes[2] = { ch.data(), ch.data() };
|
||||
oakaudio_channel_stats channels[2];
|
||||
oakaudio_meter_stats summary;
|
||||
|
||||
ASSERT_EQ(oakaudio_levelmeter_analyze(planes, 2, 1024, channels, 2,
|
||||
&summary),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
for (int c = 0; c < 2; c++) {
|
||||
EXPECT_DOUBLE_EQ(channels[c].peak_linear, 0.5);
|
||||
EXPECT_DOUBLE_EQ(channels[c].rms_linear, 0.5);
|
||||
EXPECT_NEAR(channels[c].peak_db, 20.0 * std::log10(0.5), 1e-9);
|
||||
EXPECT_NEAR(channels[c].rms_db, 20.0 * std::log10(0.5), 1e-9);
|
||||
EXPECT_DOUBLE_EQ(channels[c].vu_db, channels[c].rms_db);
|
||||
}
|
||||
|
||||
EXPECT_DOUBLE_EQ(summary.max_peak_linear, 0.5);
|
||||
EXPECT_EQ(summary.silence, 0);
|
||||
// LUFS = -0.691 + 10*log10(mean square) = -0.691 + 10*log10(0.25)
|
||||
EXPECT_NEAR(summary.integrated_lufs, -0.691 + 10.0 * std::log10(0.25),
|
||||
1e-9);
|
||||
}
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzeSilence)
|
||||
{
|
||||
std::vector<float> ch(512, 0.0f);
|
||||
const float *planes[1] = { ch.data() };
|
||||
oakaudio_meter_stats summary;
|
||||
|
||||
ASSERT_EQ(oakaudio_levelmeter_analyze(planes, 1, 512, nullptr, 0,
|
||||
&summary),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(summary.silence, 1);
|
||||
EXPECT_DOUBLE_EQ(summary.max_peak_linear, 0.0);
|
||||
EXPECT_DOUBLE_EQ(summary.integrated_lufs, -200.0);
|
||||
}
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzePeakPerChannel)
|
||||
{
|
||||
std::vector<float> quiet(256, 0.1f);
|
||||
std::vector<float> loud(256, 0.0f);
|
||||
loud[7] = -0.8f; // single peak
|
||||
const float *planes[2] = { quiet.data(), loud.data() };
|
||||
oakaudio_channel_stats channels[2];
|
||||
|
||||
ASSERT_EQ(oakaudio_levelmeter_analyze(planes, 2, 256, channels, 2,
|
||||
nullptr),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_NEAR(channels[0].peak_linear, 0.1, 1e-6);
|
||||
EXPECT_NEAR(channels[1].peak_linear, 0.8, 1e-6);
|
||||
// dB floor: the zero samples dominate, but the peak channel has signal
|
||||
EXPECT_GT(channels[1].peak_db, channels[0].peak_db);
|
||||
}
|
||||
|
||||
TEST(OakAudioLevelMeter, AnalyzeErrorPaths)
|
||||
{
|
||||
std::vector<float> ch(64, 0.5f);
|
||||
const float *planes[1] = { ch.data() };
|
||||
oakaudio_channel_stats channels[1];
|
||||
oakaudio_meter_stats summary;
|
||||
|
||||
// NULL planes
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(nullptr, 1, 64, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Zero channels
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 0, 64, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Negative frame count
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 1, -1, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Insufficient channel capacity
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 1, 64, channels, 0,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Both outs NULL
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(planes, 1, 64, nullptr, 0, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL plane inside array
|
||||
const float *bad_planes[1] = { nullptr };
|
||||
EXPECT_EQ(oakaudio_levelmeter_analyze(bad_planes, 1, 64, channels, 1,
|
||||
&summary),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/manager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kSampleFmtF32 = 10; // olive::core::SampleFormat::f32
|
||||
constexpr uint64_t kLayoutStereo = 0x3;
|
||||
|
||||
// Creates the singleton for the duration of the test; skips when no
|
||||
// audio device environment is available.
|
||||
struct ManagerFixture {
|
||||
ManagerFixture()
|
||||
{
|
||||
created = (oakaudio_manager_create_instance() == OAKAUDIO_OK) &&
|
||||
oakaudio_manager_instance().ctx != nullptr;
|
||||
}
|
||||
~ManagerFixture()
|
||||
{
|
||||
if (created) {
|
||||
oakaudio_manager_destroy_instance();
|
||||
}
|
||||
}
|
||||
bool created = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioManager, InstanceLifecycle)
|
||||
{
|
||||
// Without an instance all calls report E_STATE and instance() is empty
|
||||
OakAudioManager none = oakaudio_manager_instance();
|
||||
EXPECT_EQ(none.ctx, nullptr);
|
||||
EXPECT_EQ(none.abi_version, OAKAUDIO_ABI_VERSION);
|
||||
|
||||
double secs;
|
||||
EXPECT_EQ(oakaudio_manager_seconds(none, &secs), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_get_output_device(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_get_input_device(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_device(none, 0), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_set_input_device(none, 0), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_notify_interval(none, 1024),
|
||||
OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_clear_buffered_output(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_stop_output(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_reset_output_clock(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_hard_reset(none), OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_manager_stop_recording(none), OAKAUDIO_E_STATE);
|
||||
|
||||
float samples[2] = { 0.0f, 0.0f };
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(none, 48000, kLayoutStereo,
|
||||
kSampleFmtF32,
|
||||
reinterpret_cast<char *>(samples),
|
||||
sizeof(samples), nullptr, 0),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
oakcodec_encoding_params params;
|
||||
std::memset(¶ms, 0, sizeof(params));
|
||||
params.audio_enabled = 1;
|
||||
EXPECT_EQ(oakaudio_manager_start_recording(none, ¶ms, nullptr, 0),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
// free is a no-op and safe on NULL/empty
|
||||
oakaudio_manager_free(nullptr);
|
||||
oakaudio_manager_free(&none);
|
||||
EXPECT_EQ(none.ctx, nullptr);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, DeviceRoundTrip)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
OakAudioManager m = oakaudio_manager_instance();
|
||||
ASSERT_NE(m.ctx, nullptr);
|
||||
// Singleton: addref/release never destroy
|
||||
m.addref(m.ctx);
|
||||
m.release(m.ctx);
|
||||
EXPECT_EQ(oakaudio_manager_instance().ctx, m.ctx);
|
||||
|
||||
// Devices: whatever was detected, get/set round-trips
|
||||
const int out_device = oakaudio_manager_get_output_device(m);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_device(m, out_device), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_get_output_device(m), out_device);
|
||||
|
||||
const int in_device = oakaudio_manager_get_input_device(m);
|
||||
EXPECT_EQ(oakaudio_manager_set_input_device(m, in_device), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_get_input_device(m), in_device);
|
||||
|
||||
// Notify interval set/get-free command
|
||||
EXPECT_EQ(oakaudio_manager_set_output_notify_interval(m, 4096),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_set_output_notify_interval(m, -1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
// No stream running: seconds() is negative, clock commands are valid
|
||||
double secs = 1.0;
|
||||
EXPECT_EQ(oakaudio_manager_seconds(m, &secs), OAKAUDIO_OK);
|
||||
EXPECT_LT(secs, 0.0);
|
||||
EXPECT_EQ(oakaudio_manager_seconds(m, nullptr), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_manager_reset_output_clock(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_clear_buffered_output(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_stop_output(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_hard_reset(m), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_manager_stop_recording(m), OAKAUDIO_OK);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, PushToOutput)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
OakAudioManager m = oakaudio_manager_instance();
|
||||
if (oakaudio_manager_get_output_device(m) < 0) {
|
||||
GTEST_SKIP() << "no output device";
|
||||
}
|
||||
|
||||
// One second of silence, packed f32 stereo
|
||||
std::vector<float> silence(48000 * 2, 0.0f);
|
||||
char error[256];
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(
|
||||
m, 48000, kLayoutStereo, kSampleFmtF32,
|
||||
reinterpret_cast<char *>(silence.data()),
|
||||
int64_t(silence.size() * sizeof(float)), error,
|
||||
int(sizeof(error))),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(
|
||||
m, 0, kLayoutStereo, kSampleFmtF32,
|
||||
reinterpret_cast<char *>(silence.data()), 16, nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_manager_push_to_output(m, 48000, kLayoutStereo,
|
||||
kSampleFmtF32, nullptr, 16,
|
||||
nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
oakaudio_manager_stop_output(m);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, StartRecordingErrorPaths)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
OakAudioManager m = oakaudio_manager_instance();
|
||||
|
||||
// NULL params / audio disabled
|
||||
EXPECT_EQ(oakaudio_manager_start_recording(m, nullptr, nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
oakcodec_encoding_params params;
|
||||
std::memset(¶ms, 0, sizeof(params));
|
||||
EXPECT_EQ(oakaudio_manager_start_recording(m, ¶ms, nullptr, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioManager, FindDeviceByName)
|
||||
{
|
||||
ManagerFixture fx;
|
||||
if (!fx.created) {
|
||||
GTEST_SKIP() << "no PortAudio device environment";
|
||||
}
|
||||
|
||||
// A name that matches nothing falls back to the default device (or
|
||||
// paNoDevice on device-less systems); either way no crash and a valid
|
||||
// index or -1.
|
||||
const int out = oakaudio_manager_find_device_by_name_s(
|
||||
"definitely-not-a-real-device-name-oakaudio-test", 1);
|
||||
EXPECT_GE(out, -1);
|
||||
|
||||
const int cfg = oakaudio_manager_find_config_device_by_name_s(1);
|
||||
EXPECT_GE(cfg, -1);
|
||||
|
||||
// Error path: NULL name
|
||||
EXPECT_EQ(oakaudio_manager_find_device_by_name_s(nullptr, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/manager.h"
|
||||
#include "audio/processor.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr int kSampleFmtF32P = 4; // olive::core::SampleFormat::f32_p
|
||||
constexpr uint64_t kLayoutStereo = 0x3;
|
||||
|
||||
struct ProcessorHandle {
|
||||
OakAudioProcessor h = oakaudio_processor_init();
|
||||
~ProcessorHandle() { oakaudio_processor_free(&h); }
|
||||
};
|
||||
|
||||
// Feed a full buffer through the processor and return the total number of
|
||||
// output frames produced (input drained + flushed).
|
||||
int convert_all(OakAudioProcessor p, const std::vector<std::vector<float>> &in,
|
||||
int chunk)
|
||||
{
|
||||
const int channels = int(in.size());
|
||||
const size_t nch = size_t(channels);
|
||||
std::vector<const float *> in_planes(nch);
|
||||
std::vector<std::vector<float>> out_store(nch);
|
||||
std::vector<float *> out_planes(nch);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
in_planes[size_t(ch)] = in[size_t(ch)].data();
|
||||
out_store[size_t(ch)].resize(size_t(chunk) * 4 + 4096);
|
||||
out_planes[size_t(ch)] = out_store[size_t(ch)].data();
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
const int frames = int(in[0].size());
|
||||
for (int pos = 0; pos < frames; pos += chunk) {
|
||||
const int n = std::min(chunk, frames - pos);
|
||||
std::vector<const float *> window(nch);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
window[size_t(ch)] = in[size_t(ch)].data() + pos;
|
||||
}
|
||||
const int produced = oakaudio_processor_convert(
|
||||
p, window.data(), n, out_planes.data(), int(out_store[0].size()));
|
||||
if (produced < 0) {
|
||||
return produced;
|
||||
}
|
||||
total += produced;
|
||||
}
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_flush(p), OAKAUDIO_OK);
|
||||
// Drain the resampler's internal delay
|
||||
for (int guard = 0; guard < 64; guard++) {
|
||||
const int produced = oakaudio_processor_convert(
|
||||
p, nullptr, 0, out_planes.data(), int(out_store[0].size()));
|
||||
if (produced <= 0) {
|
||||
break;
|
||||
}
|
||||
total += produced;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
std::vector<std::vector<float>> make_sine(int channels, int frames, int rate)
|
||||
{
|
||||
const size_t nch = size_t(channels);
|
||||
std::vector<std::vector<float>> data(nch);
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
data[size_t(ch)].resize(size_t(frames));
|
||||
for (int i = 0; i < frames; i++) {
|
||||
data[size_t(ch)][size_t(i)] =
|
||||
0.5f * std::sin(2.0 * M_PI * 440.0 * i / rate);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioProcessor, InitFree)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
{
|
||||
ProcessorHandle p;
|
||||
ASSERT_NE(p.h.ctx, nullptr);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before + 1);
|
||||
}
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
|
||||
// free is a no-op on NULL / empty handles
|
||||
oakaudio_processor_free(nullptr);
|
||||
OakAudioProcessor empty = {};
|
||||
oakaudio_processor_free(&empty);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, OpenCloseIsOpen)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 0);
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 1);
|
||||
|
||||
// Error path: opening an open processor
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_close(p.h), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 0);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, OpenInvalidArgs)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
// Unsupported output format (only f32p is delivered)
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, 1, 1.0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Bad sample rate
|
||||
EXPECT_EQ(oakaudio_processor_open(p.h, 0, kLayoutStereo, kSampleFmtF32P,
|
||||
48000, kLayoutStereo, kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(p.h), 0);
|
||||
|
||||
// Empty handle
|
||||
OakAudioProcessor empty = {};
|
||||
EXPECT_EQ(oakaudio_processor_open(empty, 44100, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_is_open(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_close(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_processor_flush(empty), OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertResample441To48)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
ProcessorHandle p;
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 44100, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
const int in_frames = 44100; // one second
|
||||
const auto sine = make_sine(2, in_frames, 44100);
|
||||
const int produced = convert_all(p.h, sine, 4096);
|
||||
ASSERT_GE(produced, 0);
|
||||
|
||||
// One second at 44.1k must become (within resampler tolerance) one
|
||||
// second at 48k.
|
||||
EXPECT_NEAR(produced, 48000, 200);
|
||||
|
||||
// Re-open check for leaks
|
||||
oakaudio_processor_close(p.h);
|
||||
oakaudio_processor_free(&p.h);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertSilenceStaysSilent)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
std::vector<std::vector<float>> silence(2, std::vector<float>(4096, 0.0f));
|
||||
std::vector<std::vector<float>> out(2, std::vector<float>(8192, -1.0f));
|
||||
std::vector<const float *> in_planes = { silence[0].data(),
|
||||
silence[1].data() };
|
||||
std::vector<float *> out_planes = { out[0].data(), out[1].data() };
|
||||
|
||||
const int produced = oakaudio_processor_convert(
|
||||
p.h, in_planes.data(), 4096, out_planes.data(), 8192);
|
||||
ASSERT_GT(produced, 0);
|
||||
EXPECT_EQ(produced, 4096); // same rate in/out: 1:1 frames
|
||||
for (int i = 0; i < produced; i++) {
|
||||
EXPECT_FLOAT_EQ(out[0][size_t(i)], 0.0f);
|
||||
EXPECT_FLOAT_EQ(out[1][size_t(i)], 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertTempo)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.5),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
const auto sine = make_sine(2, 48000, 48000);
|
||||
const int produced = convert_all(p.h, sine, 4096);
|
||||
ASSERT_GE(produced, 0);
|
||||
|
||||
// 1.5x tempo: one second of input becomes roughly 2/3 second of
|
||||
// output (atempo works on correlated windows, so allow slack)
|
||||
EXPECT_NEAR(produced, int(48000 / 1.5), 3000);
|
||||
}
|
||||
|
||||
TEST(OakAudioProcessor, ConvertErrorPaths)
|
||||
{
|
||||
ProcessorHandle p;
|
||||
|
||||
// Convert on a closed processor
|
||||
float dummy = 0.0f;
|
||||
float *out_planes[1] = { &dummy };
|
||||
const float *in_planes[1] = { &dummy };
|
||||
EXPECT_EQ(oakaudio_processor_convert(p.h, in_planes, 1, out_planes, 1),
|
||||
OAKAUDIO_E_STATE);
|
||||
EXPECT_EQ(oakaudio_processor_flush(p.h), OAKAUDIO_E_STATE);
|
||||
|
||||
// Empty handle
|
||||
OakAudioProcessor empty = {};
|
||||
EXPECT_EQ(oakaudio_processor_convert(empty, in_planes, 1, out_planes, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
// NULL input planes with frames
|
||||
ASSERT_EQ(oakaudio_processor_open(p.h, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 48000, kLayoutStereo,
|
||||
kSampleFmtF32P, 1.0),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_processor_convert(p.h, nullptr, 10, out_planes, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Negative counts
|
||||
EXPECT_EQ(oakaudio_processor_convert(p.h, in_planes, -1, out_planes, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
EXPECT_EQ(oakaudio_processor_flush(p.h), OAKAUDIO_OK);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/sync.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// 8-window synthetic envelope with a distinctive shape
|
||||
std::vector<double> make_envelope()
|
||||
{
|
||||
return { 0.1, 0.5, 0.9, 0.3, 0.2, 0.8, 0.4, 0.1 };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioSync, ExtractRmsEnvelope)
|
||||
{
|
||||
// Mono, window = 4 frames: window 0 constant 0.5 -> RMS 0.5
|
||||
std::vector<float> ch(8, 0.0f);
|
||||
for (int i = 0; i < 4; i++)
|
||||
ch[size_t(i)] = 0.5f;
|
||||
const float *planes[1] = { ch.data() };
|
||||
|
||||
// Query mode
|
||||
const int windows =
|
||||
oakaudio_sync_extract_rms_envelope(planes, 1, 8, 4, nullptr, 0);
|
||||
ASSERT_EQ(windows, 2);
|
||||
|
||||
double envelope[2];
|
||||
ASSERT_EQ(oakaudio_sync_extract_rms_envelope(planes, 1, 8, 4, envelope, 2),
|
||||
2);
|
||||
EXPECT_DOUBLE_EQ(envelope[0], 0.5);
|
||||
EXPECT_DOUBLE_EQ(envelope[1], 0.0);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, ExtractRmsEnvelopeErrorPaths)
|
||||
{
|
||||
float v = 0.0f;
|
||||
const float *planes[1] = { &v };
|
||||
double out[1];
|
||||
|
||||
EXPECT_EQ(oakaudio_sync_extract_rms_envelope(nullptr, 1, 8, 4, out, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_extract_rms_envelope(planes, 0, 8, 4, out, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_extract_rms_envelope(planes, 1, 8, 0, out, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateEnvelopeOffset)
|
||||
{
|
||||
const std::vector<double> ref = make_envelope();
|
||||
// Candidate = reference shifted right by 2 windows
|
||||
std::vector<double> cand = { 0.0, 0.0 };
|
||||
cand.insert(cand.end(), ref.begin(), ref.end());
|
||||
|
||||
oakaudio_offset_result out;
|
||||
ASSERT_EQ(oakaudio_sync_estimate_envelope_offset(
|
||||
ref.data(), int(ref.size()), cand.data(), int(cand.size()),
|
||||
nullptr, nullptr, 100, 8, &out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(out.valid, 1);
|
||||
EXPECT_EQ(out.offset_samples, 2 * 100);
|
||||
EXPECT_GT(out.confidence, 0.9);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateEnvelopeOffsetWithMasks)
|
||||
{
|
||||
const std::vector<double> ref = make_envelope();
|
||||
std::vector<double> cand = { 0.0, 0.0 };
|
||||
cand.insert(cand.end(), ref.begin(), ref.end());
|
||||
|
||||
std::vector<uint8_t> all_valid_ref(ref.size(), 1);
|
||||
std::vector<uint8_t> all_valid_cand(cand.size(), 1);
|
||||
|
||||
oakaudio_offset_result out;
|
||||
ASSERT_EQ(oakaudio_sync_estimate_envelope_offset(
|
||||
ref.data(), int(ref.size()), cand.data(), int(cand.size()),
|
||||
all_valid_ref.data(), all_valid_cand.data(), 100, 8, &out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(out.valid, 1);
|
||||
EXPECT_EQ(out.offset_samples, 200);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateEnvelopeOffsetErrorPaths)
|
||||
{
|
||||
double env[4] = { 0.1, 0.2, 0.3, 0.4 };
|
||||
oakaudio_offset_result out;
|
||||
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(nullptr, 4, env, 4,
|
||||
nullptr, nullptr, 100, 4,
|
||||
&out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(env, 0, env, 4, nullptr,
|
||||
nullptr, 100, 4, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(env, 4, env, 4, nullptr,
|
||||
nullptr, 0, 4, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_estimate_envelope_offset(env, 4, env, 4, nullptr,
|
||||
nullptr, 100, 4, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateStretchAndOffset)
|
||||
{
|
||||
const std::vector<double> ref = make_envelope();
|
||||
// Candidate runs at half speed: each ref window duplicated
|
||||
std::vector<double> cand;
|
||||
for (double v : ref) {
|
||||
cand.push_back(v);
|
||||
cand.push_back(v);
|
||||
}
|
||||
|
||||
oakaudio_stretch_offset_result out;
|
||||
ASSERT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
ref.data(), int(ref.size()), cand.data(), int(cand.size()),
|
||||
nullptr, nullptr, 100, 4, 1.0, 2.0, 0.25, &out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(out.valid, 1);
|
||||
EXPECT_NEAR(out.rate, 2.0, 0.13);
|
||||
EXPECT_GT(out.confidence, 0.9);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, EstimateStretchAndOffsetErrorPaths)
|
||||
{
|
||||
double env[4] = { 0.1, 0.2, 0.3, 0.4 };
|
||||
oakaudio_stretch_offset_result out;
|
||||
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
nullptr, 4, env, 4, nullptr, nullptr, 100, 4, 1.0, 2.0, 0.5,
|
||||
&out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// min_rate <= 0
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
env, 4, env, 4, nullptr, nullptr, 100, 4, 0.0, 2.0, 0.5, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// max < min
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
env, 4, env, 4, nullptr, nullptr, 100, 4, 2.0, 1.0, 0.5, &out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL out
|
||||
EXPECT_EQ(oakaudio_sync_estimate_stretch_and_offset(
|
||||
env, 4, env, 4, nullptr, nullptr, 100, 4, 1.0, 2.0, 0.5,
|
||||
nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, PlaceBySourceTime)
|
||||
{
|
||||
oakaudio_source_clip ref = {};
|
||||
ref.source_start_time_num = 10; // source clock at 10s
|
||||
ref.source_start_time_den = 1;
|
||||
ref.media_in_num = 2; // clip head is 2s into the media
|
||||
ref.media_in_den = 1;
|
||||
ref.has_source_start_time = 1;
|
||||
|
||||
oakaudio_source_clip cand = {};
|
||||
cand.source_start_time_num = 14; // 4s later on the same source clock
|
||||
cand.source_start_time_den = 1;
|
||||
cand.media_in_num = 0;
|
||||
cand.media_in_den = 1;
|
||||
cand.has_source_start_time = 1;
|
||||
|
||||
int64_t num, den;
|
||||
int valid;
|
||||
ASSERT_EQ(oakaudio_sync_place_by_source_time(&ref, &cand, 5, 1, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 1);
|
||||
// candidate head source = 14+0, reference head source = 10+2 = 12;
|
||||
// timeline_in = 5 + 14 - 12 = 7
|
||||
EXPECT_EQ(num, 7);
|
||||
EXPECT_EQ(den, 1);
|
||||
|
||||
// Missing source start time -> invalid placement, still OAKAUDIO_OK
|
||||
cand.has_source_start_time = 0;
|
||||
ASSERT_EQ(oakaudio_sync_place_by_source_time(&ref, &cand, 5, 1, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 0);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, PlaceBySourceTimeErrorPaths)
|
||||
{
|
||||
oakaudio_source_clip clip = {};
|
||||
clip.source_start_time_den = 1;
|
||||
clip.media_in_den = 1;
|
||||
clip.has_source_start_time = 1;
|
||||
|
||||
int64_t num, den;
|
||||
int valid;
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(nullptr, &clip, 5, 1, &num,
|
||||
&den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Zero denominators
|
||||
oakaudio_source_clip bad = clip;
|
||||
bad.media_in_den = 0;
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(&clip, &bad, 5, 1, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(&clip, &clip, 5, 0, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_place_by_source_time(&clip, &clip, 5, 1, nullptr,
|
||||
&den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioSync, PlaceByWaveformOffset)
|
||||
{
|
||||
int64_t num, den;
|
||||
int valid;
|
||||
|
||||
// Reference at 5s, candidate is 24000 samples late at 48k -> 5.5s
|
||||
ASSERT_EQ(oakaudio_sync_place_by_waveform_offset(5, 1, 24000, 48000, &num,
|
||||
&den, &valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 1);
|
||||
EXPECT_NEAR(double(num) / double(den), 5.5, 1e-6);
|
||||
|
||||
// Bad sample rate -> invalid placement
|
||||
ASSERT_EQ(oakaudio_sync_place_by_waveform_offset(5, 1, 24000, 0, &num, &den,
|
||||
&valid),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_EQ(valid, 0);
|
||||
|
||||
// Error path: NULL outs / zero denominator
|
||||
EXPECT_EQ(oakaudio_sync_place_by_waveform_offset(5, 0, 24000, 48000, &num,
|
||||
&den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_sync_place_by_waveform_offset(5, 1, 24000, 48000,
|
||||
nullptr, &den, &valid),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/levelmeter.h"
|
||||
#include "audio/manager.h"
|
||||
#include "audio/waveform.h"
|
||||
#include "codec/decoder.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct WaveformHandle {
|
||||
OakAudioWaveform h = oakaudio_waveform_init();
|
||||
~WaveformHandle() { oakaudio_waveform_free(&h); }
|
||||
};
|
||||
|
||||
std::string demo_file()
|
||||
{
|
||||
return std::string(OAKAUDIO_TEST_DATA_DIR) + "/demo.mp4";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioWaveform, InitFree)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_NE(w.h.ctx, nullptr);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before + 1);
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(w.h), 0);
|
||||
}
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
|
||||
oakaudio_waveform_free(nullptr);
|
||||
OakAudioWaveform empty = {};
|
||||
oakaudio_waveform_free(&empty);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ChannelCountAndLength)
|
||||
{
|
||||
WaveformHandle w;
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(w.h, 2), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(w.h), 2);
|
||||
|
||||
int64_t num = -1, den = -1;
|
||||
EXPECT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_EQ(num, 0);
|
||||
|
||||
// Error paths
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(empty, 2), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(w.h, -1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_length(empty, &num, &den), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_length(w.h, nullptr, &den), OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSamplesAndSummary)
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
|
||||
// One second at 48k: first half +0.5, second half -0.5
|
||||
std::vector<float> data(48000);
|
||||
for (int i = 0; i < 48000; i++) {
|
||||
data[size_t(i)] = (i < 24000) ? 0.5f : -0.5f;
|
||||
}
|
||||
const float *planes[1] = { data.data() };
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 48000, 48000, 0,
|
||||
1),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Summary of the whole second: min -0.5, max +0.5
|
||||
oakaudio_min_max pairs[2];
|
||||
const int points =
|
||||
oakaudio_waveform_get_summary(w.h, 0, 1, 1, 1, pairs, 2);
|
||||
ASSERT_EQ(points, 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, -0.5f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.5f);
|
||||
|
||||
// Summary of the first half only: all +0.5
|
||||
const int first_half =
|
||||
oakaudio_waveform_get_summary(w.h, 0, 1, 1, 2, pairs, 2);
|
||||
ASSERT_EQ(first_half, 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, 0.5f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.5f);
|
||||
|
||||
// Query mode: NULL out returns the point count
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 1, 1, nullptr, 0), 1);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSamplesErrorPaths)
|
||||
{
|
||||
WaveformHandle w;
|
||||
float v = 0.0f;
|
||||
const float *planes[1] = { &v };
|
||||
|
||||
// Channel count not set
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 16, 48000, 0, 1),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
// Zero denominator
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 16, 48000, 0, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL planes / bad counts
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, nullptr, 16, 48000, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 0, 48000, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(empty, planes, 16, 48000, 0,
|
||||
1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSilenceAndTrim)
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
|
||||
// 2 seconds of silence
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(w.h, 0, 1, 2, 1), OAKAUDIO_OK);
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 2.0, 1e-9);
|
||||
|
||||
oakaudio_min_max pairs[1];
|
||||
ASSERT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 2, 1, pairs, 1), 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, 0.0f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.0f);
|
||||
|
||||
// Trim away the first second
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(w.h, 1, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Resize to half a second
|
||||
EXPECT_EQ(oakaudio_waveform_resize(w.h, 1, 2), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 0.5, 1e-9);
|
||||
|
||||
// trim_range: in 0, length 1s
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(w.h, 0, 1, 1, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(w.h, 0, 0, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(w.h, 1, 0), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_resize(w.h, 1, 0), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(w.h, 0, 1, 1, 0), OAKAUDIO_E_INVALID);
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(empty, 1, 1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_resize(empty, 1, 1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(empty, 0, 1, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(empty, 0, 1, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(empty, 0, 1, 1, 1, pairs, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 1, 0, pairs, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSums)
|
||||
{
|
||||
WaveformHandle src, dst;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(src.h, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(dst.h, 1), OAKAUDIO_OK);
|
||||
|
||||
std::vector<float> data(48000, 0.25f);
|
||||
const float *planes[1] = { data.data() };
|
||||
ASSERT_EQ(oakaudio_waveform_overwrite_samples(src.h, planes, 48000, 48000,
|
||||
0, 1),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
// Copy all of src into dst at t=0
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, src.h, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_OK);
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(dst.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
oakaudio_min_max pairs[1];
|
||||
ASSERT_EQ(oakaudio_waveform_get_summary(dst.h, 0, 1, 1, 1, pairs, 1), 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.25f);
|
||||
|
||||
// Error paths
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, empty, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(empty, src.h, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, src.h, 0, 0, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, SumSamplesStatic)
|
||||
{
|
||||
std::vector<float> ch0 = { 0.1f, -0.4f, 0.3f, 0.2f };
|
||||
std::vector<float> ch1 = { -0.9f, 0.5f, 0.1f, 0.0f };
|
||||
const float *planes[2] = { ch0.data(), ch1.data() };
|
||||
oakaudio_min_max out[2];
|
||||
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 4, out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_FLOAT_EQ(out[0].min, -0.4f);
|
||||
EXPECT_FLOAT_EQ(out[0].max, 0.3f);
|
||||
EXPECT_FLOAT_EQ(out[1].min, -0.9f);
|
||||
EXPECT_FLOAT_EQ(out[1].max, 0.5f);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(nullptr, 2, 0, 4, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 0, 0, 4, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 0, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 4, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ReSumStatic)
|
||||
{
|
||||
oakaudio_min_max in[4] = { { -0.5f, 0.4f }, { -0.2f, 0.9f },
|
||||
{ -0.7f, 0.1f }, { 0.0f, 0.3f } };
|
||||
oakaudio_min_max out[2];
|
||||
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 2, out), OAKAUDIO_OK);
|
||||
EXPECT_FLOAT_EQ(out[0].min, -0.7f);
|
||||
EXPECT_FLOAT_EQ(out[0].max, 0.4f);
|
||||
EXPECT_FLOAT_EQ(out[1].min, -0.2f);
|
||||
EXPECT_FLOAT_EQ(out[1].max, 0.9f);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(nullptr, 4, 2, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 0, 2, out), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 0, out), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 2, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ExtractFromMediaFile)
|
||||
{
|
||||
const std::string file = demo_file();
|
||||
|
||||
// Probe the expected duration independently
|
||||
OakDecoder probe = oakcodec_decoder_probe(file.c_str());
|
||||
ASSERT_NE(probe.ctx, nullptr) << "demo.mp4 not decodable";
|
||||
oakcodec_audio_stream_info info;
|
||||
ASSERT_EQ(oakcodec_decoder_probe_get_audio_stream(probe, 0, &info),
|
||||
OAKCODEC_OK);
|
||||
// The audio stream's duration_ts is not populated for this file; the
|
||||
// video stream carries the clip duration.
|
||||
oakcodec_video_stream_info vinfo;
|
||||
double duration = 0.0;
|
||||
if (oakcodec_decoder_probe_get_video_stream(probe, 0, &vinfo) ==
|
||||
OAKCODEC_OK &&
|
||||
vinfo.time_base_den > 0) {
|
||||
duration = double(vinfo.duration_ts) * vinfo.time_base_num /
|
||||
vinfo.time_base_den;
|
||||
}
|
||||
oakcodec_decoder_free(&probe);
|
||||
ASSERT_GT(duration, 0.0);
|
||||
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
|
||||
constexpr int kSamplesPerPoint = 1024;
|
||||
// Two-stage sizing: NULL out returns the required point count
|
||||
const int required = oakaudio_waveform_extract(
|
||||
file.c_str(), 0, kSamplesPerPoint, nullptr, 0, nullptr);
|
||||
ASSERT_GT(required, 0);
|
||||
|
||||
std::vector<oakaudio_min_max> pairs(size_t(required) * 4);
|
||||
int channels = 0;
|
||||
const int points =
|
||||
oakaudio_waveform_extract(file.c_str(), 0, kSamplesPerPoint,
|
||||
pairs.data(), int(pairs.size()), &channels);
|
||||
ASSERT_EQ(points, required);
|
||||
EXPECT_EQ(channels, info.channel_count);
|
||||
|
||||
// Length consistency with the stream duration (generous tolerance for
|
||||
// container/decoder rounding)
|
||||
const double covered =
|
||||
double(points) * kSamplesPerPoint / info.sample_rate;
|
||||
EXPECT_NEAR(covered, duration, std::max(0.5, duration * 0.1));
|
||||
|
||||
// Non-trivial content: at least one point carries signal
|
||||
bool any_signal = false;
|
||||
for (int i = 0; i < points * channels; i++) {
|
||||
if (pairs[size_t(i)].max > 0.0f || pairs[size_t(i)].min < 0.0f) {
|
||||
any_signal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(any_signal);
|
||||
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ExtractErrorPaths)
|
||||
{
|
||||
int channels = 0;
|
||||
oakaudio_min_max pairs[8];
|
||||
|
||||
// Nonexistent file
|
||||
EXPECT_EQ(oakaudio_waveform_extract("/nonexistent/file.mp4", 0, 1024,
|
||||
pairs, 8, &channels),
|
||||
OAKAUDIO_E_NOT_FOUND);
|
||||
// NULL filename
|
||||
EXPECT_EQ(oakaudio_waveform_extract(nullptr, 0, 1024, pairs, 8, &channels),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Bad stream index
|
||||
EXPECT_EQ(oakaudio_waveform_extract(demo_file().c_str(), 99, 1024, pairs, 8,
|
||||
&channels),
|
||||
OAKAUDIO_E_NOT_FOUND);
|
||||
// Bad samples-per-point
|
||||
EXPECT_EQ(oakaudio_waveform_extract(demo_file().c_str(), 0, 0, pairs, 8,
|
||||
&channels),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
+5
-3
@@ -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/含音频);未引入
|
||||
内存态 stub(ffmpegencoder 当前版本已不读 Config)。
|
||||
2. **Config**(config 波次已收口):`ProxyManager::proxy_params_from_config()`
|
||||
现经 `oakcommon_config_*` C ABI 读取 ProxyWidth/ProxyHeight/ProxyDivider/
|
||||
ProxyCRF/ProxyPreset/ProxyIncludeAudio,ProxyParams 成员默认值兼作
|
||||
getter fallback(与 oakcommon 编译期默认值一致:1280x720/div1/crf23/
|
||||
veryfast/含音频)。
|
||||
3. **纹理路径功能回退**(oakrender 增补 shader-blit C API 后可恢复):
|
||||
oakrender C API 无通用 shader-blit,FFmpegDecoder 的 yuv2rgb GLSL 路径与
|
||||
去隔行 shader 路径已删除;YUV 帧改在 CPU 上 swscale 转 RGBA 后
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
target_sources(oakcommon PRIVATE
|
||||
colortransform.cpp
|
||||
config.cpp
|
||||
commandlineparser.cpp
|
||||
current.cpp
|
||||
subtitleparams.cpp
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ add_library(oakcommon SHARED
|
||||
commandlineparser.cpp
|
||||
commandlineparser.h
|
||||
colortransform.h
|
||||
configstore.cpp
|
||||
configstore.h
|
||||
current.cpp
|
||||
current.h
|
||||
debug.cpp
|
||||
|
||||
@@ -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 §ion : 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;
|
||||
}
|
||||
@@ -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 ¤t();
|
||||
|
||||
/**
|
||||
* @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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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` 仍走 stub(UI 波次)。
|
||||
8. `Node::gizmo_drag_move` 的 modifiers 参数为 `int`(原 `Qt::KeyboardModifiers`)。
|
||||
9. timeformat 的 `format_date_time()` 不实现 `MMMM`(月名)/`dddd`(星期名)本地化 token
|
||||
(原默认格式 `hh:mm:ss` 用不到);负 epoch 毫秒的 `zzz` 取模与 Qt 有边界差异(实际输入不会触发)。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¤t()
|
||||
{
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ¤t() { 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
|
||||
@@ -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 ¤t()
|
||||
{
|
||||
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
|
||||
@@ -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;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
|
||||
#include "config/config.h"
|
||||
#include "configaccessor.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ¤t() { 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
|
||||
Reference in New Issue
Block a user