yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
music_bank.cc
Go to the documentation of this file.
2
3#include "absl/status/status.h"
4#include "absl/status/statusor.h"
5
6#include <algorithm>
7#include <array>
8#include <cmath>
9#include <cstdint>
10#include <cstring>
11#include <exception>
12#include <string>
13#include <utility>
14#include <vector>
15
16#include "absl/strings/str_format.h"
17#include "core/rom_settings.h"
18#include "nlohmann/json.hpp"
19#include "rom/rom.h"
20#include "util/macro.h"
23
24namespace yaze {
25namespace zelda3 {
26namespace music {
27
28namespace {
29
34
39
44
45// Vanilla song table (1-indexed song IDs)
47 const char* name;
49};
50
52 {"Invalid", MusicBank::Bank::Overworld}, // 0 (unused)
53 {"Title", MusicBank::Bank::Overworld}, // 1
54 {"Light World", MusicBank::Bank::Overworld}, // 2
55 {"Beginning", MusicBank::Bank::Overworld}, // 3
56 {"Rabbit", MusicBank::Bank::Overworld}, // 4
57 {"Forest", MusicBank::Bank::Overworld}, // 5
58 {"Intro", MusicBank::Bank::Overworld}, // 6
59 {"Town", MusicBank::Bank::Overworld}, // 7
60 {"Warp", MusicBank::Bank::Overworld}, // 8
61 {"Dark World", MusicBank::Bank::Overworld}, // 9
62 {"Master Sword", MusicBank::Bank::Overworld}, // 10
63 {"File Select", MusicBank::Bank::Overworld}, // 11
64 {"Soldier", MusicBank::Bank::Dungeon}, // 12
65 {"Mountain", MusicBank::Bank::Dungeon}, // 13
66 {"Shop", MusicBank::Bank::Dungeon}, // 14
67 {"Fanfare", MusicBank::Bank::Dungeon}, // 15
68 {"Castle", MusicBank::Bank::Dungeon}, // 16
69 {"Palace (Pendant)", MusicBank::Bank::Dungeon}, // 17
70 {"Cave", MusicBank::Bank::Dungeon}, // 18
71 {"Clear", MusicBank::Bank::Dungeon}, // 19
72 {"Church", MusicBank::Bank::Dungeon}, // 20
73 {"Boss", MusicBank::Bank::Dungeon}, // 21
74 {"Dungeon (Crystal)", MusicBank::Bank::Dungeon}, // 22
75 {"Psychic", MusicBank::Bank::Dungeon}, // 23
76 {"Secret Way", MusicBank::Bank::Dungeon}, // 24
77 {"Rescue", MusicBank::Bank::Dungeon}, // 25
78 {"Crystal", MusicBank::Bank::Dungeon}, // 26
79 {"Fountain", MusicBank::Bank::Dungeon}, // 27
80 {"Pyramid", MusicBank::Bank::Dungeon}, // 28
81 {"Kill Agahnim", MusicBank::Bank::Dungeon}, // 29
82 {"Ganon Room", MusicBank::Bank::Dungeon}, // 30
83 {"Last Boss", MusicBank::Bank::Dungeon}, // 31
84 {"Credits 1", MusicBank::Bank::Credits}, // 32
85 {"Credits 2", MusicBank::Bank::Credits}, // 33
86 {"Credits 3", MusicBank::Bank::Credits}, // 34
87};
88
89constexpr int kVanillaSongCount =
90 sizeof(kVanillaSongs) / sizeof(kVanillaSongs[0]) - 1;
91
98
100 {MusicBank::Bank::Overworld, 1, 1, 11},
101 {MusicBank::Bank::Dungeon, 2, 12, 31},
102 {MusicBank::Bank::Credits, 3, 32, 34},
103};
104
106 for (const auto& metadata : kBankMetadata) {
107 if (metadata.bank == bank) {
108 return &metadata;
109 }
110 }
111 return nullptr;
112}
113
115 int low;
116 int mid;
117 int bank;
118};
119
120constexpr BankPointerRegisters kOverworldPointerRegs{0x0914, 0x0918, 0x091C};
121constexpr BankPointerRegisters kCreditsPointerRegs{0x0932, 0x0936, 0x093A};
122
123uint8_t EncodeLoRomBank(uint32_t pc_offset) {
124 return static_cast<uint8_t>((pc_offset >> 15) & 0xFF);
125}
126
127uint8_t EncodeLoRomMid(uint32_t pc_offset) {
128 uint16_t addr = static_cast<uint16_t>(pc_offset & 0x7FFF);
129 return static_cast<uint8_t>((addr >> 8) & 0x7F);
130}
131
132uint8_t EncodeLoRomLow(uint32_t pc_offset) {
133 return static_cast<uint8_t>(pc_offset & 0xFF);
134}
135
137 const BankPointerRegisters& regs,
138 uint32_t pc_offset) {
139 uint8_t preserved_mid = 0;
140 auto mid_read = rom.ReadByte(regs.mid);
141 if (mid_read.ok()) {
142 preserved_mid = mid_read.value() & 0x80;
143 }
144
145 auto status = rom.WriteByte(regs.low, EncodeLoRomLow(pc_offset));
146 if (!status.ok())
147 return status;
148
149 status = rom.WriteByte(
150 regs.mid,
151 static_cast<uint8_t>(preserved_mid | EncodeLoRomMid(pc_offset)));
152 if (!status.ok())
153 return status;
154
155 status = rom.WriteByte(regs.bank, EncodeLoRomBank(pc_offset));
156 return status;
157}
158
160 uint32_t pc_offset) {
161 switch (bank) {
165 return UpdateBankPointerRegisters(rom, kCreditsPointerRegs, pc_offset);
166 default:
167 return absl::OkStatus();
168 }
169}
170
171// ALTTP instrument table metadata
172constexpr int kVanillaInstrumentCount = 0x19; // 25 entries
173constexpr int kInstrumentEntrySize = 6;
175 "Noise", "Rain", "Timpani", "Square wave", "Saw wave",
176 "Clink", "Wobbly lead", "Compound saw", "Tweet", "Strings A",
177 "Strings B", "Trombone", "Cymbal", "Ocarina", "Chimes",
178 "Harp", "Splash", "Trumpet", "Horn", "Snare A",
179 "Snare B", "Choir", "Flute", "Oof", "Piano"};
180
181} // namespace
182
183// =============================================================================
184// Public Methods
185// =============================================================================
186
187absl::Status MusicBank::LoadFromRom(Rom& rom) {
188 if (!rom.is_loaded()) {
189 return absl::FailedPreconditionError("ROM is not loaded");
190 }
191
192 songs_.clear();
193 instruments_.clear();
194 samples_.clear();
198
199 std::vector<MusicSong> custom_songs;
200 custom_songs.reserve(8);
201
202 // Detect Oracle of Secrets expanded music patch
203 auto status = DetectExpandedMusicPatch(rom);
204 if (!status.ok())
205 return status;
206
207 // Load songs from each vanilla bank
208 status = LoadSongTable(rom, Bank::Overworld, &custom_songs);
209 if (!status.ok())
210 return status;
211
212 status = LoadSongTable(rom, Bank::Dungeon, &custom_songs);
213 if (!status.ok())
214 return status;
215
216 status = LoadSongTable(rom, Bank::Credits, &custom_songs);
217 if (!status.ok())
218 return status;
219
220 // Load expanded bank songs if patch detected
222 status = LoadExpandedSongTable(rom, &custom_songs);
223 if (!status.ok())
224 return status;
225 }
226
227 for (auto& song : custom_songs) {
228 songs_.push_back(std::move(song));
229 }
230
231 // Load instruments
232 status = LoadInstruments(rom);
233 if (!status.ok())
234 return status;
235
236 // Load samples
237 status = LoadSamples(rom);
238 if (!status.ok())
239 return status;
240
241 loaded_ = true;
242 return absl::OkStatus();
243}
244
245absl::Status MusicBank::SaveToRom(Rom& rom) {
246 if (!loaded_) {
247 return absl::FailedPreconditionError("No music data loaded");
248 }
249
250 if (!rom.is_loaded()) {
251 return absl::FailedPreconditionError("ROM is not loaded");
252 }
253
254 // Check if everything fits
255 if (!AllSongsFit()) {
256 return absl::ResourceExhaustedError(
257 "Songs do not fit in ROM banks. Reduce song size or remove songs.");
258 }
259
260 // Save songs to each bank
261 auto status = SaveSongTable(rom, Bank::Overworld);
262 if (!status.ok())
263 return status;
264
265 status = SaveSongTable(rom, Bank::Dungeon);
266 if (!status.ok())
267 return status;
268
269 status = SaveSongTable(rom, Bank::Credits);
270 if (!status.ok())
271 return status;
272
273 // Save instruments if modified
275 status = SaveInstruments(rom);
276 if (!status.ok())
277 return status;
278 }
279
280 // Save samples if modified
281 if (samples_modified_) {
282 status = SaveSamples(rom);
283 if (!status.ok())
284 return status;
285 }
286
288 return absl::OkStatus();
289}
290
292 if (index < 0 || index >= static_cast<int>(songs_.size())) {
293 return nullptr;
294 }
295 return &songs_[index];
296}
297
298const MusicSong* MusicBank::GetSong(int index) const {
299 if (index < 0 || index >= static_cast<int>(songs_.size())) {
300 return nullptr;
301 }
302 return &songs_[index];
303}
304
306 // Song IDs are 1-indexed
307 if (song_id <= 0 || song_id > static_cast<int>(songs_.size())) {
308 return nullptr;
309 }
310 return &songs_[song_id - 1];
311}
312
313int MusicBank::CreateNewSong(const std::string& name, Bank bank) {
314 MusicSong song;
315 song.name = name;
316 song.bank = static_cast<uint8_t>(bank);
317 song.modified = true;
318
319 // Add a default empty segment with empty tracks
320 MusicSegment segment;
321 for (auto& track : segment.tracks) {
322 track.is_empty = true;
323 track.events.push_back(TrackEvent::MakeEnd(0));
324 }
325 song.segments.push_back(std::move(segment));
326
327 songs_.push_back(std::move(song));
328 return static_cast<int>(songs_.size()) - 1;
329}
330
332 auto* source = GetSong(index);
333 if (!source)
334 return -1;
335
336 MusicSong new_song = *source;
337 new_song.name += " (Copy)";
338 new_song.modified = true;
339
340 songs_.push_back(std::move(new_song));
341 return static_cast<int>(songs_.size()) - 1;
342}
343
344bool MusicBank::IsVanilla(int index) const {
345 return index >= 0 && index < kVanillaSongCount;
346}
347
348absl::Status MusicBank::DeleteSong(int index) {
349 if (index < 0 || index >= static_cast<int>(songs_.size())) {
350 return absl::InvalidArgumentError("Invalid song index");
351 }
352
353 if (IsVanilla(index)) {
354 return absl::InvalidArgumentError("Cannot delete vanilla songs");
355 }
356
357 songs_.erase(songs_.begin() + index);
358 return absl::OkStatus();
359}
360
361std::vector<MusicSong*> MusicBank::GetSongsInBank(Bank bank) {
362 std::vector<MusicSong*> result;
363 for (auto& song : songs_) {
364 if (static_cast<Bank>(song.bank) == bank) {
365 result.push_back(&song);
366 }
367 }
368 return result;
369}
370
372 if (index < 0 || index >= static_cast<int>(instruments_.size())) {
373 return nullptr;
374 }
375 return &instruments_[index];
376}
377
379 if (index < 0 || index >= static_cast<int>(instruments_.size())) {
380 return nullptr;
381 }
382 return &instruments_[index];
383}
384
385int MusicBank::CreateNewInstrument(const std::string& name) {
386 MusicInstrument inst;
387 inst.name = name;
388 inst.sample_index = 0;
389 inst.attack = 15; // Default fast attack
390 inst.decay = 7;
391 inst.sustain_level = 7;
392 inst.sustain_rate = 0;
393 inst.pitch_mult = 0x1000; // 1.0 multiplier
394
395 instruments_.push_back(std::move(inst));
397 return static_cast<int>(instruments_.size()) - 1;
398}
399
401 if (index < 0 || index >= static_cast<int>(samples_.size())) {
402 return nullptr;
403 }
404 return &samples_[index];
405}
406
407const MusicSample* MusicBank::GetSample(int index) const {
408 if (index < 0 || index >= static_cast<int>(samples_.size())) {
409 return nullptr;
410 }
411 return &samples_[index];
412}
413
414absl::StatusOr<int> MusicBank::ImportSampleFromWav(const std::string& filepath,
415 const std::string& name) {
416 // TODO: Implement proper WAV loading and BRR encoding
417 // For now, return success with a dummy sample so UI integration can be tested
418
419 MusicSample sample;
420 sample.name = name;
421 // Create dummy PCM data (sine wave)
422 sample.pcm_data.resize(1000);
423 for (int i = 0; i < 1000; ++i) {
424 sample.pcm_data[i] = static_cast<int16_t>(32040.0 * std::sin(i * 0.1));
425 }
426
427 samples_.push_back(std::move(sample));
428 samples_modified_ = true;
429
430 return static_cast<int>(samples_.size()) - 1;
431}
432
434 SpaceInfo info;
435 info.total_bytes = GetBankMaxSize(bank);
436 info.used_bytes = 0;
437
438 for (const auto& song : songs_) {
439 if (static_cast<Bank>(song.bank) == bank) {
440 info.used_bytes += CalculateSongSize(song);
441 }
442 }
443
444 info.free_bytes = info.total_bytes - info.used_bytes;
445 info.usage_percent = (info.total_bytes > 0)
446 ? (100.0f * info.used_bytes / info.total_bytes)
447 : 0.0f;
448
449 // Set warning/critical flags
450 info.is_warning = info.usage_percent > 75.0f;
451 info.is_critical = info.usage_percent > 90.0f;
452
453 // Generate recommendations
454 if (info.is_critical) {
456 info.recommendation = "Move songs to Expanded bank";
457 } else if (bank == Bank::OverworldExpanded) {
458 info.recommendation = "Move songs to Auxiliary bank";
459 } else {
460 info.recommendation = "Remove or shorten songs";
461 }
462 } else if (info.is_warning) {
463 info.recommendation = "Approaching bank limit";
464 }
465
466 return info;
467}
468
474
476 switch (bank) {
477 case Bank::Overworld:
479 case Bank::Dungeon:
480 return kDungeonBankMaxSize;
481 case Bank::Credits:
482 return kCreditsBankMaxSize;
485 case Bank::Auxiliary:
486 return kAuxBankMaxSize;
487 }
488 return 0;
489}
490
492 switch (bank) {
493 case Bank::Overworld:
494 return kOverworldBankRom;
495 case Bank::Dungeon:
496 return kDungeonBankRom;
497 case Bank::Credits:
498 return kCreditsBankRom;
500 return ExpandedOverworldBankRom();
501 case Bank::Auxiliary:
502 return ExpandedAuxBankRom();
503 }
504 return 0;
505}
506
509 return true;
510 }
511 for (const auto& song : songs_) {
512 if (song.modified) {
513 return true;
514 }
515 }
516 return false;
517}
518
520 instruments_modified_ = false;
521 samples_modified_ = false;
522 for (auto& song : songs_) {
523 song.modified = false;
524 }
525}
526
527// =============================================================================
528// Private Methods
529// =============================================================================
530
532 // Reset expanded bank info
534
535 // Check if ROM has the Oracle of Secrets expanded music hook at $008919
536 // The vanilla code at this address is NOT a JSL, but the expanded patch
537 // replaces it with: JSL LoadOverworldSongsExpanded
538 const uint32_t hook_address = ExpandedMusicHookAddress();
539 if (hook_address >= rom.size()) {
540 return absl::OkStatus(); // ROM too small, no expanded patch
541 }
542
543 auto opcode_result = rom.ReadByte(hook_address);
544 if (!opcode_result.ok()) {
545 return absl::OkStatus(); // Can't read, assume no patch
546 }
547
548 if (opcode_result.value() != kJslOpcode) {
549 return absl::OkStatus(); // Not a JSL, no expanded patch
550 }
551
552 // Read the JSL target address (3 bytes: low, mid, bank)
553 auto addr_low = rom.ReadByte(hook_address + 1);
554 auto addr_mid = rom.ReadByte(hook_address + 2);
555 auto addr_bank = rom.ReadByte(hook_address + 3);
556
557 if (!addr_low.ok() || !addr_mid.ok() || !addr_bank.ok()) {
558 return absl::OkStatus(); // Can't read address, assume no patch
559 }
560
561 // Construct the 24-bit SNES address
562 uint32_t jsl_target = static_cast<uint32_t>(addr_low.value()) |
563 (static_cast<uint32_t>(addr_mid.value()) << 8) |
564 (static_cast<uint32_t>(addr_bank.value()) << 16);
565
566 // Validate the JSL target is in a reasonable range (freespace or bank $1A-$1B)
567 // Oracle of Secrets typically places the hook handler in bank $00 or $1A
568 uint8_t target_bank = (jsl_target >> 16) & 0xFF;
569 if (target_bank > 0x3F && target_bank < 0x80) {
570 return absl::OkStatus(); // Invalid bank range
571 }
572
573 // Expanded patch detected!
576
577 // Use known Oracle of Secrets bank locations
578 // These are the standard locations used by the Oracle of Secrets expanded music patch
579 expanded_bank_info_.main_rom_offset = ExpandedOverworldBankRom();
580 expanded_bank_info_.aux_rom_offset = ExpandedAuxBankRom();
582
583 return absl::OkStatus();
584}
585
587 Rom& rom, std::vector<MusicSong>* custom_songs) {
589 return absl::OkStatus(); // No expanded patch, nothing to load
590 }
591
592 // Load songs from the expanded overworld bank
593 // This bank contains the Dark World songs in Oracle of Secrets
594 const uint32_t expanded_rom_offset = expanded_bank_info_.main_rom_offset;
595
596 // Read the block header: size (2 bytes) + ARAM dest (2 bytes)
597 if (expanded_rom_offset + 4 >= rom.size()) {
598 return absl::OkStatus(); // Can't read header
599 }
600
601 auto header_result = rom.ReadByteVector(expanded_rom_offset, 4);
602 if (!header_result.ok()) {
603 return absl::OkStatus(); // Can't read header
604 }
605
606 const auto& header = header_result.value();
607 uint16_t block_size = static_cast<uint16_t>(header[0]) |
608 (static_cast<uint16_t>(header[1]) << 8);
609 uint16_t aram_dest = static_cast<uint16_t>(header[2]) |
610 (static_cast<uint16_t>(header[3]) << 8);
611
612 // Verify this looks like a valid song bank block (dest should be $D000)
613 if (aram_dest != kSongTableAram || block_size == 0 ||
614 block_size > kExpandedOverworldBankMaxSize) {
615 return absl::OkStatus(); // Invalid header, skip expanded loading
616 }
617
618 // Use SPC bank ID 4 for expanded (same format as overworld bank 1)
619 const uint8_t expanded_spc_bank = 4;
620
621 // Read song pointers from the expanded bank
622 // Each entry is 2 bytes, count entries until we hit song data or null
623 const int max_songs =
624 16; // Oracle of Secrets uses ~15 songs in expanded bank
625 auto pointer_result = SpcParser::ReadSongPointerTable(
626 rom, kSongTableAram, expanded_spc_bank, max_songs);
627
628 if (!pointer_result.ok()) {
629 // Failed to read pointers, but don't fail completely
630 return absl::OkStatus();
631 }
632
633 std::vector<uint16_t> song_addresses = std::move(pointer_result.value());
634
635 // Parse each song in the expanded bank
636 int expanded_index = 0;
637 for (const uint16_t spc_address : song_addresses) {
638 if (spc_address == 0)
639 continue; // Skip null entries
640
641 MusicSong song;
642 auto parsed_song =
643 SpcParser::ParseSong(rom, spc_address, expanded_spc_bank);
644 if (parsed_song.ok()) {
645 song = std::move(parsed_song.value());
646 } else {
647 // Create empty placeholder on parse failure
648 MusicSegment segment;
649 for (auto& track : segment.tracks) {
650 track.is_empty = true;
651 track.events.push_back(TrackEvent::MakeEnd(0));
652 }
653 song.segments.push_back(std::move(segment));
654 }
655
656 song.name = absl::StrFormat("Expanded Song %d", ++expanded_index);
657 song.bank = static_cast<uint8_t>(Bank::OverworldExpanded);
658 song.modified = false;
659
660 if (custom_songs) {
661 custom_songs->push_back(std::move(song));
662 } else {
663 songs_.push_back(std::move(song));
664 }
665 }
666
667 expanded_song_count_ = expanded_index;
668
669 // TODO: Load auxiliary bank songs from $2B00 if needed
670 // For now, we only load the main expanded bank
671
672 return absl::OkStatus();
673}
674
675bool MusicBank::IsExpandedSong(int index) const {
676 if (index < 0 || index >= static_cast<int>(songs_.size())) {
677 return false;
678 }
679 const auto& song = songs_[index];
680 return song.bank == static_cast<uint8_t>(Bank::OverworldExpanded) ||
681 song.bank == static_cast<uint8_t>(Bank::Auxiliary);
682}
683
684absl::Status MusicBank::LoadSongTable(Rom& rom, Bank bank,
685 std::vector<MusicSong>* custom_songs) {
686 const BankSongRange range = GetBankSongRange(bank);
687 const size_t vanilla_slots = static_cast<size_t>(range.Count());
688
689 // Read only the expected number of song pointers for this bank.
690 // Each bank has its own independent song table - don't read too many entries
691 // as that would interpret song data as pointers.
692 const uint8_t spc_bank = GetSpcBankId(bank);
693 auto pointer_result = SpcParser::ReadSongPointerTable(
694 rom, GetSongTableAddress(), spc_bank, static_cast<int>(vanilla_slots));
695 if (!pointer_result.ok()) {
696 return absl::Status(
697 pointer_result.status().code(),
698 absl::StrFormat("Failed to read song table for bank %d: %s",
699 static_cast<int>(bank),
700 pointer_result.status().message()));
701 }
702
703 std::vector<uint16_t> song_addresses = std::move(pointer_result.value());
704 if (song_addresses.empty()) {
705 return absl::InvalidArgumentError(absl::StrFormat(
706 "Song table for bank %d is empty", static_cast<int>(bank)));
707 }
708
709 auto make_empty_song = []() -> MusicSong {
710 MusicSong song;
711 MusicSegment segment;
712 for (auto& track : segment.tracks) {
713 track.is_empty = true;
714 track.events.clear();
715 track.events.push_back(TrackEvent::MakeEnd(0));
716 }
717 song.segments.push_back(std::move(segment));
718 return song;
719 };
720
721 auto emit_song = [&](MusicSong&& song, bool is_custom) {
722 song.modified = false;
723 if (is_custom && custom_songs) {
724 custom_songs->push_back(std::move(song));
725 } else {
726 songs_.push_back(std::move(song));
727 }
728 };
729
730 auto parse_and_emit = [&](uint16_t spc_address,
731 const std::string& display_name,
732 bool is_custom) -> absl::Status {
733 MusicSong song;
734 if (spc_address == 0) {
735 song = make_empty_song();
736 song.rom_address = 0;
737 } else {
738 auto parsed_song = SpcParser::ParseSong(rom, spc_address, spc_bank);
739 if (!parsed_song.ok()) {
740 return absl::Status(
741 parsed_song.status().code(),
742 absl::StrFormat(
743 "Failed to parse song '%s' at $%04X (SPC bank %d): %s",
744 display_name, spc_address, spc_bank,
745 parsed_song.status().message()));
746 }
747 song = std::move(parsed_song.value());
748 }
749
750 song.name = display_name;
751 song.bank = static_cast<uint8_t>(bank);
752 emit_song(std::move(song), is_custom);
753 return absl::OkStatus();
754 };
755
756 // Vanilla slots for this bank
757 // IMPORTANT: Each bank has its OWN independent song table at ARAM $D000.
758 // When the game loads a bank, it uploads that bank's song table to $D000.
759 // - Overworld bank's $D000: indices 0-10 = songs 1-11
760 // - Dungeon bank's $D000: indices 0-19 = songs 12-31
761 // - Credits bank's $D000: indices 0-2 = songs 32-34
762 // So we always read from index 0 in each bank's table.
763 for (size_t i = 0; i < vanilla_slots; ++i) {
764 const uint16_t spc_address =
765 (i < song_addresses.size()) ? song_addresses[i] : 0;
766 const int song_id = range.start_id + static_cast<int>(i);
767 const std::string display_name =
768 (song_id > 0 && song_id <= kVanillaSongCount)
769 ? GetVanillaSongName(song_id)
770 : absl::StrFormat("Vanilla Song %d", song_id);
771 auto status =
772 parse_and_emit(spc_address, display_name, /*is_custom=*/false);
773 if (!status.ok())
774 return status;
775 }
776
777 // Custom slots (beyond vanilla range for this bank)
778 // These would be at indices after the vanilla songs in this bank's table
779 int custom_counter = 1;
780 for (size_t table_index = vanilla_slots; table_index < song_addresses.size();
781 ++table_index) {
782 const uint16_t spc_address = song_addresses[table_index];
783 // Skip null entries (no custom song at this slot)
784 if (spc_address == 0)
785 continue;
786
787 const std::string display_name =
788 absl::StrFormat("Custom Song %d", custom_counter++);
789
790 auto status = parse_and_emit(spc_address, display_name, /*is_custom=*/true);
791 if (!status.ok())
792 return status;
793 }
794
795 const int total_songs = static_cast<int>(song_addresses.size());
796 switch (bank) {
797 case Bank::Overworld:
798 overworld_song_count_ = total_songs;
799 break;
800 case Bank::Dungeon:
801 dungeon_song_count_ = total_songs;
802 break;
803 case Bank::Credits:
804 credits_song_count_ = total_songs;
805 break;
806 }
807
808 return absl::OkStatus();
809}
810
811absl::Status MusicBank::SaveSongTable(Rom& rom, Bank bank) {
812 auto songs_in_bank = GetSongsInBank(bank);
813 if (songs_in_bank.empty()) {
814 return absl::OkStatus();
815 }
816
817 const uint16_t pointer_entry_count =
818 static_cast<uint16_t>(songs_in_bank.size());
819 const uint16_t pointer_table_size =
820 static_cast<uint16_t>((pointer_entry_count + 1) * 2);
821 const uint16_t bank_base = GetSongTableAddress();
822
823 uint32_t current_spc_address =
824 static_cast<uint32_t>(bank_base) + pointer_table_size;
825 const uint32_t bank_limit =
826 static_cast<uint32_t>(bank_base) + GetBankMaxSize(bank);
827
828 std::vector<uint8_t> payload;
829 payload.resize(pointer_table_size, 0);
830
831 auto write_pointer_entry = [&](size_t index, uint16_t address) {
832 payload[index * 2] = address & 0xFF;
833 payload[index * 2 + 1] = static_cast<uint8_t>((address >> 8) & 0xFF);
834 };
835
836 size_t pointer_index = 0;
837
838 for (auto* song : songs_in_bank) {
839 auto serialized_or = SpcSerializer::SerializeSong(*song, 0);
840 if (!serialized_or.ok()) {
841 return serialized_or.status();
842 }
843 auto serialized = std::move(serialized_or.value());
844
845 const uint32_t song_size = serialized.data.size();
846 if (current_spc_address + song_size > bank_limit) {
847 return absl::ResourceExhaustedError(absl::StrFormat(
848 "Bank %d overflow (%u bytes needed, limit %u)",
849 static_cast<int>(bank), current_spc_address + song_size - bank_base,
850 GetBankMaxSize(bank)));
851 }
852
853 const uint16_t song_base = static_cast<uint16_t>(current_spc_address);
854 SpcSerializer::ApplyBaseAddress(&serialized, song_base);
855
856 write_pointer_entry(pointer_index++, song_base);
857 payload.insert(payload.end(), serialized.data.begin(),
858 serialized.data.end());
859
860 song->rom_address = song_base;
861 song->modified = false;
862 current_spc_address += song_size;
863 }
864
865 write_pointer_entry(pointer_index, 0);
866
867 if (payload.size() > static_cast<size_t>(GetBankMaxSize(bank))) {
868 return absl::ResourceExhaustedError(absl::StrFormat(
869 "Bank %d payload size %zu exceeds limit %d", static_cast<int>(bank),
870 payload.size(), GetBankMaxSize(bank)));
871 }
872
873 const uint16_t block_size = static_cast<uint16_t>(payload.size());
874 std::vector<uint8_t> block_data;
875 block_data.reserve(static_cast<size_t>(block_size) + 8);
876 block_data.push_back(block_size & 0xFF);
877 block_data.push_back(static_cast<uint8_t>((block_size >> 8) & 0xFF));
878 block_data.push_back(bank_base & 0xFF);
879 block_data.push_back(static_cast<uint8_t>((bank_base >> 8) & 0xFF));
880 block_data.insert(block_data.end(), payload.begin(), payload.end());
881 block_data.push_back(0x00);
882 block_data.push_back(0x00);
883 block_data.push_back(0x00);
884 block_data.push_back(0x00);
885
886 const uint32_t rom_offset = GetBankRomAddress(bank);
887 if (rom_offset + block_data.size() > rom.size()) {
888 return absl::OutOfRangeError(absl::StrFormat(
889 "Bank %d ROM write exceeds image size (offset=%u, "
890 "size=%zu, rom_size=%zu)",
891 static_cast<int>(bank), rom_offset, block_data.size(), rom.size()));
892 }
893
894 auto status =
895 rom.WriteVector(static_cast<int>(rom_offset), std::move(block_data));
896 if (!status.ok())
897 return status;
898
899 status = UpdateDynamicBankPointer(rom, bank, rom_offset);
900 if (!status.ok())
901 return status;
902
903 return absl::OkStatus();
904}
905
906absl::Status MusicBank::LoadInstruments(Rom& rom) {
907 instruments_.clear();
908
909 const uint32_t rom_offset =
911 if (rom_offset == 0) {
912 return absl::InvalidArgumentError(
913 "Unable to resolve instrument table address in ROM");
914 }
915
916 const size_t table_size = kVanillaInstrumentCount * kInstrumentEntrySize;
917 if (rom_offset + table_size > rom.size()) {
918 return absl::OutOfRangeError("Instrument table exceeds ROM bounds");
919 }
920
922 auto bytes,
923 rom.ReadByteVector(rom_offset, static_cast<uint32_t>(table_size)));
924
925 instruments_.reserve(kVanillaInstrumentCount);
926 for (int i = 0; i < kVanillaInstrumentCount; ++i) {
927 const size_t base = static_cast<size_t>(i) * kInstrumentEntrySize;
928 MusicInstrument inst;
929 inst.sample_index = bytes[base];
930 inst.SetFromBytes(bytes[base + 1], bytes[base + 2]);
931 inst.gain = bytes[base + 3];
932 inst.pitch_mult = static_cast<uint16_t>(
933 (static_cast<uint16_t>(bytes[base + 4]) << 8) | bytes[base + 5]);
934 inst.name = kAltTpInstrumentNames[i];
935 instruments_.push_back(std::move(inst));
936 }
937
938 return absl::OkStatus();
939}
940
941absl::Status MusicBank::SaveInstruments(Rom& rom) {
942 // TODO: Implement instrument serialization
943 return absl::UnimplementedError("SaveInstruments not yet implemented");
944}
945
946absl::Status MusicBank::LoadSamples(Rom& rom) {
947 samples_.clear();
948
949 // Read sample directory (DIR) at $3C00 in ARAM (Bank 0)
950 // Each entry is 4 bytes: [StartAddr:2][LoopAddr:2]
951 const uint16_t dir_address = kSampleTableAram;
952 int dir_length = 0;
953 const uint8_t* dir_data =
954 SpcParser::GetSpcData(rom, dir_address, 0, &dir_length);
955
956 if (!dir_data) {
957 return absl::InternalError("Failed to locate sample directory in ROM");
958 }
959
960 // Scan directory to find max valid sample index
961 // Max size is 256 bytes (64 samples), but often smaller
962 const int max_samples = std::min(64, dir_length / 4);
963
964 for (int i = 0; i < max_samples; ++i) {
965 uint16_t start_addr = dir_data[i * 4] | (dir_data[i * 4 + 1] << 8);
966 uint16_t loop_addr = dir_data[i * 4 + 2] | (dir_data[i * 4 + 3] << 8);
967
968 MusicSample sample;
969 sample.name = absl::StrFormat("Sample %02X", i);
970 // Store loop point as relative offset from start
971 sample.loop_point =
972 (loop_addr >= start_addr) ? (loop_addr - start_addr) : 0;
973
974 // Resolve start address to ROM offset
975 uint32_t rom_offset = SpcParser::SpcAddressToRomOffset(rom, start_addr, 0);
976
977 if (rom_offset == 0 || rom_offset >= rom.size()) {
978 // Invalid or empty sample slot
979 samples_.push_back(std::move(sample));
980 continue;
981 }
982
983 // Read BRR blocks until END bit is set
984 const uint8_t* rom_ptr = rom.data() + rom_offset;
985 size_t remaining = rom.size() - rom_offset;
986
987 while (remaining >= 9) {
988 // Append block to BRR data
989 sample.brr_data.insert(sample.brr_data.end(), rom_ptr, rom_ptr + 9);
990
991 // Check END bit in header (bit 0)
992 if (rom_ptr[0] & 0x01) {
993 sample.loops = (rom_ptr[0] & 0x02) != 0;
994 break;
995 }
996
997 rom_ptr += 9;
998 remaining -= 9;
999 }
1000
1001 // Decode to PCM for visualization/editing
1002 if (!sample.brr_data.empty()) {
1003 sample.pcm_data = BrrCodec::Decode(sample.brr_data);
1004 }
1005
1006 samples_.push_back(std::move(sample));
1007 }
1008
1009 return absl::OkStatus();
1010}
1011
1012absl::Status MusicBank::SaveSamples(Rom& rom) {
1013 // TODO: Implement BRR encoding and sample saving
1014 return absl::UnimplementedError("SaveSamples not yet implemented");
1015}
1016
1018 // Rough estimate: header + segment pointers + track data
1019 int size = 2; // Song header
1020
1021 for (const auto& segment : song.segments) {
1022 size += 16; // 8 track pointers (2 bytes each)
1023
1024 for (const auto& track : segment.tracks) {
1025 if (track.is_empty) {
1026 size += 1; // Just end marker
1027 } else {
1028 // Estimate: each event ~2-4 bytes on average
1029 size += static_cast<int>(track.events.size()) * 3;
1030 size += 1; // End marker
1031 }
1032 }
1033 }
1034
1035 if (song.HasLoop()) {
1036 size += 4; // Loop marker and pointer
1037 }
1038
1039 return size;
1040}
1041
1042nlohmann::json MusicBank::ToJson() const {
1043 nlohmann::json root;
1044 nlohmann::json songs = nlohmann::json::array();
1045 for (const auto& song : songs_) {
1046 nlohmann::json js;
1047 js["name"] = song.name;
1048 js["bank"] = song.bank;
1049 js["loop_point"] = song.loop_point;
1050 js["rom_address"] = song.rom_address;
1051 js["modified"] = song.modified;
1052
1053 nlohmann::json segments = nlohmann::json::array();
1054 for (const auto& segment : song.segments) {
1055 nlohmann::json jseg;
1056 jseg["rom_address"] = segment.rom_address;
1057 nlohmann::json tracks = nlohmann::json::array();
1058 for (const auto& track : segment.tracks) {
1059 nlohmann::json jt;
1060 jt["rom_address"] = track.rom_address;
1061 jt["duration_ticks"] = track.duration_ticks;
1062 jt["is_empty"] = track.is_empty;
1063 nlohmann::json events = nlohmann::json::array();
1064 for (const auto& evt : track.events) {
1065 nlohmann::json jevt;
1066 jevt["tick"] = evt.tick;
1067 jevt["rom_offset"] = evt.rom_offset;
1068 switch (evt.type) {
1070 jevt["type"] = "note";
1071 jevt["note"]["pitch"] = evt.note.pitch;
1072 jevt["note"]["duration"] = evt.note.duration;
1073 jevt["note"]["velocity"] = evt.note.velocity;
1074 jevt["note"]["has_duration_prefix"] =
1075 evt.note.has_duration_prefix;
1076 break;
1078 jevt["type"] = "command";
1079 jevt["command"]["opcode"] = evt.command.opcode;
1080 jevt["command"]["params"] = evt.command.params;
1081 break;
1083 jevt["type"] = "subroutine";
1084 jevt["command"]["opcode"] = evt.command.opcode;
1085 jevt["command"]["params"] = evt.command.params;
1086 break;
1088 default:
1089 jevt["type"] = "end";
1090 break;
1091 }
1092 events.push_back(std::move(jevt));
1093 }
1094 jt["events"] = std::move(events);
1095 tracks.push_back(std::move(jt));
1096 }
1097 jseg["tracks"] = std::move(tracks);
1098 segments.push_back(std::move(jseg));
1099 }
1100 js["segments"] = std::move(segments);
1101 songs.push_back(std::move(js));
1102 }
1103
1104 nlohmann::json instruments = nlohmann::json::array();
1105 for (const auto& inst : instruments_) {
1106 nlohmann::json ji;
1107 ji["name"] = inst.name;
1108 ji["sample_index"] = inst.sample_index;
1109 ji["attack"] = inst.attack;
1110 ji["decay"] = inst.decay;
1111 ji["sustain_level"] = inst.sustain_level;
1112 ji["sustain_rate"] = inst.sustain_rate;
1113 ji["gain"] = inst.gain;
1114 ji["pitch_mult"] = inst.pitch_mult;
1115 instruments.push_back(std::move(ji));
1116 }
1117
1118 nlohmann::json samples = nlohmann::json::array();
1119 for (const auto& sample : samples_) {
1120 nlohmann::json jsample;
1121 jsample["name"] = sample.name;
1122 jsample["loop_point"] = sample.loop_point;
1123 jsample["loops"] = sample.loops;
1124 jsample["pcm_data"] = sample.pcm_data;
1125 jsample["brr_data"] = sample.brr_data;
1126 samples.push_back(std::move(jsample));
1127 }
1128
1129 root["songs"] = std::move(songs);
1130 root["instruments"] = std::move(instruments);
1131 root["samples"] = std::move(samples);
1132 root["overworld_song_count"] = overworld_song_count_;
1133 root["dungeon_song_count"] = dungeon_song_count_;
1134 root["credits_song_count"] = credits_song_count_;
1135 return root;
1136}
1137
1138absl::Status MusicBank::LoadFromJson(const nlohmann::json& j) {
1139 try {
1140 songs_.clear();
1141 instruments_.clear();
1142 samples_.clear();
1143
1144 if (j.contains("songs") && j["songs"].is_array()) {
1145 for (const auto& js : j["songs"]) {
1146 MusicSong song;
1147 song.name = js.value("name", "");
1148 song.bank = js.value("bank", 0);
1149 song.loop_point = js.value("loop_point", -1);
1150 song.rom_address = js.value("rom_address", 0);
1151 song.modified = js.value("modified", false);
1152
1153 if (js.contains("segments") && js["segments"].is_array()) {
1154 for (const auto& jseg : js["segments"]) {
1155 MusicSegment seg;
1156 seg.rom_address = jseg.value("rom_address", 0);
1157 if (jseg.contains("tracks") && jseg["tracks"].is_array()) {
1158 int track_idx = 0;
1159 for (const auto& jt : jseg["tracks"]) {
1160 if (track_idx >= 8)
1161 break;
1162 auto& track = seg.tracks[track_idx++];
1163 track.rom_address = jt.value("rom_address", 0);
1164 track.duration_ticks = jt.value("duration_ticks", 0);
1165 track.is_empty = jt.value("is_empty", false);
1166 if (jt.contains("events") && jt["events"].is_array()) {
1167 track.events.clear();
1168 for (const auto& jevt : jt["events"]) {
1169 TrackEvent evt;
1170 evt.tick = jevt.value("tick", 0);
1171 evt.rom_offset = jevt.value("rom_offset", 0);
1172 std::string type = jevt.value("type", "end");
1173 if (type == "note" && jevt.contains("note")) {
1175 evt.note.pitch = jevt["note"].value("pitch", kNoteRest);
1176 evt.note.duration =
1177 jevt["note"].value("duration", uint8_t{0});
1178 evt.note.velocity =
1179 jevt["note"].value("velocity", uint8_t{0});
1181 jevt["note"].value("has_duration_prefix", false);
1182 } else if (type == "command" || type == "subroutine") {
1183 evt.type = (type == "subroutine")
1186 evt.command.opcode =
1187 jevt["command"].value("opcode", uint8_t{0});
1188 auto params = jevt["command"].value(
1189 "params", std::array<uint8_t, 3>{0, 0, 0});
1190 evt.command.params = params;
1191 } else {
1193 }
1194 track.events.push_back(std::move(evt));
1195 }
1196 }
1197 }
1198 }
1199 song.segments.push_back(std::move(seg));
1200 }
1201 }
1202 songs_.push_back(std::move(song));
1203 }
1204 }
1205
1206 if (j.contains("instruments") && j["instruments"].is_array()) {
1207 for (const auto& ji : j["instruments"]) {
1208 MusicInstrument inst;
1209 inst.name = ji.value("name", "");
1210 inst.sample_index = ji.value("sample_index", 0);
1211 inst.attack = ji.value("attack", 0);
1212 inst.decay = ji.value("decay", 0);
1213 inst.sustain_level = ji.value("sustain_level", 0);
1214 inst.sustain_rate = ji.value("sustain_rate", 0);
1215 inst.gain = ji.value("gain", 0);
1216 inst.pitch_mult = ji.value("pitch_mult", 0);
1217 instruments_.push_back(std::move(inst));
1218 }
1219 }
1220
1221 if (j.contains("samples") && j["samples"].is_array()) {
1222 for (const auto& jsample : j["samples"]) {
1223 MusicSample sample;
1224 sample.name = jsample.value("name", "");
1225 sample.loop_point = jsample.value("loop_point", 0);
1226 sample.loops = jsample.value("loops", false);
1227 sample.pcm_data = jsample.value("pcm_data", std::vector<int16_t>{});
1228 sample.brr_data = jsample.value("brr_data", std::vector<uint8_t>{});
1229 samples_.push_back(std::move(sample));
1230 }
1231 }
1232
1233 overworld_song_count_ = j.value("overworld_song_count", 0);
1234 dungeon_song_count_ = j.value("dungeon_song_count", 0);
1235 credits_song_count_ = j.value("credits_song_count", 0);
1236
1237 loaded_ = true;
1238 return absl::OkStatus();
1239 } catch (const std::exception& e) {
1240 return absl::InvalidArgumentError(
1241 absl::StrFormat("Failed to parse music state: %s", e.what()));
1242 }
1243}
1244
1245// =============================================================================
1246// Helper Functions
1247// =============================================================================
1248
1249const char* GetVanillaSongName(int song_id) {
1250 if (song_id <= 0 || song_id > kVanillaSongCount) {
1251 return "Unknown";
1252 }
1253 return kVanillaSongs[song_id].name;
1254}
1255
1257 if (song_id <= 0 || song_id > kVanillaSongCount) {
1259 }
1260 return kVanillaSongs[song_id].bank;
1261}
1262
1264 if (const auto* metadata = GetMetadataForBank(bank)) {
1265 return BankSongRange{metadata->vanilla_start_id, metadata->vanilla_end_id};
1266 }
1267 return {};
1268}
1269
1271 if (const auto* metadata = GetMetadataForBank(bank)) {
1272 return metadata->spc_bank;
1273 }
1274 return 0;
1275}
1276
1277} // namespace music
1278} // namespace zelda3
1279} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:28
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:431
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:476
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:408
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:548
auto data() const
Definition rom.h:139
auto size() const
Definition rom.h:138
bool is_loaded() const
Definition rom.h:132
static RomSettings & Get()
uint32_t GetAddressOr(const std::string &key, uint32_t default_value) const
static std::vector< int16_t > Decode(const std::vector< uint8_t > &brr_data, int *loop_start=nullptr)
Decode BRR data to PCM samples.
static uint8_t GetSpcBankId(Bank bank)
static uint32_t GetBankRomAddress(Bank bank)
Get the ROM address for a bank.
bool HasModifications() const
Check if any music data has been modified.
MusicInstrument * GetInstrument(int index)
Get an instrument by index.
int CreateNewInstrument(const std::string &name)
Create a new instrument.
static constexpr uint16_t GetSongTableAddress()
Definition music_bank.h:312
nlohmann::json ToJson() const
bool IsVanilla(int index) const
Check if a song is a vanilla (original) song.
absl::Status LoadFromJson(const nlohmann::json &j)
absl::Status LoadSamples(Rom &rom)
MusicSong * GetSongById(int song_id)
Get a song by vanilla ID (1-based).
MusicSong * GetSong(int index)
Get a song by index.
absl::Status DetectExpandedMusicPatch(Rom &rom)
absl::Status LoadInstruments(Rom &rom)
static int GetBankMaxSize(Bank bank)
Get the maximum size for a bank.
ExpandedBankInfo expanded_bank_info_
Definition music_bank.h:298
std::vector< MusicSample > samples_
Definition music_bank.h:283
absl::Status LoadSongTable(Rom &rom, Bank bank, std::vector< MusicSong > *custom_songs)
bool AllSongsFit() const
Check if all songs fit in their banks.
static BankSongRange GetBankSongRange(Bank bank)
absl::Status SaveInstruments(Rom &rom)
MusicSample * GetSample(int index)
Get a sample by index.
absl::Status DeleteSong(int index)
Delete a song by index.
std::vector< MusicSong > songs_
Definition music_bank.h:281
SpaceInfo CalculateSpaceUsage(Bank bank) const
Calculate space usage for a bank.
absl::StatusOr< int > ImportSampleFromWav(const std::string &filepath, const std::string &name)
Import a WAV file as a new sample.
absl::Status SaveSamples(Rom &rom)
absl::Status SaveToRom(Rom &rom)
Save all modified music data back to ROM.
absl::Status SaveSongTable(Rom &rom, Bank bank)
int CalculateSongSize(const MusicSong &song) const
bool IsExpandedSong(int index) const
Check if a song is from an expanded bank.
void ClearModifications()
Mark all data as unmodified (after save).
absl::Status LoadExpandedSongTable(Rom &rom, std::vector< MusicSong > *custom_songs)
std::vector< MusicInstrument > instruments_
Definition music_bank.h:282
int CreateNewSong(const std::string &name, Bank bank)
Create a new empty song.
absl::Status LoadFromRom(Rom &rom)
Load all music data from a ROM.
std::vector< MusicSong * > GetSongsInBank(Bank bank)
Get all songs in a specific bank.
int DuplicateSong(int index)
Duplicate a song.
static absl::StatusOr< MusicSong > ParseSong(Rom &rom, uint16_t address, uint8_t bank)
Parse a complete song from ROM.
Definition spc_parser.cc:16
static absl::StatusOr< std::vector< uint16_t > > ReadSongPointerTable(Rom &rom, uint16_t table_address, uint8_t bank, int max_entries=40)
Read the song pointer table for a given SPC bank.
static const uint8_t * GetSpcData(Rom &rom, uint16_t spc_address, uint8_t bank, int *out_length=nullptr)
Get a pointer to ROM data at an SPC address.
static uint32_t SpcAddressToRomOffset(Rom &rom, uint16_t spc_address, uint8_t bank)
Convert an SPC address to a ROM offset.
static absl::StatusOr< SerializeResult > SerializeSong(const MusicSong &song, uint16_t base_address)
static void ApplyBaseAddress(SerializeResult *result, uint16_t new_base_address)
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
constexpr char kExpandedMusicHook[]
constexpr char kExpandedMusicMain[]
constexpr char kExpandedMusicAux[]
constexpr const char * kAltTpInstrumentNames[kVanillaInstrumentCount]
absl::Status UpdateBankPointerRegisters(Rom &rom, const BankPointerRegisters &regs, uint32_t pc_offset)
absl::Status UpdateDynamicBankPointer(Rom &rom, MusicBank::Bank bank, uint32_t pc_offset)
const BankMetadata * GetMetadataForBank(MusicBank::Bank bank)
constexpr uint8_t kNoteRest
Definition song_data.h:58
constexpr uint32_t kExpandedAuxBankRom
Definition song_data.h:139
MusicBank::Bank GetVanillaSongBank(int song_id)
Get the bank for a vanilla song ID.
constexpr uint16_t kAuxSongTableAram
Definition song_data.h:140
constexpr uint16_t kSampleTableAram
Definition song_data.h:84
constexpr uint32_t kExpandedOverworldBankRom
Definition song_data.h:138
constexpr uint8_t kJslOpcode
Definition song_data.h:146
constexpr uint16_t kInstrumentTableAram
Definition song_data.h:83
constexpr uint32_t kOverworldBankRom
Definition song_data.h:77
constexpr uint16_t kSongTableAram
Definition song_data.h:82
const char * GetVanillaSongName(int song_id)
Get the vanilla name for a song ID.
constexpr int kCreditsBankMaxSize
Definition song_data.h:90
constexpr int kExpandedOverworldBankMaxSize
Definition song_data.h:141
constexpr uint32_t kCreditsBankRom
Definition song_data.h:79
constexpr int kDungeonBankMaxSize
Definition song_data.h:89
constexpr uint32_t kDungeonBankRom
Definition song_data.h:78
constexpr int kOverworldBankMaxSize
Definition song_data.h:88
constexpr int kAuxBankMaxSize
Definition song_data.h:142
constexpr uint32_t kExpandedMusicHookAddress
Definition song_data.h:145
std::array< uint8_t, 3 > params
Definition song_data.h:224
An instrument definition with ADSR envelope.
Definition song_data.h:358
void SetFromBytes(uint8_t ad, uint8_t sr)
Definition song_data.h:377
A BRR-encoded audio sample.
Definition song_data.h:388
std::vector< uint8_t > brr_data
Definition song_data.h:390
std::vector< int16_t > pcm_data
Definition song_data.h:389
A segment containing 8 parallel tracks.
Definition song_data.h:315
std::array< MusicTrack, 8 > tracks
Definition song_data.h:316
A complete song composed of segments.
Definition song_data.h:334
std::vector< MusicSegment > segments
Definition song_data.h:336
A single event in a music track (note, command, or control).
Definition song_data.h:247
static TrackEvent MakeEnd(uint16_t tick)
Definition song_data.h:280