backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
366
test/integration/audio/audio_timing_test.cc
Normal file
366
test/integration/audio/audio_timing_test.cc
Normal file
@@ -0,0 +1,366 @@
|
||||
// Audio Timing Tests for yaze MusicEditor
|
||||
//
|
||||
// These tests verify the APU and DSP timing accuracy to diagnose
|
||||
// and prevent audio playback speed issues (e.g., 1.5x speed bug).
|
||||
//
|
||||
// All tests are ROM-dependent to ensure realistic audio driver behavior.
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/audio/dsp.h"
|
||||
#include "app/emu/memory/memory.h"
|
||||
#include "app/emu/snes.h"
|
||||
#include "rom/rom.h"
|
||||
#include "test_utils.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
// =============================================================================
|
||||
// Audio Timing Constants
|
||||
// =============================================================================
|
||||
namespace audio_constants {
|
||||
|
||||
// SNES master clock frequency (NTSC)
|
||||
constexpr uint64_t kMasterClock = 21477272;
|
||||
|
||||
// APU clock frequency (~1.024 MHz)
|
||||
// Derived from: (32040 * 32) = 1,025,280 Hz
|
||||
constexpr uint64_t kApuClock = 1025280;
|
||||
|
||||
// DSP native sample rate
|
||||
constexpr int kNativeSampleRate = 32040;
|
||||
|
||||
// NTSC frame rate
|
||||
constexpr double kNtscFrameRate = 60.0988;
|
||||
|
||||
// Master cycles per NTSC frame
|
||||
constexpr uint64_t kMasterCyclesPerFrame = 357366; // 21477272 / 60.0988
|
||||
|
||||
// Expected samples per NTSC frame
|
||||
constexpr int kSamplesPerFrame = 533; // 32040 / 60.0988
|
||||
|
||||
// APU/Master clock ratio numerator and denominator (from apu.cc)
|
||||
constexpr uint64_t kApuCyclesNumerator = 32040 * 32; // 1,025,280
|
||||
constexpr uint64_t kApuCyclesDenominator = 1364 * 262 * 60; // 21,437,280
|
||||
|
||||
// Tolerance percentages for timing tests
|
||||
constexpr double kApuCycleRateTolerance = 0.01; // 1%
|
||||
constexpr double kDspSampleRateTolerance = 0.005; // 0.5%
|
||||
constexpr int kSamplesPerFrameTolerance = 2; // +/- 2 samples
|
||||
|
||||
} // namespace audio_constants
|
||||
|
||||
// =============================================================================
|
||||
// Audio Timing Test Fixture
|
||||
// =============================================================================
|
||||
|
||||
class AudioTimingTest : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
|
||||
// Reset cumulative cycle counter for each test
|
||||
cumulative_master_cycles_ = 0;
|
||||
|
||||
// Initialize SNES with ROM
|
||||
snes_ = std::make_unique<emu::Snes>();
|
||||
snes_->Init(rom()->vector());
|
||||
|
||||
// Get reference to APU
|
||||
apu_ = &snes_->apu();
|
||||
|
||||
// Reset APU cycle tracking to ensure fresh start for timing tests
|
||||
// Snes::Init() runs bootstrap cycles which advances the APU's
|
||||
// last_master_cycles_, so we need to reset it for our tests.
|
||||
apu_->Reset();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
apu_ = nullptr;
|
||||
snes_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
// Run APU for a specified number of master clock cycles
|
||||
// Returns the number of APU cycles actually executed
|
||||
uint64_t RunApuForMasterCycles(uint64_t master_cycles) {
|
||||
uint64_t apu_before = apu_->GetCycles();
|
||||
// APU expects cumulative master cycles
|
||||
cumulative_master_cycles_ += master_cycles;
|
||||
apu_->RunCycles(cumulative_master_cycles_);
|
||||
return apu_->GetCycles() - apu_before;
|
||||
}
|
||||
|
||||
// Get current DSP sample offset (for counting samples)
|
||||
uint32_t GetDspSampleOffset() const {
|
||||
return apu_->dsp().GetSampleOffset();
|
||||
}
|
||||
|
||||
// Count samples generated over a number of frames
|
||||
int CountSamplesOverFrames(int frame_count) {
|
||||
uint32_t start_offset = GetDspSampleOffset();
|
||||
|
||||
for (int i = 0; i < frame_count; ++i) {
|
||||
// APU expects cumulative master cycles, not per-frame delta
|
||||
cumulative_master_cycles_ += audio_constants::kMasterCyclesPerFrame;
|
||||
apu_->RunCycles(cumulative_master_cycles_);
|
||||
}
|
||||
|
||||
uint32_t end_offset = GetDspSampleOffset();
|
||||
|
||||
// Handle wrap-around (DSP buffer is 2048 samples with 0x7ff mask)
|
||||
constexpr uint32_t kBufferSize = 2048;
|
||||
if (end_offset >= start_offset) {
|
||||
return end_offset - start_offset;
|
||||
} else {
|
||||
return (kBufferSize - start_offset) + end_offset;
|
||||
}
|
||||
}
|
||||
|
||||
// Track cumulative master cycles for APU calls
|
||||
uint64_t cumulative_master_cycles_ = 0;
|
||||
|
||||
std::unique_ptr<emu::Snes> snes_;
|
||||
emu::Apu* apu_ = nullptr;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Core APU Timing Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(AudioTimingTest, ApuCycleRateMatchesExpected) {
|
||||
// Run APU for 1 second worth of master clock cycles
|
||||
constexpr uint64_t kOneSecondMasterCycles = audio_constants::kMasterClock;
|
||||
|
||||
uint64_t apu_cycles = RunApuForMasterCycles(kOneSecondMasterCycles);
|
||||
|
||||
// Expected APU cycles: ~1,024,000
|
||||
constexpr uint64_t kExpectedApuCycles = audio_constants::kApuClock;
|
||||
const double ratio =
|
||||
static_cast<double>(apu_cycles) / static_cast<double>(kExpectedApuCycles);
|
||||
|
||||
// Log results for debugging
|
||||
LOG_INFO("AudioTiming",
|
||||
"APU cycles in 1 second: %llu (expected: %llu, ratio: %.4f)",
|
||||
apu_cycles, kExpectedApuCycles, ratio);
|
||||
|
||||
// Verify within 1% tolerance
|
||||
EXPECT_NEAR(ratio, 1.0, audio_constants::kApuCycleRateTolerance)
|
||||
<< "APU cycle rate mismatch! Got " << apu_cycles << " cycles, expected ~"
|
||||
<< kExpectedApuCycles << " (ratio: " << ratio << ")";
|
||||
}
|
||||
|
||||
TEST_F(AudioTimingTest, DspSampleRateMatchesNative) {
|
||||
// Run APU for 1 second and count DSP samples
|
||||
constexpr int kTestFrames = 60; // ~1 second at 60fps
|
||||
|
||||
int total_samples = CountSamplesOverFrames(kTestFrames);
|
||||
|
||||
// Expected: ~32,040 samples
|
||||
constexpr int kExpectedSamples = audio_constants::kNativeSampleRate;
|
||||
const double ratio =
|
||||
static_cast<double>(total_samples) / static_cast<double>(kExpectedSamples);
|
||||
|
||||
LOG_INFO("AudioTiming",
|
||||
"DSP samples in %d frames: %d (expected: %d, ratio: %.4f)",
|
||||
kTestFrames, total_samples, kExpectedSamples, ratio);
|
||||
|
||||
EXPECT_NEAR(ratio, 1.0, audio_constants::kDspSampleRateTolerance)
|
||||
<< "DSP sample rate mismatch! Got " << total_samples
|
||||
<< " samples, expected ~" << kExpectedSamples << " (ratio: " << ratio
|
||||
<< ")";
|
||||
}
|
||||
|
||||
TEST_F(AudioTimingTest, FrameProducesCorrectSampleCount) {
|
||||
// Run exactly one NTSC frame
|
||||
uint32_t start_offset = GetDspSampleOffset();
|
||||
apu_->RunCycles(audio_constants::kMasterCyclesPerFrame);
|
||||
uint32_t end_offset = GetDspSampleOffset();
|
||||
|
||||
int samples = (end_offset >= start_offset)
|
||||
? (end_offset - start_offset)
|
||||
: (2048 - start_offset + end_offset);
|
||||
|
||||
LOG_INFO("AudioTiming", "Samples per frame: %d (expected: %d +/- %d)", samples,
|
||||
audio_constants::kSamplesPerFrame,
|
||||
audio_constants::kSamplesPerFrameTolerance);
|
||||
|
||||
EXPECT_NEAR(samples, audio_constants::kSamplesPerFrame,
|
||||
audio_constants::kSamplesPerFrameTolerance)
|
||||
<< "Frame sample count mismatch! Got " << samples << " samples";
|
||||
}
|
||||
|
||||
TEST_F(AudioTimingTest, MultipleFramesAccumulateSamplesCorrectly) {
|
||||
constexpr int kTestFrames = 60;
|
||||
constexpr int kExpectedTotal =
|
||||
audio_constants::kSamplesPerFrame * kTestFrames;
|
||||
|
||||
int total_samples = CountSamplesOverFrames(kTestFrames);
|
||||
|
||||
LOG_INFO("AudioTiming", "Total samples in %d frames: %d (expected: ~%d)",
|
||||
kTestFrames, total_samples, kExpectedTotal);
|
||||
|
||||
// Allow 1% tolerance for accumulated drift
|
||||
const double ratio =
|
||||
static_cast<double>(total_samples) / static_cast<double>(kExpectedTotal);
|
||||
EXPECT_NEAR(ratio, 1.0, 0.01)
|
||||
<< "Accumulated sample count mismatch over " << kTestFrames << " frames";
|
||||
}
|
||||
|
||||
TEST_F(AudioTimingTest, ApuMasterClockRatioIsCorrect) {
|
||||
// Verify the fixed-point ratio used in APU::RunCycles
|
||||
constexpr double kExpectedRatio =
|
||||
static_cast<double>(audio_constants::kApuCyclesNumerator) /
|
||||
static_cast<double>(audio_constants::kApuCyclesDenominator);
|
||||
|
||||
LOG_INFO("AudioTiming", "APU/Master ratio: %.6f (num=%llu, den=%llu)",
|
||||
kExpectedRatio, audio_constants::kApuCyclesNumerator,
|
||||
audio_constants::kApuCyclesDenominator);
|
||||
|
||||
// Run a small test to verify actual ratio matches expected
|
||||
constexpr uint64_t kTestMasterCycles = 1000000; // 1M master cycles
|
||||
uint64_t apu_cycles = RunApuForMasterCycles(kTestMasterCycles);
|
||||
|
||||
double actual_ratio =
|
||||
static_cast<double>(apu_cycles) / static_cast<double>(kTestMasterCycles);
|
||||
|
||||
EXPECT_NEAR(actual_ratio, kExpectedRatio, 0.0001)
|
||||
<< "APU/Master ratio mismatch! Actual: " << actual_ratio
|
||||
<< ", Expected: " << kExpectedRatio;
|
||||
}
|
||||
|
||||
TEST_F(AudioTimingTest, DspCyclesEvery32ApuCycles) {
|
||||
// The DSP should cycle once every 32 APU cycles (from apu.cc:246)
|
||||
// This is verified by checking sample generation rate
|
||||
|
||||
// Run 32000 APU cycles (should produce 1000 DSP cycles = 1000 samples)
|
||||
uint64_t start_apu = apu_->GetCycles();
|
||||
uint32_t start_samples = GetDspSampleOffset();
|
||||
|
||||
// We need to run enough master cycles to get 32000 APU cycles
|
||||
// APU cycles = master * (1025280 / 21437280) ≈ master * 0.0478
|
||||
// So master = 32000 / 0.0478 ≈ 669456
|
||||
constexpr uint64_t kTargetApuCycles = 32000;
|
||||
constexpr uint64_t kMasterCycles =
|
||||
(kTargetApuCycles * audio_constants::kApuCyclesDenominator) /
|
||||
audio_constants::kApuCyclesNumerator;
|
||||
|
||||
apu_->RunCycles(kMasterCycles);
|
||||
|
||||
uint64_t end_apu = apu_->GetCycles();
|
||||
uint32_t end_samples = GetDspSampleOffset();
|
||||
|
||||
uint64_t apu_delta = end_apu - start_apu;
|
||||
int sample_delta = (end_samples >= start_samples)
|
||||
? (end_samples - start_samples)
|
||||
: (2048 - start_samples + end_samples);
|
||||
|
||||
// Expected: 1 sample per 32 APU cycles
|
||||
double cycles_per_sample = static_cast<double>(apu_delta) / sample_delta;
|
||||
|
||||
LOG_INFO("AudioTiming",
|
||||
"APU cycles per DSP sample: %.2f (expected: 32.0), samples=%d, "
|
||||
"apu_cycles=%llu",
|
||||
cycles_per_sample, sample_delta, apu_delta);
|
||||
|
||||
EXPECT_NEAR(cycles_per_sample, 32.0, 0.5)
|
||||
<< "DSP not cycling every 32 APU cycles!";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Regression Tests for 1.5x Speed Bug
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(AudioTimingTest, PlaybackSpeedRegression_NotTooFast) {
|
||||
// This test verifies that audio doesn't play at 1.5x speed
|
||||
// If the bug is present, we'd see ~47,700 samples instead of ~32,040
|
||||
|
||||
constexpr int kTestFrames = 60; // 1 second
|
||||
int total_samples = CountSamplesOverFrames(kTestFrames);
|
||||
|
||||
// At 1.5x speed, we'd get ~48,060 samples
|
||||
constexpr int kBuggySpeed15x = 48060;
|
||||
|
||||
// Verify we're NOT close to the 1.5x buggy value
|
||||
double speed_ratio =
|
||||
static_cast<double>(total_samples) / audio_constants::kNativeSampleRate;
|
||||
|
||||
LOG_INFO("AudioTiming",
|
||||
"Speed check: %d samples in 1 second (ratio: %.2fx, 1.0x expected)",
|
||||
total_samples, speed_ratio);
|
||||
|
||||
// If speed is >= 1.3x, something is wrong
|
||||
EXPECT_LT(speed_ratio, 1.3)
|
||||
<< "Audio playback is too fast! Speed ratio: " << speed_ratio
|
||||
<< "x (samples: " << total_samples << ", expected: ~32040)";
|
||||
|
||||
// Speed should be close to 1.0x
|
||||
EXPECT_GT(speed_ratio, 0.9) << "Audio playback is too slow!";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Extended Timing Stability Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(AudioTimingTest, NoCycleDriftOver60Seconds) {
|
||||
// Run for 60 seconds of simulated time and check for drift
|
||||
constexpr int kTestSeconds = 60;
|
||||
constexpr int kFramesPerSecond = 60;
|
||||
|
||||
uint64_t cumulative_apu_cycles = 0;
|
||||
int cumulative_samples = 0;
|
||||
|
||||
for (int sec = 0; sec < kTestSeconds; ++sec) {
|
||||
uint64_t apu_before = apu_->GetCycles();
|
||||
int samples_before = GetDspSampleOffset();
|
||||
|
||||
// Run one second of frames
|
||||
// APU expects cumulative master cycles, not per-frame delta
|
||||
for (int frame = 0; frame < kFramesPerSecond; ++frame) {
|
||||
cumulative_master_cycles_ += audio_constants::kMasterCyclesPerFrame;
|
||||
apu_->RunCycles(cumulative_master_cycles_);
|
||||
}
|
||||
|
||||
uint64_t apu_after = apu_->GetCycles();
|
||||
int samples_after = GetDspSampleOffset();
|
||||
|
||||
cumulative_apu_cycles += (apu_after - apu_before);
|
||||
int sample_delta = (samples_after >= samples_before)
|
||||
? (samples_after - samples_before)
|
||||
: (2048 - samples_before + samples_after);
|
||||
cumulative_samples += sample_delta;
|
||||
}
|
||||
|
||||
// After 60 seconds, we should have very close to expected values
|
||||
constexpr uint64_t kExpectedApuCycles =
|
||||
audio_constants::kApuClock * kTestSeconds;
|
||||
constexpr int kExpectedSamples =
|
||||
audio_constants::kNativeSampleRate * kTestSeconds;
|
||||
|
||||
double apu_ratio = static_cast<double>(cumulative_apu_cycles) / kExpectedApuCycles;
|
||||
double sample_ratio = static_cast<double>(cumulative_samples) / kExpectedSamples;
|
||||
|
||||
LOG_INFO("AudioTiming",
|
||||
"60-second drift test: APU ratio=%.6f, Sample ratio=%.6f",
|
||||
apu_ratio, sample_ratio);
|
||||
|
||||
// Very tight tolerance for extended test - no drift should accumulate
|
||||
EXPECT_NEAR(apu_ratio, 1.0, 0.001)
|
||||
<< "APU cycle drift detected over 60 seconds!";
|
||||
EXPECT_NEAR(sample_ratio, 1.0, 0.005)
|
||||
<< "Sample count drift detected over 60 seconds!";
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
425
test/integration/audio/headless_audio_debug_test.cc
Normal file
425
test/integration/audio/headless_audio_debug_test.cc
Normal file
@@ -0,0 +1,425 @@
|
||||
// Headless Audio Debug Tests
|
||||
//
|
||||
// Comprehensive audio debugging tests for diagnosing timing issues.
|
||||
// Collects timing metrics and verifies audio pipeline correctness.
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "app/emu/audio/apu.h"
|
||||
#include "app/emu/audio/dsp.h"
|
||||
#include "app/emu/snes.h"
|
||||
#include "rom/rom.h"
|
||||
#include "test_utils.h"
|
||||
#include "util/log.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
// =============================================================================
|
||||
// Timing Metrics Structure
|
||||
// =============================================================================
|
||||
|
||||
struct AudioTimingMetrics {
|
||||
// Cycle counts
|
||||
uint64_t total_master_cycles = 0;
|
||||
uint64_t total_apu_cycles = 0;
|
||||
uint64_t total_dsp_samples = 0;
|
||||
|
||||
// Rates (calculated)
|
||||
double apu_cycles_per_second = 0.0;
|
||||
double dsp_samples_per_second = 0.0;
|
||||
double apu_to_master_ratio = 0.0;
|
||||
|
||||
// Per-frame statistics
|
||||
double samples_per_frame_avg = 0.0;
|
||||
int samples_per_frame_min = INT_MAX;
|
||||
int samples_per_frame_max = 0;
|
||||
|
||||
// Drift detection
|
||||
std::vector<double> per_second_apu_rates;
|
||||
std::vector<double> per_second_sample_rates;
|
||||
double max_drift_percent = 0.0;
|
||||
|
||||
// Expected values for comparison
|
||||
static constexpr uint64_t kExpectedApuCyclesPerSecond = 1025280;
|
||||
static constexpr int kExpectedSamplesPerSecond = 32040;
|
||||
static constexpr int kExpectedSamplesPerFrame = 533;
|
||||
static constexpr double kExpectedApuMasterRatio = 0.0478;
|
||||
|
||||
std::string ToString() const {
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(4);
|
||||
oss << "=== Audio Timing Metrics ===\n";
|
||||
oss << "Master cycles: " << total_master_cycles << "\n";
|
||||
oss << "APU cycles: " << total_apu_cycles
|
||||
<< " (expected/sec: " << kExpectedApuCyclesPerSecond << ")\n";
|
||||
oss << "DSP samples: " << total_dsp_samples
|
||||
<< " (expected/sec: " << kExpectedSamplesPerSecond << ")\n";
|
||||
oss << "\n";
|
||||
oss << "APU cycles/sec: " << apu_cycles_per_second
|
||||
<< " (ratio to expected: "
|
||||
<< (apu_cycles_per_second / kExpectedApuCyclesPerSecond) << ")\n";
|
||||
oss << "DSP samples/sec: " << dsp_samples_per_second
|
||||
<< " (ratio to expected: "
|
||||
<< (dsp_samples_per_second / kExpectedSamplesPerSecond) << ")\n";
|
||||
oss << "APU/Master ratio: " << apu_to_master_ratio
|
||||
<< " (expected: " << kExpectedApuMasterRatio << ")\n";
|
||||
oss << "\n";
|
||||
oss << "Samples/frame: avg=" << samples_per_frame_avg
|
||||
<< ", min=" << samples_per_frame_min << ", max=" << samples_per_frame_max
|
||||
<< " (expected: " << kExpectedSamplesPerFrame << ")\n";
|
||||
oss << "Max drift: " << (max_drift_percent * 100.0) << "%\n";
|
||||
return oss.str();
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Headless Audio Debug Test Fixture
|
||||
// =============================================================================
|
||||
|
||||
class HeadlessAudioDebugTest : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
|
||||
snes_ = std::make_unique<emu::Snes>();
|
||||
snes_->Init(rom()->vector());
|
||||
apu_ = &snes_->apu();
|
||||
|
||||
// Reset APU cycle tracking for fresh start
|
||||
// Snes::Init() runs bootstrap cycles which advances the APU's
|
||||
// last_master_cycles_, so we need to reset for accurate timing tests.
|
||||
apu_->Reset();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
apu_ = nullptr;
|
||||
snes_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
// Collect timing metrics over specified duration (in simulated seconds)
|
||||
AudioTimingMetrics CollectMetrics(int duration_seconds) {
|
||||
AudioTimingMetrics metrics;
|
||||
|
||||
constexpr int kFramesPerSecond = 60;
|
||||
constexpr uint64_t kMasterCyclesPerFrame = 357366;
|
||||
|
||||
uint64_t start_apu = apu_->GetCycles();
|
||||
uint32_t start_samples = apu_->dsp().GetSampleOffset();
|
||||
|
||||
// Track cumulative master cycles (APU expects monotonically increasing values)
|
||||
uint64_t cumulative_master_cycles = 0;
|
||||
|
||||
for (int sec = 0; sec < duration_seconds; ++sec) {
|
||||
uint64_t sec_start_apu = apu_->GetCycles();
|
||||
uint32_t sec_start_samples = apu_->dsp().GetSampleOffset();
|
||||
|
||||
int sec_samples_min = INT_MAX;
|
||||
int sec_samples_max = 0;
|
||||
int sec_total_samples = 0;
|
||||
|
||||
for (int frame = 0; frame < kFramesPerSecond; ++frame) {
|
||||
uint32_t frame_start = apu_->dsp().GetSampleOffset();
|
||||
|
||||
// APU expects cumulative master cycles, not per-frame delta
|
||||
cumulative_master_cycles += kMasterCyclesPerFrame;
|
||||
apu_->RunCycles(cumulative_master_cycles);
|
||||
metrics.total_master_cycles += kMasterCyclesPerFrame;
|
||||
|
||||
uint32_t frame_end = apu_->dsp().GetSampleOffset();
|
||||
int frame_samples = (frame_end >= frame_start)
|
||||
? (frame_end - frame_start)
|
||||
: (2048 - frame_start + frame_end);
|
||||
|
||||
sec_total_samples += frame_samples;
|
||||
sec_samples_min = std::min(sec_samples_min, frame_samples);
|
||||
sec_samples_max = std::max(sec_samples_max, frame_samples);
|
||||
metrics.samples_per_frame_min =
|
||||
std::min(metrics.samples_per_frame_min, frame_samples);
|
||||
metrics.samples_per_frame_max =
|
||||
std::max(metrics.samples_per_frame_max, frame_samples);
|
||||
}
|
||||
|
||||
uint64_t sec_end_apu = apu_->GetCycles();
|
||||
uint64_t sec_apu_delta = sec_end_apu - sec_start_apu;
|
||||
|
||||
double sec_apu_rate = static_cast<double>(sec_apu_delta);
|
||||
double sec_sample_rate = static_cast<double>(sec_total_samples);
|
||||
|
||||
metrics.per_second_apu_rates.push_back(sec_apu_rate);
|
||||
metrics.per_second_sample_rates.push_back(sec_sample_rate);
|
||||
|
||||
// Track max drift from expected
|
||||
double apu_drift =
|
||||
std::abs(sec_apu_rate - AudioTimingMetrics::kExpectedApuCyclesPerSecond) /
|
||||
AudioTimingMetrics::kExpectedApuCyclesPerSecond;
|
||||
double sample_drift =
|
||||
std::abs(sec_sample_rate - AudioTimingMetrics::kExpectedSamplesPerSecond) /
|
||||
AudioTimingMetrics::kExpectedSamplesPerSecond;
|
||||
metrics.max_drift_percent =
|
||||
std::max(metrics.max_drift_percent, std::max(apu_drift, sample_drift));
|
||||
}
|
||||
|
||||
uint64_t end_apu = apu_->GetCycles();
|
||||
uint32_t end_samples = apu_->dsp().GetSampleOffset();
|
||||
|
||||
metrics.total_apu_cycles = end_apu - start_apu;
|
||||
metrics.total_dsp_samples = (end_samples >= start_samples)
|
||||
? (end_samples - start_samples)
|
||||
: (2048 - start_samples + end_samples);
|
||||
|
||||
// For long tests, we need to track cumulative samples differently
|
||||
// since the ring buffer wraps. Use per-second totals instead.
|
||||
if (duration_seconds > 0) {
|
||||
double total_samples_from_rates = 0;
|
||||
for (double rate : metrics.per_second_sample_rates) {
|
||||
total_samples_from_rates += rate;
|
||||
}
|
||||
metrics.total_dsp_samples = static_cast<uint64_t>(total_samples_from_rates);
|
||||
}
|
||||
|
||||
// Calculate rates
|
||||
metrics.apu_cycles_per_second =
|
||||
static_cast<double>(metrics.total_apu_cycles) / duration_seconds;
|
||||
metrics.dsp_samples_per_second =
|
||||
static_cast<double>(metrics.total_dsp_samples) / duration_seconds;
|
||||
metrics.apu_to_master_ratio =
|
||||
static_cast<double>(metrics.total_apu_cycles) / metrics.total_master_cycles;
|
||||
|
||||
// Calculate per-frame average
|
||||
int total_frames = duration_seconds * kFramesPerSecond;
|
||||
metrics.samples_per_frame_avg =
|
||||
static_cast<double>(metrics.total_dsp_samples) / total_frames;
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
void LogMetricsToFile(const AudioTimingMetrics& metrics,
|
||||
const std::string& filename) {
|
||||
std::ofstream file(filename);
|
||||
if (!file) {
|
||||
LOG_ERROR("AudioDebug", "Failed to open metrics file: %s",
|
||||
filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
file << metrics.ToString();
|
||||
|
||||
// CSV data for analysis
|
||||
file << "\n=== Per-Second Data (CSV) ===\n";
|
||||
file << "second,apu_cycles,dsp_samples,apu_ratio,sample_ratio\n";
|
||||
for (size_t i = 0; i < metrics.per_second_apu_rates.size(); ++i) {
|
||||
file << i << "," << metrics.per_second_apu_rates[i] << ","
|
||||
<< metrics.per_second_sample_rates[i] << ","
|
||||
<< (metrics.per_second_apu_rates[i] /
|
||||
AudioTimingMetrics::kExpectedApuCyclesPerSecond)
|
||||
<< ","
|
||||
<< (metrics.per_second_sample_rates[i] /
|
||||
AudioTimingMetrics::kExpectedSamplesPerSecond)
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
LOG_INFO("AudioDebug", "Metrics written to %s", filename.c_str());
|
||||
}
|
||||
|
||||
std::unique_ptr<emu::Snes> snes_;
|
||||
emu::Apu* apu_ = nullptr;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Comprehensive Diagnostic Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(HeadlessAudioDebugTest, FullTimingDiagnostic) {
|
||||
// Run 10 seconds of simulated playback and collect all metrics
|
||||
constexpr int kTestDurationSeconds = 10;
|
||||
|
||||
LOG_INFO("AudioDebug", "Starting %d-second timing diagnostic...",
|
||||
kTestDurationSeconds);
|
||||
|
||||
AudioTimingMetrics metrics = CollectMetrics(kTestDurationSeconds);
|
||||
|
||||
// Log full metrics
|
||||
LOG_INFO("AudioDebug", "\n%s", metrics.ToString().c_str());
|
||||
|
||||
// Verify APU cycle rate
|
||||
double apu_ratio =
|
||||
metrics.apu_cycles_per_second / AudioTimingMetrics::kExpectedApuCyclesPerSecond;
|
||||
EXPECT_NEAR(apu_ratio, 1.0, 0.01)
|
||||
<< "APU cycle rate should be within 1% of expected. "
|
||||
<< "Got " << metrics.apu_cycles_per_second << " cycles/sec";
|
||||
|
||||
// Verify DSP sample rate
|
||||
double sample_ratio =
|
||||
metrics.dsp_samples_per_second / AudioTimingMetrics::kExpectedSamplesPerSecond;
|
||||
EXPECT_NEAR(sample_ratio, 1.0, 0.01)
|
||||
<< "DSP sample rate should be within 1% of expected. "
|
||||
<< "Got " << metrics.dsp_samples_per_second << " samples/sec";
|
||||
|
||||
// Verify samples per frame
|
||||
EXPECT_NEAR(metrics.samples_per_frame_avg,
|
||||
AudioTimingMetrics::kExpectedSamplesPerFrame, 2.0)
|
||||
<< "Samples per frame should be ~533";
|
||||
|
||||
// Verify no significant drift
|
||||
EXPECT_LT(metrics.max_drift_percent, 0.02)
|
||||
<< "Max drift should be < 2%";
|
||||
}
|
||||
|
||||
TEST_F(HeadlessAudioDebugTest, CycleRateDriftOverTime) {
|
||||
// Run extended simulation to detect timing drift
|
||||
constexpr int kTestDurationSeconds = 60;
|
||||
|
||||
LOG_INFO("AudioDebug", "Starting %d-second drift detection test...",
|
||||
kTestDurationSeconds);
|
||||
|
||||
AudioTimingMetrics metrics = CollectMetrics(kTestDurationSeconds);
|
||||
|
||||
// Log to file for detailed analysis
|
||||
LogMetricsToFile(metrics, "/tmp/audio_timing_drift.txt");
|
||||
|
||||
// Check for drift: compare first half to second half
|
||||
if (metrics.per_second_apu_rates.size() >= 2) {
|
||||
size_t half = metrics.per_second_apu_rates.size() / 2;
|
||||
|
||||
double first_half_avg = 0;
|
||||
double second_half_avg = 0;
|
||||
|
||||
for (size_t i = 0; i < half; ++i) {
|
||||
first_half_avg += metrics.per_second_apu_rates[i];
|
||||
}
|
||||
first_half_avg /= half;
|
||||
|
||||
for (size_t i = half; i < metrics.per_second_apu_rates.size(); ++i) {
|
||||
second_half_avg += metrics.per_second_apu_rates[i];
|
||||
}
|
||||
second_half_avg /= (metrics.per_second_apu_rates.size() - half);
|
||||
|
||||
double drift = std::abs(second_half_avg - first_half_avg) / first_half_avg;
|
||||
|
||||
LOG_INFO("AudioDebug",
|
||||
"Drift analysis: first_half=%.0f, second_half=%.0f, drift=%.4f%%",
|
||||
first_half_avg, second_half_avg, drift * 100);
|
||||
|
||||
EXPECT_LT(drift, 0.001)
|
||||
<< "APU cycle rate should not drift over time. "
|
||||
<< "First half avg: " << first_half_avg
|
||||
<< ", Second half avg: " << second_half_avg;
|
||||
}
|
||||
|
||||
// Overall timing should still be accurate
|
||||
double overall_ratio =
|
||||
metrics.apu_cycles_per_second / AudioTimingMetrics::kExpectedApuCyclesPerSecond;
|
||||
EXPECT_NEAR(overall_ratio, 1.0, 0.005)
|
||||
<< "After 60 seconds, timing should be within 0.5% of expected";
|
||||
}
|
||||
|
||||
TEST_F(HeadlessAudioDebugTest, SampleBufferDoesNotOverflow) {
|
||||
// Run continuous simulation and verify buffer wrapping works correctly
|
||||
constexpr int kTestFrames = 3600; // 1 minute at 60fps
|
||||
|
||||
uint32_t prev_offset = apu_->dsp().GetSampleOffset();
|
||||
int wrap_count = 0;
|
||||
|
||||
for (int frame = 0; frame < kTestFrames; ++frame) {
|
||||
apu_->RunCycles(357366); // One NTSC frame
|
||||
|
||||
uint32_t curr_offset = apu_->dsp().GetSampleOffset();
|
||||
|
||||
// Detect wrap-around
|
||||
if (curr_offset < prev_offset) {
|
||||
wrap_count++;
|
||||
}
|
||||
|
||||
// Offset should always be within buffer bounds (0-2047)
|
||||
EXPECT_LT(curr_offset, 2048u)
|
||||
<< "Sample offset exceeded buffer size at frame " << frame;
|
||||
|
||||
prev_offset = curr_offset;
|
||||
}
|
||||
|
||||
LOG_INFO("AudioDebug", "Buffer wrapped %d times in %d frames", wrap_count,
|
||||
kTestFrames);
|
||||
|
||||
// With ~533 samples/frame and 2048 buffer size, we should wrap about
|
||||
// every 4 frames. In 3600 frames, expect ~900 wraps.
|
||||
EXPECT_GT(wrap_count, 800) << "Buffer should wrap regularly";
|
||||
EXPECT_LT(wrap_count, 1000) << "Buffer wrap count seems off";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Speed Bug Regression Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(HeadlessAudioDebugTest, NotPlayingAt15xSpeed) {
|
||||
// Specific test for the 1.5x speed bug
|
||||
constexpr int kTestDurationSeconds = 5;
|
||||
|
||||
AudioTimingMetrics metrics = CollectMetrics(kTestDurationSeconds);
|
||||
|
||||
// At 1.5x speed, we'd see ~48060 samples/sec instead of ~32040
|
||||
double speed_ratio =
|
||||
metrics.dsp_samples_per_second / AudioTimingMetrics::kExpectedSamplesPerSecond;
|
||||
|
||||
LOG_INFO("AudioDebug", "Speed ratio: %.4fx (1.0x expected)", speed_ratio);
|
||||
|
||||
// If bug is present, ratio would be ~1.5
|
||||
EXPECT_LT(speed_ratio, 1.3)
|
||||
<< "Audio should not be playing at 1.5x speed! "
|
||||
<< "Got " << metrics.dsp_samples_per_second << " samples/sec";
|
||||
|
||||
EXPECT_GT(speed_ratio, 0.9)
|
||||
<< "Audio should not be playing too slowly! "
|
||||
<< "Got " << metrics.dsp_samples_per_second << " samples/sec";
|
||||
}
|
||||
|
||||
TEST_F(HeadlessAudioDebugTest, ApuMasterRatioIsCorrect) {
|
||||
// Verify the fixed-point ratio calculation
|
||||
constexpr int kTestDurationSeconds = 5;
|
||||
|
||||
AudioTimingMetrics metrics = CollectMetrics(kTestDurationSeconds);
|
||||
|
||||
LOG_INFO("AudioDebug", "APU/Master ratio: %.6f (expected: ~0.0478)",
|
||||
metrics.apu_to_master_ratio);
|
||||
|
||||
EXPECT_NEAR(metrics.apu_to_master_ratio, 0.0478, 0.001)
|
||||
<< "APU/Master clock ratio is incorrect";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Diagnostic Output Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(HeadlessAudioDebugTest, GenerateTimingReport) {
|
||||
// Generate a comprehensive timing report for debugging
|
||||
constexpr int kTestDurationSeconds = 10;
|
||||
|
||||
AudioTimingMetrics metrics = CollectMetrics(kTestDurationSeconds);
|
||||
|
||||
std::string report = metrics.ToString();
|
||||
|
||||
// Write to stdout for immediate visibility
|
||||
std::cout << "\n" << report << std::endl;
|
||||
|
||||
// Also write to file
|
||||
LogMetricsToFile(metrics, "/tmp/audio_timing_report.txt");
|
||||
|
||||
// This test always passes - it's for generating debug output
|
||||
SUCCEED() << "Timing report generated";
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
217
test/integration/audio/music_player_headless_test.cc
Normal file
217
test/integration/audio/music_player_headless_test.cc
Normal file
@@ -0,0 +1,217 @@
|
||||
// MusicPlayer Headless Integration Tests
|
||||
//
|
||||
// Tests MusicPlayer functionality without requiring display or audio output.
|
||||
// Uses NullAudioBackend to verify audio timing and playback behavior.
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
#include "app/editor/music/music_player.h"
|
||||
#include "app/emu/audio/audio_backend.h"
|
||||
#include "app/emu/emulator.h"
|
||||
#include "rom/rom.h"
|
||||
#include "test_utils.h"
|
||||
#include "util/log.h"
|
||||
#include "zelda3/music/music_bank.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
// =============================================================================
|
||||
// MusicPlayer Headless Test Fixture
|
||||
// =============================================================================
|
||||
|
||||
class MusicPlayerHeadlessTest : public TestRomManager::BoundRomTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
BoundRomTest::SetUp();
|
||||
|
||||
// Create music bank from ROM
|
||||
music_bank_ = std::make_unique<zelda3::music::MusicBank>();
|
||||
|
||||
// Initialize music player with null music bank for basic tests
|
||||
// Full music bank loading requires ROM parsing
|
||||
player_ = std::make_unique<editor::music::MusicPlayer>(nullptr);
|
||||
player_->SetRom(rom());
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
player_.reset();
|
||||
music_bank_.reset();
|
||||
BoundRomTest::TearDown();
|
||||
}
|
||||
|
||||
// Simulate N frames of playback by calling Update() repeatedly
|
||||
void SimulatePlayback(int frames) {
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
player_->Update();
|
||||
// Simulate ~16.6ms per frame (NTSC timing)
|
||||
// Note: In tests we don't actually sleep, just call Update()
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<zelda3::music::MusicBank> music_bank_;
|
||||
std::unique_ptr<editor::music::MusicPlayer> player_;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Basic Initialization Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, InitializesWithRom) {
|
||||
// Player should be created
|
||||
EXPECT_NE(player_, nullptr);
|
||||
|
||||
// Initially not ready until a song is played
|
||||
EXPECT_FALSE(player_->IsAudioReady());
|
||||
}
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, InitialStateIsStopped) {
|
||||
auto state = player_->GetState();
|
||||
|
||||
EXPECT_FALSE(state.is_playing);
|
||||
EXPECT_FALSE(state.is_paused);
|
||||
EXPECT_EQ(state.playing_song_index, -1);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Playback State Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, TogglePlayPauseFromStopped) {
|
||||
// When stopped with no song, toggle should do nothing
|
||||
player_->TogglePlayPause();
|
||||
|
||||
auto state = player_->GetState();
|
||||
// Still stopped since no song was selected
|
||||
EXPECT_FALSE(state.is_playing);
|
||||
}
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, StopClearsPlaybackState) {
|
||||
// Start playback then stop
|
||||
player_->PlaySong(0);
|
||||
player_->Stop();
|
||||
|
||||
auto state = player_->GetState();
|
||||
EXPECT_FALSE(state.is_playing);
|
||||
EXPECT_FALSE(state.is_paused);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Audio Timing Verification Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, UpdateDoesNotCrashWithoutPlayback) {
|
||||
// Calling Update() when not playing should be safe
|
||||
EXPECT_NO_THROW(SimulatePlayback(60));
|
||||
}
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, DirectSpcModeCanBeEnabled) {
|
||||
// Direct SPC mode bypasses game CPU and plays audio directly
|
||||
// This is set via SetDirectSpcMode() - no getter exposed, just verify no crash
|
||||
EXPECT_NO_THROW(player_->SetDirectSpcMode(true));
|
||||
EXPECT_NO_THROW(player_->SetDirectSpcMode(false));
|
||||
}
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, InterpolationTypeCanBeSet) {
|
||||
// Interpolation type is set via SetInterpolationType()
|
||||
// No getter exposed, just verify no crash
|
||||
EXPECT_NO_THROW(player_->SetInterpolationType(0)); // Linear
|
||||
EXPECT_NO_THROW(player_->SetInterpolationType(2)); // Gaussian (default SNES)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Playback Speed Regression Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, PlaybackStateTracksSpeedCorrectly) {
|
||||
auto state = player_->GetState();
|
||||
|
||||
// Playback speed should always be 1.0x (varispeed was removed)
|
||||
EXPECT_FLOAT_EQ(state.playback_speed, 1.0f)
|
||||
<< "Playback speed should be 1.0x";
|
||||
}
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, TicksPerSecondMatchesTempo) {
|
||||
// Default tempo of 150 should produce specific ticks per second
|
||||
// Formula: ticks_per_second = 500.0f * (tempo / 256.0f)
|
||||
// At tempo 150: 500 * (150/256) = 292.97
|
||||
|
||||
constexpr float kDefaultTempo = 150.0f;
|
||||
constexpr float kExpectedTps = 500.0f * (kDefaultTempo / 256.0f);
|
||||
|
||||
// Get state and verify ticks_per_second is reasonable
|
||||
auto state = player_->GetState();
|
||||
|
||||
// Initially ticks_per_second may be 0 if no song is playing
|
||||
// After playing a song, it should match the formula
|
||||
LOG_INFO("MusicPlayerTest", "Initial ticks_per_second: %.2f (expected ~%.2f for tempo 150)",
|
||||
state.ticks_per_second, kExpectedTps);
|
||||
|
||||
// If a song is playing, verify the value
|
||||
if (state.is_playing) {
|
||||
EXPECT_NEAR(state.ticks_per_second, kExpectedTps, 10.0f)
|
||||
<< "Ticks per second should match tempo-based calculation";
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Frame Timing Tests
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, UpdateProcessesFramesCorrectly) {
|
||||
// This test verifies Update() can be called repeatedly without issues
|
||||
// In a real scenario, Update() would process audio frames
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Simulate 10 seconds of updates (600 frames)
|
||||
constexpr int kTestFrames = 600;
|
||||
SimulatePlayback(kTestFrames);
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration<double>(end - start).count();
|
||||
|
||||
LOG_INFO("MusicPlayerTest", "Processed %d Update() calls in %.3f seconds",
|
||||
kTestFrames, elapsed);
|
||||
|
||||
// Update() should be fast (no blocking)
|
||||
EXPECT_LT(elapsed, 1.0) << "Update() calls should be fast (not blocking)";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cleanup and Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, DestructorCleansUpProperly) {
|
||||
// Start playback to initialize audio
|
||||
player_->PlaySong(0);
|
||||
|
||||
// Simulate some activity
|
||||
SimulatePlayback(10);
|
||||
|
||||
// Reset should clean up without crashes
|
||||
player_.reset();
|
||||
|
||||
SUCCEED() << "MusicPlayer destructor completed without crash";
|
||||
}
|
||||
|
||||
TEST_F(MusicPlayerHeadlessTest, MultiplePlaySongsAreSafe) {
|
||||
// Call PlaySong multiple times
|
||||
player_->PlaySong(0);
|
||||
player_->PlaySong(0);
|
||||
player_->PlaySong(0);
|
||||
|
||||
// Should still work
|
||||
SimulatePlayback(10);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
Reference in New Issue
Block a user