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