Automated with clang-tidy readability-identifier-naming (config added to .clang-tidy) plus scripted passes, per the updated rules now documented in CONTRIBUTING.md: - types (class/struct/enum/alias/template params): PascalCase - functions, variables, members: snake_case (incl. rational -> Rational) - private/protected members: trailing underscore; static member variables likewise (instance_, available_themes_) - constants and enum values: snake_case (kLinear -> k_linear, F32P -> f32p); ALL_CAPS reserved for macros - macros: OAK_ prefix (OLIVE_ADD_TEST/OLIVE_ASSERT/OLIVE_CONFIG -> OAK_ADD_TEST/OAK_ASSERT/OAK_CONFIG, GL_PREAMBLE -> OAK_GL_PREAMBLE, include guards -> OAK_*) - file names: all lowercase (Current/Plugin/OliveHost/OliveClip/ OlivePluginInstance -> current/plugin/olivehost/oliveclip/ oliveplugininstance) - getters share the member name sans underscore, setters set_foo() - Qt and third-party (OpenFX) virtual overrides and framework callbacks keep their original names (exempt in .clang-tidy) Manual follow-ups required where automation could not reach: - string-based QMetaObject/SIGNAL/SLOT references updated to renamed methods (AddTask, CreatedFile, DeleteSpecificFile, moveSelectionUp, ...) - macro bodies referencing renamed methods (OLIVE_CONFIG, NODE_DEFAULT_DESTRUCTOR, MANAGEDDISPLAYWIDGET_*) - self-shadowing locals renamed where signals/methods became same-named (size_changed, worker_count, selected_items, import param, filters) - third_party OFX member/namespace usages restored (OFX::Host::*, _created, _clipPrefsDirty, createInstance, clearPersistentMessage) - STL protocol aliases restored (const_iterator) with .clang-tidy ignore rules; qHash overloads restored Full build and test suite pass: ctest 4/4, ~1960 gtest cases green.
913 lines
30 KiB
C++
913 lines
30 KiB
C++
/***
|
|
Oak Video Editor - Extended tests for AudioVisualWaveform and AudioProcessor
|
|
Copyright (C) 2026 Oak Team
|
|
***/
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include <QColor>
|
|
#include <QImage>
|
|
#include <QPainter>
|
|
|
|
#include "audio/audioprocessor.h"
|
|
#include "audio/audiovisualwaveform.h"
|
|
#include "olive/core/render/audioparams.h"
|
|
#include "olive/core/render/samplebuffer.h"
|
|
#include "olive/core/render/sampleformat.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
constexpr int k_sample_rate = 48000;
|
|
|
|
olive::core::AudioParams make_params(uint64_t channel_layout)
|
|
{
|
|
return olive::core::AudioParams(k_sample_rate, channel_layout,
|
|
olive::core::SampleFormat::f32_p);
|
|
}
|
|
|
|
// Returns a buffer of `sample_count` samples per channel, every sample set to
|
|
// `value`. Constant buffers keep mipmap chunk boundaries irrelevant, so
|
|
// summaries can be checked with exact float comparisons.
|
|
olive::core::SampleBuffer make_constant_buffer(
|
|
const olive::core::AudioParams ¶ms, size_t sample_count, float value)
|
|
{
|
|
olive::core::SampleBuffer buffer(params, sample_count);
|
|
for (int ch = 0; ch < buffer.channel_count(); ch++) {
|
|
float *data = buffer.data(ch);
|
|
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
|
data[i] = value;
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
// Returns one second of mono `first_value` followed by one second of mono
|
|
// `second_value`. The split lands exactly on a chunk boundary at every mipmap
|
|
// rate when kSampleRate is 48000, so per-range summaries stay exact.
|
|
olive::core::SampleBuffer make_split_mono_buffer(float first_value,
|
|
float second_value)
|
|
{
|
|
olive::core::SampleBuffer buffer(
|
|
make_params(olive::core::k_channel_layout_mono), size_t(k_sample_rate * 2));
|
|
float *data = buffer.data(0);
|
|
for (size_t i = 0; i < size_t(k_sample_rate); i++) {
|
|
data[i] = first_value;
|
|
}
|
|
for (size_t i = size_t(k_sample_rate); i < buffer.sample_count(); i++) {
|
|
data[i] = second_value;
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
olive::core::SampleBuffer make_mono_constant(size_t sample_count, float value)
|
|
{
|
|
return make_constant_buffer(make_params(olive::core::k_channel_layout_mono),
|
|
sample_count, value);
|
|
}
|
|
|
|
void expect_summary(const olive::AudioVisualWaveform::Sample &summary,
|
|
size_t channel, float expected_min, float expected_max)
|
|
{
|
|
ASSERT_LT(channel, summary.size());
|
|
// ReSumSamples aggregates starting from a zero-initialized accumulator, so
|
|
// a summary always spans zero for single-signed data.
|
|
EXPECT_FLOAT_EQ(summary.at(channel).min, std::min(expected_min, 0.0f));
|
|
EXPECT_FLOAT_EQ(summary.at(channel).max, std::max(expected_max, 0.0f));
|
|
}
|
|
|
|
// Pushes input through the processor, then flushes and drains everything the
|
|
// filter graph still holds, returning the accumulated per-plane output.
|
|
// Draining after a flush ends at EOF, which AudioProcessor reports as a
|
|
// negative return value, so the final Convert result is intentionally unused.
|
|
olive::AudioProcessor::Buffer convert_and_drain(olive::AudioProcessor &processor,
|
|
float **input, int nb_samples)
|
|
{
|
|
olive::AudioProcessor::Buffer output;
|
|
EXPECT_GE(processor.convert(input, nb_samples, &output), 0);
|
|
|
|
processor.flush();
|
|
|
|
olive::AudioProcessor::Buffer rest;
|
|
processor.convert(nullptr, 0, &rest);
|
|
|
|
if (output.size() < rest.size()) {
|
|
output.resize(rest.size());
|
|
}
|
|
for (int i = 0; i < rest.size(); i++) {
|
|
output[i].append(rest.at(i));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::OverwriteSamples
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSamplesWithZeroChannelsIsIgnored)
|
|
{
|
|
olive::AudioVisualWaveform waveform; // channel count defaults to zero
|
|
|
|
olive::core::SampleBuffer buffer(
|
|
make_params(olive::core::k_channel_layout_stereo), size_t(100));
|
|
waveform.overwrite_samples(buffer, k_sample_rate, olive::core::Rational(0));
|
|
|
|
// Nothing is written and no length is recorded
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(0));
|
|
EXPECT_TRUE(waveform
|
|
.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1))
|
|
.empty());
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSamplesAtNonZeroStartSetsLength)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.75f),
|
|
k_sample_rate, olive::core::Rational(2));
|
|
|
|
// length() tracks the absolute end time of the written data
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(3));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSamplesReplacesPreviousData)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.8f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.2f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
|
|
// Overwriting must replace, not mix or extend
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(1));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.2f, 0.2f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSamplesAfterGapLeavesSilence)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.25f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.75f),
|
|
k_sample_rate, olive::core::Rational(2));
|
|
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(3));
|
|
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.25f);
|
|
|
|
// The unwritten gap between the two writes reads back as zeros
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(1),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSamplesBeforeExistingDataPrependsZeros)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.75f),
|
|
k_sample_rate, olive::core::Rational(2));
|
|
ASSERT_EQ(waveform.length(), olive::core::Rational(3));
|
|
|
|
// Writing before the current virtual start pushes the existing data back
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.25f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.25f);
|
|
|
|
// Two seconds (one written, one gap) were prepended
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(1),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
|
|
// The original data is still intact at its absolute position
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
|
|
// The data now spans [0, 3): prepending via a negative TrimIn keeps the
|
|
// absolute end time tracked by length()
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(3));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::GetSummaryFromTime
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, GetSummaryFromTimeReturnsExactMinMaxPerChannel)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
olive::core::SampleBuffer buffer(
|
|
make_params(olive::core::k_channel_layout_stereo), size_t(k_sample_rate));
|
|
float *left = buffer.data(0);
|
|
float *right = buffer.data(1);
|
|
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
|
left[i] = 0.5f;
|
|
right[i] = -0.25f;
|
|
}
|
|
waveform.overwrite_samples(buffer, k_sample_rate, olive::core::Rational(0));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, 0.5f, 0.5f);
|
|
expect_summary(summary, 1, -0.25f, -0.25f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, GetSummaryFromTimeResolvesDistinctRanges)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
|
|
// One second of 0.25 followed by one second of 0.75
|
|
waveform.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
ASSERT_EQ(waveform.length(), olive::core::Rational(2));
|
|
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.25f);
|
|
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(1),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
|
|
// A range covering both seconds merges their extremes
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(2));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, GetSummaryFromTimeHandlesSurroundChannels)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(6);
|
|
|
|
olive::core::SampleBuffer buffer(
|
|
make_params(olive::core::k_channel_layout5_point1), size_t(k_sample_rate));
|
|
for (int ch = 0; ch < buffer.channel_count(); ch++) {
|
|
float *data = buffer.data(ch);
|
|
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
|
data[i] = 0.1f * float(ch + 1);
|
|
}
|
|
}
|
|
waveform.overwrite_samples(buffer, k_sample_rate, olive::core::Rational(0));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
|
|
ASSERT_EQ(summary.size(), 6);
|
|
for (size_t ch = 0; ch < 6; ch++) {
|
|
const float expected = 0.1f * float(ch + 1);
|
|
expect_summary(summary, ch, expected, expected);
|
|
}
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, GetSummaryFromTimeOnEmptyWaveformReturnsNullSamples)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
|
|
// No data written: one zeroed entry per channel
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
expect_summary(summary, 1, 0.0f, 0.0f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform,
|
|
GetSummaryFromTimeShorterThanOneSampleReturnsNullSamples)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.5f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
|
|
// Shorter than a single frame even at the highest mipmap rate (1024 Hz),
|
|
// so the request quantizes down to zero frames
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1, 100000));
|
|
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::OverwriteSums
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSumsCopiesEntireWaveform)
|
|
{
|
|
olive::AudioVisualWaveform source;
|
|
source.set_channel_count(1);
|
|
source.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
|
|
olive::AudioVisualWaveform dest;
|
|
dest.set_channel_count(1);
|
|
dest.overwrite_sums(source, olive::core::Rational(0));
|
|
|
|
EXPECT_EQ(dest.length(), olive::core::Rational(2));
|
|
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
dest.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.25f);
|
|
|
|
summary = dest.get_summary_from_time(olive::core::Rational(1),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSumsAtDestinationOffset)
|
|
{
|
|
olive::AudioVisualWaveform source;
|
|
source.set_channel_count(1);
|
|
source.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
|
|
olive::AudioVisualWaveform dest;
|
|
dest.set_channel_count(1);
|
|
dest.overwrite_sums(source, olive::core::Rational(1));
|
|
|
|
EXPECT_EQ(dest.length(), olive::core::Rational(3));
|
|
|
|
// The copied data lands one second later; because the destination's
|
|
// virtual start moved to the destination offset, only [1, 3) can be
|
|
// queried safely
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
dest.get_summary_from_time(olive::core::Rational(1),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.25f);
|
|
|
|
summary = dest.get_summary_from_time(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSumsWithSourceOffset)
|
|
{
|
|
olive::AudioVisualWaveform source;
|
|
source.set_channel_count(1);
|
|
source.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
|
|
olive::AudioVisualWaveform dest;
|
|
dest.set_channel_count(1);
|
|
dest.overwrite_sums(source, olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
|
|
// Only the source's second second is copied
|
|
EXPECT_EQ(dest.length(), olive::core::Rational(1));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
dest.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSumsWithLengthLimit)
|
|
{
|
|
olive::AudioVisualWaveform source;
|
|
source.set_channel_count(1);
|
|
source.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
|
|
olive::AudioVisualWaveform dest;
|
|
dest.set_channel_count(1);
|
|
dest.overwrite_sums(source, olive::core::Rational(0),
|
|
olive::core::Rational(0), olive::core::Rational(1));
|
|
|
|
// Only the source's first second is copied
|
|
EXPECT_EQ(dest.length(), olive::core::Rational(1));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
dest.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.25f, 0.25f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSumsWithOffsetBeyondSourceIsIgnored)
|
|
{
|
|
olive::AudioVisualWaveform source;
|
|
source.set_channel_count(1);
|
|
source.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
|
|
olive::AudioVisualWaveform dest;
|
|
dest.set_channel_count(1);
|
|
dest.overwrite_sums(source, olive::core::Rational(0),
|
|
olive::core::Rational(10));
|
|
|
|
// The offset starts past the end of every source mipmap, so nothing is
|
|
// copied and the destination stays empty
|
|
EXPECT_EQ(dest.length(), olive::core::Rational(0));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
dest.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::OverwriteSilence
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSilenceZeroesRange)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
waveform.overwrite_samples(
|
|
make_constant_buffer(make_params(olive::core::k_channel_layout_stereo),
|
|
size_t(k_sample_rate * 2), 0.8f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
|
|
// Silence [0.5, 1.5)
|
|
waveform.overwrite_silence(olive::core::Rational(1, 2),
|
|
olive::core::Rational(1));
|
|
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1, 2));
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, 0.8f, 0.8f);
|
|
expect_summary(summary, 1, 0.8f, 0.8f);
|
|
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(1, 2),
|
|
olive::core::Rational(1, 2));
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
expect_summary(summary, 1, 0.0f, 0.0f);
|
|
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(3, 2),
|
|
olive::core::Rational(1, 2));
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, 0.8f, 0.8f);
|
|
expect_summary(summary, 1, 0.8f, 0.8f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, OverwriteSilenceExtendsLength)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.5f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
ASSERT_EQ(waveform.length(), olive::core::Rational(1));
|
|
|
|
// Silencing past the end grows the buffer with zeros
|
|
waveform.overwrite_silence(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(3));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::Mid / Resize / TrimIn
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, MidFromOffsetReturnsTail)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
waveform.overwrite_samples(make_split_mono_buffer(0.25f, 0.75f), k_sample_rate,
|
|
olive::core::Rational(0));
|
|
|
|
const olive::AudioVisualWaveform mid =
|
|
waveform.mid(olive::core::Rational(1));
|
|
|
|
EXPECT_EQ(mid.length(), olive::core::Rational(1));
|
|
EXPECT_EQ(mid.channel_count(), 1);
|
|
|
|
// The original is untouched
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(2));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
mid.get_summary_from_time(olive::core::Rational(1),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.75f, 0.75f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, ResizeExtendPadsWithZeros)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.5f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
|
|
waveform.resize(olive::core::Rational(3));
|
|
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(3));
|
|
|
|
olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.5f, 0.5f);
|
|
|
|
// The extended region is zero-filled
|
|
summary = waveform.get_summary_from_time(olive::core::Rational(2),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.0f, 0.0f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, TrimInZeroIsNoOp)
|
|
{
|
|
olive::AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(1);
|
|
waveform.overwrite_samples(make_mono_constant(size_t(k_sample_rate), 0.5f),
|
|
k_sample_rate, olive::core::Rational(0));
|
|
|
|
waveform.trim_in(olive::core::Rational(0));
|
|
|
|
EXPECT_EQ(waveform.length(), olive::core::Rational(1));
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
waveform.get_summary_from_time(olive::core::Rational(0),
|
|
olive::core::Rational(1));
|
|
ASSERT_EQ(summary.size(), 1);
|
|
expect_summary(summary, 0, 0.5f, 0.5f);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::SumSamples / ReSumSamples
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, SumSamplesHonorsStartOffsetAndChannels)
|
|
{
|
|
olive::core::SampleBuffer buffer(
|
|
make_params(olive::core::k_channel_layout_stereo), size_t(100));
|
|
float *left = buffer.data(0);
|
|
float *right = buffer.data(1);
|
|
for (size_t i = 0; i < buffer.sample_count(); i++) {
|
|
left[i] = float(i) / 100.0f;
|
|
right[i] = -float(i) / 100.0f;
|
|
}
|
|
|
|
// Summarize samples [10, 30) only
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
olive::AudioVisualWaveform::sum_samples(buffer, 10, 20);
|
|
|
|
ASSERT_EQ(summary.size(), 2);
|
|
// SumSamples (unlike ReSumSamples) reports the true extremes of the range
|
|
EXPECT_FLOAT_EQ(summary.at(0).min, float(10) / 100.0f);
|
|
EXPECT_FLOAT_EQ(summary.at(0).max, float(29) / 100.0f);
|
|
EXPECT_FLOAT_EQ(summary.at(1).min, -float(29) / 100.0f);
|
|
EXPECT_FLOAT_EQ(summary.at(1).max, -float(10) / 100.0f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, ReSumSamplesMergesExtremesAcrossFrames)
|
|
{
|
|
std::vector<olive::AudioVisualWaveform::SamplePerChannel> frames(4);
|
|
frames[0] = { -0.2f, 0.3f }; // channel 0, narrow range
|
|
frames[1] = { 0.0f, 0.1f }; // channel 1
|
|
frames[2] = { -0.9f, 0.1f }; // channel 0, wider minimum
|
|
frames[3] = { 0.05f, 0.08f }; // channel 1
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
olive::AudioVisualWaveform::re_sum_samples(frames.data(), 4, 2);
|
|
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, -0.9f, 0.3f);
|
|
expect_summary(summary, 1, 0.0f, 0.1f);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, ReSumSamplesSingleFrameIsIdentity)
|
|
{
|
|
std::vector<olive::AudioVisualWaveform::SamplePerChannel> frames(2);
|
|
frames[0] = { -0.3f, 0.7f };
|
|
frames[1] = { -0.1f, 0.2f };
|
|
|
|
const olive::AudioVisualWaveform::Sample summary =
|
|
olive::AudioVisualWaveform::re_sum_samples(frames.data(), 2, 2);
|
|
|
|
ASSERT_EQ(summary.size(), 2);
|
|
expect_summary(summary, 0, -0.3f, 0.7f);
|
|
expect_summary(summary, 1, -0.1f, 0.2f);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioVisualWaveform::DrawSample
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioVisualWaveform, DrawSamplePaintsVerticalSpan)
|
|
{
|
|
QImage image(4, 100, QImage::Format_ARGB32);
|
|
image.fill(Qt::transparent);
|
|
|
|
{
|
|
QPainter painter(&image);
|
|
const olive::AudioVisualWaveform::Sample sample = { { -1.0f, 1.0f } };
|
|
olive::AudioVisualWaveform::draw_sample(&painter, sample, 1, 0, 100,
|
|
false);
|
|
}
|
|
|
|
// A full-scale sample spans the whole column
|
|
EXPECT_GT(image.pixelColor(1, 10).alpha(), 0);
|
|
EXPECT_GT(image.pixelColor(1, 90).alpha(), 0);
|
|
}
|
|
|
|
TEST(AudioVisualWaveform, DrawSampleIgnoresEmptySample)
|
|
{
|
|
QImage image(4, 100, QImage::Format_ARGB32);
|
|
image.fill(Qt::transparent);
|
|
|
|
{
|
|
QPainter painter(&image);
|
|
olive::AudioVisualWaveform::draw_sample(
|
|
&painter, olive::AudioVisualWaveform::Sample(), 1, 0, 100, false);
|
|
}
|
|
|
|
EXPECT_EQ(image.pixelColor(1, 10).alpha(), 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AudioProcessor
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(AudioProcessor, ConvertPassthroughCopiesInputSamples)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams params =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
ASSERT_TRUE(processor.open(params, params, 1.0));
|
|
|
|
constexpr int k_samples = 1024;
|
|
std::vector<float> left(k_samples, 0.5f);
|
|
std::vector<float> right(k_samples, -0.25f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
olive::AudioProcessor::Buffer output;
|
|
EXPECT_EQ(processor.convert(input, k_samples, &output), 0);
|
|
|
|
// Planar output keeps one byte plane per channel
|
|
ASSERT_EQ(output.size(), 2);
|
|
ASSERT_EQ(output.at(0).size(), k_samples * int(sizeof(float)));
|
|
ASSERT_EQ(output.at(1).size(), k_samples * int(sizeof(float)));
|
|
|
|
float value = 0.0f;
|
|
std::memcpy(&value, output.at(0).constData(), sizeof(float));
|
|
EXPECT_FLOAT_EQ(value, 0.5f);
|
|
std::memcpy(&value,
|
|
output.at(0).constData() + (k_samples - 1) * sizeof(float),
|
|
sizeof(float));
|
|
EXPECT_FLOAT_EQ(value, 0.5f);
|
|
std::memcpy(&value, output.at(1).constData(), sizeof(float));
|
|
EXPECT_FLOAT_EQ(value, -0.25f);
|
|
}
|
|
|
|
TEST(AudioProcessor, ConvertToPackedInterleavesChannels)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams from =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
const olive::core::AudioParams to(k_sample_rate,
|
|
olive::core::k_channel_layout_stereo,
|
|
olive::core::SampleFormat::f32);
|
|
ASSERT_TRUE(processor.open(from, to, 1.0));
|
|
|
|
constexpr int k_samples = 1024;
|
|
std::vector<float> left(k_samples, 0.5f);
|
|
std::vector<float> right(k_samples, -0.25f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
olive::AudioProcessor::Buffer output;
|
|
EXPECT_EQ(processor.convert(input, k_samples, &output), 0);
|
|
|
|
// Packed output folds both channels into a single interleaved plane
|
|
ASSERT_EQ(output.size(), 1);
|
|
ASSERT_EQ(output.at(0).size(), k_samples * 2 * int(sizeof(float)));
|
|
|
|
float left_value = 0.0f;
|
|
float right_value = 0.0f;
|
|
std::memcpy(&left_value, output.at(0).constData(), sizeof(float));
|
|
std::memcpy(&right_value, output.at(0).constData() + sizeof(float),
|
|
sizeof(float));
|
|
EXPECT_FLOAT_EQ(left_value, 0.5f);
|
|
EXPECT_FLOAT_EQ(right_value, -0.25f);
|
|
}
|
|
|
|
TEST(AudioProcessor, ConvertDownmixToMonoReducesPlaneCount)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
ASSERT_TRUE(processor.open(make_params(olive::core::k_channel_layout_stereo),
|
|
make_params(olive::core::k_channel_layout_mono),
|
|
1.0));
|
|
|
|
constexpr int k_samples = 1024;
|
|
std::vector<float> left(k_samples, 0.5f);
|
|
std::vector<float> right(k_samples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
olive::AudioProcessor::Buffer output;
|
|
EXPECT_EQ(processor.convert(input, k_samples, &output), 0);
|
|
|
|
ASSERT_EQ(output.size(), 1);
|
|
ASSERT_EQ(output.at(0).size(), k_samples * int(sizeof(float)));
|
|
|
|
// The downmix of two identical channels must stay audible regardless of
|
|
// the exact mixing coefficients
|
|
float value = 0.0f;
|
|
std::memcpy(&value, output.at(0).constData(), sizeof(float));
|
|
EXPECT_GT(value, 0.0f);
|
|
EXPECT_LE(value, 1.0f);
|
|
}
|
|
|
|
TEST(AudioProcessor, ConvertResampleDrainProducesExpectedSampleCount)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams from =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
const olive::core::AudioParams to(k_sample_rate / 2,
|
|
olive::core::k_channel_layout_stereo,
|
|
olive::core::SampleFormat::f32_p);
|
|
ASSERT_TRUE(processor.open(from, to, 1.0));
|
|
|
|
// One second of input
|
|
constexpr int k_samples = 48000;
|
|
std::vector<float> left(k_samples, 0.5f);
|
|
std::vector<float> right(k_samples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
const olive::AudioProcessor::Buffer output =
|
|
convert_and_drain(processor, input, k_samples);
|
|
|
|
ASSERT_EQ(output.size(), 2);
|
|
EXPECT_EQ(output.at(0).size(), output.at(1).size());
|
|
|
|
// 48 kHz downsampled to 24 kHz must produce about half the samples; the
|
|
// resampler's filter delay makes the exact total version-dependent
|
|
const int converted = output.at(0).size() / int(sizeof(float));
|
|
EXPECT_GE(converted, 23000);
|
|
EXPECT_LE(converted, 24500);
|
|
}
|
|
|
|
TEST(AudioProcessor, ConvertTempoDrainReducesSampleCount)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams params =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
ASSERT_TRUE(processor.open(params, params, 2.0));
|
|
|
|
// One second of input
|
|
constexpr int k_samples = 48000;
|
|
std::vector<float> left(k_samples, 0.5f);
|
|
std::vector<float> right(k_samples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
const olive::AudioProcessor::Buffer output =
|
|
convert_and_drain(processor, input, k_samples);
|
|
|
|
ASSERT_EQ(output.size(), 2);
|
|
|
|
// 2x tempo must output roughly half the input; atempo works in windows,
|
|
// so allow generous margins
|
|
const int converted = output.at(0).size() / int(sizeof(float));
|
|
EXPECT_GE(converted, 20000);
|
|
EXPECT_LE(converted, 28000);
|
|
}
|
|
|
|
TEST(AudioProcessor, ConvertWithNullOutputOnlyPushes)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams params =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
ASSERT_TRUE(processor.open(params, params, 1.0));
|
|
|
|
constexpr int k_samples = 1024;
|
|
std::vector<float> left(k_samples, 0.5f);
|
|
std::vector<float> right(k_samples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
// A null output buffer means push-only and is not an error
|
|
EXPECT_EQ(processor.convert(input, k_samples, nullptr), 0);
|
|
}
|
|
|
|
TEST(AudioProcessor, ConvertWithNoInputReturnsZeroWithEmptyPlanes)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams params =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
ASSERT_TRUE(processor.open(params, params, 1.0));
|
|
|
|
olive::AudioProcessor::Buffer output;
|
|
EXPECT_EQ(processor.convert(nullptr, 0, &output), 0);
|
|
|
|
// The output is still sized to the planar channel count, but empty
|
|
ASSERT_EQ(output.size(), 2);
|
|
EXPECT_TRUE(output.at(0).isEmpty());
|
|
EXPECT_TRUE(output.at(1).isEmpty());
|
|
}
|
|
|
|
TEST(AudioProcessor, OpenFixesZeroChannelLayout)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
|
|
// A zero layout mask is unusable by the filter graph and must be replaced
|
|
// with a default layout derived from the channel count
|
|
const olive::core::AudioParams from(k_sample_rate, 0,
|
|
olive::core::SampleFormat::f32_p);
|
|
const olive::core::AudioParams to =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
|
|
ASSERT_TRUE(processor.open(from, to, 1.0));
|
|
EXPECT_NE(processor.from().channel_layout(), uint64_t(0));
|
|
EXPECT_EQ(processor.from().channel_count(), 2);
|
|
}
|
|
|
|
TEST(AudioProcessor, CloseIsIdempotentAndReopenSucceeds)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
const olive::core::AudioParams params =
|
|
make_params(olive::core::k_channel_layout_stereo);
|
|
|
|
// Closing an unopened processor must be safe
|
|
processor.close();
|
|
EXPECT_FALSE(processor.is_open());
|
|
|
|
ASSERT_TRUE(processor.open(params, params, 1.0));
|
|
processor.close();
|
|
processor.close();
|
|
EXPECT_FALSE(processor.is_open());
|
|
|
|
EXPECT_TRUE(processor.open(params, params, 1.0));
|
|
EXPECT_TRUE(processor.is_open());
|
|
}
|
|
|
|
TEST(AudioProcessor, FlushWithoutOpenDoesNotCrash)
|
|
{
|
|
olive::AudioProcessor processor;
|
|
|
|
// Logs an error but must not crash
|
|
processor.flush();
|
|
EXPECT_FALSE(processor.is_open());
|
|
}
|