yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
spc_parser.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstring>
5
6#include "absl/strings/str_format.h"
7
8namespace yaze {
9namespace zelda3 {
10namespace music {
11
12// =============================================================================
13// SpcParser Implementation
14// =============================================================================
15
16absl::StatusOr<MusicSong> SpcParser::ParseSong(Rom& rom, uint16_t address,
17 uint8_t bank) {
18 if (!rom.is_loaded()) {
19 return absl::FailedPreconditionError("ROM is not loaded");
20 }
21
22 if (address == 0) {
23 return absl::InvalidArgumentError("Invalid song address");
24 }
25
26 // Get pointer to song data
27 int data_length = 0;
28 const uint8_t* song_data = GetSpcData(rom, address, bank, &data_length);
29 if (!song_data) {
30 return absl::NotFoundError(
31 absl::StrFormat("Song data not found at address $%04X bank %d",
32 address, bank));
33 }
34
35 MusicSong song;
36 song.rom_address = address;
37 song.bank = bank;
38
39 // Parse segment table
40 // Song format: list of 16-bit segment pointers, terminated by value < 256
41 size_t offset = 0;
42 std::vector<uint16_t> segment_addresses;
43
44 while (offset + 1 < static_cast<size_t>(data_length)) {
45 uint16_t segment_ptr = song_data[offset] | (song_data[offset + 1] << 8);
46
47 // Check for end marker (value < 256 indicates end or loop)
48 if (segment_ptr < 256) {
49 if (segment_ptr > 0) {
50 // This is a loop indicator
51 // Next word is the loop target segment pointer
52 if (offset + 3 < static_cast<size_t>(data_length)) {
53 uint16_t loop_target = song_data[offset + 2] | (song_data[offset + 3] << 8);
54 // Find which segment this loops to
55 for (size_t i = 0; i < segment_addresses.size(); ++i) {
56 if (segment_addresses[i] == loop_target) {
57 song.loop_point = static_cast<int>(i);
58 break;
59 }
60 }
61 }
62 }
63 break;
64 }
65
66 segment_addresses.push_back(segment_ptr);
67 offset += 2;
68 }
69
70 // Parse each segment
71 for (uint16_t seg_addr : segment_addresses) {
72 MusicSegment segment;
73 segment.rom_address = seg_addr;
74
75 const uint8_t* seg_data = GetSpcData(rom, seg_addr, bank, nullptr);
76 if (!seg_data) {
77 // Skip invalid segments
78 song.segments.push_back(std::move(segment));
79 continue;
80 }
81
82 // Each segment has 8 track pointers (16 bytes)
83 for (int ch = 0; ch < 8; ++ch) {
84 uint16_t track_addr = seg_data[ch * 2] | (seg_data[ch * 2 + 1] << 8);
85
86 if (track_addr == 0) {
87 segment.tracks[ch].is_empty = true;
88 continue;
89 }
90
91 auto track_result = ParseTrack(rom, track_addr, bank);
92 if (track_result.ok()) {
93 segment.tracks[ch] = std::move(*track_result);
94 } else {
95 // Mark as empty on error
96 segment.tracks[ch].is_empty = true;
97 }
98 }
99
100 song.segments.push_back(std::move(segment));
101 }
102
103 return song;
104}
105
106absl::StatusOr<std::vector<uint16_t>> SpcParser::ReadSongPointerTable(
107 Rom& rom, uint16_t table_address, uint8_t bank, int max_entries) {
108 if (!rom.is_loaded()) {
109 return absl::FailedPreconditionError("ROM is not loaded");
110 }
111
112 int data_length = 0;
113 const uint8_t* table_data =
114 GetSpcData(rom, table_address, bank, &data_length);
115 if (!table_data) {
116 return absl::NotFoundError(absl::StrFormat(
117 "Song pointer table not found at $%04X for bank %d", table_address,
118 bank));
119 }
120
121 if (data_length < 2) {
122 return absl::InvalidArgumentError(
123 "Song pointer table is too small to contain entries");
124 }
125
126 std::vector<uint16_t> pointers;
127 pointers.reserve(static_cast<size_t>(max_entries));
128
129 // Each entry is a little-endian SPC address.
130 // Note: Null entries ($0000) are valid - they indicate "no song at this slot"
131 // for that bank. Different banks share the same song table address space but
132 // have different songs at each slot.
133 int entries_read = 0;
134 for (int offset = 0; offset + 1 < data_length && entries_read < max_entries;
135 offset += 2) {
136 uint16_t entry = table_data[offset] | (table_data[offset + 1] << 8);
137 pointers.push_back(entry);
138 ++entries_read;
139 }
140
141 return pointers;
142}
143
144absl::StatusOr<MusicTrack> SpcParser::ParseTrack(Rom& rom, uint16_t address,
145 uint8_t bank, int max_ticks) {
146 ParseContext ctx;
147 ctx.rom = &rom;
148 ctx.current_bank = bank;
149 ctx.bank_offset = kSoundBankOffsets[bank % 4];
150 ctx.max_parse_depth = 100;
151 ctx.current_depth = 0;
152
153 return ParseTrackInternal(ctx, address, max_ticks);
154}
155
156absl::StatusOr<MusicTrack> SpcParser::ParseTrackInternal(ParseContext& ctx,
157 uint16_t address,
158 int max_ticks) {
159 if (!ctx.rom || !ctx.rom->is_loaded()) {
160 return absl::FailedPreconditionError("ROM is not loaded");
161 }
162
163 if (address == 0) {
164 MusicTrack empty_track;
165 empty_track.is_empty = true;
166 return empty_track;
167 }
168
169 // Check for infinite loop
170 if (std::find(ctx.visited_addresses.begin(), ctx.visited_addresses.end(),
171 address) != ctx.visited_addresses.end()) {
172 return absl::InvalidArgumentError(
173 absl::StrFormat("Circular reference detected at $%04X", address));
174 }
175 ctx.visited_addresses.push_back(address);
176
177 // Check depth limit
178 if (ctx.current_depth >= ctx.max_parse_depth) {
179 return absl::ResourceExhaustedError("Maximum parse depth exceeded");
180 }
181 ++ctx.current_depth;
182
183 int data_length = 0;
184 const uint8_t* track_data = GetSpcData(*ctx.rom, address, ctx.current_bank,
185 &data_length);
186 if (!track_data) {
187 --ctx.current_depth;
188 return absl::NotFoundError(
189 absl::StrFormat("Track data not found at $%04X", address));
190 }
191
192 MusicTrack track;
193 track.rom_address = address;
194 track.is_empty = false;
195
196 uint16_t current_tick = 0;
197 uint8_t current_duration = 0;
198 uint8_t current_velocity = 0;
199 size_t pos = 0;
200
201 while (pos < static_cast<size_t>(data_length) && current_tick < max_ticks) {
202 uint8_t byte = track_data[pos];
203
204 // End of track
205 if (byte == kTrackEnd) {
206 track.events.push_back(TrackEvent::MakeEnd(current_tick));
207 break;
208 }
209
210 // Duration byte (< 0x80)
211 if (IsDuration(byte)) {
212 current_duration = byte;
213 ++pos;
214
215 // Check for velocity byte
216 if (pos < static_cast<size_t>(data_length)) {
217 uint8_t next = track_data[pos];
218 if (IsDuration(next)) {
219 current_velocity = next;
220 ++pos;
221 }
222 }
223 continue;
224 }
225
226 // Note/Rest/Tie (0x80-0xC9)
227 if (IsNotePitch(byte)) {
228 TrackEvent event;
230 event.tick = current_tick;
231 event.rom_offset = address + static_cast<uint16_t>(pos);
232 event.note.pitch = byte;
233 event.note.duration = current_duration;
234 event.note.velocity = current_velocity;
235 event.note.has_duration_prefix = true;
236
237 track.events.push_back(event);
238
239 // Only advance time for notes and rests, not ties
240 if (byte != kNoteTie) {
241 current_tick += current_duration;
242 }
243
244 ++pos;
245 continue;
246 }
247
248 // Command (0xE0-0xFF)
249 if (IsCommand(byte)) {
250 uint8_t opcode = byte;
251 int param_count = GetCommandParamCount(opcode);
252
253 TrackEvent event;
255 event.tick = current_tick;
256 event.rom_offset = address + static_cast<uint16_t>(pos);
257 event.command.opcode = opcode;
258
259 ++pos;
260
261 // Read parameters
262 for (int i = 0; i < param_count && pos < static_cast<size_t>(data_length); ++i) {
263 event.command.params[i] = track_data[pos];
264 ++pos;
265 }
266
267 // Handle subroutine calls specially
268 if (opcode == 0xEF) {
269 uint16_t sub_addr = event.command.GetSubroutineAddress();
270 uint8_t repeat = event.command.GetSubroutineRepeatCount();
271
272 // Parse subroutine to calculate duration
273 auto sub_result = ParseSubroutine(ctx, sub_addr, repeat,
274 max_ticks - current_tick);
275 if (sub_result.ok()) {
276 current_tick += *sub_result;
277 }
278 }
279
280 track.events.push_back(event);
281 continue;
282 }
283
284 // Unknown byte, skip
285 ++pos;
286 }
287
288 track.CalculateDuration();
289 --ctx.current_depth;
290 return track;
291}
292
294 uint16_t address,
295 int repeat_count,
296 int remaining_ticks) {
297 if (repeat_count == 0) return 0;
298
299 // Parse the subroutine track once
300 auto track_result = ParseTrackInternal(ctx, address,
301 remaining_ticks / repeat_count);
302 if (!track_result.ok()) {
303 return track_result.status();
304 }
305
306 // Total duration is track duration * repeat count
307 return static_cast<int>(track_result->duration_ticks) * repeat_count;
308}
309
310uint32_t SpcParser::SpcAddressToRomOffset(Rom& rom, uint16_t spc_address,
311 uint8_t bank) {
312 // The game's sound data is stored in blocks with headers:
313 // [size:2][spc_addr:2][data:size]
314 // We search through the blocks to find where the SPC address maps to ROM.
315 //
316 // ROM Layout (from usdasm disassembly):
317 // - Banks 0, 1, 3 (Common, Overworld, Credits) are all contiguous starting
318 // at $19:8000 (PC 0xC8000). The game uploads different portions to ARAM.
319 // - Bank 2 (Dungeon) is separate at $1B:8000 (PC 0xD8000).
320 //
321 // Song table blocks that load to ARAM $D000:
322 // - Overworld: PC 0xD1EF5 (within the 0xC8000 region)
323 // - Credits: PC 0xD5380 (within the 0xC8000 region)
324 // - Dungeon: PC 0xD8000 (start of dungeon region)
325
326 // Detect SMC header (ROM size % 0x8000 == 0x200 means header present)
327 size_t rom_size = rom.size();
328 uint32_t header_offset = (rom_size % 0x8000 == 0x200) ? 0x200 : 0;
329
330 uint32_t bank_offset = 0;
331
332 // Get bank base offset for block search
333 // Note: Banks 1 and 3 both have blocks that load to $D000, so we need to
334 // start searching from the correct block for each bank.
335 switch (bank) {
336 case 0:
337 // Bank 0 (common/SPC program): Search from start of common region
338 bank_offset = 0xC8000 + header_offset;
339 break;
340 case 1:
341 // Bank 1 (Overworld): Song table block starts at 0xD1EF5
342 // (from usdasm: $1A:9EF5 -> PC 0xD1EF5)
343 bank_offset = 0xD1EF5 + header_offset;
344 break;
345 case 2:
346 // Bank 2 (Dungeon): Separate region at 0xD8000
347 // (from usdasm: $1B:8000 -> PC 0xD8000)
348 bank_offset = 0xD8000 + header_offset;
349 break;
350 case 3:
351 // Bank 3 (Credits): Song table block starts at 0xD5380
352 // (from usdasm: $1A:D380 -> PC 0xD5380)
353 bank_offset = 0xD5380 + header_offset;
354 break;
355 default:
356 bank_offset = 0xC8000 + header_offset;
357 break;
358 }
359
360 // Validate bank offset
361 if (bank_offset >= rom_size) {
362 return 0;
363 }
364
365 // Search through the bank's block table
366 uint32_t rom_ptr = bank_offset;
367 const uint8_t* rom_data = rom.data();
368
369 for (int iterations = 0; iterations < 1000; ++iterations) { // Safety limit
370 if (rom_ptr + 4 >= rom_size) break;
371
372 uint16_t block_size = rom_data[rom_ptr] | (rom_data[rom_ptr + 1] << 8);
373 uint16_t block_addr = rom_data[rom_ptr + 2] | (rom_data[rom_ptr + 3] << 8);
374
375 // End of table (size 0 or invalid)
376 if (block_size == 0 || block_size > 0x10000) break;
377
378 rom_ptr += 4;
379
380 // Check if address falls within this block
381 if (spc_address >= block_addr && spc_address < block_addr + block_size) {
382 return rom_ptr + (spc_address - block_addr);
383 }
384
385 rom_ptr += block_size;
386 }
387
388 // Not found in specified bank, try bank 0 (common data)
389 if (bank != 0) {
390 return SpcAddressToRomOffset(rom, spc_address, 0);
391 }
392
393 return 0; // Not found
394}
395
396const uint8_t* SpcParser::GetSpcData(Rom& rom, uint16_t spc_address,
397 uint8_t bank, int* out_length) {
398 uint32_t rom_offset = SpcAddressToRomOffset(rom, spc_address, bank);
399 if (rom_offset == 0 || rom_offset >= rom.size()) {
400 if (out_length) *out_length = 0;
401 return nullptr;
402 }
403
404 // Calculate remaining length (rough estimate)
405 if (out_length) {
406 // Return a safe default length
407 *out_length = std::min(static_cast<size_t>(0x1000),
408 rom.size() - rom_offset);
409 }
410
411 return rom.data() + rom_offset;
412}
413
414
415
416// =============================================================================
417// BrrCodec Implementation
418// =============================================================================
419
420std::vector<int16_t> BrrCodec::Decode(const std::vector<uint8_t>& brr_data,
421 int* loop_start) {
422 std::vector<int16_t> pcm;
423
424 if (brr_data.size() < 9) {
425 return pcm;
426 }
427
428 int prev1 = 0, prev2 = 0;
429
430 for (size_t block = 0; block < brr_data.size(); block += 9) {
431 if (block + 9 > brr_data.size()) break;
432
433 uint8_t header = brr_data[block];
434 int range = (header >> 4) & 0x0F;
435 int filter = (header >> 2) & 0x03;
436 bool end = (header & 0x01) != 0;
437 bool loop = (header & 0x02) != 0;
438
439 for (int i = 0; i < 8; ++i) {
440 uint8_t byte = brr_data[block + 1 + i];
441
442 for (int nibble = 0; nibble < 2; ++nibble) {
443 int sample = (nibble == 0) ? (byte >> 4) : (byte & 0x0F);
444
445 // Sign extend
446 if (sample >= 8) sample -= 16;
447
448 // Apply range
449 sample <<= range;
450
451 // Apply filter
452 switch (filter) {
453 case 1:
454 sample += (prev1 * kFilter1[1]) >> kFilter2[1];
455 break;
456 case 2:
457 sample += (prev1 * kFilter1[2]) >> kFilter2[2];
458 sample -= (prev2 * kFilter3[2]) >> 4;
459 break;
460 case 3:
461 sample += (prev1 * kFilter1[3]) >> kFilter2[3];
462 sample -= (prev2 * kFilter3[3]) >> 4;
463 break;
464 }
465
466 // Clamp
467 if (sample > 0x7FFF) sample = 0x7FFF;
468 if (sample < -0x8000) sample = -0x8000;
469
470 pcm.push_back(static_cast<int16_t>(sample));
471
472 prev2 = prev1;
473 prev1 = sample;
474 }
475 }
476
477 if (end) {
478 if (loop && loop_start) {
479 // Loop point would need to be calculated from the source BRR
480 *loop_start = 0; // Placeholder
481 }
482 break;
483 }
484 }
485
486 return pcm;
487}
488
489std::vector<uint8_t> BrrCodec::Encode(const std::vector<int16_t>& pcm_data,
490 int loop_start) {
491 std::vector<uint8_t> brr;
492
493 // Pad to multiple of 16 samples
494 std::vector<int16_t> padded = pcm_data;
495 while (padded.size() % 16 != 0) {
496 padded.push_back(0);
497 }
498
499 int prev1 = 0, prev2 = 0;
500
501 for (size_t i = 0; i < padded.size(); i += 16) {
502 // Find best range and filter for this block
503 int best_range = 0;
504 int best_filter = 0;
505 int best_error = INT_MAX;
506
507 for (int range = 0; range < 13; ++range) {
508 for (int filter = 0; filter < 4; ++filter) {
509 int error = 0;
510 int p1 = prev1, p2 = prev2;
511
512 for (int j = 0; j < 16; ++j) {
513 int sample = padded[i + j];
514
515 // Apply inverse filter
516 int predicted = 0;
517 switch (filter) {
518 case 1:
519 predicted = (p1 * kFilter1[1]) >> kFilter2[1];
520 break;
521 case 2:
522 predicted = (p1 * kFilter1[2]) >> kFilter2[2];
523 predicted -= (p2 * kFilter3[2]) >> 4;
524 break;
525 case 3:
526 predicted = (p1 * kFilter1[3]) >> kFilter2[3];
527 predicted -= (p2 * kFilter3[3]) >> 4;
528 break;
529 }
530
531 int diff = sample - predicted;
532 int encoded = (diff >> range);
533
534 // Clamp to 4 bits
535 if (encoded > 7) encoded = 7;
536 if (encoded < -8) encoded = -8;
537
538 // Reconstruct
539 int reconstructed = (encoded << range) + predicted;
540 error += (sample - reconstructed) * (sample - reconstructed);
541
542 p2 = p1;
543 p1 = reconstructed;
544 }
545
546 if (error < best_error) {
547 best_error = error;
548 best_range = range;
549 best_filter = filter;
550 }
551 }
552 }
553
554 // Encode block with best parameters
555 uint8_t header = (best_range << 4) | (best_filter << 2);
556 if (i + 16 >= padded.size()) {
557 header |= 0x01; // End flag
558 if (loop_start >= 0) {
559 header |= 0x02; // Loop flag
560 }
561 }
562
563 brr.push_back(header);
564
565 for (int j = 0; j < 8; ++j) {
566 int s1 = padded[i + j * 2];
567 int s2 = padded[i + j * 2 + 1];
568
569 // Apply inverse filter and encode
570 int predicted1 = 0, predicted2 = 0;
571 switch (best_filter) {
572 case 1:
573 predicted1 = (prev1 * kFilter1[1]) >> kFilter2[1];
574 break;
575 case 2:
576 predicted1 = (prev1 * kFilter1[2]) >> kFilter2[2];
577 predicted1 -= (prev2 * kFilter3[2]) >> 4;
578 break;
579 case 3:
580 predicted1 = (prev1 * kFilter1[3]) >> kFilter2[3];
581 predicted1 -= (prev2 * kFilter3[3]) >> 4;
582 break;
583 }
584
585 int enc1 = ((s1 - predicted1) >> best_range);
586 if (enc1 > 7) enc1 = 7;
587 if (enc1 < -8) enc1 = -8;
588 enc1 &= 0x0F;
589
590 int reconstructed1 = (((enc1 >= 8) ? enc1 - 16 : enc1) << best_range) + predicted1;
591 prev2 = prev1;
592 prev1 = reconstructed1;
593
594 switch (best_filter) {
595 case 1:
596 predicted2 = (prev1 * kFilter1[1]) >> kFilter2[1];
597 break;
598 case 2:
599 predicted2 = (prev1 * kFilter1[2]) >> kFilter2[2];
600 predicted2 -= (prev2 * kFilter3[2]) >> 4;
601 break;
602 case 3:
603 predicted2 = (prev1 * kFilter1[3]) >> kFilter2[3];
604 predicted2 -= (prev2 * kFilter3[3]) >> 4;
605 break;
606 }
607
608 int enc2 = ((s2 - predicted2) >> best_range);
609 if (enc2 > 7) enc2 = 7;
610 if (enc2 < -8) enc2 = -8;
611 enc2 &= 0x0F;
612
613 int reconstructed2 = (((enc2 >= 8) ? enc2 - 16 : enc2) << best_range) + predicted2;
614 prev2 = prev1;
615 prev1 = reconstructed2;
616
617 brr.push_back((enc1 << 4) | enc2);
618 }
619 }
620
621 return brr;
622}
623
624} // namespace music
625} // namespace zelda3
626} // 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
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
bool is_loaded() const
Definition rom.h:128
static constexpr int kFilter2[4]
Definition spc_parser.h:270
static std::vector< int16_t > Decode(const std::vector< uint8_t > &brr_data, int *loop_start=nullptr)
Decode BRR data to PCM samples.
static std::vector< uint8_t > Encode(const std::vector< int16_t > &pcm_data, int loop_start=-1)
Encode PCM samples to BRR format.
static constexpr int kFilter3[4]
Definition spc_parser.h:271
static constexpr int kFilter1[4]
Definition spc_parser.h:269
static bool IsDuration(uint8_t byte)
Check if a byte is a duration value (< 128).
Definition spc_parser.h:145
static bool IsCommand(uint8_t byte)
Check if a byte is a command opcode.
Definition spc_parser.h:152
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 bool IsNotePitch(uint8_t byte)
Check if a byte is a note pitch.
Definition spc_parser.h:138
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 absl::StatusOr< MusicTrack > ParseTrackInternal(ParseContext &ctx, uint16_t address, int max_ticks)
static absl::StatusOr< int > ParseSubroutine(ParseContext &ctx, uint16_t address, int repeat_count, int remaining_ticks)
static absl::StatusOr< MusicTrack > ParseTrack(Rom &rom, uint16_t address, uint8_t bank, int max_ticks=50000)
Parse a single track from ROM.
static int GetCommandParamCount(uint8_t opcode)
Get the parameter count for an N-SPC command.
Definition spc_parser.h:130
static uint32_t SpcAddressToRomOffset(Rom &rom, uint16_t spc_address, uint8_t bank)
Convert an SPC address to a ROM offset.
constexpr uint8_t kTrackEnd
Definition song_data.h:59
constexpr uint8_t kNoteTie
Definition song_data.h:57
constexpr uint32_t kSoundBankOffsets[]
Definition spc_parser.h:25
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
One of 8 channels in a music segment.
Definition song_data.h:291
std::vector< TrackEvent > events
Definition song_data.h:292
Context for parsing operations.
Definition spc_parser.h:48
std::vector< uint16_t > visited_addresses
Definition spc_parser.h:54
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