// Integration tests for Music Editor with real ROM data // Tests song loading, parsing, and emulator audio stability #include #include #include #include "app/emu/emulator.h" #include "rom/rom.h" #include "test/test_utils.h" #include "zelda3/music/music_bank.h" #include "zelda3/music/song_data.h" #include "zelda3/music/spc_parser.h" namespace yaze { namespace zelda3 { namespace test { using namespace yaze::zelda3::music; // ============================================================================= // Test Fixture // ============================================================================= class MusicIntegrationTest : public ::testing::Test { protected: void SetUp() override { rom_ = std::make_unique(); yaze::test::TestRomManager::SkipIfRomMissing( yaze::test::RomRole::kVanilla, "MusicIntegrationTest"); const std::string rom_path = yaze::test::TestRomManager::GetRomPath(yaze::test::RomRole::kVanilla); auto status = rom_->LoadFromFile(rom_path); if (!status.ok()) { GTEST_SKIP() << "ROM file not available: " << status.message(); } // Verify it's an ALTTP ROM if (rom_->title().find("ZELDA") == std::string::npos && rom_->title().find("zelda") == std::string::npos) { GTEST_SKIP() << "ROM is not ALTTP: " << rom_->title(); } } void TearDown() override { rom_.reset(); } std::unique_ptr rom_; MusicBank music_bank_; }; // ============================================================================= // Song Loading Tests // ============================================================================= TEST_F(MusicIntegrationTest, LoadVanillaSongsFromRom) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << "Failed to load music: " << status.message(); // Should load all 34 vanilla songs size_t song_count = music_bank_.GetSongCount(); EXPECT_GE(song_count, 34) << "Expected at least 34 vanilla songs"; // Verify some known vanilla songs exist const MusicSong* title_song = music_bank_.GetSong(0); // Song ID 1 (index 0) ASSERT_NE(title_song, nullptr) << "Title song should exist"; EXPECT_EQ(title_song->name, "Title"); const MusicSong* light_world = music_bank_.GetSong(1); // Song ID 2 (index 1) ASSERT_NE(light_world, nullptr) << "Light World song should exist"; EXPECT_EQ(light_world->name, "Light World"); const MusicSong* dark_world = music_bank_.GetSong(8); // Song ID 9 (index 8) ASSERT_NE(dark_world, nullptr) << "Dark World song should exist"; EXPECT_EQ(dark_world->name, "Dark World"); } TEST_F(MusicIntegrationTest, VerifySongStructure) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Check each vanilla song has valid structure for (int i = 0; i < 34; ++i) { SCOPED_TRACE("Song index: " + std::to_string(i)); const MusicSong* song = music_bank_.GetSong(i); ASSERT_NE(song, nullptr) << "Song " << i << " should exist"; // Song should have at least one segment EXPECT_GE(song->segments.size(), 1) << "Song '" << song->name << "' should have at least one segment"; // Each segment should have 8 tracks for (size_t seg_idx = 0; seg_idx < song->segments.size(); ++seg_idx) { SCOPED_TRACE("Segment: " + std::to_string(seg_idx)); const auto& segment = song->segments[seg_idx]; EXPECT_EQ(segment.tracks.size(), 8) << "Segment should have 8 tracks"; // At least one track should have content (not all empty) bool has_content = false; for (const auto& track : segment.tracks) { if (!track.is_empty && !track.events.empty()) { has_content = true; break; } } // Some songs may have empty segments for intro/loop purposes // but most should have content } } } TEST_F(MusicIntegrationTest, VerifyBankAssignment) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Songs 1-11 should be Overworld bank for (int i = 0; i < 11; ++i) { const MusicSong* song = music_bank_.GetSong(i); ASSERT_NE(song, nullptr); EXPECT_EQ(song->bank, static_cast(MusicBank::Bank::Overworld)) << "Song " << i << " (" << song->name << ") should be Overworld bank"; } // Songs 12-31 should be Dungeon bank for (int i = 11; i < 31; ++i) { const MusicSong* song = music_bank_.GetSong(i); ASSERT_NE(song, nullptr); EXPECT_EQ(song->bank, static_cast(MusicBank::Bank::Dungeon)) << "Song " << i << " (" << song->name << ") should be Dungeon bank"; } // Songs 32-34 should be Credits bank for (int i = 31; i < 34; ++i) { const MusicSong* song = music_bank_.GetSong(i); ASSERT_NE(song, nullptr); EXPECT_EQ(song->bank, static_cast(MusicBank::Bank::Credits)) << "Song " << i << " (" << song->name << ") should be Credits bank"; } } TEST_F(MusicIntegrationTest, VerifyTrackEvents) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Check Light World song has valid events const MusicSong* light_world = music_bank_.GetSong(1); ASSERT_NE(light_world, nullptr); ASSERT_GE(light_world->segments.size(), 1); int total_events = 0; int note_count = 0; int command_count = 0; for (const auto& segment : light_world->segments) { for (const auto& track : segment.tracks) { if (track.is_empty) continue; for (const auto& event : track.events) { total_events++; switch (event.type) { case TrackEvent::Type::Note: note_count++; // Verify note is in valid range EXPECT_TRUE(SpcParser::IsNotePitch(event.note.pitch) || event.note.pitch == kNoteTie || event.note.pitch == kNoteRest) << "Invalid note pitch: 0x" << std::hex << static_cast(event.note.pitch); break; case TrackEvent::Type::Command: command_count++; // Verify command opcode is valid EXPECT_TRUE(SpcParser::IsCommand(event.command.opcode)) << "Invalid command opcode: 0x" << std::hex << static_cast(event.command.opcode); break; case TrackEvent::Type::End: // End marker is always valid break; } } } } // Light World should have significant content EXPECT_GT(total_events, 100) << "Light World should have many events"; EXPECT_GT(note_count, 50) << "Light World should have many notes"; EXPECT_GT(command_count, 10) << "Light World should have setup commands"; } // ============================================================================= // Space Calculation Tests // ============================================================================= TEST_F(MusicIntegrationTest, CalculateVanillaBankUsage) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Check Overworld bank usage auto ow_space = music_bank_.CalculateSpaceUsage(MusicBank::Bank::Overworld); EXPECT_GT(ow_space.used_bytes, 0) << "Overworld bank should have content"; EXPECT_LE(ow_space.used_bytes, ow_space.total_bytes) << "Overworld usage should not exceed limit"; EXPECT_LT(ow_space.usage_percent, 100.0f) << "Overworld should not be over capacity"; // Check Dungeon bank usage auto dg_space = music_bank_.CalculateSpaceUsage(MusicBank::Bank::Dungeon); EXPECT_GT(dg_space.used_bytes, 0) << "Dungeon bank should have content"; EXPECT_LE(dg_space.used_bytes, dg_space.total_bytes) << "Dungeon usage should not exceed limit"; // Check Credits bank usage auto cr_space = music_bank_.CalculateSpaceUsage(MusicBank::Bank::Credits); EXPECT_GT(cr_space.used_bytes, 0) << "Credits bank should have content"; EXPECT_LE(cr_space.used_bytes, cr_space.total_bytes) << "Credits usage should not exceed limit"; // All songs should fit EXPECT_TRUE(music_bank_.AllSongsFit()) << "All vanilla songs should fit"; } // ============================================================================= // Emulator Integration Tests // ============================================================================= TEST_F(MusicIntegrationTest, EmulatorInitializesWithRom) { emu::Emulator emulator; // Try to initialize the emulator bool initialized = emulator.EnsureInitialized(rom_.get()); EXPECT_TRUE(initialized) << "Emulator should initialize with valid ROM"; EXPECT_TRUE(emulator.is_snes_initialized()) << "SNES core should be initialized"; } TEST_F(MusicIntegrationTest, EmulatorCanRunFrames) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized) << "Emulator must initialize for this test"; emulator.set_running(true); // Run a few frames without crashing for (int i = 0; i < 10; ++i) { emulator.RunFrameOnly(); } // Should still be running EXPECT_TRUE(emulator.running()); EXPECT_TRUE(emulator.is_snes_initialized()); } TEST_F(MusicIntegrationTest, EmulatorGeneratesAudioSamples) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized) << "Emulator must initialize for this test"; emulator.set_running(true); // Run several frames to generate audio for (int i = 0; i < 60; ++i) { emulator.RunFrameOnly(); } // Check that DSP is producing samples auto& dsp = emulator.snes().apu().dsp(); const int16_t* sample_buffer = dsp.GetSampleBuffer(); ASSERT_NE(sample_buffer, nullptr) << "DSP should have sample buffer"; // Check for non-zero audio samples (some sound should be playing) // At startup, there might be silence, but the buffer should exist uint16_t sample_offset = dsp.GetSampleOffset(); EXPECT_GT(sample_offset, 0) << "DSP should have processed samples"; } TEST_F(MusicIntegrationTest, MusicTriggerWritesToRam) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized); emulator.set_running(true); // Run some frames to let the game initialize for (int i = 0; i < 30; ++i) { emulator.RunFrameOnly(); } // Write a music ID to the music register uint8_t song_id = 0x02; // Light World emulator.snes().Write(0x7E012C, song_id); // Verify the write auto read_result = emulator.snes().Read(0x7E012C); EXPECT_EQ(read_result, song_id) << "Music register should hold the written value"; } // ============================================================================= // Round-Trip Tests // ============================================================================= TEST_F(MusicIntegrationTest, ParseSerializeRoundTrip) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Test round-trip for Light World const MusicSong* original = music_bank_.GetSong(1); ASSERT_NE(original, nullptr); // Serialize the song auto serialize_result = SpcSerializer::SerializeSong(*original, 0xD100); ASSERT_TRUE(serialize_result.ok()) << serialize_result.status().message(); auto& serialized = serialize_result.value(); EXPECT_GT(serialized.data.size(), 0) << "Serialized data should not be empty"; // The serialized size should be reasonable EXPECT_LT(serialized.data.size(), 10000) << "Serialized size should be reasonable"; } // ============================================================================= // Vanilla Song Name Tests // ============================================================================= TEST_F(MusicIntegrationTest, AllVanillaSongsHaveNames) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); std::vector expected_names = {"Title", "Light World", "Beginning", "Rabbit", "Forest", "Intro", "Town", "Warp", "Dark World", "Master Sword", "File Select", "Soldier", "Mountain", "Shop", "Fanfare", "Castle", "Palace (Pendant)", "Cave", "Clear", "Church", "Boss", "Dungeon (Crystal)", "Psychic", "Secret Way", "Rescue", "Crystal", "Fountain", "Pyramid", "Kill Agahnim", "Ganon Room", "Last Boss", "Credits 1", "Credits 2", "Credits 3"}; for (size_t i = 0; i < expected_names.size() && i < music_bank_.GetSongCount(); ++i) { const MusicSong* song = music_bank_.GetSong(i); ASSERT_NE(song, nullptr) << "Song " << i << " should exist"; EXPECT_EQ(song->name, expected_names[i]) << "Song " << i << " name mismatch"; } } // ============================================================================= // Instrument/Sample Loading Tests // ============================================================================= TEST_F(MusicIntegrationTest, InstrumentsLoaded) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Should have default instruments EXPECT_GE(music_bank_.GetInstrumentCount(), 16) << "Should have at least 16 instruments"; // Check first instrument exists const MusicInstrument* inst = music_bank_.GetInstrument(0); ASSERT_NE(inst, nullptr); EXPECT_FALSE(inst->name.empty()) << "Instrument should have a name"; } TEST_F(MusicIntegrationTest, SamplesLoaded) { auto status = music_bank_.LoadFromRom(*rom_); ASSERT_TRUE(status.ok()) << status.message(); // Should have samples EXPECT_GE(music_bank_.GetSampleCount(), 16) << "Should have at least 16 samples"; } // ============================================================================= // Direct SPC Upload Tests // ============================================================================= TEST_F(MusicIntegrationTest, DirectSpcUploadCommonBank) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized) << "Emulator must initialize for this test"; auto& apu = emulator.snes().apu(); // Reset APU to clean state apu.Reset(); // Upload common bank (Bank 0) from ROM offset 0xC8000 // This contains: driver code, sample pointers, instruments, BRR samples constexpr uint32_t kCommonBankOffset = 0xC8000; const uint8_t* rom_data = rom_->data(); const size_t rom_size = rom_->size(); ASSERT_GT(rom_size, kCommonBankOffset + 4) << "ROM should have common bank data"; // Parse and upload blocks: [size:2][aram_addr:2][data:size] uint32_t offset = kCommonBankOffset; int block_count = 0; int total_bytes_uploaded = 0; while (offset + 4 < rom_size) { uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8); uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8); if (block_size == 0 || block_size > 0x10000) break; if (offset + 4 + block_size > rom_size) break; apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size); std::cout << "[DirectSpcUpload] Block " << block_count << ": " << block_size << " bytes -> ARAM $" << std::hex << aram_addr << std::dec << std::endl; offset += 4 + block_size; block_count++; total_bytes_uploaded += block_size; } EXPECT_GT(block_count, 0) << "Should upload at least one block"; EXPECT_GT(total_bytes_uploaded, 1000) << "Should upload significant data"; std::cout << "[DirectSpcUpload] Uploaded " << block_count << " blocks, " << total_bytes_uploaded << " bytes total" << std::endl; // Verify some data was written to ARAM // SPC driver should be at $0800 uint8_t driver_check = apu.ram[0x0800]; EXPECT_NE(driver_check, 0) << "SPC driver area should have data"; } TEST_F(MusicIntegrationTest, DirectSpcUploadSongBank) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized); auto& apu = emulator.snes().apu(); apu.Reset(); // First upload common bank constexpr uint32_t kCommonBankOffset = 0xC8000; const uint8_t* rom_data = rom_->data(); const size_t rom_size = rom_->size(); uint32_t offset = kCommonBankOffset; while (offset + 4 < rom_size) { uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8); uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8); if (block_size == 0 || block_size > 0x10000) break; if (offset + 4 + block_size > rom_size) break; apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size); offset += 4 + block_size; } // Now upload overworld song bank (ROM offset 0xD1EF5) constexpr uint32_t kOverworldBankOffset = 0xD1EF5; ASSERT_GT(rom_size, kOverworldBankOffset + 4) << "ROM should have overworld bank data"; offset = kOverworldBankOffset; int song_block_count = 0; while (offset + 4 < rom_size) { uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8); uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8); if (block_size == 0 || block_size > 0x10000) break; if (offset + 4 + block_size > rom_size) break; apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size); std::cout << "[DirectSpcUpload] Song block " << song_block_count << ": " << block_size << " bytes -> ARAM $" << std::hex << aram_addr << std::dec << std::endl; offset += 4 + block_size; song_block_count++; } EXPECT_GT(song_block_count, 0) << "Should upload song bank blocks"; // Song pointers should be at ARAM $D000 uint16_t song_ptr_0 = apu.ram[0xD000] | (apu.ram[0xD001] << 8); std::cout << "[DirectSpcUpload] Song 0 pointer: $" << std::hex << song_ptr_0 << std::dec << std::endl; // Should have valid pointer (non-zero, within song data range) EXPECT_GT(song_ptr_0, 0xD000) << "Song pointer should be valid"; EXPECT_LT(song_ptr_0, 0xFFFF) << "Song pointer should be within ARAM range"; } TEST_F(MusicIntegrationTest, DirectSpcPortCommunication) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized); auto& apu = emulator.snes().apu(); // Test port communication // Write to in_ports (CPU -> SPC) apu.in_ports_[0] = 0x42; apu.in_ports_[1] = 0x00; EXPECT_EQ(apu.in_ports_[0], 0x42) << "Port 0 should hold written value"; EXPECT_EQ(apu.in_ports_[1], 0x00) << "Port 1 should hold written value"; std::cout << "[DirectSpcPort] Wrote song index 0x42 to port 0" << std::endl; // Run some cycles to let SPC process emulator.set_running(true); for (int i = 0; i < 10; ++i) { emulator.RunFrameOnly(); } // Check out_ports (SPC -> CPU) for acknowledgment std::cout << "[DirectSpcPort] Out ports: " << std::hex << (int)apu.out_ports_[0] << " " << (int)apu.out_ports_[1] << " " << (int)apu.out_ports_[2] << " " << (int)apu.out_ports_[3] << std::dec << std::endl; } TEST_F(MusicIntegrationTest, DirectSpcAudioGeneration) { emu::Emulator emulator; bool initialized = emulator.EnsureInitialized(rom_.get()); ASSERT_TRUE(initialized); auto& apu = emulator.snes().apu(); apu.Reset(); // Upload common bank const uint8_t* rom_data = rom_->data(); const size_t rom_size = rom_->size(); auto upload_bank = [&](uint32_t bank_offset) { uint32_t offset = bank_offset; while (offset + 4 < rom_size) { uint16_t block_size = rom_data[offset] | (rom_data[offset + 1] << 8); uint16_t aram_addr = rom_data[offset + 2] | (rom_data[offset + 3] << 8); if (block_size == 0 || block_size > 0x10000) break; if (offset + 4 + block_size > rom_size) break; apu.WriteDma(aram_addr, &rom_data[offset + 4], block_size); offset += 4 + block_size; } }; // Upload common bank (driver, samples, instruments) upload_bank(0xC8000); // Upload overworld song bank upload_bank(0xD1EF5); // Send play command for song 0 (Title) apu.in_ports_[0] = 0x00; // Song index 0 apu.in_ports_[1] = 0x00; // Play command std::cout << "[DirectSpcAudio] Starting playback test..." << std::endl; emulator.set_running(true); // Run frames and check for audio generation auto& dsp = apu.dsp(); int frames_with_audio = 0; for (int frame = 0; frame < 120; ++frame) { emulator.RunFrameOnly(); if (frame % 30 == 0) { const int16_t* samples = dsp.GetSampleBuffer(); uint16_t sample_offset = dsp.GetSampleOffset(); // Check if any samples are non-zero bool has_audio = false; for (int i = 0; i < std::min(256, (int)sample_offset * 2); ++i) { if (samples[i] != 0) { has_audio = true; break; } } if (has_audio) { frames_with_audio++; } std::cout << "[DirectSpcAudio] Frame " << frame << ": sample_offset=" << sample_offset << ", has_audio=" << (has_audio ? "yes" : "no") << std::endl; } } // Check DSP channel states for (int ch = 0; ch < 8; ++ch) { const auto& channel = dsp.GetChannel(ch); std::cout << "[DirectSpcAudio] Ch" << ch << ": vol=" << (int)channel.volumeL << "/" << (int)channel.volumeR << ", pitch=$" << std::hex << channel.pitch << std::dec << ", keyOn=" << channel.keyOn << std::endl; } // We may or may not get audio depending on SPC driver state // But the test verifies the upload and port communication work std::cout << "[DirectSpcAudio] Frames with detected audio: " << frames_with_audio << "/4 checks" << std::endl; } TEST_F(MusicIntegrationTest, VerifyAllBankUploadOffsets) { // Verify the ROM has valid block headers at all bank offsets const uint8_t* rom_data = rom_->data(); const size_t rom_size = rom_->size(); struct BankInfo { const char* name; uint32_t offset; }; BankInfo banks[] = { {"Common", 0xC8000}, {"Overworld", 0xD1EF5}, {"Dungeon", 0xD8000}, {"Credits", 0xD5380} }; for (const auto& bank : banks) { SCOPED_TRACE(bank.name); ASSERT_GT(rom_size, bank.offset + 4) << bank.name << " bank offset should be within ROM"; // Read first block header uint16_t block_size = rom_data[bank.offset] | (rom_data[bank.offset + 1] << 8); uint16_t aram_addr = rom_data[bank.offset + 2] | (rom_data[bank.offset + 3] << 8); std::cout << "[BankVerify] " << bank.name << " (0x" << std::hex << bank.offset << "): " << "size=" << std::dec << block_size << ", aram=$" << std::hex << aram_addr << std::dec << std::endl; // Block should have valid size and address EXPECT_GT(block_size, 0) << bank.name << " should have non-zero first block"; EXPECT_LT(block_size, 0x10000) << bank.name << " block size should be reasonable"; EXPECT_GT(aram_addr, 0) << bank.name << " should have non-zero ARAM address"; } } } // namespace test } // namespace zelda3 } // namespace yaze