515 lines
15 KiB
C++
515 lines
15 KiB
C++
#include "zelda3/music/spc_parser.h"
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "zelda3/music/music_bank.h"
|
|
#include "zelda3/music/song_data.h"
|
|
|
|
namespace yaze {
|
|
namespace test {
|
|
|
|
using namespace yaze::zelda3::music;
|
|
|
|
// =============================================================================
|
|
// Song Data Tests
|
|
// =============================================================================
|
|
|
|
class SongDataTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {}
|
|
};
|
|
|
|
TEST_F(SongDataTest, NoteGetNoteName_ValidPitches) {
|
|
Note note;
|
|
|
|
// C1 = 0x80
|
|
note.pitch = 0x80;
|
|
EXPECT_EQ(note.GetNoteName(), "C1");
|
|
|
|
// C#1 = 0x81
|
|
note.pitch = 0x81;
|
|
EXPECT_EQ(note.GetNoteName(), "C#1");
|
|
|
|
// D1 = 0x82
|
|
note.pitch = 0x82;
|
|
EXPECT_EQ(note.GetNoteName(), "D1");
|
|
|
|
// C2 = 0x8C
|
|
note.pitch = 0x8C;
|
|
EXPECT_EQ(note.GetNoteName(), "C2");
|
|
|
|
// A4 = 0xAD (concert pitch)
|
|
// Calculation: 0x80 + (octave-1)*12 + semitone
|
|
// A is semitone 9, so A4 = 0x80 + 3*12 + 9 = 0x80 + 36 + 9 = 0xAD
|
|
note.pitch = 0xAD;
|
|
EXPECT_EQ(note.GetNoteName(), "A4");
|
|
|
|
// B6 = 0xC7 (highest note)
|
|
note.pitch = 0xC7;
|
|
EXPECT_EQ(note.GetNoteName(), "B6");
|
|
}
|
|
|
|
TEST_F(SongDataTest, NoteGetNoteName_SpecialValues) {
|
|
Note note;
|
|
|
|
// Tie
|
|
note.pitch = kNoteTie;
|
|
EXPECT_EQ(note.GetNoteName(), "---");
|
|
|
|
// Rest
|
|
note.pitch = kNoteRest;
|
|
EXPECT_EQ(note.GetNoteName(), "...");
|
|
}
|
|
|
|
TEST_F(SongDataTest, NoteHelpers) {
|
|
Note note;
|
|
|
|
note.pitch = 0x8C; // C2
|
|
EXPECT_TRUE(note.IsNote());
|
|
EXPECT_FALSE(note.IsTie());
|
|
EXPECT_FALSE(note.IsRest());
|
|
EXPECT_EQ(note.GetOctave(), 2);
|
|
EXPECT_EQ(note.GetSemitone(), 0); // C
|
|
|
|
note.pitch = 0x8F; // D#2
|
|
EXPECT_EQ(note.GetOctave(), 2);
|
|
EXPECT_EQ(note.GetSemitone(), 3); // D#
|
|
|
|
note.pitch = kNoteTie;
|
|
EXPECT_FALSE(note.IsNote());
|
|
EXPECT_TRUE(note.IsTie());
|
|
EXPECT_FALSE(note.IsRest());
|
|
|
|
note.pitch = kNoteRest;
|
|
EXPECT_FALSE(note.IsNote());
|
|
EXPECT_FALSE(note.IsTie());
|
|
EXPECT_TRUE(note.IsRest());
|
|
}
|
|
|
|
TEST_F(SongDataTest, MusicCommandParamCount) {
|
|
MusicCommand cmd;
|
|
|
|
cmd.opcode = 0xE0; // SetInstrument
|
|
EXPECT_EQ(cmd.GetParamCount(), 1);
|
|
|
|
cmd.opcode = 0xE3; // VibratoOn
|
|
EXPECT_EQ(cmd.GetParamCount(), 3);
|
|
|
|
cmd.opcode = 0xE4; // VibratoOff
|
|
EXPECT_EQ(cmd.GetParamCount(), 0);
|
|
|
|
cmd.opcode = 0xEF; // CallSubroutine
|
|
EXPECT_EQ(cmd.GetParamCount(), 3);
|
|
}
|
|
|
|
TEST_F(SongDataTest, MusicCommandSubroutine) {
|
|
MusicCommand cmd;
|
|
cmd.opcode = 0xEF;
|
|
cmd.params = {0x00, 0xD0, 0x02}; // Address $D000, repeat 2
|
|
|
|
EXPECT_TRUE(cmd.IsSubroutine());
|
|
EXPECT_EQ(cmd.GetSubroutineAddress(), 0xD000);
|
|
EXPECT_EQ(cmd.GetSubroutineRepeatCount(), 2);
|
|
}
|
|
|
|
TEST_F(SongDataTest, TrackEventFactory) {
|
|
auto note_event = TrackEvent::MakeNote(100, 0x8C, 72, 0x40);
|
|
EXPECT_EQ(note_event.type, TrackEvent::Type::Note);
|
|
EXPECT_EQ(note_event.tick, 100);
|
|
EXPECT_EQ(note_event.note.pitch, 0x8C);
|
|
EXPECT_EQ(note_event.note.duration, 72);
|
|
EXPECT_EQ(note_event.note.velocity, 0x40);
|
|
|
|
auto cmd_event = TrackEvent::MakeCommand(50, 0xE0, 0x0B);
|
|
EXPECT_EQ(cmd_event.type, TrackEvent::Type::Command);
|
|
EXPECT_EQ(cmd_event.tick, 50);
|
|
EXPECT_EQ(cmd_event.command.opcode, 0xE0);
|
|
EXPECT_EQ(cmd_event.command.params[0], 0x0B);
|
|
|
|
auto end_event = TrackEvent::MakeEnd(200);
|
|
EXPECT_EQ(end_event.type, TrackEvent::Type::End);
|
|
EXPECT_EQ(end_event.tick, 200);
|
|
}
|
|
|
|
TEST_F(SongDataTest, MusicTrackDuration) {
|
|
MusicTrack track;
|
|
track.events.push_back(TrackEvent::MakeNote(0, 0x8C, 72));
|
|
track.events.push_back(TrackEvent::MakeNote(72, 0x8E, 36));
|
|
track.events.push_back(TrackEvent::MakeEnd(108));
|
|
|
|
track.CalculateDuration();
|
|
|
|
EXPECT_EQ(track.duration_ticks, 108);
|
|
EXPECT_FALSE(track.is_empty);
|
|
}
|
|
|
|
TEST_F(SongDataTest, MusicSegmentDuration) {
|
|
MusicSegment segment;
|
|
|
|
// Track 0: 100 ticks
|
|
segment.tracks[0].events.push_back(TrackEvent::MakeNote(0, 0x8C, 100));
|
|
segment.tracks[0].CalculateDuration();
|
|
|
|
// Track 1: 200 ticks
|
|
segment.tracks[1].events.push_back(TrackEvent::MakeNote(0, 0x8C, 200));
|
|
segment.tracks[1].CalculateDuration();
|
|
|
|
// Other tracks empty
|
|
for (int i = 2; i < 8; ++i) {
|
|
segment.tracks[i].is_empty = true;
|
|
segment.tracks[i].duration_ticks = 0;
|
|
}
|
|
|
|
EXPECT_EQ(segment.GetDuration(), 200);
|
|
}
|
|
|
|
// =============================================================================
|
|
// SpcParser Tests
|
|
// =============================================================================
|
|
|
|
class SpcParserTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {}
|
|
};
|
|
|
|
TEST_F(SpcParserTest, GetCommandParamCount) {
|
|
EXPECT_EQ(SpcParser::GetCommandParamCount(0xE0), 1); // SetInstrument
|
|
EXPECT_EQ(SpcParser::GetCommandParamCount(0xE4), 0); // VibratoOff
|
|
EXPECT_EQ(SpcParser::GetCommandParamCount(0xE7), 1); // SetTempo
|
|
EXPECT_EQ(SpcParser::GetCommandParamCount(0xEF), 3); // CallSubroutine
|
|
EXPECT_EQ(SpcParser::GetCommandParamCount(0x80), 0); // Not a command
|
|
}
|
|
|
|
TEST_F(SpcParserTest, IsNotePitch) {
|
|
EXPECT_TRUE(SpcParser::IsNotePitch(0x80)); // C1
|
|
EXPECT_TRUE(SpcParser::IsNotePitch(0xC7)); // B6
|
|
EXPECT_TRUE(SpcParser::IsNotePitch(0xC8)); // Tie
|
|
EXPECT_TRUE(SpcParser::IsNotePitch(0xC9)); // Rest
|
|
EXPECT_FALSE(SpcParser::IsNotePitch(0x7F)); // Duration
|
|
EXPECT_FALSE(SpcParser::IsNotePitch(0xE0)); // Command
|
|
}
|
|
|
|
TEST_F(SpcParserTest, IsDuration) {
|
|
EXPECT_TRUE(SpcParser::IsDuration(0x00));
|
|
EXPECT_TRUE(SpcParser::IsDuration(0x48)); // Quarter note
|
|
EXPECT_TRUE(SpcParser::IsDuration(0x7F)); // Max duration
|
|
EXPECT_FALSE(SpcParser::IsDuration(0x80)); // Note
|
|
EXPECT_FALSE(SpcParser::IsDuration(0xE0)); // Command
|
|
}
|
|
|
|
TEST_F(SpcParserTest, IsCommand) {
|
|
EXPECT_TRUE(SpcParser::IsCommand(0xE0));
|
|
EXPECT_TRUE(SpcParser::IsCommand(0xFF));
|
|
EXPECT_FALSE(SpcParser::IsCommand(0xDF));
|
|
EXPECT_FALSE(SpcParser::IsCommand(0x80));
|
|
}
|
|
|
|
// =============================================================================
|
|
// SpcSerializer Tests
|
|
// =============================================================================
|
|
|
|
class SpcSerializerTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {}
|
|
};
|
|
|
|
TEST_F(SpcSerializerTest, SerializeNote) {
|
|
Note note;
|
|
note.pitch = 0x8C;
|
|
note.duration = 0x48;
|
|
note.velocity = 0;
|
|
note.has_duration_prefix = true;
|
|
|
|
uint8_t current_duration = 0;
|
|
auto bytes = SpcSerializer::SerializeNote(note, ¤t_duration);
|
|
|
|
// Should output duration + pitch
|
|
ASSERT_EQ(bytes.size(), 2);
|
|
EXPECT_EQ(bytes[0], 0x48); // Duration
|
|
EXPECT_EQ(bytes[1], 0x8C); // Pitch
|
|
EXPECT_EQ(current_duration, 0x48);
|
|
|
|
// Next note with same duration
|
|
Note note2;
|
|
note2.pitch = 0x8E;
|
|
note2.duration = 0x48;
|
|
note2.has_duration_prefix = true;
|
|
|
|
auto bytes2 = SpcSerializer::SerializeNote(note2, ¤t_duration);
|
|
|
|
// Should only output pitch (duration unchanged)
|
|
ASSERT_EQ(bytes2.size(), 1);
|
|
EXPECT_EQ(bytes2[0], 0x8E);
|
|
}
|
|
|
|
TEST_F(SpcSerializerTest, SerializeCommand) {
|
|
MusicCommand cmd;
|
|
cmd.opcode = 0xE0;
|
|
cmd.params = {0x0B, 0, 0}; // SetInstrument(Piano)
|
|
|
|
auto bytes = SpcSerializer::SerializeCommand(cmd);
|
|
|
|
ASSERT_EQ(bytes.size(), 2);
|
|
EXPECT_EQ(bytes[0], 0xE0);
|
|
EXPECT_EQ(bytes[1], 0x0B);
|
|
}
|
|
|
|
TEST_F(SpcSerializerTest, SerializeTrack) {
|
|
MusicTrack track;
|
|
|
|
// SetInstrument(Piano)
|
|
track.events.push_back(TrackEvent::MakeCommand(0, 0xE0, 0x0B));
|
|
|
|
// SetChannelVolume(192)
|
|
track.events.push_back(TrackEvent::MakeCommand(0, 0xED, 0xC0));
|
|
|
|
// Quarter note C2 with duration prefix
|
|
TrackEvent note_event = TrackEvent::MakeNote(0, 0x8C, 0x48);
|
|
note_event.note.has_duration_prefix = true;
|
|
track.events.push_back(note_event);
|
|
|
|
// End
|
|
track.events.push_back(TrackEvent::MakeEnd(72));
|
|
|
|
auto bytes = SpcSerializer::SerializeTrack(track);
|
|
|
|
// Expected: E0 0B ED C0 48 8C 00
|
|
ASSERT_GE(bytes.size(), 7);
|
|
EXPECT_EQ(bytes[0], 0xE0);
|
|
EXPECT_EQ(bytes[1], 0x0B);
|
|
EXPECT_EQ(bytes[2], 0xED);
|
|
EXPECT_EQ(bytes[3], 0xC0);
|
|
EXPECT_EQ(bytes[4], 0x48);
|
|
EXPECT_EQ(bytes[5], 0x8C);
|
|
EXPECT_EQ(bytes.back(), 0x00); // End marker
|
|
}
|
|
|
|
// =============================================================================
|
|
// BrrCodec Tests
|
|
// =============================================================================
|
|
|
|
class BrrCodecTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {}
|
|
};
|
|
|
|
TEST_F(BrrCodecTest, EncodeDecodeRoundtrip) {
|
|
// Create a simple sine wave
|
|
std::vector<int16_t> original;
|
|
for (int i = 0; i < 64; ++i) {
|
|
double t = i / 64.0 * 2 * 3.14159;
|
|
original.push_back(static_cast<int16_t>(sin(t) * 10000));
|
|
}
|
|
|
|
// Encode to BRR
|
|
auto brr = BrrCodec::Encode(original);
|
|
EXPECT_GT(brr.size(), 0);
|
|
|
|
// Decode back
|
|
auto decoded = BrrCodec::Decode(brr);
|
|
EXPECT_GT(decoded.size(), 0);
|
|
|
|
// Should be similar (BRR is lossy, so allow some error)
|
|
ASSERT_EQ(decoded.size(), original.size());
|
|
|
|
int max_error = 0;
|
|
for (size_t i = 0; i < original.size(); ++i) {
|
|
int error = abs(original[i] - decoded[i]);
|
|
if (error > max_error) max_error = error;
|
|
}
|
|
|
|
// BRR compression should keep error reasonable
|
|
EXPECT_LT(max_error, 5000); // Allow up to ~15% error
|
|
}
|
|
|
|
// =============================================================================
|
|
// MusicBank Tests
|
|
// =============================================================================
|
|
|
|
class MusicBankTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {}
|
|
};
|
|
|
|
TEST_F(MusicBankTest, GetVanillaSongName) {
|
|
EXPECT_STREQ(GetVanillaSongName(1), "Title");
|
|
EXPECT_STREQ(GetVanillaSongName(2), "Light World");
|
|
EXPECT_STREQ(GetVanillaSongName(12), "Soldier");
|
|
EXPECT_STREQ(GetVanillaSongName(21), "Boss");
|
|
EXPECT_STREQ(GetVanillaSongName(0), "Unknown");
|
|
EXPECT_STREQ(GetVanillaSongName(100), "Unknown");
|
|
}
|
|
|
|
TEST_F(MusicBankTest, GetVanillaSongBank) {
|
|
EXPECT_EQ(GetVanillaSongBank(1), MusicBank::Bank::Overworld);
|
|
EXPECT_EQ(GetVanillaSongBank(11), MusicBank::Bank::Overworld);
|
|
EXPECT_EQ(GetVanillaSongBank(12), MusicBank::Bank::Dungeon);
|
|
EXPECT_EQ(GetVanillaSongBank(31), MusicBank::Bank::Dungeon);
|
|
EXPECT_EQ(GetVanillaSongBank(32), MusicBank::Bank::Credits);
|
|
}
|
|
|
|
TEST_F(MusicBankTest, BankMaxSize) {
|
|
EXPECT_EQ(MusicBank::GetBankMaxSize(MusicBank::Bank::Overworld),
|
|
kOverworldBankMaxSize);
|
|
EXPECT_EQ(MusicBank::GetBankMaxSize(MusicBank::Bank::Dungeon),
|
|
kDungeonBankMaxSize);
|
|
EXPECT_EQ(MusicBank::GetBankMaxSize(MusicBank::Bank::Credits),
|
|
kCreditsBankMaxSize);
|
|
}
|
|
|
|
TEST_F(MusicBankTest, CreateNewSong) {
|
|
MusicBank bank;
|
|
|
|
int index = bank.CreateNewSong("Test Song", MusicBank::Bank::Overworld);
|
|
EXPECT_GE(index, 0);
|
|
|
|
auto* song = bank.GetSong(index);
|
|
ASSERT_NE(song, nullptr);
|
|
EXPECT_EQ(song->name, "Test Song");
|
|
EXPECT_EQ(song->bank, static_cast<uint8_t>(MusicBank::Bank::Overworld));
|
|
EXPECT_TRUE(song->modified);
|
|
EXPECT_EQ(song->segments.size(), 1);
|
|
}
|
|
|
|
TEST_F(MusicBankTest, SpaceCalculation) {
|
|
MusicBank bank;
|
|
|
|
// Empty bank
|
|
auto space = bank.CalculateSpaceUsage(MusicBank::Bank::Overworld);
|
|
EXPECT_EQ(space.used_bytes, 0);
|
|
EXPECT_EQ(space.free_bytes, kOverworldBankMaxSize);
|
|
EXPECT_EQ(space.total_bytes, kOverworldBankMaxSize);
|
|
EXPECT_EQ(space.usage_percent, 0.0f);
|
|
|
|
// Add a song
|
|
bank.CreateNewSong("Test", MusicBank::Bank::Overworld);
|
|
|
|
space = bank.CalculateSpaceUsage(MusicBank::Bank::Overworld);
|
|
EXPECT_GT(space.used_bytes, 0);
|
|
EXPECT_LT(space.free_bytes, kOverworldBankMaxSize);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Direct SPC Bank Mapping Tests
|
|
// =============================================================================
|
|
|
|
class DirectSpcMappingTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {}
|
|
|
|
// Helper to test bank ROM offset mapping
|
|
// Note: These match the logic in MusicEditor::GetBankRomOffset
|
|
uint32_t GetBankRomOffset(uint8_t bank) const {
|
|
constexpr uint32_t kSoundBankOffsets[] = {
|
|
0xC8000, // ROM Bank 0 (common) - driver + samples + instruments
|
|
0xD1EF5, // ROM Bank 1 (overworld songs)
|
|
0xD8000, // ROM Bank 2 (dungeon songs)
|
|
0xD5380 // ROM Bank 3 (credits songs)
|
|
};
|
|
if (bank < 4) {
|
|
return kSoundBankOffsets[bank];
|
|
}
|
|
return kSoundBankOffsets[0];
|
|
}
|
|
|
|
// Helper to convert song.bank enum to ROM bank
|
|
uint8_t SongBankToRomBank(uint8_t song_bank) const {
|
|
// song.bank: 0=overworld, 1=dungeon, 2=credits
|
|
// ROM bank: 1=overworld, 2=dungeon, 3=credits
|
|
return song_bank + 1;
|
|
}
|
|
|
|
// Helper to test song index in bank calculation
|
|
// Matches MusicEditor::GetSongIndexInBank
|
|
int GetSongIndexInBank(int song_id, uint8_t bank) const {
|
|
switch (bank) {
|
|
case 0: // Overworld
|
|
return song_id - 1; // Songs 1-11 → 0-10
|
|
case 1: // Dungeon
|
|
return song_id - 12; // Songs 12-31 → 0-19
|
|
case 2: // Credits
|
|
return song_id - 32; // Songs 32-34 → 0-2
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
TEST_F(DirectSpcMappingTest, BankRomOffsets) {
|
|
// ROM Bank 0: Common bank (driver, samples, instruments)
|
|
EXPECT_EQ(GetBankRomOffset(0), 0xC8000u);
|
|
|
|
// ROM Bank 1: Overworld songs
|
|
EXPECT_EQ(GetBankRomOffset(1), 0xD1EF5u);
|
|
|
|
// ROM Bank 2: Dungeon songs
|
|
EXPECT_EQ(GetBankRomOffset(2), 0xD8000u);
|
|
|
|
// ROM Bank 3: Credits songs
|
|
EXPECT_EQ(GetBankRomOffset(3), 0xD5380u);
|
|
|
|
// Invalid bank should return common
|
|
EXPECT_EQ(GetBankRomOffset(99), 0xC8000u);
|
|
}
|
|
|
|
TEST_F(DirectSpcMappingTest, SongBankToRomBankMapping) {
|
|
// song.bank 0 (overworld) → ROM bank 1 (0xD1EF5)
|
|
EXPECT_EQ(SongBankToRomBank(0), 1);
|
|
EXPECT_EQ(GetBankRomOffset(SongBankToRomBank(0)), 0xD1EF5u);
|
|
|
|
// song.bank 1 (dungeon) → ROM bank 2 (0xD8000)
|
|
EXPECT_EQ(SongBankToRomBank(1), 2);
|
|
EXPECT_EQ(GetBankRomOffset(SongBankToRomBank(1)), 0xD8000u);
|
|
|
|
// song.bank 2 (credits) → ROM bank 3 (0xD5380)
|
|
EXPECT_EQ(SongBankToRomBank(2), 3);
|
|
EXPECT_EQ(GetBankRomOffset(SongBankToRomBank(2)), 0xD5380u);
|
|
}
|
|
|
|
TEST_F(DirectSpcMappingTest, OverworldSongIndices) {
|
|
// Overworld songs: 1-11 (global ID) → 0-10 (bank index)
|
|
EXPECT_EQ(GetSongIndexInBank(1, 0), 0); // Title
|
|
EXPECT_EQ(GetSongIndexInBank(2, 0), 1); // Light World
|
|
EXPECT_EQ(GetSongIndexInBank(11, 0), 10); // File Select
|
|
}
|
|
|
|
TEST_F(DirectSpcMappingTest, DungeonSongIndices) {
|
|
// Dungeon songs: 12-31 (global ID) → 0-19 (bank index)
|
|
EXPECT_EQ(GetSongIndexInBank(12, 1), 0); // Soldier
|
|
EXPECT_EQ(GetSongIndexInBank(13, 1), 1); // Mountain
|
|
EXPECT_EQ(GetSongIndexInBank(21, 1), 9); // Boss
|
|
EXPECT_EQ(GetSongIndexInBank(31, 1), 19); // Last Boss
|
|
}
|
|
|
|
TEST_F(DirectSpcMappingTest, CreditsSongIndices) {
|
|
// Credits songs: 32-34 (global ID) → 0-2 (bank index)
|
|
EXPECT_EQ(GetSongIndexInBank(32, 2), 0); // Credits 1
|
|
EXPECT_EQ(GetSongIndexInBank(33, 2), 1); // Credits 2
|
|
EXPECT_EQ(GetSongIndexInBank(34, 2), 2); // Credits 3
|
|
}
|
|
|
|
TEST_F(DirectSpcMappingTest, BankIndexConsistency) {
|
|
// Verify bank index is non-negative for all vanilla songs
|
|
for (int song_id = 1; song_id <= 11; ++song_id) {
|
|
int index = GetSongIndexInBank(song_id, 0);
|
|
EXPECT_GE(index, 0) << "Overworld song " << song_id << " should have non-negative index";
|
|
EXPECT_LE(index, 10) << "Overworld song " << song_id << " should be <= 10";
|
|
}
|
|
|
|
for (int song_id = 12; song_id <= 31; ++song_id) {
|
|
int index = GetSongIndexInBank(song_id, 1);
|
|
EXPECT_GE(index, 0) << "Dungeon song " << song_id << " should have non-negative index";
|
|
EXPECT_LE(index, 19) << "Dungeon song " << song_id << " should be <= 19";
|
|
}
|
|
|
|
for (int song_id = 32; song_id <= 34; ++song_id) {
|
|
int index = GetSongIndexInBank(song_id, 2);
|
|
EXPECT_GE(index, 0) << "Credits song " << song_id << " should have non-negative index";
|
|
EXPECT_LE(index, 2) << "Credits song " << song_id << " should be <= 2";
|
|
}
|
|
}
|
|
|
|
} // namespace test
|
|
} // namespace yaze
|