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