/*** Oak Video Editor - Extended tests for AudioVisualWaveform and AudioProcessor Copyright (C) 2026 Oak Team ***/ #include #include #include #include #include #include #include #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 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 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 left(k_samples, 0.5f); std::vector 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 left(k_samples, 0.5f); std::vector 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 left(k_samples, 0.5f); std::vector 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 left(k_samples, 0.5f); std::vector 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 left(k_samples, 0.5f); std::vector 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 left(k_samples, 0.5f); std::vector 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()); }