685 lines
24 KiB
C++
685 lines
24 KiB
C++
// Integration tests for Music Editor with real ROM data
|
|
// Tests song loading, parsing, and emulator audio stability
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <string>
|
|
|
|
#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<Rom>();
|
|
|
|
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> 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<uint8_t>(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<uint8_t>(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<uint8_t>(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<int>(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<int>(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<std::string> 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
|