yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
music_editor.cc
Go to the documentation of this file.
1#include "music_editor.h"
2
3#include <algorithm>
4#include <cmath>
5#include <ctime>
6#include <iomanip>
7#include <sstream>
8
9#include "absl/strings/str_format.h"
21#include "app/emu/emulator.h"
23#include "app/gui/core/icons.h"
24#include "app/gui/core/input.h"
27#include "imgui/imgui.h"
28#include "imgui/misc/cpp/imgui_stdlib.h"
29#include "core/project.h"
30#include "nlohmann/json.hpp"
31#include "util/log.h"
32#include "util/macro.h"
33
34#ifdef __EMSCRIPTEN__
36#endif
37
38namespace yaze {
39namespace editor {
40
42 LOG_INFO("MusicEditor", "Initialize() START: rom_=%p, emulator_=%p",
43 static_cast<void*>(rom_), static_cast<void*>(emulator_));
44
45 // Note: song_window_class_ initialization is deferred to first Update() call
46 // because ImGui::GetID() requires a valid window context which doesn't exist
47 // during Initialize()
48 song_window_class_.DockingAllowUnclassed = true;
49 song_window_class_.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_None;
50
51 // ==========================================================================
52 // Create SINGLE audio backend - owned here and shared with all emulators
53 // This eliminates the dual-backend bug entirely
54 // ==========================================================================
55 if (!audio_backend_) {
56#ifdef __EMSCRIPTEN__
59#else
62#endif
63
65 config.sample_rate = 48000;
66 config.channels = 2;
67 config.buffer_frames = 1024;
69
70 if (audio_backend_->Initialize(config)) {
71 LOG_INFO("MusicEditor", "Created shared audio backend: %s @ %dHz",
72 audio_backend_->GetBackendName().c_str(), config.sample_rate);
73 } else {
74 LOG_ERROR("MusicEditor", "Failed to initialize audio backend!");
75 audio_backend_.reset();
76 }
77 }
78
79 // Share the audio backend with the main emulator (if available)
82 LOG_INFO("MusicEditor", "Shared audio backend with main emulator");
83 } else {
84 LOG_WARN("MusicEditor", "Cannot share with main emulator: backend=%p, emulator=%p",
85 static_cast<void*>(audio_backend_.get()), static_cast<void*>(emulator_));
86 }
87
88 music_player_ = std::make_unique<editor::music::MusicPlayer>(&music_bank_);
89 if (rom_) {
90 music_player_->SetRom(rom_);
91 LOG_INFO("MusicEditor", "Set ROM on MusicPlayer");
92 } else {
93 LOG_WARN("MusicEditor", "No ROM available for MusicPlayer!");
94 }
95
96 // Inject the main emulator into MusicPlayer
97 if (emulator_) {
98 music_player_->SetEmulator(emulator_);
99 LOG_INFO("MusicEditor", "Injected main emulator into MusicPlayer");
100 } else {
101 LOG_WARN("MusicEditor", "No emulator available to inject into MusicPlayer!");
102 }
103
105 return;
106 auto* panel_manager = dependencies_.panel_manager;
107
108 // Register PanelDescriptors for menu/sidebar visibility
109 panel_manager->RegisterPanel({.card_id = "music.song_browser",
110 .display_name = "Song Browser",
111 .window_title = " Song Browser",
112 .icon = ICON_MD_LIBRARY_MUSIC,
113 .category = "Music",
114 .shortcut_hint = "Ctrl+Shift+B",
115 .priority = 5});
116 panel_manager->RegisterPanel({.card_id = "music.tracker",
117 .display_name = "Playback Control",
118 .window_title = " Playback Control",
119 .icon = ICON_MD_PLAY_CIRCLE,
120 .category = "Music",
121 .shortcut_hint = "Ctrl+Shift+M",
122 .priority = 10});
123 panel_manager->RegisterPanel({.card_id = "music.piano_roll",
124 .display_name = "Piano Roll",
125 .window_title = " Piano Roll",
126 .icon = ICON_MD_PIANO,
127 .category = "Music",
128 .shortcut_hint = "Ctrl+Shift+P",
129 .priority = 15});
130 panel_manager->RegisterPanel({.card_id = "music.instrument_editor",
131 .display_name = "Instrument Editor",
132 .window_title = " Instrument Editor",
133 .icon = ICON_MD_SPEAKER,
134 .category = "Music",
135 .shortcut_hint = "Ctrl+Shift+I",
136 .priority = 20});
137 panel_manager->RegisterPanel({.card_id = "music.sample_editor",
138 .display_name = "Sample Editor",
139 .window_title = " Sample Editor",
140 .icon = ICON_MD_WAVES,
141 .category = "Music",
142 .shortcut_hint = "Ctrl+Shift+S",
143 .priority = 25});
144 panel_manager->RegisterPanel({.card_id = "music.assembly",
145 .display_name = "Assembly View",
146 .window_title = " Music Assembly",
147 .icon = ICON_MD_CODE,
148 .category = "Music",
149 .shortcut_hint = "Ctrl+Shift+A",
150 .priority = 30});
151 panel_manager->RegisterPanel({.card_id = "music.audio_debug",
152 .display_name = "Audio Debug",
153 .window_title = " Audio Debug",
154 .icon = ICON_MD_BUG_REPORT,
155 .category = "Music",
156 .shortcut_hint = "",
157 .priority = 95});
158 panel_manager->RegisterPanel({.card_id = "music.help",
159 .display_name = "Help",
160 .window_title = " Music Editor Help",
161 .icon = ICON_MD_HELP,
162 .category = "Music",
163 .priority = 99});
164
165 // ==========================================================================
166 // Phase 5: Create and register EditorPanel instances
167 // Note: Callbacks are set up on the view classes during Draw() since
168 // PanelManager takes ownership of the panels.
169 // ==========================================================================
170
171 // Song Browser Panel - callbacks are set on song_browser_view_ directly
172 auto song_browser = std::make_unique<MusicSongBrowserPanel>(
174 panel_manager->RegisterEditorPanel(std::move(song_browser));
175
176 // Playback Control Panel
177 auto playback_control = std::make_unique<MusicPlaybackControlPanel>(
179 playback_control->SetOnOpenSong([this](int index) { OpenSong(index); });
180 playback_control->SetOnOpenPianoRoll(
181 [this](int index) { OpenSongPianoRoll(index); });
182 panel_manager->RegisterEditorPanel(std::move(playback_control));
183
184 // Piano Roll Panel
185 auto piano_roll = std::make_unique<MusicPianoRollPanel>(
188 panel_manager->RegisterEditorPanel(std::move(piano_roll));
189
190 // Instrument Editor Panel - callbacks set on instrument_editor_view_
191 auto instrument_editor = std::make_unique<MusicInstrumentEditorPanel>(
193 panel_manager->RegisterEditorPanel(std::move(instrument_editor));
194
195 // Sample Editor Panel - callbacks set on sample_editor_view_
196 auto sample_editor = std::make_unique<MusicSampleEditorPanel>(
198 panel_manager->RegisterEditorPanel(std::move(sample_editor));
199
200 // Assembly Panel
201 auto assembly = std::make_unique<MusicAssemblyPanel>(&assembly_editor_);
202 panel_manager->RegisterEditorPanel(std::move(assembly));
203
204 // Audio Debug Panel
205 auto audio_debug = std::make_unique<MusicAudioDebugPanel>(music_player_.get());
206 panel_manager->RegisterEditorPanel(std::move(audio_debug));
207
208 // Help Panel
209 auto help = std::make_unique<MusicHelpPanel>();
210 panel_manager->RegisterEditorPanel(std::move(help));
211}
212
214 LOG_INFO("MusicEditor", "set_emulator(%p): audio_backend_=%p",
215 static_cast<void*>(emulator), static_cast<void*>(audio_backend_.get()));
217 // Share our audio backend with the main emulator (single backend architecture)
218 if (emulator_ && audio_backend_) {
220 LOG_INFO("MusicEditor", "Shared audio backend with main emulator (deferred)");
221 }
222
223 // Inject emulator into MusicPlayer
224 if (music_player_) {
225 music_player_->SetEmulator(emulator_);
226 }
227}
228
239
240absl::Status MusicEditor::Load() {
241 gfx::ScopedTimer timer("MusicEditor::Load");
242 if (project_) {
245 if (music_storage_key_.empty()) {
247 }
248 }
249
250#ifdef __EMSCRIPTEN__
252 auto restore = RestoreMusicState();
253 if (restore.ok() && restore.value()) {
254 LOG_INFO("MusicEditor", "Restored music state from web storage");
255 return absl::OkStatus();
256 } else if (!restore.ok()) {
257 LOG_WARN("MusicEditor", "Failed to restore music state: %s",
258 restore.status().ToString().c_str());
259 }
260 }
261#endif
262
263 if (rom_) {
264 if (music_player_) {
265 music_player_->SetRom(rom_);
266 LOG_INFO("MusicEditor", "Load(): Set ROM on MusicPlayer, IsAudioReady=%d",
267 music_player_->IsAudioReady());
268 }
270 } else {
271 LOG_WARN("MusicEditor", "Load(): No ROM available!");
272 }
273 return absl::OkStatus();
274}
275
277 if (!music_player_) return;
278 auto state = music_player_->GetState();
279 if (state.is_playing && !state.is_paused) {
280 music_player_->Pause();
281 } else if (state.is_paused) {
282 music_player_->Resume();
283 } else {
284 music_player_->PlaySong(state.playing_song_index);
285 }
286}
287
289 if (music_player_) {
290 music_player_->Stop();
291 }
292}
293
294void MusicEditor::SpeedUp(float delta) {
295 if (music_player_) {
296 auto state = music_player_->GetState();
297 music_player_->SetPlaybackSpeed(state.playback_speed + delta);
298 }
299}
300
301void MusicEditor::SlowDown(float delta) {
302 if (music_player_) {
303 auto state = music_player_->GetState();
304 music_player_->SetPlaybackSpeed(state.playback_speed - delta);
305 }
306}
307
308absl::Status MusicEditor::Update() {
309 // Deferred initialization: Initialize song_window_class_.ClassId on first Update()
310 // because ImGui::GetID() requires a valid window context
311 if (song_window_class_.ClassId == 0) {
312 song_window_class_.ClassId = ImGui::GetID("SongTrackerWindowClass");
313 }
314
315 // Update MusicPlayer - this runs the emulator's audio frame
316 // MusicPlayer now controls the main emulator directly for playback.
317 if (music_player_) music_player_->Update();
318
319#ifdef __EMSCRIPTEN__
322 music_dirty_ = true;
323 }
324 auto now = std::chrono::steady_clock::now();
325 const auto elapsed = now - last_music_persist_;
326 if (music_dirty_ &&
327 (last_music_persist_.time_since_epoch().count() == 0 ||
328 elapsed > std::chrono::seconds(3))) {
329 auto status = PersistMusicState("autosave");
330 if (!status.ok()) {
331 LOG_WARN("MusicEditor", "Music autosave failed: %s",
332 status.ToString().c_str());
333 }
334 }
335 }
336#endif
337
339 return absl::OkStatus();
340 auto* panel_manager = dependencies_.panel_manager;
341
342 // ==========================================================================
343 // Phase 5 Complete: Static panels now drawn by DrawAllVisiblePanels()
344 // Only auto-show logic and dynamic song windows remain here
345 // ==========================================================================
346
347 // Auto-show Song Browser on first load
348 bool* browser_visible = panel_manager->GetVisibilityFlag("music.song_browser");
349 if (browser_visible && !song_browser_auto_shown_) {
350 *browser_visible = true;
352 }
353
354 // Auto-show Playback Control on first load
355 bool* playback_visible = panel_manager->GetVisibilityFlag("music.tracker");
356 if (playback_visible && !tracker_auto_shown_) {
357 *playback_visible = true;
358 tracker_auto_shown_ = true;
359 }
360
361 // Auto-show Piano Roll on first load
362 static bool piano_roll_auto_shown = false;
363 bool* piano_roll_visible = panel_manager->GetVisibilityFlag("music.piano_roll");
364 if (piano_roll_visible && !piano_roll_auto_shown) {
365 *piano_roll_visible = true;
366 piano_roll_auto_shown = true;
367 }
368
369 // ==========================================================================
370 // Dynamic Per-Song Windows (like dungeon room cards)
371 // TODO(Phase 6): Migrate to ResourcePanel with LRU limits
372 // ==========================================================================
373
374 // Per-Song Tracker Windows - synced with PanelManager for Activity Bar
375 for (int i = 0; i < active_songs_.Size; i++) {
376 int song_index = active_songs_[i];
377 // Use base ID - PanelManager handles session prefixing
378 std::string card_id = absl::StrFormat("music.song_%d", song_index);
379
380 // Check if panel was hidden via Activity Bar
381 bool panel_visible = true;
383 panel_visible = dependencies_.panel_manager->IsPanelVisible(card_id);
384 }
385
386 // If hidden via Activity Bar, close the song
387 if (!panel_visible) {
390 }
391 song_cards_.erase(song_index);
392 song_trackers_.erase(song_index);
393 active_songs_.erase(active_songs_.Data + i);
394 i--;
395 continue;
396 }
397
398 // Category filtering: only draw if Music is active OR panel is pinned
399 bool is_pinned = dependencies_.panel_manager &&
401 std::string active_category = dependencies_.panel_manager ?
403
404 if (active_category != "Music" && !is_pinned) {
405 // Not in Music editor and not pinned - skip drawing but keep registered
406 // Panel will reappear when user returns to Music editor
407 continue;
408 }
409
410 bool open = true;
411
412 // Get song name for window title (icon is handled by EditorPanel)
413 auto* song = music_bank_.GetSong(song_index);
414 std::string song_name = song ? song->name : "Unknown";
415 std::string card_title =
416 absl::StrFormat("[%02X] %s###SongTracker%d",
417 song_index + 1, song_name, song_index);
418
419 // Create card instance if needed
420 if (song_cards_.find(song_index) == song_cards_.end()) {
421 song_cards_[song_index] =
422 std::make_shared<gui::PanelWindow>(card_title.c_str(), ICON_MD_MUSIC_NOTE, &open);
423 song_cards_[song_index]->SetDefaultSize(900, 700);
424
425 // Create dedicated tracker view for this song
426 song_trackers_[song_index] = std::make_unique<editor::music::TrackerView>();
427 song_trackers_[song_index]->SetOnEditCallback([this]() { PushUndoState(); });
428 }
429
430 auto& song_card = song_cards_[song_index];
431
432 // Use docking class to group song windows together
433 ImGui::SetNextWindowClass(&song_window_class_);
434
435 if (song_card->Begin(&open)) {
436 DrawSongTrackerWindow(song_index);
437 }
438 song_card->End();
439
440 // Handle close button
441 if (!open) {
442 // Unregister from PanelManager
445 }
446 song_cards_.erase(song_index);
447 song_trackers_.erase(song_index);
448 active_songs_.erase(active_songs_.Data + i);
449 i--;
450 }
451 }
452
453 // Per-song piano roll windows - synced with PanelManager for Activity Bar
454 for (auto it = song_piano_rolls_.begin(); it != song_piano_rolls_.end();) {
455 int song_index = it->first;
456 auto& window = it->second;
457 auto* song = music_bank_.GetSong(song_index);
458 // Use base ID - PanelManager handles session prefixing
459 std::string card_id = absl::StrFormat("music.piano_roll_%d", song_index);
460
461 if (!song || !window.card || !window.view) {
464 }
465 it = song_piano_rolls_.erase(it);
466 continue;
467 }
468
469 // Check if panel was hidden via Activity Bar
470 bool panel_visible = true;
472 panel_visible = dependencies_.panel_manager->IsPanelVisible(card_id);
473 }
474
475 // If hidden via Activity Bar, close the piano roll
476 if (!panel_visible) {
479 }
480 delete window.visible_flag;
481 it = song_piano_rolls_.erase(it);
482 continue;
483 }
484
485 // Category filtering: only draw if Music is active OR panel is pinned
486 bool is_pinned = dependencies_.panel_manager &&
488 std::string active_category = dependencies_.panel_manager ?
490
491 if (active_category != "Music" && !is_pinned) {
492 // Not in Music editor and not pinned - skip drawing but keep registered
493 ++it;
494 continue;
495 }
496
497 bool open = true;
498
499 // Use same docking class as tracker windows so they can dock together
500 ImGui::SetNextWindowClass(&song_window_class_);
501
502 if (window.card->Begin(&open)) {
503 window.view->SetOnEditCallback([this]() { PushUndoState(); });
504 window.view->SetOnNotePreview(
505 [this, song_index](const zelda3::music::TrackEvent& evt,
506 int segment_idx, int channel_idx) {
507 auto* target = music_bank_.GetSong(song_index);
508 if (!target || !music_player_) return;
509 music_player_->PreviewNote(*target, evt, segment_idx, channel_idx);
510 });
511 window.view->SetOnSegmentPreview(
512 [this, song_index](const zelda3::music::MusicSong& /*unused*/,
513 int segment_idx) {
514 auto* target = music_bank_.GetSong(song_index);
515 if (!target || !music_player_) return;
516 music_player_->PreviewSegment(*target, segment_idx);
517 });
518 // Update playback state for cursor visualization
519 auto state = music_player_ ? music_player_->GetState() : editor::music::PlaybackState{};
520 window.view->SetPlaybackState(state.is_playing, state.is_paused,
521 state.current_tick);
522 window.view->Draw(song);
523 }
524 window.card->End();
525
526 if (!open) {
527 // Unregister from PanelManager
530 }
531 delete window.visible_flag;
532 it = song_piano_rolls_.erase(it);
533 } else {
534 ++it;
535 }
536 }
537
539
540 return absl::OkStatus();
541}
542
543absl::Status MusicEditor::Save() {
544 if (!rom_) return absl::FailedPreconditionError("No ROM loaded");
546
547#ifdef __EMSCRIPTEN__
548 auto persist_status = PersistMusicState("save");
549 if (!persist_status.ok()) {
550 return persist_status;
551 }
552#endif
553
554 return absl::OkStatus();
555}
556
557absl::StatusOr<bool> MusicEditor::RestoreMusicState() {
558#ifdef __EMSCRIPTEN__
559 if (music_storage_key_.empty()) {
560 return false;
561 }
562
563 auto storage_or =
564 platform::WasmStorage::LoadProject(music_storage_key_);
565 if (!storage_or.ok()) {
566 return false; // Nothing persisted yet
567 }
568
569 try {
570 auto parsed = nlohmann::json::parse(storage_or.value());
572 music_dirty_ = false;
573 last_music_persist_ = std::chrono::steady_clock::now();
574 return true;
575 } catch (const std::exception& e) {
576 return absl::InvalidArgumentError(
577 absl::StrFormat("Failed to parse stored music state: %s", e.what()));
578 }
579#else
580 return false;
581#endif
582}
583
584absl::Status MusicEditor::PersistMusicState(const char* reason) {
585#ifdef __EMSCRIPTEN__
587 return absl::OkStatus();
588 }
589
590 auto serialized = music_bank_.ToJson().dump();
592 platform::WasmStorage::SaveProject(music_storage_key_, serialized));
593
594 if (project_) {
595 auto now = std::chrono::system_clock::now();
596 auto time_t = std::chrono::system_clock::to_time_t(now);
597 std::stringstream ss;
598 ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
601 }
602
603 music_dirty_ = false;
604 last_music_persist_ = std::chrono::steady_clock::now();
605 if (reason) {
606 LOG_DEBUG("MusicEditor", "Persisted music state (%s)", reason);
607 }
608 return absl::OkStatus();
609#else
610 (void)reason;
611 return absl::OkStatus();
612#endif
613}
614
616
617absl::Status MusicEditor::Cut() {
618 Copy();
619 // In a real implementation, this would delete the selected events
620 // TrackerView::DeleteSelection();
622 return absl::OkStatus();
623}
624
625absl::Status MusicEditor::Copy() {
626 // TODO: Serialize selected events to clipboard
627 // TrackerView should expose a GetSelection() method
628 return absl::UnimplementedError("Copy not yet implemented - clipboard support coming soon");
629}
630
631absl::Status MusicEditor::Paste() {
632 // TODO: Paste from clipboard
633 // Need to deserialize events and insert at cursor position
634 return absl::UnimplementedError("Paste not yet implemented - clipboard support coming soon");
635}
636
637absl::Status MusicEditor::Undo() {
638 if (undo_stack_.empty()) return absl::FailedPreconditionError("Nothing to undo");
639
640 // Save current state to redo stack
641 UndoState current_state;
642 if (auto* song = music_bank_.GetSong(current_song_index_)) {
643 current_state.song_snapshot = *song;
644 current_state.song_index = current_song_index_;
645 redo_stack_.push_back(current_state);
646 }
647
649 undo_stack_.pop_back();
650 return absl::OkStatus();
651}
652
653absl::Status MusicEditor::Redo() {
654 if (redo_stack_.empty()) return absl::FailedPreconditionError("Nothing to redo");
655
656 // Save current state to undo stack
657 UndoState current_state;
658 if (auto* song = music_bank_.GetSong(current_song_index_)) {
659 current_state.song_snapshot = *song;
660 current_state.song_index = current_song_index_;
661 undo_stack_.push_back(current_state);
662 }
663
665 redo_stack_.pop_back();
666 return absl::OkStatus();
667}
668
671 if (!song) return;
672
673 UndoState state;
674 state.song_snapshot = *song;
676 undo_stack_.push_back(state);
678
679 // Limit undo stack size to prevent unbounded memory growth
680 constexpr size_t kMaxUndoStates = 50;
681 while (undo_stack_.size() > kMaxUndoStates) {
682 undo_stack_.erase(undo_stack_.begin());
683 }
684
685 // Clear redo stack on new action
686 redo_stack_.clear();
687}
688
690 // Ensure we are on the correct song
691 if (state.song_index >= 0 && state.song_index < static_cast<int>(music_bank_.GetSongCount())) {
693 // This is a heavy copy, but safe for now
696 }
697}
698
707
708void MusicEditor::OpenSong(int song_index) {
709 // Update current selection
710 current_song_index_ = song_index;
712
713 // Check if already open
714 for (int i = 0; i < active_songs_.Size; i++) {
715 if (active_songs_[i] == song_index) {
716 // Focus the existing window
717 FocusSong(song_index);
718 return;
719 }
720 }
721
722 // Add new song to active list
723 active_songs_.push_back(song_index);
724
725 // Register with PanelManager so it appears in Activity Bar
727 auto* song = music_bank_.GetSong(song_index);
728 std::string song_name = song ? song->name : absl::StrFormat("Song %02X", song_index);
729 // Use base ID - RegisterPanel handles session prefixing
730 std::string card_id = absl::StrFormat("music.song_%d", song_index);
731
733 {.card_id = card_id,
734 .display_name = song_name,
735 .window_title = ICON_MD_MUSIC_NOTE " " + song_name,
736 .icon = ICON_MD_MUSIC_NOTE,
737 .category = "Music",
738 .shortcut_hint = "",
739 .visibility_flag = nullptr,
740 .priority = 200 + song_index});
741
743
744 // NOT auto-pinned - user must explicitly pin to persist across editors
745 }
746
747 LOG_INFO("MusicEditor", "Opened song %d tracker window", song_index);
748}
749
750void MusicEditor::FocusSong(int song_index) {
751 auto it = song_cards_.find(song_index);
752 if (it != song_cards_.end()) {
753 it->second->Focus();
754 }
755}
756
757void MusicEditor::OpenSongPianoRoll(int song_index) {
758 if (song_index < 0 ||
759 song_index >= static_cast<int>(music_bank_.GetSongCount())) {
760 return;
761 }
762
763 auto it = song_piano_rolls_.find(song_index);
764 if (it != song_piano_rolls_.end()) {
765 if (it->second.card && it->second.visible_flag) {
766 *it->second.visible_flag = true;
767 it->second.card->Focus();
768 }
769 return;
770 }
771
772 auto* song = music_bank_.GetSong(song_index);
773 std::string song_name = song ? song->name : absl::StrFormat("Song %02X", song_index);
774 std::string card_title = absl::StrFormat(
775 "[%02X] %s - Piano Roll###SongPianoRoll%d", song_index + 1,
776 song_name, song_index);
777
778 SongPianoRollWindow window;
779 window.visible_flag = new bool(true);
780 window.card =
781 std::make_shared<gui::PanelWindow>(card_title.c_str(), ICON_MD_PIANO,
782 window.visible_flag);
783 window.card->SetDefaultSize(900, 450);
784 window.view = std::make_unique<editor::music::PianoRollView>();
785 window.view->SetActiveChannel(0);
786 window.view->SetActiveSegment(0);
787
788 song_piano_rolls_[song_index] = std::move(window);
789
790 // Register with PanelManager so it appears in Activity Bar
792 // Use base ID - RegisterPanel handles session prefixing
793 std::string card_id = absl::StrFormat("music.piano_roll_%d", song_index);
794
796 {.card_id = card_id,
797 .display_name = song_name + " (Piano)",
798 .window_title = ICON_MD_PIANO " " + song_name + " (Piano)",
799 .icon = ICON_MD_PIANO,
800 .category = "Music",
801 .shortcut_hint = "",
802 .visibility_flag = nullptr,
803 .priority = 250 + song_index});
804
806 // NOT auto-pinned - user must explicitly pin to persist across editors
807 }
808}
809
811 auto* song = music_bank_.GetSong(song_index);
812 if (!song) {
813 ImGui::TextDisabled("Song not loaded");
814 return;
815 }
816
817 // Compact toolbar for this song window
818 bool can_play = music_player_ && music_player_->IsAudioReady();
819 auto state = music_player_ ? music_player_->GetState() : editor::music::PlaybackState{};
820 bool is_playing_this_song = state.is_playing && (state.playing_song_index == song_index);
821 bool is_paused_this_song = state.is_paused && (state.playing_song_index == song_index);
822
823 // === Row 1: Playback Transport ===
824 if (!can_play) ImGui::BeginDisabled();
825
826 // Play/Pause button with status indication
827 if (is_playing_this_song && !is_paused_this_song) {
828 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
829 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.3f, 1.0f));
830 if (ImGui::Button(ICON_MD_PAUSE " Pause")) {
831 music_player_->Pause();
832 }
833 ImGui::PopStyleColor(2);
834 } else if (is_paused_this_song) {
835 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.2f, 1.0f));
836 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.6f, 0.6f, 0.3f, 1.0f));
837 if (ImGui::Button(ICON_MD_PLAY_ARROW " Resume")) {
838 music_player_->Resume();
839 }
840 ImGui::PopStyleColor(2);
841 } else {
842 if (ImGui::Button(ICON_MD_PLAY_ARROW " Play")) {
843 music_player_->PlaySong(song_index);
844 }
845 }
846
847 ImGui::SameLine();
848 if (ImGui::Button(ICON_MD_STOP)) {
849 music_player_->Stop();
850 }
851 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Stop playback");
852
853 if (!can_play) ImGui::EndDisabled();
854
855 // Keyboard shortcuts (when window is focused)
856 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && can_play) {
857 // Focused-window shortcuts remain as fallbacks; also registered with ShortcutManager.
858 if (ImGui::IsKeyPressed(ImGuiKey_Space, false)) {
860 }
861 if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
862 StopPlayback();
863 }
864 if (ImGui::IsKeyPressed(ImGuiKey_Equal, false) ||
865 ImGui::IsKeyPressed(ImGuiKey_KeypadAdd, false)) {
866 SpeedUp();
867 }
868 if (ImGui::IsKeyPressed(ImGuiKey_Minus, false) ||
869 ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract, false)) {
870 SlowDown();
871 }
872 }
873
874 // Status indicator
875 ImGui::SameLine();
876 if (is_playing_this_song && !is_paused_this_song) {
877 ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f), ICON_MD_GRAPHIC_EQ);
878 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Playing");
879 } else if (is_paused_this_song) {
880 ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.3f, 1.0f), ICON_MD_PAUSE_CIRCLE);
881 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Paused");
882 }
883
884 // Right side controls
885 float right_offset = ImGui::GetWindowWidth() - 200;
886 ImGui::SameLine(right_offset);
887
888 // Speed control (with mouse wheel support)
889 ImGui::Text(ICON_MD_SPEED);
890 ImGui::SameLine();
891 ImGui::SetNextItemWidth(55);
892 float speed = state.playback_speed;
893 if (gui::SliderFloatWheel("##Speed", &speed, 0.25f, 2.0f, "%.1fx",
894 0.1f)) {
895 if (music_player_) {
896 music_player_->SetPlaybackSpeed(speed);
897 }
898 }
899 if (ImGui::IsItemHovered())
900 ImGui::SetTooltip("Playback speed (0.25x - 2.0x) - use mouse wheel");
901
902 ImGui::SameLine();
903 if (ImGui::Button(ICON_MD_PIANO)) {
904 OpenSongPianoRoll(song_index);
905 }
906 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open Piano Roll view");
907
908 // === Row 2: Song Info ===
909 const char* bank_name = nullptr;
910 switch (song->bank) {
911 case 0: bank_name = "Overworld"; break;
912 case 1: bank_name = "Dungeon"; break;
913 case 2: bank_name = "Credits"; break;
914 case 3: bank_name = "Expanded"; break;
915 case 4: bank_name = "Auxiliary"; break;
916 default: bank_name = "Unknown"; break;
917 }
918 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "[%02X]", song_index + 1);
919 ImGui::SameLine();
920 ImGui::Text("%s", song->name.c_str());
921 ImGui::SameLine();
922 ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.7f, 1.0f), "(%s)", bank_name);
923
924 if (song->modified) {
925 ImGui::SameLine();
926 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), ICON_MD_EDIT " Modified");
927 }
928
929 // Segment count
930 ImGui::SameLine(right_offset);
931 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
932 "%zu segments", song->segments.size());
933
934 ImGui::Separator();
935
936 // Channel overview shows DSP state when playing
937 if (is_playing_this_song) {
939 ImGui::Separator();
940 }
941
942 // Draw the tracker view for this specific song
943 auto it = song_trackers_.find(song_index);
944 if (it != song_trackers_.end()) {
945 it->second->Draw(song, &music_bank_);
946 } else {
947 // Fallback - shouldn't happen but just in case
949 }
950}
951
952// Playback Control panel - focused on audio playback and current song status
954 DrawToolset();
955
956 ImGui::Separator();
957
958 // Current song info
960 auto state = music_player_ ? music_player_->GetState() : editor::music::PlaybackState{};
961
962 if (song) {
963 ImGui::Text("Selected Song:");
964 ImGui::SameLine();
965 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "[%02X] %s",
966 current_song_index_ + 1, song->name.c_str());
967
968 // Song details
969 ImGui::SameLine();
970 ImGui::TextDisabled("| %zu segments", song->segments.size());
971 if (song->modified) {
972 ImGui::SameLine();
973 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), ICON_MD_EDIT " Modified");
974 }
975 }
976
977 // Playback status bar
978 if (state.is_playing || state.is_paused) {
979 ImGui::Separator();
980
981 // Timeline progress
982 if (song && !song->segments.empty()) {
983 uint32_t total_duration = 0;
984 for (const auto& seg : song->segments) {
985 total_duration += seg.GetDuration();
986 }
987
988 float progress = (total_duration > 0)
989 ? static_cast<float>(state.current_tick) / total_duration
990 : 0.0f;
991 progress = std::clamp(progress, 0.0f, 1.0f);
992
993 // Time display
994 float current_seconds = state.ticks_per_second > 0
995 ? state.current_tick / state.ticks_per_second
996 : 0.0f;
997 float total_seconds = state.ticks_per_second > 0
998 ? total_duration / state.ticks_per_second
999 : 0.0f;
1000
1001 int cur_min = static_cast<int>(current_seconds) / 60;
1002 int cur_sec = static_cast<int>(current_seconds) % 60;
1003 int tot_min = static_cast<int>(total_seconds) / 60;
1004 int tot_sec = static_cast<int>(total_seconds) % 60;
1005
1006 ImGui::Text("%d:%02d / %d:%02d", cur_min, cur_sec, tot_min, tot_sec);
1007 ImGui::SameLine();
1008
1009 // Progress bar
1010 ImGui::ProgressBar(progress, ImVec2(-1, 0), "");
1011 }
1012
1013 // Segment info
1014 ImGui::Text("Segment: %d | Tick: %u",
1015 state.current_segment_index + 1, state.current_tick);
1016 ImGui::SameLine();
1017 ImGui::TextDisabled("| %.1f ticks/sec | %.2fx speed",
1018 state.ticks_per_second, state.playback_speed);
1019 }
1020
1021 // Channel overview when playing
1022 if (state.is_playing) {
1023 ImGui::Separator();
1025 }
1026
1027 ImGui::Separator();
1028
1029 // Quick action buttons
1030 if (ImGui::Button(ICON_MD_OPEN_IN_NEW " Open Tracker")) {
1032 }
1033 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open song in dedicated tracker window");
1034
1035 ImGui::SameLine();
1036 if (ImGui::Button(ICON_MD_PIANO " Open Piano Roll")) {
1038 }
1039 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open piano roll view for this song");
1040
1041 // Help section (collapsed by default)
1042 if (ImGui::CollapsingHeader(ICON_MD_KEYBOARD " Keyboard Shortcuts")) {
1043 ImGui::BulletText("Space: Play/Pause toggle");
1044 ImGui::BulletText("Escape: Stop playback");
1045 ImGui::BulletText("+/-: Increase/decrease speed");
1046 ImGui::BulletText("Arrow keys: Navigate in tracker/piano roll");
1047 ImGui::BulletText("Z,S,X,D,C,V,G,B,H,N,J,M: Piano keyboard (C to B)");
1048 ImGui::BulletText("Ctrl+Wheel: Zoom (Piano Roll)");
1049 }
1050}
1051
1052// Legacy DrawTrackerView for compatibility (calls the tracker view directly)
1057
1060 if (song && current_segment_index_ >=
1061 static_cast<int>(song->segments.size())) {
1063 }
1064
1069 [this, song_index = current_song_index_](
1070 const zelda3::music::TrackEvent& evt, int segment_idx,
1071 int channel_idx) {
1072 auto* target = music_bank_.GetSong(song_index);
1073 if (!target || !music_player_) return;
1074 music_player_->PreviewNote(*target, evt, segment_idx, channel_idx);
1075 });
1077 [this, song_index = current_song_index_](
1078 const zelda3::music::MusicSong& /*unused*/, int segment_idx) {
1079 auto* target = music_bank_.GetSong(song_index);
1080 if (!target || !music_player_) return;
1081 music_player_->PreviewSegment(*target, segment_idx);
1082 });
1083
1084 // Update playback state for cursor visualization
1085 auto state = music_player_ ? music_player_->GetState() : editor::music::PlaybackState{};
1086 piano_roll_view_.SetPlaybackState(state.is_playing, state.is_paused, state.current_tick);
1087
1091}
1092
1096
1100
1102 static int current_volume = 100;
1103 auto state = music_player_ ? music_player_->GetState() : editor::music::PlaybackState{};
1104 bool can_play = music_player_ && music_player_->IsAudioReady();
1105
1106 // Row 1: Transport controls and song info
1108
1109 if (!can_play) ImGui::BeginDisabled();
1110
1111 // Transport: Play/Pause with visual state indication
1112 const ImVec4 paused_color(0.9f, 0.7f, 0.2f, 1.0f);
1113
1114 if (state.is_playing && !state.is_paused) {
1115 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
1116 if (ImGui::Button(ICON_MD_PAUSE "##Pause")) music_player_->Pause();
1117 ImGui::PopStyleColor();
1118 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Pause (Space)");
1119 } else if (state.is_paused) {
1120 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.4f, 0.1f, 1.0f));
1121 if (ImGui::Button(ICON_MD_PLAY_ARROW "##Resume")) music_player_->Resume();
1122 ImGui::PopStyleColor();
1123 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Resume (Space)");
1124 } else {
1125 if (ImGui::Button(ICON_MD_PLAY_ARROW "##Play")) music_player_->PlaySong(current_song_index_);
1126 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Play (Space)");
1127 }
1128
1129 ImGui::SameLine();
1130 if (ImGui::Button(ICON_MD_STOP "##Stop")) music_player_->Stop();
1131 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Stop (Escape)");
1132
1133 if (!can_play) ImGui::EndDisabled();
1134
1135 // Song label with animated playing indicator
1136 ImGui::SameLine();
1137 if (song) {
1138 if (state.is_playing && !state.is_paused) {
1139 // Animated playing indicator
1140 float t = static_cast<float>(ImGui::GetTime() * 3.0);
1141 float alpha = 0.5f + 0.5f * std::sin(t);
1142 ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, alpha), ICON_MD_GRAPHIC_EQ);
1143 ImGui::SameLine();
1144 } else if (state.is_paused) {
1145 ImGui::TextColored(paused_color, ICON_MD_PAUSE_CIRCLE);
1146 ImGui::SameLine();
1147 }
1148 ImGui::Text("%s", song->name.c_str());
1149 if (song->modified) {
1150 ImGui::SameLine();
1151 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), ICON_MD_EDIT);
1152 }
1153 } else {
1154 ImGui::TextDisabled("No song selected");
1155 }
1156
1157 // Time display (when playing)
1158 if (state.is_playing || state.is_paused) {
1159 ImGui::SameLine();
1160 float seconds = state.ticks_per_second > 0
1161 ? state.current_tick / state.ticks_per_second
1162 : 0.0f;
1163 int mins = static_cast<int>(seconds) / 60;
1164 int secs = static_cast<int>(seconds) % 60;
1165 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.8f, 1.0f), " %d:%02d", mins, secs);
1166 }
1167
1168 // Right-aligned controls
1169 float right_offset = ImGui::GetWindowWidth() - 380;
1170 ImGui::SameLine(right_offset);
1171
1172 // Speed control with visual feedback
1173 ImGui::Text(ICON_MD_SPEED);
1174 ImGui::SameLine();
1175 ImGui::SetNextItemWidth(70);
1176 float speed = state.playback_speed;
1177 if (gui::SliderFloatWheel("##Speed", &speed, 0.25f, 2.0f, "%.2fx", 0.1f)) {
1178 if (music_player_) {
1179 music_player_->SetPlaybackSpeed(speed);
1180 }
1181 }
1182 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Playback speed (+/- keys)");
1183
1184 ImGui::SameLine();
1185 ImGui::Text(ICON_MD_VOLUME_UP);
1186 ImGui::SameLine();
1187 ImGui::SetNextItemWidth(60);
1188 if (gui::SliderIntWheel("##Vol", &current_volume, 0, 100, "%d%%", 5)) {
1189 if (music_player_) music_player_->SetVolume(current_volume / 100.0f);
1190 }
1191 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Volume");
1192
1193 ImGui::SameLine();
1194 if (ImGui::Button(ICON_MD_REFRESH)) {
1196 song_names_.clear();
1197 }
1198 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reload from ROM");
1199
1200 // Interpolation Control
1201 ImGui::SameLine();
1202 ImGui::SetNextItemWidth(100);
1203 {
1204 static int interpolation_type = 2; // Default: Gaussian
1205 const char* items[] = {"Linear", "Hermite", "Gaussian", "Cosine", "Cubic"};
1206 if (ImGui::Combo("##Interp", &interpolation_type, items, IM_ARRAYSIZE(items))) {
1207 if (music_player_) music_player_->SetInterpolationType(interpolation_type);
1208 }
1209 if (ImGui::IsItemHovered()) ImGui::SetTooltip("Audio interpolation quality\nGaussian = authentic SNES sound");
1210 }
1211
1212 ImGui::Separator();
1213
1214 // Mixer / Visualizer Panel
1215 if (ImGui::BeginTable("MixerPanel", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
1216 // Channel Headers
1217 ImGui::TableSetupColumn("Master", ImGuiTableColumnFlags_WidthFixed, 60.0f);
1218 for (int i = 0; i < 8; i++) {
1219 ImGui::TableSetupColumn(absl::StrFormat("Ch %d", i + 1).c_str());
1220 }
1221 ImGui::TableHeadersRow();
1222
1223 ImGui::TableNextRow();
1224
1225 // Master Oscilloscope (Column 0)
1226 ImGui::TableSetColumnIndex(0);
1227 // Use MusicPlayer's emulator for visualization
1228 emu::Emulator* audio_emu = music_player_ ? music_player_->emulator() : nullptr;
1229 if (audio_emu && audio_emu->is_snes_initialized()) {
1230 auto& dsp = audio_emu->snes().apu().dsp();
1231
1232 ImGui::Text("Scope");
1233
1234 // Oscilloscope
1235 const int16_t* buffer = dsp.GetSampleBuffer();
1236 uint16_t offset = dsp.GetSampleOffset();
1237
1238 static float scope_values[128];
1239 // Handle ring buffer wrap-around correctly (buffer size is 0x400 samples)
1240 constexpr int kBufferSize = 0x400;
1241 for (int i = 0; i < 128; i++) {
1242 int sample_idx = ((offset - 128 + i + kBufferSize) & (kBufferSize - 1));
1243 scope_values[i] = static_cast<float>(buffer[sample_idx * 2]) / 32768.0f; // Left channel
1244 }
1245
1246 ImGui::PlotLines("##Scope", scope_values, 128, 0, nullptr, -1.0f, 1.0f, ImVec2(50, 60));
1247 }
1248
1249 // Channel Strips (Columns 1-8)
1250 for (int i = 0; i < 8; i++) {
1251 ImGui::TableSetColumnIndex(i + 1);
1252
1253 if (audio_emu && audio_emu->is_snes_initialized()) {
1254 auto& dsp = audio_emu->snes().apu().dsp();
1255 const auto& ch = dsp.GetChannel(i);
1256
1257 // Mute/Solo Buttons
1258 bool is_muted = dsp.GetChannelMute(i);
1259 bool is_solo = channel_soloed_[i];
1260 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
1261
1262 if (is_muted) {
1263 ImGui::PushStyleColor(ImGuiCol_Button, gui::ConvertColorToImVec4(theme.error));
1264 }
1265 if (ImGui::Button(absl::StrFormat("M##%d", i).c_str(), ImVec2(25, 20))) {
1266 dsp.SetChannelMute(i, !is_muted);
1267 }
1268 if (is_muted) ImGui::PopStyleColor();
1269
1270 ImGui::SameLine();
1271
1272 if (is_solo) {
1273 ImGui::PushStyleColor(ImGuiCol_Button, gui::ConvertColorToImVec4(theme.warning));
1274 }
1275 if (ImGui::Button(absl::StrFormat("S##%d", i).c_str(), ImVec2(25, 20))) {
1277
1278 bool any_solo = false;
1279 for(int j=0; j<8; j++) if(channel_soloed_[j]) any_solo = true;
1280
1281 for(int j=0; j<8; j++) {
1282 if (any_solo) {
1283 dsp.SetChannelMute(j, !channel_soloed_[j]);
1284 } else {
1285 dsp.SetChannelMute(j, false);
1286 }
1287 }
1288 }
1289 if (is_solo) ImGui::PopStyleColor();
1290
1291 // VU Meter
1292 float level = std::abs(ch.sampleOut) / 32768.0f;
1293 ImGui::ProgressBar(level, ImVec2(-1, 60), "");
1294
1295 // Info
1296 ImGui::Text("Vol: %d %d", ch.volumeL, ch.volumeR);
1297 ImGui::Text("Pitch: %04X", ch.pitch);
1298
1299 // Key On Indicator
1300 if (ch.keyOn) {
1301 ImGui::TextColored(gui::ConvertColorToImVec4(theme.success), "KEY ON");
1302 } else {
1303 ImGui::TextDisabled("---");
1304 }
1305 } else {
1306 ImGui::TextDisabled("Offline");
1307 }
1308 }
1309
1310 ImGui::EndTable();
1311 }
1312
1313 // Quick audio status (detailed debug in Audio Debug panel)
1314 if (ImGui::CollapsingHeader(ICON_MD_BUG_REPORT " Audio Status")) {
1315 emu::Emulator* debug_emu = music_player_ ? music_player_->emulator() : nullptr;
1316 if (debug_emu && debug_emu->is_snes_initialized()) {
1317 auto* audio_backend = debug_emu->audio_backend();
1318 if (audio_backend) {
1319 auto status = audio_backend->GetStatus();
1320 auto config = audio_backend->GetConfig();
1321 bool resampling = audio_backend->IsAudioStreamEnabled();
1322
1323 // Compact status line
1324 ImGui::Text("Backend: %s @ %dHz | Queue: %u frames",
1325 audio_backend->GetBackendName().c_str(),
1326 config.sample_rate, status.queued_frames);
1327
1328 // Resampling indicator with warning if disabled
1329 if (resampling) {
1330 ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f),
1331 "Resampling: 32040 -> %d Hz", config.sample_rate);
1332 } else {
1333 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
1334 ICON_MD_WARNING " Resampling DISABLED - 1.5x speed bug!");
1335 }
1336
1337 if (status.has_underrun) {
1338 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
1339 ICON_MD_WARNING " Buffer underrun");
1340 }
1341
1342 ImGui::TextDisabled("Open Audio Debug panel for full diagnostics");
1343 }
1344 } else {
1345 ImGui::TextDisabled("Play a song to see audio status");
1346 }
1347 }
1348}
1349
1351 if (!music_player_) {
1352 ImGui::TextDisabled("Music player not initialized");
1353 return;
1354 }
1355
1356 // Check if audio emulator is initialized (created on first play)
1357 auto* audio_emu = music_player_->emulator();
1358 if (!audio_emu || !audio_emu->is_snes_initialized()) {
1359 ImGui::TextDisabled("Play a song to see channel activity");
1360 return;
1361 }
1362
1363 // Check available space to avoid ImGui table assertion
1364 ImVec2 avail = ImGui::GetContentRegionAvail();
1365 if (avail.y < 50.0f) {
1366 ImGui::TextDisabled("(Channel view - expand for details)");
1367 return;
1368 }
1369
1370 auto channel_states = music_player_->GetChannelStates();
1371
1372 if (ImGui::BeginTable("ChannelOverview", 9,
1373 ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
1374 ImGui::TableSetupColumn("Master", ImGuiTableColumnFlags_WidthFixed, 70.0f);
1375 for (int i = 0; i < 8; i++) {
1376 ImGui::TableSetupColumn(absl::StrFormat("Ch %d", i + 1).c_str());
1377 }
1378 ImGui::TableHeadersRow();
1379
1380 ImGui::TableNextRow();
1381
1382 ImGui::TableSetColumnIndex(0);
1383 ImGui::Text("DSP Live");
1384
1385 for (int ch = 0; ch < 8; ++ch) {
1386 ImGui::TableSetColumnIndex(ch + 1);
1387 const auto& state = channel_states[ch];
1388
1389 // Visual indicator for Key On
1390 if (state.key_on) {
1391 ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.2f, 1.0f), "ON");
1392 } else {
1393 ImGui::TextDisabled("OFF");
1394 }
1395
1396 // Volume bars
1397 float vol_l = state.volume_l / 128.0f;
1398 float vol_r = state.volume_r / 128.0f;
1399 ImGui::ProgressBar(vol_l, ImVec2(-1, 6.0f), "");
1400 ImGui::ProgressBar(vol_r, ImVec2(-1, 6.0f), "");
1401
1402 // Info
1403 ImGui::Text("S: %02X", state.sample_index);
1404 ImGui::Text("P: %04X", state.pitch);
1405
1406 // ADSR State
1407 const char* adsr_str = "???";
1408 switch (state.adsr_state) {
1409 case 0: adsr_str = "Att"; break;
1410 case 1: adsr_str = "Dec"; break;
1411 case 2: adsr_str = "Sus"; break;
1412 case 3: adsr_str = "Rel"; break;
1413 }
1414 ImGui::Text("%s", adsr_str);
1415 }
1416
1417 ImGui::EndTable();
1418 }
1419}
1420
1421// ============================================================================
1422// Audio Control Methods (Emulator Integration)
1423// ============================================================================
1424
1425void MusicEditor::SeekToSegment(int segment_index) {
1426 if (music_player_) music_player_->SeekToSegment(segment_index);
1427}
1428
1429// ============================================================================
1430// ASM Export/Import
1431// ============================================================================
1432
1433void MusicEditor::ExportSongToAsm(int song_index) {
1434 auto* song = music_bank_.GetSong(song_index);
1435 if (!song) {
1436 LOG_WARN("MusicEditor", "ExportSongToAsm: Invalid song index %d", song_index);
1437 return;
1438 }
1439
1440 // Configure export options
1442 options.label_prefix = song->name;
1443 // Remove spaces and special characters from label
1444 std::replace(options.label_prefix.begin(), options.label_prefix.end(), ' ', '_');
1445 options.include_comments = true;
1446 options.use_instrument_macros = true;
1447
1448 // Set ARAM address based on bank
1449 if (music_bank_.IsExpandedSong(song_index)) {
1451 } else {
1453 }
1454
1455 // Export to string
1457 auto result = exporter.ExportSong(*song, options);
1458 if (!result.ok()) {
1459 LOG_ERROR("MusicEditor", "ExportSongToAsm failed: %s",
1460 result.status().message().data());
1461 return;
1462 }
1463
1464 // For now, copy to assembly editor buffer
1465 // TODO: Add native file dialog for export path selection
1466 asm_buffer_ = *result;
1468
1469 LOG_INFO("MusicEditor", "Exported song '%s' to ASM (%zu bytes)",
1470 song->name.c_str(), asm_buffer_.size());
1471}
1472
1474 asm_import_target_index_ = song_index;
1475
1476 // If no source is present, open the import dialog for user input
1477 if (asm_buffer_.empty()) {
1478 LOG_INFO("MusicEditor", "No ASM source to import - showing import dialog");
1479 asm_import_error_.clear();
1481 return;
1482 }
1483
1484 // Attempt immediate import using existing buffer
1485 if (!ImportAsmBufferToSong(song_index)) {
1487 return;
1488 }
1489
1490 show_asm_import_popup_ = false;
1492}
1493
1495 auto* song = music_bank_.GetSong(song_index);
1496 if (!song) {
1497 asm_import_error_ = absl::StrFormat("Invalid song index %d", song_index);
1498 LOG_WARN("MusicEditor", "%s", asm_import_error_.c_str());
1499 return false;
1500 }
1501
1502 // Configure import options
1504 options.strict_mode = false;
1505 options.verbose_errors = true;
1506
1507 // Parse the ASM source
1509 auto result = importer.ImportSong(asm_buffer_, options);
1510 if (!result.ok()) {
1511 asm_import_error_ = result.status().message();
1512 LOG_ERROR("MusicEditor", "ImportSongFromAsm failed: %s",
1513 asm_import_error_.c_str());
1514 return false;
1515 }
1516
1517 // Log any warnings
1518 for (const auto& warning : result->warnings) {
1519 LOG_WARN("MusicEditor", "ASM import warning: %s", warning.c_str());
1520 }
1521
1522 // Copy parsed song data to target song
1523 // Keep original name if import didn't provide one
1524 std::string original_name = song->name;
1525 *song = result->song;
1526 if (song->name.empty()) {
1527 song->name = original_name;
1528 }
1529 song->modified = true;
1530
1531 LOG_INFO("MusicEditor", "Imported ASM to song '%s' (%d lines, %d bytes)",
1532 song->name.c_str(), result->lines_parsed, result->bytes_generated);
1533
1534 // Notify that edits occurred
1535 PushUndoState();
1536 asm_import_error_.clear();
1537 return true;
1538}
1539
1540// ============================================================================
1541// Custom Song Preview (In-Memory Playback)
1542// ============================================================================
1543
1546 ImGui::OpenPopup("Export Song ASM");
1547 show_asm_export_popup_ = false;
1548 }
1550 ImGui::OpenPopup("Import Song ASM");
1551 // Keep flag true until user closes
1552 }
1553
1554 if (ImGui::BeginPopupModal("Export Song ASM", nullptr,
1555 ImGuiWindowFlags_AlwaysAutoResize)) {
1556 ImGui::TextWrapped("Copy the generated ASM below or tweak before saving.");
1557 ImGui::InputTextMultiline("##AsmExportText", &asm_buffer_,
1558 ImVec2(520, 260),
1559 ImGuiInputTextFlags_AllowTabInput);
1560
1561 if (ImGui::Button("Copy to Clipboard")) {
1562 ImGui::SetClipboardText(asm_buffer_.c_str());
1563 }
1564 ImGui::SameLine();
1565 if (ImGui::Button("Close")) {
1566 ImGui::CloseCurrentPopup();
1567 }
1568
1569 ImGui::EndPopup();
1570 }
1571
1572 if (ImGui::BeginPopupModal("Import Song ASM", nullptr,
1573 ImGuiWindowFlags_AlwaysAutoResize)) {
1574 int song_slot = (asm_import_target_index_ >= 0)
1576 : -1;
1577 if (song_slot > 0) {
1578 ImGui::Text("Target Song: [%02X]", song_slot);
1579 } else {
1580 ImGui::TextDisabled("Select a song to import into");
1581 }
1582 ImGui::TextWrapped("Paste Oracle of Secrets-compatible ASM here.");
1583
1584 ImGui::InputTextMultiline("##AsmImportText", &asm_buffer_,
1585 ImVec2(520, 260),
1586 ImGuiInputTextFlags_AllowTabInput);
1587
1588 if (!asm_import_error_.empty()) {
1589 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
1590 ImGui::TextWrapped("%s", asm_import_error_.c_str());
1591 ImGui::PopStyleColor();
1592 }
1593
1594 bool can_import = asm_import_target_index_ >= 0 && !asm_buffer_.empty();
1595 if (!can_import) {
1596 ImGui::BeginDisabled();
1597 }
1598 if (ImGui::Button("Import")) {
1600 show_asm_import_popup_ = false;
1602 ImGui::CloseCurrentPopup();
1603 }
1604 }
1605 if (!can_import) {
1606 ImGui::EndDisabled();
1607 }
1608
1609 ImGui::SameLine();
1610 if (ImGui::Button("Cancel")) {
1611 asm_import_error_.clear();
1612 show_asm_import_popup_ = false;
1614 ImGui::CloseCurrentPopup();
1615 }
1616
1617 ImGui::EndPopup();
1618 } else if (!show_asm_import_popup_) {
1619 // Clear stale error when popup is closed
1620 asm_import_error_.clear();
1621 }
1622}
1623
1624} // namespace editor
1625} // namespace yaze
EditorDependencies dependencies_
Definition editor.h:237
std::unordered_map< int, std::unique_ptr< editor::music::TrackerView > > song_trackers_
void FocusSong(int song_index)
std::unique_ptr< emu::audio::IAudioBackend > audio_backend_
ImVector< int > active_songs_
std::vector< bool > channel_soloed_
void SlowDown(float delta=0.1f)
void DrawSongTrackerWindow(int song_index)
emu::Emulator * emulator_
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > song_cards_
absl::Status Paste() override
zelda3::music::MusicBank music_bank_
std::unordered_map< int, SongPianoRollWindow > song_piano_rolls_
void SetProject(project::YazeProject *project)
void Initialize() override
std::vector< UndoState > undo_stack_
void OpenSong(int song_index)
emu::Emulator * emulator() const
void ExportSongToAsm(int song_index)
editor::music::SampleEditorView sample_editor_view_
absl::Status Save() override
absl::Status Cut() override
void OpenSongPianoRoll(int song_index)
absl::Status Load() override
void RestoreState(const UndoState &state)
void ImportSongFromAsm(int song_index)
absl::StatusOr< bool > RestoreMusicState()
absl::Status Copy() override
std::vector< UndoState > redo_stack_
void SpeedUp(float delta=0.1f)
absl::Status PersistMusicState(const char *reason=nullptr)
absl::Status Update() override
editor::music::InstrumentEditorView instrument_editor_view_
std::unique_ptr< editor::music::MusicPlayer > music_player_
void set_emulator(emu::Emulator *emulator)
absl::Status Undo() override
absl::Status Redo() override
std::vector< std::string > song_names_
std::chrono::steady_clock::time_point last_music_persist_
AssemblyEditor assembly_editor_
bool ImportAsmBufferToSong(int song_index)
project::YazeProject * project_
void SeekToSegment(int segment_index)
editor::music::TrackerView tracker_view_
editor::music::SongBrowserView song_browser_view_
editor::music::PianoRollView piano_roll_view_
ImGuiWindowClass song_window_class_
bool * GetVisibilityFlag(size_t session_id, const std::string &base_card_id)
bool ShowPanel(size_t session_id, const std::string &base_card_id)
void RegisterPanel(size_t session_id, const PanelDescriptor &base_info)
std::string GetActiveCategory() const
bool IsPanelVisible(size_t session_id, const std::string &base_card_id) const
void UnregisterPanel(size_t session_id, const std::string &base_card_id)
bool IsPanelPinned(size_t session_id, const std::string &base_card_id) const
void Draw(MusicBank &bank)
Draw the instrument editor.
void SetPlaybackState(bool is_playing, bool is_paused, uint32_t current_tick)
void Draw(zelda3::music::MusicSong *song, const zelda3::music::MusicBank *bank=nullptr)
Draw the piano roll view for the given song.
void SetOnNotePreview(std::function< void(const zelda3::music::TrackEvent &, int, int)> callback)
Set callback for note preview.
void SetOnEditCallback(std::function< void()> callback)
Set callback for when edits occur.
void SetOnSegmentPreview(std::function< void(const zelda3::music::MusicSong &, int)> callback)
Set callback for segment preview.
void Draw(MusicBank &bank)
Draw the sample editor.
void Draw(MusicBank &bank)
Draw the song browser.
void Draw(MusicSong *song, const MusicBank *bank=nullptr)
Draw the tracker view for the given song.
A class for emulating and debugging SNES games.
Definition emulator.h:39
void SetExternalAudioBackend(audio::IAudioBackend *backend)
Definition emulator.h:81
bool is_snes_initialized() const
Definition emulator.h:122
audio::IAudioBackend * audio_backend()
Definition emulator.h:74
auto snes() -> Snes &
Definition emulator.h:58
static std::unique_ptr< IAudioBackend > Create(BackendType type)
virtual AudioStatus GetStatus() const =0
RAII timer for automatic timing management.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
Exports MusicSong to Oracle of Secrets music_macros.asm format.
absl::StatusOr< std::string > ExportSong(const MusicSong &song, const AsmExportOptions &options)
Export a song to ASM string.
Imports music_macros.asm format files into MusicSong.
absl::StatusOr< AsmParseResult > ImportSong(const std::string &asm_source, const AsmImportOptions &options)
Import a song from ASM string.
bool HasModifications() const
Check if any music data has been modified.
nlohmann::json ToJson() const
absl::Status LoadFromJson(const nlohmann::json &j)
MusicSong * GetSong(int index)
Get a song by index.
size_t GetSongCount() const
Get the number of songs loaded.
Definition music_bank.h:97
absl::Status SaveToRom(Rom &rom)
Save all modified music data back to ROM.
bool IsExpandedSong(int index) const
Check if a song is from an expanded bank.
absl::Status LoadFromRom(Rom &rom)
Load all music data from a ROM.
#define ICON_MD_PAUSE_CIRCLE
Definition icons.h:1390
#define ICON_MD_PAUSE
Definition icons.h:1389
#define ICON_MD_PIANO
Definition icons.h:1462
#define ICON_MD_LIBRARY_MUSIC
Definition icons.h:1080
#define ICON_MD_WAVES
Definition icons.h:2133
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_VOLUME_UP
Definition icons.h:2111
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_STOP
Definition icons.h:1862
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_GRAPHIC_EQ
Definition icons.h:890
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_SPEED
Definition icons.h:1817
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1264
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_SPEAKER
Definition icons.h:1812
#define ICON_MD_PLAY_CIRCLE
Definition icons.h:1480
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_HELP
Definition icons.h:933
#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
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:23
bool SliderIntWheel(const char *label, int *v, int v_min, int v_max, const char *format, int wheel_step, ImGuiSliderFlags flags)
Definition input.cc:765
bool SliderFloatWheel(const char *label, float *v, float v_min, float v_max, const char *format, float wheel_step, ImGuiSliderFlags flags)
Definition input.cc:749
constexpr uint16_t kAuxSongTableAram
Definition song_data.h:140
constexpr uint16_t kSongTableAram
Definition song_data.h:82
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::unique_ptr< editor::music::PianoRollView > view
std::shared_ptr< gui::PanelWindow > card
zelda3::music::MusicSong song_snapshot
Represents the current playback state of the music player.
Modern project structure with comprehensive settings consolidation.
Definition project.h:84
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:210
struct yaze::project::YazeProject::MusicPersistence music_persistence
Options for ASM export in music_macros.asm format.
Options for ASM import from music_macros.asm format.
A complete song composed of segments.
Definition song_data.h:334
A single event in a music track (note, command, or control).
Definition song_data.h:247