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,353 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "audio/levelmeter.h"
|
||||
#include "audio/manager.h"
|
||||
#include "audio/waveform.h"
|
||||
#include "codec/decoder.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct WaveformHandle {
|
||||
OakAudioWaveform h = oakaudio_waveform_init();
|
||||
~WaveformHandle() { oakaudio_waveform_free(&h); }
|
||||
};
|
||||
|
||||
std::string demo_file()
|
||||
{
|
||||
return std::string(OAKAUDIO_TEST_DATA_DIR) + "/demo.mp4";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(OakAudioWaveform, InitFree)
|
||||
{
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_NE(w.h.ctx, nullptr);
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before + 1);
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(w.h), 0);
|
||||
}
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
|
||||
oakaudio_waveform_free(nullptr);
|
||||
OakAudioWaveform empty = {};
|
||||
oakaudio_waveform_free(&empty);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ChannelCountAndLength)
|
||||
{
|
||||
WaveformHandle w;
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(w.h, 2), OAKAUDIO_OK);
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(w.h), 2);
|
||||
|
||||
int64_t num = -1, den = -1;
|
||||
EXPECT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_EQ(num, 0);
|
||||
|
||||
// Error paths
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_get_channel_count(empty), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(empty, 2), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_set_channel_count(w.h, -1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_length(empty, &num, &den), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_length(w.h, nullptr, &den), OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSamplesAndSummary)
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
|
||||
// One second at 48k: first half +0.5, second half -0.5
|
||||
std::vector<float> data(48000);
|
||||
for (int i = 0; i < 48000; i++) {
|
||||
data[size_t(i)] = (i < 24000) ? 0.5f : -0.5f;
|
||||
}
|
||||
const float *planes[1] = { data.data() };
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 48000, 48000, 0,
|
||||
1),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Summary of the whole second: min -0.5, max +0.5
|
||||
oakaudio_min_max pairs[2];
|
||||
const int points =
|
||||
oakaudio_waveform_get_summary(w.h, 0, 1, 1, 1, pairs, 2);
|
||||
ASSERT_EQ(points, 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, -0.5f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.5f);
|
||||
|
||||
// Summary of the first half only: all +0.5
|
||||
const int first_half =
|
||||
oakaudio_waveform_get_summary(w.h, 0, 1, 1, 2, pairs, 2);
|
||||
ASSERT_EQ(first_half, 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, 0.5f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.5f);
|
||||
|
||||
// Query mode: NULL out returns the point count
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 1, 1, nullptr, 0), 1);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSamplesErrorPaths)
|
||||
{
|
||||
WaveformHandle w;
|
||||
float v = 0.0f;
|
||||
const float *planes[1] = { &v };
|
||||
|
||||
// Channel count not set
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 16, 48000, 0, 1),
|
||||
OAKAUDIO_E_STATE);
|
||||
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
// Zero denominator
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 16, 48000, 0, 0),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// NULL planes / bad counts
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, nullptr, 16, 48000, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(w.h, planes, 0, 48000, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_samples(empty, planes, 16, 48000, 0,
|
||||
1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSilenceAndTrim)
|
||||
{
|
||||
WaveformHandle w;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(w.h, 1), OAKAUDIO_OK);
|
||||
|
||||
// 2 seconds of silence
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(w.h, 0, 1, 2, 1), OAKAUDIO_OK);
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 2.0, 1e-9);
|
||||
|
||||
oakaudio_min_max pairs[1];
|
||||
ASSERT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 2, 1, pairs, 1), 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].min, 0.0f);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.0f);
|
||||
|
||||
// Trim away the first second
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(w.h, 1, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Resize to half a second
|
||||
EXPECT_EQ(oakaudio_waveform_resize(w.h, 1, 2), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 0.5, 1e-9);
|
||||
|
||||
// trim_range: in 0, length 1s
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(w.h, 0, 1, 1, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_length(w.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(w.h, 0, 0, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(w.h, 1, 0), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_resize(w.h, 1, 0), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(w.h, 0, 1, 1, 0), OAKAUDIO_E_INVALID);
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_trim_in(empty, 1, 1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_resize(empty, 1, 1), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_trim_range(empty, 0, 1, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_silence(empty, 0, 1, 1, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(empty, 0, 1, 1, 1, pairs, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_get_summary(w.h, 0, 1, 1, 0, pairs, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, OverwriteSums)
|
||||
{
|
||||
WaveformHandle src, dst;
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(src.h, 1), OAKAUDIO_OK);
|
||||
ASSERT_EQ(oakaudio_waveform_set_channel_count(dst.h, 1), OAKAUDIO_OK);
|
||||
|
||||
std::vector<float> data(48000, 0.25f);
|
||||
const float *planes[1] = { data.data() };
|
||||
ASSERT_EQ(oakaudio_waveform_overwrite_samples(src.h, planes, 48000, 48000,
|
||||
0, 1),
|
||||
OAKAUDIO_OK);
|
||||
|
||||
// Copy all of src into dst at t=0
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, src.h, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_OK);
|
||||
int64_t num, den;
|
||||
ASSERT_EQ(oakaudio_waveform_length(dst.h, &num, &den), OAKAUDIO_OK);
|
||||
EXPECT_NEAR(double(num) / double(den), 1.0, 1e-9);
|
||||
|
||||
oakaudio_min_max pairs[1];
|
||||
ASSERT_EQ(oakaudio_waveform_get_summary(dst.h, 0, 1, 1, 1, pairs, 1), 1);
|
||||
EXPECT_FLOAT_EQ(pairs[0].max, 0.25f);
|
||||
|
||||
// Error paths
|
||||
OakAudioWaveform empty = {};
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, empty, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(empty, src.h, 0, 1, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_overwrite_sums(dst.h, src.h, 0, 0, 0, 1, 0, 1),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, SumSamplesStatic)
|
||||
{
|
||||
std::vector<float> ch0 = { 0.1f, -0.4f, 0.3f, 0.2f };
|
||||
std::vector<float> ch1 = { -0.9f, 0.5f, 0.1f, 0.0f };
|
||||
const float *planes[2] = { ch0.data(), ch1.data() };
|
||||
oakaudio_min_max out[2];
|
||||
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 4, out),
|
||||
OAKAUDIO_OK);
|
||||
EXPECT_FLOAT_EQ(out[0].min, -0.4f);
|
||||
EXPECT_FLOAT_EQ(out[0].max, 0.3f);
|
||||
EXPECT_FLOAT_EQ(out[1].min, -0.9f);
|
||||
EXPECT_FLOAT_EQ(out[1].max, 0.5f);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(nullptr, 2, 0, 4, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 0, 0, 4, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 0, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_sum_samples_s(planes, 2, 0, 4, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ReSumStatic)
|
||||
{
|
||||
oakaudio_min_max in[4] = { { -0.5f, 0.4f }, { -0.2f, 0.9f },
|
||||
{ -0.7f, 0.1f }, { 0.0f, 0.3f } };
|
||||
oakaudio_min_max out[2];
|
||||
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 2, out), OAKAUDIO_OK);
|
||||
EXPECT_FLOAT_EQ(out[0].min, -0.7f);
|
||||
EXPECT_FLOAT_EQ(out[0].max, 0.4f);
|
||||
EXPECT_FLOAT_EQ(out[1].min, -0.2f);
|
||||
EXPECT_FLOAT_EQ(out[1].max, 0.9f);
|
||||
|
||||
// Error paths
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(nullptr, 4, 2, out),
|
||||
OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 0, 2, out), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 0, out), OAKAUDIO_E_INVALID);
|
||||
EXPECT_EQ(oakaudio_waveform_re_sum_s(in, 4, 2, nullptr),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ExtractFromMediaFile)
|
||||
{
|
||||
const std::string file = demo_file();
|
||||
|
||||
// Probe the expected duration independently
|
||||
OakDecoder probe = oakcodec_decoder_probe(file.c_str());
|
||||
ASSERT_NE(probe.ctx, nullptr) << "demo.mp4 not decodable";
|
||||
oakcodec_audio_stream_info info;
|
||||
ASSERT_EQ(oakcodec_decoder_probe_get_audio_stream(probe, 0, &info),
|
||||
OAKCODEC_OK);
|
||||
// The audio stream's duration_ts is not populated for this file; the
|
||||
// video stream carries the clip duration.
|
||||
oakcodec_video_stream_info vinfo;
|
||||
double duration = 0.0;
|
||||
if (oakcodec_decoder_probe_get_video_stream(probe, 0, &vinfo) ==
|
||||
OAKCODEC_OK &&
|
||||
vinfo.time_base_den > 0) {
|
||||
duration = double(vinfo.duration_ts) * vinfo.time_base_num /
|
||||
vinfo.time_base_den;
|
||||
}
|
||||
oakcodec_decoder_free(&probe);
|
||||
ASSERT_GT(duration, 0.0);
|
||||
|
||||
const int before = oakaudio_debug_alive_count();
|
||||
|
||||
constexpr int kSamplesPerPoint = 1024;
|
||||
// Two-stage sizing: NULL out returns the required point count
|
||||
const int required = oakaudio_waveform_extract(
|
||||
file.c_str(), 0, kSamplesPerPoint, nullptr, 0, nullptr);
|
||||
ASSERT_GT(required, 0);
|
||||
|
||||
std::vector<oakaudio_min_max> pairs(size_t(required) * 4);
|
||||
int channels = 0;
|
||||
const int points =
|
||||
oakaudio_waveform_extract(file.c_str(), 0, kSamplesPerPoint,
|
||||
pairs.data(), int(pairs.size()), &channels);
|
||||
ASSERT_EQ(points, required);
|
||||
EXPECT_EQ(channels, info.channel_count);
|
||||
|
||||
// Length consistency with the stream duration (generous tolerance for
|
||||
// container/decoder rounding)
|
||||
const double covered =
|
||||
double(points) * kSamplesPerPoint / info.sample_rate;
|
||||
EXPECT_NEAR(covered, duration, std::max(0.5, duration * 0.1));
|
||||
|
||||
// Non-trivial content: at least one point carries signal
|
||||
bool any_signal = false;
|
||||
for (int i = 0; i < points * channels; i++) {
|
||||
if (pairs[size_t(i)].max > 0.0f || pairs[size_t(i)].min < 0.0f) {
|
||||
any_signal = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(any_signal);
|
||||
|
||||
EXPECT_EQ(oakaudio_debug_alive_count(), before);
|
||||
}
|
||||
|
||||
TEST(OakAudioWaveform, ExtractErrorPaths)
|
||||
{
|
||||
int channels = 0;
|
||||
oakaudio_min_max pairs[8];
|
||||
|
||||
// Nonexistent file
|
||||
EXPECT_EQ(oakaudio_waveform_extract("/nonexistent/file.mp4", 0, 1024,
|
||||
pairs, 8, &channels),
|
||||
OAKAUDIO_E_NOT_FOUND);
|
||||
// NULL filename
|
||||
EXPECT_EQ(oakaudio_waveform_extract(nullptr, 0, 1024, pairs, 8, &channels),
|
||||
OAKAUDIO_E_INVALID);
|
||||
// Bad stream index
|
||||
EXPECT_EQ(oakaudio_waveform_extract(demo_file().c_str(), 99, 1024, pairs, 8,
|
||||
&channels),
|
||||
OAKAUDIO_E_NOT_FOUND);
|
||||
// Bad samples-per-point
|
||||
EXPECT_EQ(oakaudio_waveform_extract(demo_file().c_str(), 0, 0, pairs, 8,
|
||||
&channels),
|
||||
OAKAUDIO_E_INVALID);
|
||||
}
|
||||
Reference in New Issue
Block a user