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