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