yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
music_player.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <cstring>
6
7#include "app/emu/emulator.h"
11#include "util/log.h"
12
13namespace yaze {
14namespace editor {
15namespace music {
16
17constexpr int kNativeSampleRate = 32040; // Actual SPC700 rate
18
19
21 : music_bank_(music_bank) {}
22
26
30
32 rom_ = rom;
33}
34
46
48 if (mode_ == new_mode) return;
49
50 PlaybackMode old_mode = mode_;
51 mode_ = new_mode;
52
53 // Notify external systems about audio exclusivity changes
54 // When we start playing, request exclusive audio control
55 // When we stop, release it
57 bool was_active = (old_mode == PlaybackMode::Playing || old_mode == PlaybackMode::Previewing);
58 bool is_active = (new_mode == PlaybackMode::Playing || new_mode == PlaybackMode::Previewing);
59
60 if (is_active && !was_active) {
61 LOG_INFO("MusicPlayer", "Requesting exclusive audio control");
63 } else if (!is_active && was_active) {
64 LOG_INFO("MusicPlayer", "Releasing exclusive audio control");
66 }
67 }
68
69 LOG_DEBUG("MusicPlayer", "State transition: %d -> %d",
70 static_cast<int>(old_mode), static_cast<int>(new_mode));
71}
72
74 if (!emulator_) {
75 LOG_ERROR("MusicPlayer", "PrepareAudioPlayback: No emulator");
76 return;
77 }
78
79 auto* audio = emulator_->audio_backend();
80 if (!audio) {
81 LOG_ERROR("MusicPlayer", "PrepareAudioPlayback: No audio backend");
82 return;
83 }
84
85 // TRACE: Log audio pipeline state before preparation
86 auto config = audio->GetConfig();
87 LOG_INFO("MusicPlayer", "PrepareAudioPlayback: backend=%s, device_rate=%dHz, "
88 "resampling=%s, native_rate=%dHz",
89 audio->GetBackendName().c_str(), config.sample_rate,
90 audio->IsAudioStreamEnabled() ? "ENABLED" : "DISABLED",
92
93 // Reset DSP sample buffer for clean start
94 auto& dsp = emulator_->snes().apu().dsp();
95 dsp.ResetSampleBuffer();
96
97 // Run one audio frame to generate initial samples
98 emulator_->snes().RunAudioFrame();
99
100 // Reset frame timing to prevent accumulated time from causing fast playback
102
103 // Queue initial samples to prime the audio buffer
104 constexpr int kInitialSamples = 533; // ~1 frame worth
105 static int16_t prime_buffer[2048];
106 std::memset(prime_buffer, 0, sizeof(prime_buffer));
107 emulator_->snes().SetSamples(prime_buffer, kInitialSamples);
108
109 bool queued = audio->QueueSamplesNative(prime_buffer, kInitialSamples, 2, kNativeSampleRate);
110
111 // TRACE: Log queue result and verify resampling is still active
112 LOG_INFO("MusicPlayer", "PrepareAudioPlayback: queued=%s, samples=%d, "
113 "resampling_after=%s",
114 queued ? "YES" : "NO", kInitialSamples,
115 audio->IsAudioStreamEnabled() ? "ENABLED" : "DISABLED");
116
117 if (!queued) {
118 LOG_ERROR("MusicPlayer", "PrepareAudioPlayback: CRITICAL - Failed to queue samples! "
119 "Audio will not play correctly.");
120 }
121
122 audio->Play();
123
124 // Enable audio-focused mode and start emulator
126 emulator_->set_running(true);
127
128 // Initialize frame timing for Update() loop - CRITICAL for correct playback speed
129 last_frame_time_ = std::chrono::steady_clock::now();
130}
131
133 switch (mode_) {
136 Pause();
137 break;
139 Resume();
140 break;
142 if (playing_song_index_ >= 0) {
144 }
145 break;
146 }
147}
148
150 // DIAGNOSTIC: Log Update() entry to verify it's being called
151 static int update_count = 0;
152 static bool first_update = true;
153 if (first_update || update_count % 300 == 0) {
154 LOG_INFO("MusicPlayer", "Update() #%d: emu=%p, init=%d, running=%d, focus=%d",
155 update_count,
156 static_cast<void*>(emulator_),
158 emulator_ ? emulator_->running() : false,
160 first_update = false;
161 }
162 update_count++;
163
164 // Run audio frame if we're playing and have an initialized emulator
166 emulator_->running()) {
167
168 // CRITICAL: Verify audio stream resampling is still enabled
169 // If disabled, samples play at wrong speed (1.5x due to 48000/32040 mismatch)
170 if (auto* audio = emulator_->audio_backend()) {
171 if (!audio->IsAudioStreamEnabled()) {
172 LOG_ERROR("MusicPlayer", "AUDIO STREAM DISABLED during playback! Re-enabling...");
173 audio->SetAudioStreamResampling(true, kNativeSampleRate, 2);
174 }
175 }
176
177 // Simple frame pacing: check if enough time has passed for one frame
178 auto now = std::chrono::steady_clock::now();
179 auto elapsed = std::chrono::duration<double>(now - last_frame_time_).count();
180
181 // Use emulator's frame timing (handles NTSC/PAL correctly)
182 double frame_time = emulator_->wanted_frames();
183 if (frame_time <= 0.0) {
184 frame_time = 1.0 / 60.0988; // Fallback to NTSC
185 }
186
187 if (elapsed >= frame_time) {
188 // DIAGNOSTIC: Check for timing anomalies
189 static int speed_log_counter = 0;
190 if (++speed_log_counter % 60 == 0) {
191 double current_fps = 1.0 / elapsed;
192 LOG_INFO("MusicPlayer", "Playback Speed: %.2f FPS (Target: %.2f), FrameTime: %.4fms, Wanted: %.4fms",
193 current_fps, 1.0/frame_time, elapsed*1000.0, frame_time*1000.0);
194 }
195
196 last_frame_time_ = now;
197
198 // DIAGNOSTIC: Log which path we're taking
199 static int frame_exec_count = 0;
200 if (frame_exec_count < 5 || frame_exec_count % 300 == 0) {
201 LOG_INFO("MusicPlayer", "Executing frame #%d: focus_mode=%d",
202 frame_exec_count, emulator_->is_audio_focus_mode());
203 }
204 frame_exec_count++;
205
206 // Use simplified audio frame execution (RunAudioFrame processes exactly 1 frame)
209 } else {
211 }
212 }
213
214 // Debug: log APU cycle rate periodically
215 static int debug_counter = 0;
216 static uint64_t last_apu_cycles = 0;
217 static auto last_log_time = std::chrono::steady_clock::now();
218
219 if (++debug_counter % 60 == 0) {
220 uint64_t apu_cycles = emulator_->snes().apu().GetCycles();
221 auto now_log = std::chrono::steady_clock::now();
222 auto log_elapsed = std::chrono::duration<double>(now_log - last_log_time).count();
223
224 uint64_t cycle_delta = apu_cycles - last_apu_cycles;
225 double cycles_per_sec = cycle_delta / log_elapsed;
226 double rate_ratio = cycles_per_sec / 1024000.0;
227
228 LOG_INFO("MusicPlayer", "APU: %llu cycles in %.2fs = %.0f/sec (%.2fx expected)",
229 cycle_delta, log_elapsed, cycles_per_sec, rate_ratio);
230
231 last_apu_cycles = apu_cycles;
232 last_log_time = now_log;
233
234 if (auto* audio = emulator_->audio_backend()) {
235 auto status = audio->GetStatus();
236 LOG_DEBUG("MusicPlayer", "Audio: playing=%d queued=%u bytes=%u",
237 status.is_playing, status.queued_frames, status.queued_bytes);
238 }
239 }
240 }
241
242 // Only poll game state when not in direct SPC mode
244 // Poll the game's current song ID (for game-based playback mode)
245 // 0x7E012C is the RAM address for the current song ID in Zelda 3
246 uint8_t current_song_id = emulator_->snes().Read(0x7E012C);
247
248 // If the song ID changed externally (by the game), update our state
249 // Note: Song IDs are 1-based in game, 0-based in editor
250 if (current_song_id > 0 && (current_song_id - 1) != playing_song_index_) {
251 playing_song_index_ = current_song_id - 1;
252
253 // Reset timing if song changed
254 playback_start_time_ = std::chrono::steady_clock::now();
257
258 // Update tempo for the new song
259 if (music_bank_) {
261 if (song) {
262 uint8_t tempo = GetSongTempo(*song);
264 }
265 }
266
267 // Update mode if not already playing
270 }
271 }
272 }
273}
274
276 // We are ready if we have a ROM. Backend is set up lazily in EnsureAudioReady().
277 return rom_ != nullptr;
278}
279
281 if (!rom_) {
282 LOG_WARN("MusicPlayer", "EnsureAudioReady: No ROM loaded");
283 return false;
284 }
285
286 if (!emulator_) {
287 LOG_ERROR("MusicPlayer", "EnsureAudioReady: No emulator set");
288 return false;
289 }
290
292 LOG_INFO("MusicPlayer", "Initializing SNES for audio playback...");
294 LOG_ERROR("MusicPlayer", "Failed to initialize emulator");
295 return false;
296 }
297 }
298
299 // CRITICAL: Enable SDL audio stream mode on the emulator FIRST.
301 LOG_INFO("MusicPlayer", "Set use_sdl_audio_stream=true, wanted_samples=%d",
303
304 // Enable audio stream resampling for proper 32kHz -> 48kHz conversion
305 if (auto* audio = emulator_->audio_backend()) {
306 auto config = audio->GetConfig();
307 LOG_INFO("MusicPlayer", "Audio backend: %s, config=%dHz/%dch, initialized=%d",
308 audio->GetBackendName().c_str(), config.sample_rate, config.channels,
309 audio->IsInitialized());
310
311 if (audio->SupportsAudioStream()) {
312 LOG_INFO("MusicPlayer", "Calling SetAudioStreamResampling(%d Hz -> %d Hz)",
313 kNativeSampleRate, config.sample_rate);
314 audio->SetAudioStreamResampling(true, kNativeSampleRate, 2);
315 // Prevent RunAudioFrame() from overriding our configuration
317 }
318 } else {
319 LOG_ERROR("MusicPlayer", "No audio backend available!");
320 return false;
321 }
322
323 if (!spc_initialized_) {
325 if (!spc_initialized_) {
326 LOG_ERROR("MusicPlayer", "Failed to initialize SPC");
327 return false;
328 }
329 }
330
332
333 return true;
334}
335
337 if (!EnsureAudioReady()) return false;
338
339 if (preview_initialized_) return true;
340
343}
344
346 if (!emulator_ || !rom_) return;
347 // Force re-initialization if requested (spc_initialized_ is false)
348
349 auto& apu = emulator_->snes().apu();
350 LOG_INFO("MusicPlayer", "Initializing direct SPC playback");
351
352 preview_initialized_ = false;
353
354 // Reset APU
355 apu.Reset();
356
357 // Upload Driver (Bank 0)
359 // PatchDriver removed - fixing root cause in APU emulation instead
360
361 // 4. Start Driver
362 apu.BootstrapDirect(kDriverEntryPoint);
363
364 // Initialize song pointers
365 // UploadSongToAram(song_pointers, 0xD000);
366
367 // 5. Run init cycles
368 for (int i = 0; i < kSpcResetCycles; i++) {
369 apu.Cycle();
370 }
371
372 spc_initialized_ = true;
373 current_spc_bank_ = 0xFF;
374}
375
377 if (!emulator_ || !rom_) return;
378 if (preview_initialized_) return;
379
380 auto& apu = emulator_->snes().apu();
381 LOG_INFO("MusicPlayer", "Initializing preview mode");
382
383 apu.Reset();
385
386 apu.WriteToDsp(kDspDir, 0x3C);
387 apu.WriteToDsp(kDspKeyOn, 0x00);
388 apu.WriteToDsp(kDspKeyOff, 0x00);
389 apu.WriteToDsp(kDspMainVolL, 0x7F);
390 apu.WriteToDsp(kDspMainVolR, 0x7F);
391 apu.WriteToDsp(kDspEchoVolL, 0x00);
392 apu.WriteToDsp(kDspEchoVolR, 0x00);
393 apu.WriteToDsp(kDspFlg, 0x20);
394
395 for (int i = 0; i < 1000; i++) {
396 apu.Cycle();
397 }
398
400}
401
402void MusicPlayer::PlaySong(int song_index) {
403 if (!rom_) {
404 LOG_WARN("MusicPlayer", "No ROM loaded - cannot play song");
405 return;
406 }
407
408 // Stop any existing playback (check mode, not legacy flag)
410 Stop();
411 }
412
413 if (use_direct_spc_) {
414 PlaySongDirect(song_index + 1); // 1-based ID for game
415 return;
416 }
417
418 // Request exclusive audio control BEFORE initializing to prevent audio mixing
420 LOG_INFO("MusicPlayer", "Pre-requesting exclusive audio control for game-based playback");
422 }
423
424 // Game-based playback logic
425 if (!EnsureAudioReady()) return;
426
428
429 if (!emulator_->running()) {
430 emulator_->set_running(true);
431 }
432
433 if (auto* audio = emulator_->audio_backend()) {
434 // Prime the buffer with silence to prevent immediate underrun
435 // Queue ~6 frames (100ms) of silence
436 constexpr int kPrimeFrames = 6;
437 constexpr int kPrimeSamples = 533 * kPrimeFrames;
438 std::vector<int16_t> silence(kPrimeSamples * 2, 0); // Stereo
439
440 constexpr int kNativeSampleRate = 32040;
441 audio->QueueSamplesNative(silence.data(), kPrimeSamples, 2, kNativeSampleRate);
442
443 if (!audio->GetStatus().is_playing) {
444 audio->Play();
445 }
446 }
447
448 // Write song ID to game RAM (game-based playback mode)
449 emulator_->snes().Write(0x7E012C, static_cast<uint8_t>(song_index + 1));
450
451 // Update playback state
452 playing_song_index_ = song_index;
453 playback_start_time_ = std::chrono::steady_clock::now();
456
457 // Calculate timing
458 auto* song = music_bank_->GetSong(song_index);
459 uint8_t tempo = song ? GetSongTempo(*song) : 150;
461
462 // Initialize frame timing for Update() loop - CRITICAL for correct playback speed
463 last_frame_time_ = std::chrono::steady_clock::now();
464
466}
467
469 if (!rom_) return;
470
471 // IMPORTANT: Initialize audio backend FIRST, then request exclusivity
472 // If we pause main emulator before our backend is ready, we get silence on first play
475 }
476
477 if (!EnsureAudioReady()) return;
478
479 // NOW request exclusive audio control - our backend is ready to take over
481 LOG_INFO("MusicPlayer", "Requesting exclusive audio control (backend ready)");
483 }
484
485 int song_index = song_id - 1;
486 const zelda3::music::MusicSong* song = nullptr;
487
488 if (music_bank_ && song_index >= 0 && song_index < music_bank_->GetSongCount()) {
489 song = music_bank_->GetSong(song_index);
490 }
491
492 if (!song) return;
493
494 if (song->modified) {
495 PreviewCustomSong(song_index);
496 return;
497 }
498
499 uint8_t song_bank = song->bank;
500 bool is_expanded = (song_bank == 3 || song_bank == 4);
501
502 LOG_INFO("MusicPlayer", "Playing song %d (%s) from song_bank=%d",
503 song_id, song->name.c_str(), song_bank);
504
505 auto& apu = emulator_->snes().apu();
506
507 if (current_spc_bank_ != song_bank) {
508 uint8_t rom_bank = song_bank + 1;
509 if (is_expanded && !music_bank_->HasExpandedMusicPatch()) {
510 rom_bank = 1;
511 song_bank = 0;
512 }
514 current_spc_bank_ = song_bank;
515 }
516
517 // Ensure audio backend is ready
519 if (auto* audio = emulator_->audio_backend()) {
520 // audio_ready_ is removed
521 }
522
523 // Calculate SPC song index
524 uint8_t spc_song_index;
525 if (is_expanded) {
526 int vanilla_count = 34;
527 int expanded_index = (song_id - 1) - vanilla_count;
528 spc_song_index = static_cast<uint8_t>(expanded_index + 1);
529 } else {
530 spc_song_index = static_cast<uint8_t>(song_id);
531 }
532
533 // Trigger song playback via APU ports
534 static uint8_t trigger_byte = 0x00;
535 trigger_byte ^= 0x01;
536
537 apu.in_ports_[0] = spc_song_index;
538 apu.in_ports_[1] = trigger_byte;
539
540 // Run APU cycles to let the driver start the song
541 // This also generates initial audio samples
542 for (int i = 0; i < kSpcInitCycles; i++) {
543 apu.Cycle();
544 }
545
546 // Clear any stale audio from previous playback before priming
547 if (auto* audio = emulator_->audio_backend()) {
548 audio->Clear();
549 }
550
551 // Reset DSP sample buffer for clean start - prevents stale samples from
552 // previous playback causing timing/position issues on first play
553 auto& dsp = emulator_->snes().apu().dsp();
554 dsp.ResetSampleBuffer();
555
556 // Run one full frame worth of audio generation to fill the buffer
557 // Note: RunAudioFrame() handles NewFrame() internally at vblank
558 emulator_->snes().RunAudioFrame();
559
560 // Now reset timing and start playback with buffer already primed
562
563 // Prime the audio queue with initial samples
564 constexpr int kNativeSampleRate = 32040;
565 constexpr int kInitialSamples = 533; // ~1 frame worth
566 static int16_t prime_buffer[2048];
567 std::memset(prime_buffer, 0, sizeof(prime_buffer));
568 emulator_->snes().SetSamples(prime_buffer, kInitialSamples);
569 if (auto* audio = emulator_->audio_backend()) {
570 bool queued = audio->QueueSamplesNative(prime_buffer, kInitialSamples, 2, kNativeSampleRate);
571 LOG_INFO("MusicPlayer", "Initial samples queued: %s", queued ? "YES" : "NO (RESAMPLING FAILED!)");
572 }
573
574 // Start audio playback
575 if (auto* audio = emulator_->audio_backend()) {
576 // Prime the buffer with silence to prevent immediate underrun
577 // Queue ~6 frames (100ms) of silence
578 constexpr int kPrimeFrames = 6;
579 constexpr int kPrimeSamples = 533 * kPrimeFrames;
580 std::vector<int16_t> silence(kPrimeSamples * 2, 0); // Stereo
581
582 // Use the native rate (32040) so it gets resampled correctly if needed
583 constexpr int kNativeSampleRate2 = 32040;
584 bool silenceQueued = audio->QueueSamplesNative(silence.data(), kPrimeSamples, 2, kNativeSampleRate2);
585 LOG_INFO("MusicPlayer", "Silence buffer queued: %s", silenceQueued ? "YES" : "NO (RESAMPLING FAILED!)");
586
587 auto status = audio->GetStatus();
588 LOG_INFO("MusicPlayer", "Audio status before Play(): playing=%d, queued_frames=%u",
589 status.is_playing, status.queued_frames);
590
591 // Always call Play() to ensure audio device is ready
592 // SDL's Play() is idempotent - safe to call even if already playing
593 audio->Play();
594
595 status = audio->GetStatus();
596 LOG_INFO("MusicPlayer", "Audio status after Play(): playing=%d, queued_frames=%u",
597 status.is_playing, status.queued_frames);
598 }
599
600 // Enable audio-focused mode for efficient playback
602 emulator_->set_running(true);
603
604 // Update playback state
605 playing_song_index_ = song_id - 1;
606 playback_start_time_ = std::chrono::steady_clock::now();
609
610 // Calculate timing for this song
611 uint8_t tempo = GetSongTempo(*song);
613
614 // Initialize frame timing for Update() loop - CRITICAL for correct playback speed
615 last_frame_time_ = std::chrono::steady_clock::now();
616
618 LOG_INFO("MusicPlayer", "Started playing song %d at %.1f ticks/sec",
620}
621
623 if (!emulator_) return;
625
626 // Save current position before pausing
628
629 // Pause emulator and audio
630 emulator_->set_running(false);
631 if (auto* audio = emulator_->audio_backend()) {
632 audio->Pause();
633 }
634
636 LOG_DEBUG("MusicPlayer", "Paused at tick %u", playback_start_tick_);
637}
638
640 if (!emulator_) return;
641 if (mode_ != PlaybackMode::Paused) return;
642
643 // Reset frame timing to prevent accumulated time from causing fast-forward
645 emulator_->set_running(true);
646
647 if (auto* audio = emulator_->audio_backend()) {
648 audio->Clear(); // Clear buffer to prevent stale audio
649 audio->Play();
650 }
651
652 // Start counting from where we paused
653 playback_start_time_ = std::chrono::steady_clock::now();
654
655 // Initialize frame timing for Update() loop - CRITICAL for correct playback speed
656 last_frame_time_ = std::chrono::steady_clock::now();
657
659 LOG_DEBUG("MusicPlayer", "Resumed from tick %u", playback_start_tick_);
660}
661
663 if (!emulator_) return;
664 if (mode_ == PlaybackMode::Stopped) return;
665
666 // Send stop command to SPC700
667 auto& apu = emulator_->snes().apu();
668 apu.in_ports_[0] = 0x00;
669 apu.in_ports_[1] = 0xFF; // Stop command
670
671 // Run APU cycles to process the stop command
672 for (int i = 0; i < kSpcStopCycles; i++) {
673 apu.Cycle();
674 }
675
676 // Stop emulator and audio
677 emulator_->set_running(false);
679
680 if (auto* audio = emulator_->audio_backend()) {
681 audio->Stop();
682 audio->Clear(); // Clear stale audio to prevent glitches on restart
683 }
684
685 // Reset state but keep playing_song_index_ for TogglePlayPause() replay
686 // playing_song_index_ is intentionally NOT reset to -1
689 ticks_per_second_ = 0.0f;
690
692 LOG_DEBUG("MusicPlayer", "Stopped playback");
693}
694
695void MusicPlayer::SetVolume(float volume) {
697 emulator_->audio_backend()->SetVolume(std::clamp(volume, 0.0f, 1.0f));
698 }
699}
700
701void MusicPlayer::SetPlaybackSpeed(float /*speed*/) {
702 // Varispeed removed - always plays at 1.0x speed for correct audio timing
703 // The playback_speed_ member no longer exists
704}
705
712
714 use_direct_spc_ = enabled;
715}
716
717void MusicPlayer::UploadSoundBankFromRom(uint32_t rom_offset) {
718 if (!emulator_ || !rom_) return;
719
720 auto& apu = emulator_->snes().apu();
721 const uint8_t* rom_data = rom_->data();
722 const size_t rom_size = rom_->size();
723
724 LOG_INFO("MusicPlayer", "Uploading sound bank from ROM offset 0x%X", rom_offset);
725
726 int block_count = 0;
727 while (rom_offset + 4 < rom_size) {
728 uint16_t block_size = rom_data[rom_offset] | (rom_data[rom_offset + 1] << 8);
729 uint16_t aram_addr = rom_data[rom_offset + 2] | (rom_data[rom_offset + 3] << 8);
730
731 if (block_size == 0 || block_size > 0x10000) {
732 break;
733 }
734
735 if (rom_offset + 4 + block_size > rom_size) {
736 LOG_WARN("MusicPlayer", "Block at 0x%X extends past ROM end", rom_offset);
737 break;
738 }
739
740 apu.WriteDma(aram_addr, &rom_data[rom_offset + 4], block_size);
741
742 rom_offset += 4 + block_size;
743 block_count++;
744 }
745}
746
747void MusicPlayer::UploadSongToAram(const std::vector<uint8_t>& data, uint16_t aram_address) {
748 if (!emulator_) return;
749 auto& apu = emulator_->snes().apu();
750 for (size_t i = 0; i < data.size(); ++i) {
751 apu.ram[aram_address + i] = data[i];
752 }
753}
754
755uint32_t MusicPlayer::GetBankRomOffset(uint8_t bank) const {
756 if (bank < 6) {
757 return kSoundBankOffsets[bank];
758 }
759 return kSoundBankOffsets[0];
760}
761
762int MusicPlayer::GetSongIndexInBank(int song_id, uint8_t bank) const {
763 switch (bank) {
764 case 0: return song_id - 1;
765 case 1: return song_id - 12;
766 case 2: return song_id - 32;
767 default: return 0;
768 }
769}
770
772 constexpr uint8_t kDefaultTempo = 150;
773 if (song.segments.empty()) return kDefaultTempo;
774
775 const auto& segment = song.segments[0];
776 for (const auto& track : segment.tracks) {
777 for (const auto& event : track.events) {
779 event.command.opcode == kOpcodeTempo) {
780 return event.command.params[0];
781 }
782 }
783 }
784 return kDefaultTempo;
785}
786
787float MusicPlayer::CalculateTicksPerSecond(uint8_t tempo) const {
788 // The SNES SPC700 driver uses a timer (usually Timer 0) running at 8000Hz.
789 // The timer has a divider (usually 16), resulting in a 500Hz base tick.
790 // The driver accumulates the tempo value every 500Hz tick.
791 // When the accumulator overflows, a music tick is generated.
792 // Formula: Rate = Base_Freq * (Tempo / 256)
793 // Base_Freq = 8000 / Divider (16) = 500Hz
794
795 return 500.0f * (static_cast<float>(tempo) / 256.0f);
796}
797
799 // Only count ticks when actively playing (not stopped or paused)
800 bool is_active = (mode_ == PlaybackMode::Playing || mode_ == PlaybackMode::Previewing);
801 if (!is_active) return playback_start_tick_;
802
803 auto now = std::chrono::steady_clock::now();
804 float elapsed_seconds = std::chrono::duration<float>(now - playback_start_time_).count();
805
806 return playback_start_tick_ + static_cast<uint32_t>(elapsed_seconds * ticks_per_second_);
807}
808
809// Preview methods (ported from MusicEditor)
811 const zelda3::music::TrackEvent& event,
812 int segment_index, int channel_index) {
813 if (event.type != zelda3::music::TrackEvent::Type::Note || !event.note.IsNote()) {
814 return;
815 }
816
817 if (!EnsureAudioReady()) return;
818
819 auto& apu = emulator_->snes().apu();
820
821 // Resolve instrument
822 const zelda3::music::MusicSegment* segment = nullptr;
823 if (segment_index >= 0 && segment_index < static_cast<int>(song.segments.size())) {
824 segment = &song.segments[segment_index];
825 }
826
827 // Helper to resolve instrument (duplicated logic for now, could be shared)
828 int instrument_index = -1;
829 if (segment && channel_index >= 0 && channel_index < 8) {
830 const auto& track = segment->tracks[channel_index];
831 for (const auto& evt : track.events) {
832 if (evt.tick > event.tick) break;
833 if (evt.type == zelda3::music::TrackEvent::Type::Command && evt.command.opcode == 0xE0) {
834 instrument_index = evt.command.params[0];
835 }
836 }
837 }
838
839 const auto* instrument = music_bank_->GetInstrument(instrument_index);
840
841 int ch_base = channel_index * 0x10;
842 int inst_idx = instrument ? instrument->sample_index : 0;
843
844 apu.WriteToDsp(ch_base + kDspSrcn, inst_idx);
845
846 uint16_t pitch = zelda3::music::LookupNSpcPitch(event.note.pitch);
847 apu.WriteToDsp(ch_base + kDspPitchLow, pitch & 0xFF);
848 apu.WriteToDsp(ch_base + kDspPitchHigh, (pitch >> 8) & 0x3F);
849
850 apu.WriteToDsp(ch_base + kDspVolL, 0x7F);
851 apu.WriteToDsp(ch_base + kDspVolR, 0x7F);
852
853 if (instrument) {
854 apu.WriteToDsp(ch_base + kDspAdsr1, instrument->GetADByte());
855 apu.WriteToDsp(ch_base + kDspAdsr2, instrument->GetSRByte());
856 } else {
857 apu.WriteToDsp(ch_base + kDspAdsr1, 0xFF);
858 apu.WriteToDsp(ch_base + kDspAdsr2, 0xE0);
859 }
860
861 apu.WriteToDsp(kDspKeyOn, 1 << channel_index);
862
863 for (int i = 0; i < kSpcPreviewCycles; i++) apu.Cycle();
864
867}
868
870 ChannelState state;
871 if (!emulator_ || !emulator_->is_snes_initialized() || channel_index < 0 || channel_index >= 8) {
872 return state; // Default initialized
873 }
874 const auto& dsp = emulator_->snes().apu().dsp();
875 const auto& ch = dsp.GetChannel(channel_index);
876 state.key_on = ch.keyOn;
877 state.sample_index = ch.srcn;
878 state.pitch = ch.pitch;
879 state.volume_l = static_cast<uint8_t>(std::abs(ch.volumeL));
880 state.volume_r = static_cast<uint8_t>(std::abs(ch.volumeR));
881 state.gain = ch.gain;
882 state.adsr_state = ch.adsrState;
883 return state;
884}
885
886std::array<ChannelState, 8> MusicPlayer::GetChannelStates() const {
887 std::array<ChannelState, 8> states;
889 return states; // Default initialized
890 }
891
892 const auto& dsp = emulator_->snes().apu().dsp();
893 for (int i = 0; i < 8; ++i) {
894 const auto& ch = dsp.GetChannel(i);
895 states[i].key_on = ch.keyOn;
896 states[i].sample_index = ch.srcn;
897 states[i].pitch = ch.pitch;
898 states[i].volume_l = static_cast<uint8_t>(std::abs(ch.volumeL)); // Use abs for visualization
899 states[i].volume_r = static_cast<uint8_t>(std::abs(ch.volumeR));
900 states[i].gain = ch.gain;
901 states[i].adsr_state = ch.adsrState;
902 }
903 return states;
904}
905
906void MusicPlayer::PreviewSegment(const zelda3::music::MusicSong& song, int segment_index) {
907 if (!EnsureAudioReady()) return;
908 if (segment_index < 0 || segment_index >= static_cast<int>(song.segments.size())) return;
909
910 zelda3::music::MusicSong temp_song;
911 temp_song.name = "Preview Segment";
912 temp_song.bank = song.bank;
913 temp_song.segments.push_back(song.segments[segment_index]);
914 temp_song.loop_point = -1;
915
916 uint16_t base_aram = zelda3::music::kSongTableAram;
917 auto result = zelda3::music::SpcSerializer::SerializeSong(temp_song, base_aram);
918 if (!result.ok()) {
919 LOG_ERROR("MusicPlayer", "Failed to serialize segment: %s", result.status().message().data());
920 return;
921 }
922
923 UploadSongToAram(result->data, result->base_address);
924
925 UploadSongToAram(result->data, result->base_address);
926
927 auto& apu = emulator_->snes().apu();
928 static uint8_t trigger = 0x00;
929 trigger ^= 0x01;
930
931 apu.in_ports_[0] = 1;
932 apu.in_ports_[1] = trigger;
933
935
936 // Calculate segment start tick for timeline positioning
937 uint32_t segment_start_tick = 0;
938 for (int i = 0; i < segment_index; ++i) {
939 segment_start_tick += song.segments[i].GetDuration();
940 }
941
942 playback_start_time_ = std::chrono::steady_clock::now();
943 playback_start_tick_ = segment_start_tick;
944 playback_segment_index_ = segment_index;
945
946 uint8_t tempo = GetSongTempo(song);
948
950 LOG_DEBUG("MusicPlayer", "Previewing segment %d at tick %u", segment_index, segment_start_tick);
951}
952
953void MusicPlayer::PreviewInstrument(int instrument_index) {
954 if (!EnsurePreviewReady()) return;
955
956 auto* instrument = music_bank_->GetInstrument(instrument_index);
957 if (!instrument) return;
958
959 auto& apu = emulator_->snes().apu();
960 int ch = 0;
961 int ch_base = ch * 0x10;
962
963 // Clear any stale audio before preview
964 if (auto* audio = emulator_->audio_backend()) {
965 audio->Clear();
966 }
967
968 apu.WriteToDsp(kDspKeyOff, 1 << ch);
969 for(int i=0; i<500; ++i) apu.Cycle(); // More cycles for DSP to stabilize
970
971 apu.WriteToDsp(ch_base + kDspSrcn, instrument->sample_index);
972 apu.WriteToDsp(ch_base + kDspAdsr1, instrument->GetADByte());
973 apu.WriteToDsp(ch_base + kDspAdsr2, instrument->GetSRByte());
974 apu.WriteToDsp(ch_base + kDspGain, instrument->gain);
975
976 uint16_t pitch = zelda3::music::LookupNSpcPitch(0x80 + 36); // C4
977 pitch = (static_cast<uint32_t>(pitch) * instrument->pitch_mult) >> 12;
978
979 apu.WriteToDsp(ch_base + kDspPitchLow, pitch & 0xFF);
980 apu.WriteToDsp(ch_base + kDspPitchHigh, (pitch >> 8) & 0x3F);
981
982 apu.WriteToDsp(ch_base + kDspVolL, 0x7F);
983 apu.WriteToDsp(ch_base + kDspVolR, 0x7F);
984
985 apu.WriteToDsp(kDspKeyOn, 1 << ch);
986
989}
990
991void MusicPlayer::PreviewSample(int sample_index) {
992 if (!EnsurePreviewReady()) return;
993
994 auto* sample = music_bank_->GetSample(sample_index);
995 if (!sample) return;
996
997 uint16_t temp_addr = 0x8000;
998 UploadSongToAram(sample->brr_data, temp_addr);
999
1000 uint16_t loop_addr = temp_addr + sample->loop_point;
1001 std::vector<uint8_t> dir = {
1002 static_cast<uint8_t>(temp_addr & 0xFF),
1003 static_cast<uint8_t>(temp_addr >> 8),
1004 static_cast<uint8_t>(loop_addr & 0xFF),
1005 static_cast<uint8_t>(loop_addr >> 8)
1006 };
1007 UploadSongToAram(dir, 0x3C00);
1008
1009 auto& apu = emulator_->snes().apu();
1010 int ch = 0;
1011 int ch_base = ch * 0x10;
1012
1013 // Clear any stale audio before preview
1014 if (auto* audio = emulator_->audio_backend()) {
1015 audio->Clear();
1016 }
1017
1018 apu.WriteToDsp(kDspKeyOff, 1 << ch);
1019 for(int i=0; i<500; ++i) apu.Cycle(); // More cycles for DSP to stabilize
1020
1021 apu.WriteToDsp(ch_base + kDspSrcn, 0x00); // Sample 0
1022 apu.WriteToDsp(ch_base + kDspAdsr1, 0xFF);
1023 apu.WriteToDsp(ch_base + kDspAdsr2, 0xE0);
1024 apu.WriteToDsp(ch_base + kDspGain, 0x7F);
1025
1026 uint16_t pitch = 0x1000;
1027 apu.WriteToDsp(ch_base + kDspPitchLow, pitch & 0xFF);
1028 apu.WriteToDsp(ch_base + kDspPitchHigh, (pitch >> 8) & 0x3F);
1029
1030 apu.WriteToDsp(ch_base + kDspVolL, 0x7F);
1031 apu.WriteToDsp(ch_base + kDspVolR, 0x7F);
1032
1033 apu.WriteToDsp(kDspKeyOn, 1 << ch);
1034
1037}
1038
1040 if (!EnsureAudioReady()) return;
1041
1042 auto* song = music_bank_->GetSong(song_index);
1043 if (!song) return;
1044
1045 LOG_INFO("MusicPlayer", "Previewing custom song: %s", song->name.c_str());
1046
1047 // Serialize the modified song from memory
1048 uint16_t base_aram = zelda3::music::kSongTableAram;
1049 auto result = zelda3::music::SpcSerializer::SerializeSong(*song, base_aram);
1050 if (!result.ok()) {
1051 LOG_ERROR("MusicPlayer", "Failed to serialize song: %s", result.status().message().data());
1052 return;
1053 }
1054
1055 // Upload serialized song data to APU RAM
1056 UploadSongToAram(result->data, result->base_address);
1057
1058 // Upload serialized song data to APU RAM
1059 UploadSongToAram(result->data, result->base_address);
1060
1061 auto& apu = emulator_->snes().apu();
1062
1063 // Trigger song 1 (our uploaded song is at the beginning of the table)
1064 apu.in_ports_[0] = 1;
1065 apu.in_ports_[1] = 0x00;
1066
1067 // Run APU cycles to start playback
1068 for (int i = 0; i < kSpcInitCycles; i++) apu.Cycle();
1069
1071
1072 // Update state
1073 playing_song_index_ = song_index;
1074 playback_start_time_ = std::chrono::steady_clock::now();
1077
1078 uint8_t tempo = GetSongTempo(*song);
1080
1082}
1083
1084void MusicPlayer::SeekToSegment(int segment_index) {
1086 if (!song || segment_index < 0 ||
1087 segment_index >= static_cast<int>(song->segments.size())) {
1088 return;
1089 }
1090
1091 // Calculate tick offset for this segment
1092 uint32_t tick_offset = 0;
1093 for (int i = 0; i < segment_index; ++i) {
1094 tick_offset += song->segments[i].GetDuration();
1095 }
1096
1097 playback_start_time_ = std::chrono::steady_clock::now();
1098 playback_start_tick_ = tick_offset;
1099 playback_segment_index_ = segment_index;
1100
1101 // Update tempo from segment (use first track's tempo if available)
1102 const auto& segment = song->segments[segment_index];
1103 if (!segment.tracks.empty()) {
1104 for (const auto& event : segment.tracks[0].events) {
1105 if (event.type == zelda3::music::TrackEvent::Type::Command &&
1106 event.command.opcode == kOpcodeTempo) { // Tempo command
1107 ticks_per_second_ = CalculateTicksPerSecond(event.command.params[0]);
1108 break;
1109 }
1110 }
1111 }
1112}
1113
1115 const zelda3::music::MusicSegment& segment, int channel_index,
1116 uint16_t tick) const {
1117 if (channel_index < 0 || channel_index >= 8) return nullptr;
1118
1119 int instrument_index = -1;
1120 const auto& track = segment.tracks[channel_index];
1121
1122 for (const auto& evt : track.events) {
1123 if (evt.tick > tick) break;
1125 evt.command.opcode == 0xE0) { // SetInstrument
1126 instrument_index = evt.command.params[0];
1127 }
1128 }
1129
1130 if (instrument_index == -1) return nullptr;
1131 return music_bank_->GetInstrument(instrument_index);
1132}
1133
1134// === Debug Diagnostics ===
1135
1137 DspDebugStatus status;
1139 return status;
1140 }
1141
1142 const auto& dsp = emulator_->snes().apu().dsp();
1143 status.sample_offset = dsp.GetSampleOffset();
1144 status.frame_boundary = dsp.GetFrameBoundary();
1145 status.master_vol_l = dsp.GetMasterVolumeL();
1146 status.master_vol_r = dsp.GetMasterVolumeR();
1147 status.mute = dsp.IsMuted();
1148 status.reset = dsp.IsReset();
1149 status.echo_enabled = dsp.IsEchoEnabled();
1150 status.echo_delay = dsp.GetEchoDelay();
1151 return status;
1152}
1153
1155 ApuDebugStatus status;
1157 return status;
1158 }
1159
1160 const auto& apu = emulator_->snes().apu();
1161 status.cycles = apu.GetCycles();
1162
1163 // Timer 0
1164 const auto& t0 = apu.GetTimer(0);
1165 status.timer0_enabled = t0.enabled;
1166 status.timer0_counter = t0.counter;
1167 status.timer0_target = t0.target;
1168
1169 // Timer 1
1170 const auto& t1 = apu.GetTimer(1);
1171 status.timer1_enabled = t1.enabled;
1172 status.timer1_counter = t1.counter;
1173 status.timer1_target = t1.target;
1174
1175 // Timer 2
1176 const auto& t2 = apu.GetTimer(2);
1177 status.timer2_enabled = t2.enabled;
1178 status.timer2_counter = t2.counter;
1179 status.timer2_target = t2.target;
1180
1181 // Port state
1182 status.port0_in = apu.in_ports_[0];
1183 status.port1_in = apu.in_ports_[1];
1184 status.port0_out = apu.out_ports_[0];
1185 status.port1_out = apu.out_ports_[1];
1186
1187 return status;
1188}
1189
1191 AudioQueueStatus status;
1192 if (!emulator_) {
1193 return status;
1194 }
1195
1196 if (auto* audio = emulator_->audio_backend()) {
1197 auto backend_status = audio->GetStatus();
1198 status.is_playing = backend_status.is_playing;
1199 status.queued_frames = backend_status.queued_frames;
1200 status.queued_bytes = backend_status.queued_bytes;
1201 status.has_underrun = backend_status.has_underrun;
1202
1203 auto config = audio->GetConfig();
1204 status.sample_rate = config.sample_rate;
1205 status.backend_name = audio->GetBackendName();
1206 }
1207
1208 return status;
1209}
1210
1211// === Debug Actions ===
1212
1214 if (!emulator_) return;
1215
1216 if (auto* audio = emulator_->audio_backend()) {
1217 audio->Clear();
1218 LOG_INFO("MusicPlayer", "Audio queue cleared");
1219 }
1220}
1221
1223 if (!emulator_ || !emulator_->is_snes_initialized()) return;
1224
1225 auto& dsp = emulator_->snes().apu().dsp();
1226 dsp.ResetSampleBuffer();
1227 LOG_INFO("MusicPlayer", "DSP buffer reset");
1228}
1229
1231 if (!emulator_ || !emulator_->is_snes_initialized()) return;
1232
1233 auto& dsp = emulator_->snes().apu().dsp();
1234 dsp.NewFrame();
1235 LOG_INFO("MusicPlayer", "Forced DSP NewFrame()");
1236}
1237
1239 if (!emulator_) return;
1240
1241 // Stop current playback
1242 Stop();
1243
1244 // Reset SPC initialization state to force reinit on next play
1245 spc_initialized_ = false;
1246 preview_initialized_ = false;
1247 // audio_ready_ removed
1248 current_spc_bank_ = 0xFF;
1249
1250 LOG_INFO("MusicPlayer", "Audio system marked for reinitialization");
1251}
1252
1253} // namespace music
1254} // namespace editor
1255} // 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
void PreviewSegment(const zelda3::music::MusicSong &song, int segment_index)
Preview a specific segment of a song.
ApuDebugStatus GetApuStatus() const
Get APU timing diagnostic status.
void Stop()
Stop playback completely.
void PreviewCustomSong(int song_index)
Preview a custom (modified) song from memory.
void ReinitAudio()
Reinitialize the audio system.
float CalculateTicksPerSecond(uint8_t tempo) const
void SetPlaybackSpeed(float speed)
Set the playback speed (0.25x to 2.0x).
std::function< void(bool)> audio_exclusivity_callback_
void ForceNewFrame()
Force a DSP NewFrame() call.
ChannelState GetChannelState(int channel_index) const
int GetSongIndexInBank(int song_id, uint8_t bank) const
void SetInterpolationType(int type)
Set the DSP interpolation type for audio quality.
void SetVolume(float volume)
Set the master volume (0.0 to 1.0).
uint8_t GetSongTempo(const zelda3::music::MusicSong &song) const
std::chrono::steady_clock::time_point playback_start_time_
void Update()
Call once per frame to update playback state.
void UploadSoundBankFromRom(uint32_t rom_offset)
void PrepareAudioPlayback()
Prepare audio pipeline for playback.
void Pause()
Pause the current playback.
uint32_t GetCurrentPlaybackTick() const
AudioQueueStatus GetAudioQueueStatus() const
Get audio queue diagnostic status.
std::array< ChannelState, 8 > GetChannelStates() const
void TransitionTo(PlaybackMode new_mode)
void PlaySong(int song_index)
Start playing a song by index.
void UploadSongToAram(const std::vector< uint8_t > &data, uint16_t aram_address)
DspDebugStatus GetDspStatus() const
Get DSP buffer diagnostic status.
void SetDirectSpcMode(bool enabled)
Enable/disable direct SPC mode (bypasses game CPU).
void Resume()
Resume paused playback.
bool IsAudioReady() const
Check if the audio system is ready for playback.
uint32_t GetBankRomOffset(uint8_t bank) const
void PreviewNote(const zelda3::music::MusicSong &song, const zelda3::music::TrackEvent &event, int segment_index, int channel_index)
Preview a single note with the current instrument.
std::chrono::steady_clock::time_point last_frame_time_
void TogglePlayPause()
Toggle between play/pause states.
const zelda3::music::MusicInstrument * ResolveInstrumentForEvent(const zelda3::music::MusicSegment &segment, int channel_index, uint16_t tick) const
Resolve the instrument used at a specific tick in a track.
void SetEmulator(emu::Emulator *emulator)
Set the main emulator instance to use for playback.
PlaybackState GetState() const
zelda3::music::MusicBank * music_bank_
void ResetDspBuffer()
Reset the DSP sample buffer.
void PreviewSample(int sample_index)
Preview a raw BRR sample.
void PreviewInstrument(int instrument_index)
Preview an instrument at middle C.
void SeekToSegment(int segment_index)
Seek to a specific segment in the current song.
void ClearAudioQueue()
Clear the audio queue (stops sound immediately).
MusicPlayer(zelda3::music::MusicBank *music_bank)
A class for emulating and debugging SNES games.
Definition emulator.h:39
auto wanted_samples() const -> int
Definition emulator.h:95
void set_use_sdl_audio_stream(bool enabled)
Definition emulator.cc:79
auto wanted_frames() const -> float
Definition emulator.h:96
bool is_audio_focus_mode() const
Definition emulator.h:108
void set_interpolation_type(int type)
Definition emulator.cc:96
bool EnsureInitialized(Rom *rom)
Definition emulator.cc:173
void set_audio_focus_mode(bool focus)
Definition emulator.h:109
void set_running(bool running)
Definition emulator.h:60
bool is_snes_initialized() const
Definition emulator.h:122
audio::IAudioBackend * audio_backend()
Definition emulator.h:74
auto snes() -> Snes &
Definition emulator.h:58
auto running() const -> bool
Definition emulator.h:59
void mark_audio_stream_configured()
Definition emulator.h:91
virtual void SetVolume(float volume)=0
Manages the collection of songs, instruments, and samples from a ROM.
Definition music_bank.h:27
MusicInstrument * GetInstrument(int index)
Get an instrument by index.
MusicSong * GetSong(int index)
Get a song by index.
MusicSample * GetSample(int index)
Get a sample by index.
bool HasExpandedMusicPatch() const
Check if the ROM has the Oracle of Secrets expanded music patch.
Definition music_bank.h:147
static absl::StatusOr< SerializeResult > SerializeSong(const MusicSong &song, uint16_t base_address)
Serialize a complete song to binary format.
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
constexpr int kSpcResetCycles
constexpr uint8_t kDspMainVolL
constexpr uint32_t kSoundBankOffsets[]
constexpr uint8_t kDspKeyOn
constexpr uint8_t kDspSrcn
constexpr int kSpcPreviewCycles
constexpr int kSpcInitCycles
constexpr uint16_t kDriverEntryPoint
constexpr uint8_t kDspGain
constexpr uint8_t kDspMainVolR
constexpr uint8_t kDspEchoVolR
constexpr uint8_t kOpcodeTempo
constexpr uint8_t kDspAdsr1
constexpr uint8_t kDspVolL
constexpr uint8_t kDspFlg
constexpr int kNativeSampleRate
constexpr uint8_t kDspKeyOff
constexpr uint8_t kDspVolR
constexpr int kSpcStopCycles
constexpr uint8_t kDspPitchLow
constexpr uint8_t kDspEchoVolL
constexpr uint8_t kDspDir
PlaybackMode
Playback mode for the music player.
constexpr uint8_t kDspPitchHigh
constexpr uint8_t kDspAdsr2
constexpr uint16_t kSongTableAram
Definition song_data.h:82
uint16_t LookupNSpcPitch(uint8_t note_byte)
Look up the DSP pitch value for an N-SPC note byte.
Definition song_data.h:126
APU timing diagnostic status for debug UI.
Audio queue diagnostic status for debug UI.
Represents the state of a single DSP channel for visualization.
DSP buffer diagnostic status for debug UI.
Represents the current playback state of the music player.
An instrument definition with ADSR envelope.
Definition song_data.h:358
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