Fake tests rewritten to assert real behavior: - audio_smoke: conversion tests now actually Convert() samples and verify output; waveform length/summary assertions tightened to exact values - plugin_format_conversion: RowBytes/U8ToU16/LoadImageFile now call real production code (VideoParams::GetBytesPerPixel, sws scaler, OIIO decode of tests/img.png with known pixel values) - core_color: HSV round trip now verifies fromHsv(toHsv(c)) == c instead of comparing toHsv against its own accessors - core_bezier/node_inputimmediate: expected values replaced with independently derived constants instead of re-running the code under test - common_commandlineparser/common_debug/common_jobtime: capture stdout/stderr/qDebug and assert actual output content - proxy_manager: ProxyFinished test now drives a real proxy job instead of emitting the signal itself; proxy_dialog/panel/proxy/preferences/timeruler tests assert real widget state - viewer_smoke/preview_autocacher/render_misc: zero-assertion tests given observable-state assertions or removed where nothing is observable Duplicates removed: - plugin_smoke_test.cpp: 18 tests duplicated from plugin_paraminstance / plugin_support_* / plugin_renderer_readback (751 -> 180 lines) - module_smoke HumanStrings tests covered precisely by ui_humanstrings_test - render_misc duplicate kDefaultInterpolation constant check Removed by policy (skip allowed, never disabled): - all DISABLED_ prefixes: re-enabled as real offscreen tests or deleted - ffmpeg_decoder_hw: hardcoded personal path replaced with OAK_TEST_HW_DECODE_FILE env var, GTEST_SKIP when unset Also: - config_test: restore Config defaults after run (cross-test pollution) - render_worker_footage: drop /tmp debug-output scaffolding - previewaudiodevice construction test asserts the real bugfix
1037 lines
31 KiB
C++
1037 lines
31 KiB
C++
/*
|
|
* Oak Video Editor - Audio Subsystem Smoke Tests
|
|
* Copyright (C) 2025 Olive CE Team
|
|
*
|
|
* Comprehensive smoke tests for the audio subsystem including:
|
|
* - AudioManager lifecycle and device management
|
|
* - AudioProcessor format conversion and tempo
|
|
* - AudioVisualWaveform operations
|
|
* - SampleBuffer management
|
|
* - AudioParams validation and conversions
|
|
*/
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#include <QCoreApplication>
|
|
#include <QThread>
|
|
#include <QPainter>
|
|
#include <QImage>
|
|
|
|
// Audio headers
|
|
#include "audio/audiomanager.h"
|
|
#include "audio/audioprocessor.h"
|
|
#include "audio/audiovisualwaveform.h"
|
|
#include "render/previewaudiodevice.h"
|
|
#include "olive/core/render/samplebuffer.h"
|
|
#include "olive/core/render/audioparams.h"
|
|
#include "olive/core/render/sampleformat.h"
|
|
|
|
using namespace olive;
|
|
using namespace olive::core;
|
|
|
|
namespace olive
|
|
{
|
|
namespace audio
|
|
{
|
|
namespace test
|
|
{
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
static AudioParams MakeAudioParams(int sample_rate, uint64_t channel_layout,
|
|
SampleFormat format)
|
|
{
|
|
return AudioParams(sample_rate, channel_layout, format);
|
|
}
|
|
|
|
static void FillSampleBuffer(SampleBuffer &buffer, float value)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
static AudioProcessor::Buffer ConvertAndDrain(AudioProcessor &processor,
|
|
float **input, int nb_samples)
|
|
{
|
|
AudioProcessor::Buffer output;
|
|
EXPECT_GE(processor.Convert(input, nb_samples, &output), 0);
|
|
|
|
processor.Flush();
|
|
|
|
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;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: AudioParams
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokeParams, DefaultConstruction)
|
|
{
|
|
AudioParams params;
|
|
EXPECT_FALSE(params.is_valid());
|
|
EXPECT_EQ(params.sample_rate(), 0);
|
|
EXPECT_EQ(params.channel_count(), 0);
|
|
EXPECT_EQ(params.format(), SampleFormat::INVALID);
|
|
}
|
|
|
|
TEST(AudioSmokeParams, ValidConstruction)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
|
|
EXPECT_TRUE(params.is_valid());
|
|
EXPECT_EQ(params.sample_rate(), 48000);
|
|
EXPECT_EQ(params.channel_count(), 2);
|
|
EXPECT_EQ(params.format(), SampleFormat::F32P);
|
|
EXPECT_EQ(params.bytes_per_sample_per_channel(), 4);
|
|
EXPECT_EQ(params.bits_per_sample(), 32);
|
|
}
|
|
|
|
TEST(AudioSmokeParams, MonoChannelLayout)
|
|
{
|
|
AudioParams params(44100, kChannelLayoutMono, SampleFormat::S16);
|
|
|
|
EXPECT_TRUE(params.is_valid());
|
|
EXPECT_EQ(params.sample_rate(), 44100);
|
|
EXPECT_EQ(params.channel_count(), 1);
|
|
}
|
|
|
|
TEST(AudioSmokeParams, SurroundChannelLayout)
|
|
{
|
|
AudioParams params(48000, kChannelLayout5Point1, SampleFormat::F32P);
|
|
|
|
EXPECT_TRUE(params.is_valid());
|
|
EXPECT_EQ(params.channel_count(), 6);
|
|
}
|
|
|
|
TEST(AudioSmokeParams, TimeConversions)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
|
|
// Time to samples
|
|
EXPECT_EQ(params.time_to_samples(1.0), 48000);
|
|
EXPECT_EQ(params.time_to_samples(0.5), 24000);
|
|
EXPECT_EQ(params.time_to_samples(2.0), 96000);
|
|
|
|
// Samples to bytes
|
|
EXPECT_EQ(params.samples_to_bytes(48000),
|
|
48000 * 2 * 4); // samples * channels * bytes_per_sample
|
|
|
|
// Time to bytes
|
|
EXPECT_EQ(params.time_to_bytes(1.0), 48000 * 2 * 4);
|
|
}
|
|
|
|
TEST(AudioSmokeParams, EqualityOperators)
|
|
{
|
|
AudioParams params1(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams params2(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams params3(44100, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams params4(48000, kChannelLayoutMono, SampleFormat::F32P);
|
|
AudioParams params5(48000, kChannelLayoutStereo, SampleFormat::S16);
|
|
|
|
EXPECT_TRUE(params1 == params2);
|
|
EXPECT_FALSE(params1 != params2);
|
|
|
|
EXPECT_FALSE(params1 == params3); // Different sample rate
|
|
EXPECT_FALSE(params1 == params4); // Different channel layout
|
|
EXPECT_FALSE(params1 == params5); // Different format
|
|
}
|
|
|
|
TEST(AudioSmokeParams, CopyConstruction)
|
|
{
|
|
AudioParams original(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams copy(original);
|
|
|
|
EXPECT_TRUE(copy.is_valid());
|
|
EXPECT_EQ(copy.sample_rate(), original.sample_rate());
|
|
EXPECT_EQ(copy.channel_count(), original.channel_count());
|
|
EXPECT_EQ(copy.format(), original.format());
|
|
|
|
// Modifying copy should not affect original
|
|
copy.set_sample_rate(44100);
|
|
EXPECT_EQ(original.sample_rate(), 48000);
|
|
EXPECT_EQ(copy.sample_rate(), 44100);
|
|
}
|
|
|
|
TEST(AudioSmokeParams, CopyAssignment)
|
|
{
|
|
AudioParams original(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams copy;
|
|
copy = original;
|
|
|
|
EXPECT_TRUE(copy.is_valid());
|
|
EXPECT_EQ(copy.sample_rate(), original.sample_rate());
|
|
EXPECT_EQ(copy.channel_count(), original.channel_count());
|
|
EXPECT_EQ(copy.format(), original.format());
|
|
}
|
|
|
|
TEST(AudioSmokeParams, ChannelLayoutModification)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutMono, SampleFormat::F32P);
|
|
EXPECT_EQ(params.channel_count(), 1);
|
|
|
|
// Change to stereo
|
|
params.set_channel_layout(kChannelLayoutStereo);
|
|
EXPECT_EQ(params.channel_count(), 2);
|
|
|
|
// Change to 5.1
|
|
params.set_channel_layout(kChannelLayout5Point1);
|
|
EXPECT_EQ(params.channel_count(), 6);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: SampleBuffer
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokeBuffer, DefaultConstruction)
|
|
{
|
|
SampleBuffer buffer;
|
|
EXPECT_FALSE(buffer.is_allocated());
|
|
EXPECT_EQ(buffer.channel_count(), 0);
|
|
EXPECT_EQ(buffer.sample_count(), 0);
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, Allocation)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(48000)); // 1 second of samples
|
|
|
|
EXPECT_TRUE(buffer.is_allocated());
|
|
EXPECT_EQ(buffer.channel_count(), 2);
|
|
EXPECT_EQ(buffer.sample_count(), 48000);
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, DataAccess)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(100));
|
|
|
|
// Fill with test data
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
|
|
// Verify data
|
|
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
|
|
const float *data = buffer.data(ch);
|
|
for (size_t i = 0; i < buffer.sample_count(); ++i) {
|
|
EXPECT_FLOAT_EQ(data[i], 0.5f);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, Silence)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(100));
|
|
|
|
// Fill with non-zero values
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
|
|
// Apply silence
|
|
buffer.silence();
|
|
|
|
// Verify silence
|
|
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
|
|
const float *data = buffer.data(ch);
|
|
for (size_t i = 0; i < buffer.sample_count(); ++i) {
|
|
EXPECT_FLOAT_EQ(data[i], 0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, VolumeTransform)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(100));
|
|
|
|
// Fill with 1.0
|
|
FillSampleBuffer(buffer, 1.0f);
|
|
|
|
// Apply volume transform (50%)
|
|
buffer.transform_volume(0.5f);
|
|
|
|
// Verify volume change
|
|
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
|
|
const float *data = buffer.data(ch);
|
|
for (size_t i = 0; i < buffer.sample_count(); ++i) {
|
|
EXPECT_FLOAT_EQ(data[i], 0.5f);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, Clamp)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(100));
|
|
|
|
// Fill with values outside [-1, 1]
|
|
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] = (i % 2 == 0) ? 2.0f : -2.0f;
|
|
}
|
|
}
|
|
|
|
// Apply clamp
|
|
buffer.clamp();
|
|
|
|
// Verify clamping
|
|
for (int ch = 0; ch < buffer.channel_count(); ++ch) {
|
|
const float *data = buffer.data(ch);
|
|
for (size_t i = 0; i < buffer.sample_count(); ++i) {
|
|
EXPECT_GE(data[i], -1.0f);
|
|
EXPECT_LE(data[i], 1.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, FastSet)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer source(params, size_t(100));
|
|
SampleBuffer dest(params, size_t(100));
|
|
|
|
FillSampleBuffer(source, 0.75f);
|
|
dest.silence();
|
|
|
|
// Fast copy from source to dest
|
|
dest.fast_set(source, 0); // Copy to channel 0
|
|
|
|
// Verify channel 0 copied
|
|
const float *dest_data = dest.data(0);
|
|
for (size_t i = 0; i < dest.sample_count(); ++i) {
|
|
EXPECT_FLOAT_EQ(dest_data[i], 0.75f);
|
|
}
|
|
}
|
|
|
|
TEST(AudioSmokeBuffer, RipChannel)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(100));
|
|
|
|
// Fill channel 0 with 0.5, channel 1 with 0.25
|
|
float *ch0 = buffer.data(0);
|
|
float *ch1 = buffer.data(1);
|
|
for (size_t i = 0; i < buffer.sample_count(); ++i) {
|
|
ch0[i] = 0.5f;
|
|
ch1[i] = 0.25f;
|
|
}
|
|
|
|
// Rip channel 0
|
|
SampleBuffer ripped = buffer.rip_channel(0);
|
|
|
|
EXPECT_EQ(ripped.channel_count(), 1);
|
|
EXPECT_EQ(ripped.sample_count(), buffer.sample_count());
|
|
|
|
const float *ripped_data = ripped.data(0);
|
|
for (size_t i = 0; i < ripped.sample_count(); ++i) {
|
|
EXPECT_FLOAT_EQ(ripped_data[i], 0.5f);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: AudioVisualWaveform
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokeWaveform, DefaultConstruction)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
EXPECT_EQ(waveform.channel_count(), 0);
|
|
EXPECT_EQ(waveform.length(), rational(0));
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, ChannelCount)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
EXPECT_EQ(waveform.channel_count(), 2);
|
|
|
|
waveform.set_channel_count(6);
|
|
EXPECT_EQ(waveform.channel_count(), 6);
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, OverwriteSamples)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Create sample buffer with sine wave-like data
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(4800)); // 0.1 seconds
|
|
|
|
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] = std::sin(float(i) * 0.1f);
|
|
}
|
|
}
|
|
|
|
// Write samples to waveform
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
// 4800 samples at 48000 Hz is exactly 0.1 seconds
|
|
EXPECT_EQ(waveform.length(), rational(1, 10));
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, OverwriteSilence)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// First add some samples
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(4800));
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
// Overwrite with silence
|
|
waveform.OverwriteSilence(rational(0), rational(1, 10)); // 0.1 seconds
|
|
|
|
// The silence covers exactly the written region, so the length is
|
|
// unchanged at exactly 0.1 seconds
|
|
EXPECT_EQ(waveform.length(), rational(1, 10));
|
|
|
|
// ...and the overwritten region is actually silent
|
|
auto summary = waveform.GetSummaryFromTime(rational(0), rational(1, 10));
|
|
ASSERT_EQ(summary.size(), 2);
|
|
EXPECT_FLOAT_EQ(summary[0].min, 0.0f);
|
|
EXPECT_FLOAT_EQ(summary[0].max, 0.0f);
|
|
EXPECT_FLOAT_EQ(summary[1].min, 0.0f);
|
|
EXPECT_FLOAT_EQ(summary[1].max, 0.0f);
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, TrimIn)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Add samples
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(48000)); // 1 second
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
EXPECT_EQ(waveform.length(), rational(1));
|
|
|
|
// Trim 0.25 seconds from start
|
|
waveform.TrimIn(rational(1, 4));
|
|
|
|
EXPECT_EQ(waveform.length(), rational(3, 4));
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, Resize)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Add samples
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(48000));
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
EXPECT_EQ(waveform.length(), rational(1));
|
|
|
|
// Resize to 0.5 seconds
|
|
waveform.Resize(rational(1, 2));
|
|
|
|
EXPECT_EQ(waveform.length(), rational(1, 2));
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, TrimRange)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Add 2 seconds of samples
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(96000));
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
EXPECT_EQ(waveform.length(), rational(2));
|
|
|
|
// Trim to range [0.5, 1.0] (0.5 seconds duration starting at 0.5)
|
|
waveform.TrimRange(rational(1, 2), rational(1, 2));
|
|
|
|
EXPECT_EQ(waveform.length(), rational(1, 2));
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, Mid)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Add 2 seconds of samples
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(96000));
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
// Get mid section [0.5, 1.5]
|
|
AudioVisualWaveform mid = waveform.Mid(rational(1, 2), rational(1));
|
|
|
|
EXPECT_EQ(mid.length(), rational(1));
|
|
EXPECT_EQ(mid.channel_count(), 2);
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, GetSummaryFromTime)
|
|
{
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Add samples with varying values
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(4800));
|
|
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] = (i % 2 == 0) ? 0.8f : -0.8f;
|
|
}
|
|
}
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
// Get summary for first half
|
|
auto summary = waveform.GetSummaryFromTime(rational(0), rational(1, 20));
|
|
|
|
ASSERT_EQ(summary.size(), 2); // 2 channels
|
|
// Samples alternate between +0.8 and -0.8, so the summary is exactly that
|
|
EXPECT_FLOAT_EQ(summary[0].min, -0.8f);
|
|
EXPECT_FLOAT_EQ(summary[0].max, 0.8f);
|
|
EXPECT_FLOAT_EQ(summary[1].min, -0.8f);
|
|
EXPECT_FLOAT_EQ(summary[1].max, 0.8f);
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, SumSamples)
|
|
{
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(100));
|
|
|
|
// Fill with known pattern
|
|
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] = float(i) / 100.0f;
|
|
}
|
|
}
|
|
|
|
auto summary = AudioVisualWaveform::SumSamples(buffer, 0, 100);
|
|
|
|
EXPECT_EQ(summary.size(), 2);
|
|
EXPECT_FLOAT_EQ(summary[0].min, 0.0f);
|
|
EXPECT_FLOAT_EQ(summary[0].max, 0.99f);
|
|
}
|
|
|
|
TEST(AudioSmokeWaveform, ReSumSamples)
|
|
{
|
|
// Create sample data
|
|
std::vector<AudioVisualWaveform::SamplePerChannel> samples(200);
|
|
for (size_t i = 0; i < 100; ++i) {
|
|
samples[i * 2].min = -0.5f;
|
|
samples[i * 2].max = 0.5f;
|
|
samples[i * 2 + 1].min = -0.3f;
|
|
samples[i * 2 + 1].max = 0.3f;
|
|
}
|
|
|
|
auto summary = AudioVisualWaveform::ReSumSamples(samples.data(), 200, 2);
|
|
|
|
EXPECT_EQ(summary.size(), 2);
|
|
EXPECT_FLOAT_EQ(summary[0].min, -0.5f);
|
|
EXPECT_FLOAT_EQ(summary[0].max, 0.5f);
|
|
EXPECT_FLOAT_EQ(summary[1].min, -0.3f);
|
|
EXPECT_FLOAT_EQ(summary[1].max, 0.3f);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: AudioProcessor
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokeProcessor, DefaultConstruction)
|
|
{
|
|
AudioProcessor processor;
|
|
EXPECT_FALSE(processor.IsOpen());
|
|
}
|
|
|
|
TEST(AudioSmokeProcessor, OpenClose)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams to(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
|
|
EXPECT_TRUE(processor.Open(from, to, 1.0));
|
|
EXPECT_TRUE(processor.IsOpen());
|
|
|
|
processor.Close();
|
|
EXPECT_FALSE(processor.IsOpen());
|
|
}
|
|
|
|
TEST(AudioSmokeProcessor, SampleRateConversion)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams to(44100, kChannelLayoutStereo, SampleFormat::F32P);
|
|
|
|
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
|
ASSERT_TRUE(processor.IsOpen());
|
|
EXPECT_EQ(processor.from().sample_rate(), 48000);
|
|
EXPECT_EQ(processor.to().sample_rate(), 44100);
|
|
|
|
// Push one second of a constant signal
|
|
constexpr int kSamples = 48000;
|
|
std::vector<float> left(kSamples, 0.5f);
|
|
std::vector<float> right(kSamples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
const AudioProcessor::Buffer output =
|
|
ConvertAndDrain(processor, input, kSamples);
|
|
|
|
ASSERT_EQ(output.size(), 2);
|
|
ASSERT_EQ(output.at(0).size(), output.at(1).size());
|
|
|
|
// 48000 -> 44100 must produce ~44100 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, 43500);
|
|
EXPECT_LE(converted, 44600);
|
|
|
|
// A constant signal stays constant through resampling
|
|
float value = 0.0f;
|
|
std::memcpy(&value,
|
|
output.at(0).constData() + (converted / 2) * sizeof(float),
|
|
sizeof(float));
|
|
EXPECT_NEAR(value, 0.5f, 0.01f);
|
|
}
|
|
|
|
TEST(AudioSmokeProcessor, ChannelLayoutConversion)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams to(48000, kChannelLayoutMono, SampleFormat::F32P);
|
|
|
|
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
|
ASSERT_TRUE(processor.IsOpen());
|
|
EXPECT_EQ(processor.from().channel_count(), 2);
|
|
EXPECT_EQ(processor.to().channel_count(), 1);
|
|
|
|
constexpr int kSamples = 1024;
|
|
std::vector<float> left(kSamples, 0.5f);
|
|
std::vector<float> right(kSamples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
AudioProcessor::Buffer output;
|
|
ASSERT_EQ(processor.Convert(input, kSamples, &output), 0);
|
|
|
|
// Downmixing folds both channels into a single mono plane
|
|
ASSERT_EQ(output.size(), 1);
|
|
ASSERT_EQ(output.at(0).size(), kSamples * 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(AudioSmokeProcessor, FormatConversion)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams to(48000, kChannelLayoutStereo, SampleFormat::S16P);
|
|
|
|
ASSERT_TRUE(processor.Open(from, to, 1.0));
|
|
ASSERT_TRUE(processor.IsOpen());
|
|
|
|
constexpr int kSamples = 1024;
|
|
std::vector<float> left(kSamples, 0.5f);
|
|
std::vector<float> right(kSamples, -0.25f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
AudioProcessor::Buffer output;
|
|
ASSERT_EQ(processor.Convert(input, kSamples, &output), 0);
|
|
|
|
// Planar 16-bit output keeps one plane per channel at 2 bytes per sample
|
|
ASSERT_EQ(output.size(), 2);
|
|
ASSERT_EQ(output.at(0).size(), kSamples * int(sizeof(int16_t)));
|
|
ASSERT_EQ(output.at(1).size(), kSamples * int(sizeof(int16_t)));
|
|
|
|
// Known float values land on the expected 16-bit codes
|
|
int16_t value = 0;
|
|
std::memcpy(&value, output.at(0).constData(), sizeof(value));
|
|
EXPECT_NEAR(value, 16384, 1); // 0.5 * 32768
|
|
std::memcpy(&value, output.at(1).constData(), sizeof(value));
|
|
EXPECT_NEAR(value, -8192, 1); // -0.25 * 32768
|
|
}
|
|
|
|
TEST(AudioSmokeProcessor, TempoChange)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams to(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
|
|
// Open with 2x tempo
|
|
ASSERT_TRUE(processor.Open(from, to, 2.0));
|
|
ASSERT_TRUE(processor.IsOpen());
|
|
|
|
// One second of input
|
|
constexpr int kSamples = 48000;
|
|
std::vector<float> left(kSamples, 0.5f);
|
|
std::vector<float> right(kSamples, 0.5f);
|
|
float *input[2] = { left.data(), right.data() };
|
|
|
|
const AudioProcessor::Buffer output =
|
|
ConvertAndDrain(processor, input, kSamples);
|
|
|
|
ASSERT_EQ(output.size(), 2);
|
|
ASSERT_EQ(output.at(0).size(), output.at(1).size());
|
|
|
|
// 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);
|
|
|
|
// Tempo changes timing, not sample values
|
|
float value = 0.0f;
|
|
std::memcpy(&value,
|
|
output.at(0).constData() + (converted / 2) * sizeof(float),
|
|
sizeof(float));
|
|
EXPECT_NEAR(value, 0.5f, 0.05f);
|
|
}
|
|
|
|
TEST(AudioSmokeProcessor, InvalidOpen)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
// Open with valid params
|
|
AudioParams from(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
AudioParams to(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
EXPECT_TRUE(processor.Open(from, to, 1.0));
|
|
|
|
// Try to open again while already open (should fail)
|
|
EXPECT_FALSE(processor.Open(from, to, 1.0));
|
|
}
|
|
|
|
TEST(AudioSmokeProcessor, ConvertWithoutOpen)
|
|
{
|
|
AudioProcessor processor;
|
|
|
|
// Create input data
|
|
float *input[2] = { nullptr, nullptr };
|
|
std::vector<float> ch0(100, 0.5f);
|
|
std::vector<float> ch1(100, 0.5f);
|
|
input[0] = ch0.data();
|
|
input[1] = ch1.data();
|
|
|
|
AudioProcessor::Buffer output;
|
|
|
|
// Should fail since processor is not open
|
|
EXPECT_EQ(processor.Convert(input, 100, &output), -1);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: PreviewAudioDevice
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokePreviewDevice, Construction)
|
|
{
|
|
PreviewAudioDevice device;
|
|
EXPECT_TRUE(device.isSequential());
|
|
|
|
// Without params the frame size is unknown and reported as zero
|
|
EXPECT_EQ(device.bytes_per_frame(), 0);
|
|
|
|
// SetParams derives the frame size from the audio format:
|
|
// bytes per sample per channel * channel count
|
|
device.SetParams(AudioParams(48000, kChannelLayoutStereo, SampleFormat::F32P));
|
|
EXPECT_EQ(device.bytes_per_frame(), 8);
|
|
|
|
device.SetParams(AudioParams(48000, kChannelLayoutMono, SampleFormat::S16));
|
|
EXPECT_EQ(device.bytes_per_frame(), 2);
|
|
}
|
|
|
|
TEST(AudioSmokePreviewDevice, BytesPerFrame)
|
|
{
|
|
PreviewAudioDevice device;
|
|
|
|
device.set_bytes_per_frame(8); // 2 channels * 4 bytes (F32)
|
|
EXPECT_EQ(device.bytes_per_frame(), 8);
|
|
|
|
device.set_bytes_per_frame(4); // 2 channels * 2 bytes (S16)
|
|
EXPECT_EQ(device.bytes_per_frame(), 4);
|
|
}
|
|
|
|
TEST(AudioSmokePreviewDevice, NotifyInterval)
|
|
{
|
|
PreviewAudioDevice device;
|
|
device.open(QIODevice::ReadWrite);
|
|
|
|
// The notify interval is measured in bytes: Notify fires when the total
|
|
// number of bytes read crosses a multiple of the interval. readData() is
|
|
// called directly to bypass QIODevice's read-ahead buffer, which would
|
|
// otherwise coalesce the reads and hide the per-read transitions.
|
|
device.set_notify_interval(64);
|
|
|
|
int notify_count = 0;
|
|
QObject::connect(&device, &PreviewAudioDevice::Notify, &device,
|
|
[¬ify_count]() { ++notify_count; });
|
|
|
|
QByteArray data(256, 0x01);
|
|
ASSERT_EQ(device.write(data), 256);
|
|
|
|
// Nothing read yet, so no notification
|
|
EXPECT_EQ(notify_count, 0);
|
|
|
|
char buf[128];
|
|
ASSERT_EQ(device.readData(buf, 64), 64);
|
|
EXPECT_EQ(notify_count, 1); // crossed the 64-byte mark
|
|
|
|
ASSERT_EQ(device.readData(buf, 64), 64);
|
|
EXPECT_EQ(notify_count, 2); // crossed the 128-byte mark
|
|
|
|
// Crossing two intervals in one read emits a single notification
|
|
ASSERT_EQ(device.readData(buf, 128), 128);
|
|
EXPECT_EQ(notify_count, 3);
|
|
|
|
// Buffer drained: no more reads, no more notifications
|
|
EXPECT_EQ(device.readData(buf, 64), 0);
|
|
EXPECT_EQ(notify_count, 3);
|
|
|
|
// An interval of zero disables notifications entirely
|
|
PreviewAudioDevice quiet_device;
|
|
quiet_device.open(QIODevice::ReadWrite);
|
|
int quiet_count = 0;
|
|
QObject::connect(&quiet_device, &PreviewAudioDevice::Notify, &quiet_device,
|
|
[&quiet_count]() { ++quiet_count; });
|
|
ASSERT_EQ(quiet_device.write(data), 256);
|
|
EXPECT_EQ(quiet_device.readData(buf, 128), 128);
|
|
EXPECT_EQ(quiet_count, 0);
|
|
}
|
|
|
|
TEST(AudioSmokePreviewDevice, Clear)
|
|
{
|
|
PreviewAudioDevice device;
|
|
device.open(QIODevice::ReadWrite);
|
|
|
|
device.set_notify_interval(64);
|
|
int notify_count = 0;
|
|
QObject::connect(&device, &PreviewAudioDevice::Notify, &device,
|
|
[¬ify_count]() { ++notify_count; });
|
|
|
|
// Write some data and read it back (readData() is called directly to
|
|
// bypass QIODevice's read-ahead buffer)
|
|
QByteArray data(128, 0xAB);
|
|
ASSERT_EQ(device.write(data), 128);
|
|
char buf[128];
|
|
ASSERT_EQ(device.readData(buf, sizeof(buf)), 128);
|
|
EXPECT_EQ(notify_count, 1);
|
|
|
|
// Queue new data, then clear it
|
|
ASSERT_EQ(device.write(data), 128);
|
|
device.clear();
|
|
|
|
// After clear the device holds no data: a read returns 0 bytes, which is
|
|
// how the output callback knows to fill the stream with silence
|
|
EXPECT_EQ(device.readData(buf, sizeof(buf)), 0);
|
|
|
|
// clear() also resets the read counter, so notifications start over, and
|
|
// the device keeps working: data written after the clear reads back intact
|
|
ASSERT_EQ(device.write(data), 128);
|
|
ASSERT_EQ(device.readData(buf, sizeof(buf)), 128);
|
|
EXPECT_EQ(QByteArray(buf, data.size()), data);
|
|
EXPECT_EQ(notify_count, 2);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: Sample Format
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokeSampleFormat, ByteCount)
|
|
{
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::INVALID), 0);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::U8), 1);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::U8P), 1);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S16), 2);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S16P), 2);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S32), 4);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S32P), 4);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F32), 4);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F32P), 4);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S64), 8);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::S64P), 8);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F64), 8);
|
|
EXPECT_EQ(SampleFormat::byte_count(SampleFormat::F64P), 8);
|
|
}
|
|
|
|
TEST(AudioSmokeSampleFormat, PackedVsPlanar)
|
|
{
|
|
// Packed formats
|
|
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::U8));
|
|
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::S16));
|
|
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::S32));
|
|
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::F32));
|
|
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::S64));
|
|
EXPECT_TRUE(SampleFormat::is_packed(SampleFormat::F64));
|
|
|
|
// Planar formats
|
|
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::U8P));
|
|
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::S16P));
|
|
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::S32P));
|
|
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::F32P));
|
|
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::S64P));
|
|
EXPECT_TRUE(SampleFormat::is_planar(SampleFormat::F64P));
|
|
}
|
|
|
|
TEST(AudioSmokeSampleFormat, StringConversion)
|
|
{
|
|
// Test to_string (values may vary based on FFmpeg version)
|
|
EXPECT_EQ(SampleFormat::to_string(SampleFormat::U8), "u8");
|
|
EXPECT_EQ(SampleFormat::to_string(SampleFormat::S16), "s16");
|
|
EXPECT_EQ(SampleFormat::to_string(SampleFormat::S32), "s32");
|
|
// F32 can be "flt" or "f32" depending on FFmpeg version
|
|
std::string f32_str = SampleFormat::to_string(SampleFormat::F32);
|
|
EXPECT_TRUE(f32_str == "flt" || f32_str == "f32");
|
|
// F64 can be "dbl" or "f64" depending on FFmpeg version
|
|
std::string f64_str = SampleFormat::to_string(SampleFormat::F64);
|
|
EXPECT_TRUE(f64_str == "dbl" || f64_str == "f64");
|
|
|
|
// Test from_string
|
|
EXPECT_EQ(SampleFormat::from_string("u8"), SampleFormat::U8);
|
|
EXPECT_EQ(SampleFormat::from_string("s16"), SampleFormat::S16);
|
|
// from_string may not support all format names
|
|
EXPECT_EQ(SampleFormat::from_string(""), SampleFormat::INVALID);
|
|
EXPECT_EQ(SampleFormat::from_string("unknown"), SampleFormat::INVALID);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Smoke Test: Thread Safety
|
|
// ============================================================================
|
|
|
|
TEST(AudioSmokeThread, ConcurrentWaveformAccess)
|
|
{
|
|
const int num_threads = 4;
|
|
const int num_ops_per_thread = 50;
|
|
|
|
AudioVisualWaveform waveform;
|
|
waveform.set_channel_count(2);
|
|
|
|
// Pre-populate with data
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
SampleBuffer buffer(params, size_t(4800));
|
|
FillSampleBuffer(buffer, 0.5f);
|
|
waveform.OverwriteSamples(buffer, 48000, rational(0));
|
|
|
|
std::vector<std::thread> threads;
|
|
std::atomic<int> success_count{ 0 };
|
|
|
|
for (int t = 0; t < num_threads; ++t) {
|
|
threads.emplace_back([&waveform, &success_count, num_ops_per_thread]() {
|
|
for (int i = 0; i < num_ops_per_thread; ++i) {
|
|
// Read summary from different times
|
|
auto summary = waveform.GetSummaryFromTime(
|
|
rational(i % 10, 100), // 0.00 to 0.09 seconds
|
|
rational(1, 100) // 0.01 second duration
|
|
);
|
|
|
|
if (summary.size() == 2) {
|
|
success_count++;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto &t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
EXPECT_EQ(success_count.load(), num_threads * num_ops_per_thread);
|
|
}
|
|
|
|
TEST(AudioSmokeThread, ConcurrentSampleBufferOperations)
|
|
{
|
|
// SampleBuffer instances are independent value objects with no shared
|
|
// state, so operating on separate instances from multiple threads is
|
|
// race-free and must produce deterministic results
|
|
const int num_threads = 4;
|
|
|
|
AudioParams params(48000, kChannelLayoutStereo, SampleFormat::F32P);
|
|
|
|
std::vector<SampleBuffer> buffers;
|
|
buffers.reserve(num_threads);
|
|
for (int t = 0; t < num_threads; ++t) {
|
|
buffers.emplace_back(params, size_t(1000));
|
|
FillSampleBuffer(buffers.back(), 0.5f);
|
|
}
|
|
|
|
std::vector<std::thread> threads;
|
|
for (int t = 0; t < num_threads; ++t) {
|
|
threads.emplace_back([&buffers, t]() {
|
|
SampleBuffer &buffer = buffers[static_cast<size_t>(t)];
|
|
switch (t % 4) {
|
|
case 0:
|
|
buffer.transform_volume(0.8f);
|
|
break;
|
|
case 1:
|
|
buffer.transform_volume(4.0f);
|
|
buffer.clamp();
|
|
break;
|
|
case 2:
|
|
buffer.silence();
|
|
break;
|
|
case 3:
|
|
buffer.transform_volume_for_channel(1, 0.0f);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto &t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
// Each buffer must hold the exact deterministic outcome of its operation
|
|
for (size_t i = 0; i < buffers[0].sample_count(); ++i) {
|
|
EXPECT_FLOAT_EQ(buffers[0].data(0)[i], 0.4f); // 0.5 * 0.8
|
|
EXPECT_FLOAT_EQ(buffers[0].data(1)[i], 0.4f);
|
|
|
|
EXPECT_FLOAT_EQ(buffers[1].data(0)[i], 1.0f); // 0.5 * 4 clamped
|
|
EXPECT_FLOAT_EQ(buffers[1].data(1)[i], 1.0f);
|
|
|
|
EXPECT_FLOAT_EQ(buffers[2].data(0)[i], 0.0f); // silenced
|
|
EXPECT_FLOAT_EQ(buffers[2].data(1)[i], 0.0f);
|
|
|
|
EXPECT_FLOAT_EQ(buffers[3].data(0)[i], 0.5f); // untouched channel
|
|
EXPECT_FLOAT_EQ(buffers[3].data(1)[i], 0.0f); // zeroed channel
|
|
}
|
|
}
|
|
|
|
} // namespace test
|
|
} // namespace audio
|
|
} // namespace olive
|