// 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 #include #include #include #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(); 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 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(apu_cycles) / static_cast(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(total_samples) / static_cast(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(total_samples) / static_cast(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(audio_constants::kApuCyclesNumerator) / static_cast(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(apu_cycles) / static_cast(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(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(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(cumulative_apu_cycles) / kExpectedApuCycles; double sample_ratio = static_cast(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