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