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