9#include "absl/strings/str_format.h"
27#include "imgui/imgui.h"
28#include "imgui/misc/cpp/imgui_stdlib.h"
30#include "nlohmann/json.hpp"
42 LOG_INFO(
"MusicEditor",
"Initialize() START: rom_=%p, emulator_=%p",
71 LOG_INFO(
"MusicEditor",
"Created shared audio backend: %s @ %dHz",
74 LOG_ERROR(
"MusicEditor",
"Failed to initialize audio backend!");
82 LOG_INFO(
"MusicEditor",
"Shared audio backend with main emulator");
84 LOG_WARN(
"MusicEditor",
"Cannot share with main emulator: backend=%p, emulator=%p",
91 LOG_INFO(
"MusicEditor",
"Set ROM on MusicPlayer");
93 LOG_WARN(
"MusicEditor",
"No ROM available for MusicPlayer!");
99 LOG_INFO(
"MusicEditor",
"Injected main emulator into MusicPlayer");
101 LOG_WARN(
"MusicEditor",
"No emulator available to inject into MusicPlayer!");
109 panel_manager->
RegisterPanel({.card_id =
"music.song_browser",
110 .display_name =
"Song Browser",
111 .window_title =
" Song Browser",
114 .shortcut_hint =
"Ctrl+Shift+B",
116 panel_manager->RegisterPanel({.card_id =
"music.tracker",
117 .display_name =
"Playback Control",
118 .window_title =
" Playback Control",
121 .shortcut_hint =
"Ctrl+Shift+M",
123 panel_manager->RegisterPanel({.card_id =
"music.piano_roll",
124 .display_name =
"Piano Roll",
125 .window_title =
" Piano Roll",
128 .shortcut_hint =
"Ctrl+Shift+P",
130 panel_manager->RegisterPanel({.card_id =
"music.instrument_editor",
131 .display_name =
"Instrument Editor",
132 .window_title =
" Instrument Editor",
135 .shortcut_hint =
"Ctrl+Shift+I",
137 panel_manager->RegisterPanel({.card_id =
"music.sample_editor",
138 .display_name =
"Sample Editor",
139 .window_title =
" Sample Editor",
142 .shortcut_hint =
"Ctrl+Shift+S",
144 panel_manager->RegisterPanel({.card_id =
"music.assembly",
145 .display_name =
"Assembly View",
146 .window_title =
" Music Assembly",
149 .shortcut_hint =
"Ctrl+Shift+A",
151 panel_manager->RegisterPanel({.card_id =
"music.audio_debug",
152 .display_name =
"Audio Debug",
153 .window_title =
" Audio Debug",
158 panel_manager->RegisterPanel({.card_id =
"music.help",
159 .display_name =
"Help",
160 .window_title =
" Music Editor Help",
172 auto song_browser = std::make_unique<MusicSongBrowserPanel>(
174 panel_manager->RegisterEditorPanel(std::move(song_browser));
177 auto playback_control = std::make_unique<MusicPlaybackControlPanel>(
179 playback_control->SetOnOpenSong([
this](
int index) {
OpenSong(index); });
180 playback_control->SetOnOpenPianoRoll(
182 panel_manager->RegisterEditorPanel(std::move(playback_control));
185 auto piano_roll = std::make_unique<MusicPianoRollPanel>(
188 panel_manager->RegisterEditorPanel(std::move(piano_roll));
191 auto instrument_editor = std::make_unique<MusicInstrumentEditorPanel>(
193 panel_manager->RegisterEditorPanel(std::move(instrument_editor));
196 auto sample_editor = std::make_unique<MusicSampleEditorPanel>(
198 panel_manager->RegisterEditorPanel(std::move(sample_editor));
202 panel_manager->RegisterEditorPanel(std::move(assembly));
205 auto audio_debug = std::make_unique<MusicAudioDebugPanel>(
music_player_.get());
206 panel_manager->RegisterEditorPanel(std::move(audio_debug));
209 auto help = std::make_unique<MusicHelpPanel>();
210 panel_manager->RegisterEditorPanel(std::move(help));
214 LOG_INFO(
"MusicEditor",
"set_emulator(%p): audio_backend_=%p",
220 LOG_INFO(
"MusicEditor",
"Shared audio backend with main emulator (deferred)");
253 if (restore.ok() && restore.value()) {
254 LOG_INFO(
"MusicEditor",
"Restored music state from web storage");
255 return absl::OkStatus();
256 }
else if (!restore.ok()) {
257 LOG_WARN(
"MusicEditor",
"Failed to restore music state: %s",
258 restore.status().ToString().c_str());
266 LOG_INFO(
"MusicEditor",
"Load(): Set ROM on MusicPlayer, IsAudioReady=%d",
271 LOG_WARN(
"MusicEditor",
"Load(): No ROM available!");
273 return absl::OkStatus();
279 if (state.is_playing && !state.is_paused) {
281 }
else if (state.is_paused) {
297 music_player_->SetPlaybackSpeed(state.playback_speed + delta);
304 music_player_->SetPlaybackSpeed(state.playback_speed - delta);
324 auto now = std::chrono::steady_clock::now();
328 elapsed > std::chrono::seconds(3))) {
331 LOG_WARN(
"MusicEditor",
"Music autosave failed: %s",
332 status.ToString().c_str());
339 return absl::OkStatus();
350 *browser_visible =
true;
355 bool* playback_visible = panel_manager->GetVisibilityFlag(
"music.tracker");
357 *playback_visible =
true;
362 static bool piano_roll_auto_shown =
false;
363 bool* piano_roll_visible = panel_manager->GetVisibilityFlag(
"music.piano_roll");
364 if (piano_roll_visible && !piano_roll_auto_shown) {
365 *piano_roll_visible =
true;
366 piano_roll_auto_shown =
true;
378 std::string card_id = absl::StrFormat(
"music.song_%d", song_index);
381 bool panel_visible =
true;
387 if (!panel_visible) {
404 if (active_category !=
"Music" && !is_pinned) {
414 std::string song_name = song ? song->
name :
"Unknown";
415 std::string card_title =
416 absl::StrFormat(
"[%02X] %s###SongTracker%d",
417 song_index + 1, song_name, song_index);
426 song_trackers_[song_index] = std::make_unique<editor::music::TrackerView>();
435 if (song_card->Begin(&open)) {
455 int song_index = it->first;
456 auto& window = it->second;
459 std::string card_id = absl::StrFormat(
"music.piano_roll_%d", song_index);
461 if (!song || !window.card || !window.view) {
470 bool panel_visible =
true;
476 if (!panel_visible) {
480 delete window.visible_flag;
491 if (active_category !=
"Music" && !is_pinned) {
502 if (window.card->Begin(&open)) {
503 window.view->SetOnEditCallback([
this]() {
PushUndoState(); });
504 window.view->SetOnNotePreview(
506 int segment_idx,
int channel_idx) {
509 music_player_->PreviewNote(*target, evt, segment_idx, channel_idx);
511 window.view->SetOnSegmentPreview(
520 window.view->SetPlaybackState(state.is_playing, state.is_paused,
522 window.view->Draw(song);
531 delete window.visible_flag;
540 return absl::OkStatus();
544 if (!
rom_)
return absl::FailedPreconditionError(
"No ROM loaded");
549 if (!persist_status.ok()) {
550 return persist_status;
554 return absl::OkStatus();
565 if (!storage_or.ok()) {
570 auto parsed = nlohmann::json::parse(storage_or.value());
575 }
catch (
const std::exception& e) {
576 return absl::InvalidArgumentError(
577 absl::StrFormat(
"Failed to parse stored music state: %s", e.what()));
587 return absl::OkStatus();
595 auto now = std::chrono::system_clock::now();
596 auto time_t = std::chrono::system_clock::to_time_t(now);
597 std::stringstream ss;
598 ss << std::put_time(std::localtime(&time_t),
"%Y-%m-%d %H:%M:%S");
606 LOG_DEBUG(
"MusicEditor",
"Persisted music state (%s)", reason);
608 return absl::OkStatus();
611 return absl::OkStatus();
622 return absl::OkStatus();
628 return absl::UnimplementedError(
"Copy not yet implemented - clipboard support coming soon");
634 return absl::UnimplementedError(
"Paste not yet implemented - clipboard support coming soon");
638 if (
undo_stack_.empty())
return absl::FailedPreconditionError(
"Nothing to undo");
650 return absl::OkStatus();
654 if (
redo_stack_.empty())
return absl::FailedPreconditionError(
"Nothing to redo");
666 return absl::OkStatus();
680 constexpr size_t kMaxUndoStates = 50;
728 std::string song_name = song ? song->
name : absl::StrFormat(
"Song %02X", song_index);
730 std::string card_id = absl::StrFormat(
"music.song_%d", song_index);
734 .display_name = song_name,
739 .visibility_flag =
nullptr,
740 .priority = 200 + song_index});
747 LOG_INFO(
"MusicEditor",
"Opened song %d tracker window", song_index);
758 if (song_index < 0 ||
765 if (it->second.card && it->second.visible_flag) {
766 *it->second.visible_flag =
true;
767 it->second.card->Focus();
773 std::string song_name = song ? song->
name : absl::StrFormat(
"Song %02X", song_index);
774 std::string card_title = absl::StrFormat(
775 "[%02X] %s - Piano Roll###SongPianoRoll%d", song_index + 1,
776 song_name, song_index);
781 std::make_shared<gui::PanelWindow>(card_title.c_str(),
ICON_MD_PIANO,
783 window.
card->SetDefaultSize(900, 450);
784 window.
view = std::make_unique<editor::music::PianoRollView>();
785 window.
view->SetActiveChannel(0);
786 window.
view->SetActiveSegment(0);
793 std::string card_id = absl::StrFormat(
"music.piano_roll_%d", song_index);
797 .display_name = song_name +
" (Piano)",
802 .visibility_flag =
nullptr,
803 .priority = 250 + song_index});
813 ImGui::TextDisabled(
"Song not loaded");
820 bool is_playing_this_song = state.
is_playing && (state.playing_song_index == song_index);
821 bool is_paused_this_song = state.is_paused && (state.playing_song_index == song_index);
824 if (!can_play) ImGui::BeginDisabled();
827 if (is_playing_this_song && !is_paused_this_song) {
828 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
829 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.3f, 1.0f));
833 ImGui::PopStyleColor(2);
834 }
else if (is_paused_this_song) {
835 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.2f, 1.0f));
836 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.6f, 0.6f, 0.3f, 1.0f));
840 ImGui::PopStyleColor(2);
851 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Stop playback");
853 if (!can_play) ImGui::EndDisabled();
856 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && can_play) {
858 if (ImGui::IsKeyPressed(ImGuiKey_Space,
false)) {
861 if (ImGui::IsKeyPressed(ImGuiKey_Escape,
false)) {
864 if (ImGui::IsKeyPressed(ImGuiKey_Equal,
false) ||
865 ImGui::IsKeyPressed(ImGuiKey_KeypadAdd,
false)) {
868 if (ImGui::IsKeyPressed(ImGuiKey_Minus,
false) ||
869 ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract,
false)) {
876 if (is_playing_this_song && !is_paused_this_song) {
878 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Playing");
879 }
else if (is_paused_this_song) {
881 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Paused");
885 float right_offset = ImGui::GetWindowWidth() - 200;
886 ImGui::SameLine(right_offset);
891 ImGui::SetNextItemWidth(55);
892 float speed = state.playback_speed;
899 if (ImGui::IsItemHovered())
900 ImGui::SetTooltip(
"Playback speed (0.25x - 2.0x) - use mouse wheel");
906 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Open Piano Roll view");
909 const char* bank_name =
nullptr;
910 switch (song->bank) {
911 case 0: bank_name =
"Overworld";
break;
912 case 1: bank_name =
"Dungeon";
break;
913 case 2: bank_name =
"Credits";
break;
914 case 3: bank_name =
"Expanded";
break;
915 case 4: bank_name =
"Auxiliary";
break;
916 default: bank_name =
"Unknown";
break;
918 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
"[%02X]", song_index + 1);
920 ImGui::Text(
"%s", song->name.c_str());
922 ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.7f, 1.0f),
"(%s)", bank_name);
924 if (song->modified) {
926 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f),
ICON_MD_EDIT " Modified");
930 ImGui::SameLine(right_offset);
931 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f),
932 "%zu segments", song->segments.size());
937 if (is_playing_this_song) {
963 ImGui::Text(
"Selected Song:");
965 ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f),
"[%02X] %s",
970 ImGui::TextDisabled(
"| %zu segments", song->segments.size());
971 if (song->modified) {
973 ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f),
ICON_MD_EDIT " Modified");
978 if (state.is_playing || state.is_paused) {
982 if (song && !song->segments.empty()) {
983 uint32_t total_duration = 0;
984 for (
const auto& seg : song->segments) {
985 total_duration += seg.GetDuration();
988 float progress = (total_duration > 0)
989 ?
static_cast<float>(state.current_tick) / total_duration
991 progress = std::clamp(progress, 0.0f, 1.0f);
994 float current_seconds = state.ticks_per_second > 0
995 ? state.current_tick / state.ticks_per_second
997 float total_seconds = state.ticks_per_second > 0
998 ? total_duration / state.ticks_per_second
1001 int cur_min =
static_cast<int>(current_seconds) / 60;
1002 int cur_sec =
static_cast<int>(current_seconds) % 60;
1003 int tot_min =
static_cast<int>(total_seconds) / 60;
1004 int tot_sec =
static_cast<int>(total_seconds) % 60;
1006 ImGui::Text(
"%d:%02d / %d:%02d", cur_min, cur_sec, tot_min, tot_sec);
1010 ImGui::ProgressBar(progress, ImVec2(-1, 0),
"");
1014 ImGui::Text(
"Segment: %d | Tick: %u",
1015 state.current_segment_index + 1, state.current_tick);
1017 ImGui::TextDisabled(
"| %.1f ticks/sec | %.2fx speed",
1018 state.ticks_per_second, state.playback_speed);
1022 if (state.is_playing) {
1033 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Open song in dedicated tracker window");
1039 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Open piano roll view for this song");
1043 ImGui::BulletText(
"Space: Play/Pause toggle");
1044 ImGui::BulletText(
"Escape: Stop playback");
1045 ImGui::BulletText(
"+/-: Increase/decrease speed");
1046 ImGui::BulletText(
"Arrow keys: Navigate in tracker/piano roll");
1047 ImGui::BulletText(
"Z,S,X,D,C,V,G,B,H,N,J,M: Piano keyboard (C to B)");
1048 ImGui::BulletText(
"Ctrl+Wheel: Zoom (Piano Roll)");
1061 static_cast<int>(song->segments.size())) {
1074 music_player_->PreviewNote(*target, evt, segment_idx, channel_idx);
1102 static int current_volume = 100;
1109 if (!can_play) ImGui::BeginDisabled();
1112 const ImVec4 paused_color(0.9f, 0.7f, 0.2f, 1.0f);
1114 if (state.is_playing && !state.is_paused) {
1115 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.2f, 1.0f));
1117 ImGui::PopStyleColor();
1118 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Pause (Space)");
1119 }
else if (state.is_paused) {
1120 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.4f, 0.1f, 1.0f));
1122 ImGui::PopStyleColor();
1123 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Resume (Space)");
1126 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Play (Space)");
1131 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Stop (Escape)");
1133 if (!can_play) ImGui::EndDisabled();
1138 if (state.is_playing && !state.is_paused) {
1140 float t =
static_cast<float>(ImGui::GetTime() * 3.0);
1141 float alpha = 0.5f + 0.5f * std::sin(t);
1144 }
else if (state.is_paused) {
1148 ImGui::Text(
"%s", song->name.c_str());
1149 if (song->modified) {
1151 ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
ICON_MD_EDIT);
1154 ImGui::TextDisabled(
"No song selected");
1158 if (state.is_playing || state.is_paused) {
1160 float seconds = state.ticks_per_second > 0
1161 ? state.current_tick / state.ticks_per_second
1163 int mins =
static_cast<int>(seconds) / 60;
1164 int secs =
static_cast<int>(seconds) % 60;
1165 ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.8f, 1.0f),
" %d:%02d", mins, secs);
1169 float right_offset = ImGui::GetWindowWidth() - 380;
1170 ImGui::SameLine(right_offset);
1175 ImGui::SetNextItemWidth(70);
1176 float speed = state.playback_speed;
1182 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Playback speed (+/- keys)");
1187 ImGui::SetNextItemWidth(60);
1191 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Volume");
1198 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Reload from ROM");
1202 ImGui::SetNextItemWidth(100);
1204 static int interpolation_type = 2;
1205 const char* items[] = {
"Linear",
"Hermite",
"Gaussian",
"Cosine",
"Cubic"};
1206 if (ImGui::Combo(
"##Interp", &interpolation_type, items, IM_ARRAYSIZE(items))) {
1209 if (ImGui::IsItemHovered()) ImGui::SetTooltip(
"Audio interpolation quality\nGaussian = authentic SNES sound");
1215 if (ImGui::BeginTable(
"MixerPanel", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
1217 ImGui::TableSetupColumn(
"Master", ImGuiTableColumnFlags_WidthFixed, 60.0f);
1218 for (
int i = 0; i < 8; i++) {
1219 ImGui::TableSetupColumn(absl::StrFormat(
"Ch %d", i + 1).c_str());
1221 ImGui::TableHeadersRow();
1223 ImGui::TableNextRow();
1226 ImGui::TableSetColumnIndex(0);
1230 auto& dsp = audio_emu->
snes().apu().dsp();
1232 ImGui::Text(
"Scope");
1235 const int16_t* buffer = dsp.GetSampleBuffer();
1236 uint16_t offset = dsp.GetSampleOffset();
1238 static float scope_values[128];
1240 constexpr int kBufferSize = 0x400;
1241 for (
int i = 0; i < 128; i++) {
1242 int sample_idx = ((offset - 128 + i + kBufferSize) & (kBufferSize - 1));
1243 scope_values[i] =
static_cast<float>(buffer[sample_idx * 2]) / 32768.0f;
1246 ImGui::PlotLines(
"##Scope", scope_values, 128, 0,
nullptr, -1.0f, 1.0f, ImVec2(50, 60));
1250 for (
int i = 0; i < 8; i++) {
1251 ImGui::TableSetColumnIndex(i + 1);
1254 auto& dsp = audio_emu->
snes().apu().dsp();
1255 const auto& ch = dsp.GetChannel(i);
1258 bool is_muted = dsp.GetChannelMute(i);
1265 if (ImGui::Button(absl::StrFormat(
"M##%d", i).c_str(), ImVec2(25, 20))) {
1266 dsp.SetChannelMute(i, !is_muted);
1268 if (is_muted) ImGui::PopStyleColor();
1275 if (ImGui::Button(absl::StrFormat(
"S##%d", i).c_str(), ImVec2(25, 20))) {
1278 bool any_solo =
false;
1281 for(
int j=0; j<8; j++) {
1285 dsp.SetChannelMute(j,
false);
1289 if (is_solo) ImGui::PopStyleColor();
1292 float level = std::abs(ch.sampleOut) / 32768.0f;
1293 ImGui::ProgressBar(level, ImVec2(-1, 60),
"");
1296 ImGui::Text(
"Vol: %d %d", ch.volumeL, ch.volumeR);
1297 ImGui::Text(
"Pitch: %04X", ch.pitch);
1303 ImGui::TextDisabled(
"---");
1306 ImGui::TextDisabled(
"Offline");
1318 if (audio_backend) {
1319 auto status = audio_backend->
GetStatus();
1320 auto config = audio_backend->GetConfig();
1321 bool resampling = audio_backend->IsAudioStreamEnabled();
1324 ImGui::Text(
"Backend: %s @ %dHz | Queue: %u frames",
1325 audio_backend->GetBackendName().c_str(),
1326 config.sample_rate, status.queued_frames);
1330 ImGui::TextColored(ImVec4(0.3f, 0.9f, 0.3f, 1.0f),
1331 "Resampling: 32040 -> %d Hz", config.sample_rate);
1333 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
1337 if (status.has_underrun) {
1338 ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f),
1342 ImGui::TextDisabled(
"Open Audio Debug panel for full diagnostics");
1345 ImGui::TextDisabled(
"Play a song to see audio status");
1352 ImGui::TextDisabled(
"Music player not initialized");
1358 if (!audio_emu || !audio_emu->is_snes_initialized()) {
1359 ImGui::TextDisabled(
"Play a song to see channel activity");
1364 ImVec2 avail = ImGui::GetContentRegionAvail();
1365 if (avail.y < 50.0f) {
1366 ImGui::TextDisabled(
"(Channel view - expand for details)");
1372 if (ImGui::BeginTable(
"ChannelOverview", 9,
1373 ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp)) {
1374 ImGui::TableSetupColumn(
"Master", ImGuiTableColumnFlags_WidthFixed, 70.0f);
1375 for (
int i = 0; i < 8; i++) {
1376 ImGui::TableSetupColumn(absl::StrFormat(
"Ch %d", i + 1).c_str());
1378 ImGui::TableHeadersRow();
1380 ImGui::TableNextRow();
1382 ImGui::TableSetColumnIndex(0);
1383 ImGui::Text(
"DSP Live");
1385 for (
int ch = 0; ch < 8; ++ch) {
1386 ImGui::TableSetColumnIndex(ch + 1);
1387 const auto& state = channel_states[ch];
1391 ImGui::TextColored(ImVec4(0.2f, 1.0f, 0.2f, 1.0f),
"ON");
1393 ImGui::TextDisabled(
"OFF");
1397 float vol_l = state.volume_l / 128.0f;
1398 float vol_r = state.volume_r / 128.0f;
1399 ImGui::ProgressBar(vol_l, ImVec2(-1, 6.0f),
"");
1400 ImGui::ProgressBar(vol_r, ImVec2(-1, 6.0f),
"");
1403 ImGui::Text(
"S: %02X", state.sample_index);
1404 ImGui::Text(
"P: %04X", state.pitch);
1407 const char* adsr_str =
"???";
1408 switch (state.adsr_state) {
1409 case 0: adsr_str =
"Att";
break;
1410 case 1: adsr_str =
"Dec";
break;
1411 case 2: adsr_str =
"Sus";
break;
1412 case 3: adsr_str =
"Rel";
break;
1414 ImGui::Text(
"%s", adsr_str);
1436 LOG_WARN(
"MusicEditor",
"ExportSongToAsm: Invalid song index %d", song_index);
1457 auto result = exporter.
ExportSong(*song, options);
1459 LOG_ERROR(
"MusicEditor",
"ExportSongToAsm failed: %s",
1460 result.status().message().data());
1469 LOG_INFO(
"MusicEditor",
"Exported song '%s' to ASM (%zu bytes)",
1478 LOG_INFO(
"MusicEditor",
"No ASM source to import - showing import dialog");
1512 LOG_ERROR(
"MusicEditor",
"ImportSongFromAsm failed: %s",
1518 for (
const auto& warning : result->warnings) {
1519 LOG_WARN(
"MusicEditor",
"ASM import warning: %s", warning.c_str());
1524 std::string original_name = song->name;
1525 *song = result->song;
1526 if (song->name.empty()) {
1527 song->name = original_name;
1529 song->modified =
true;
1531 LOG_INFO(
"MusicEditor",
"Imported ASM to song '%s' (%d lines, %d bytes)",
1532 song->name.c_str(), result->lines_parsed, result->bytes_generated);
1546 ImGui::OpenPopup(
"Export Song ASM");
1550 ImGui::OpenPopup(
"Import Song ASM");
1554 if (ImGui::BeginPopupModal(
"Export Song ASM",
nullptr,
1555 ImGuiWindowFlags_AlwaysAutoResize)) {
1556 ImGui::TextWrapped(
"Copy the generated ASM below or tweak before saving.");
1557 ImGui::InputTextMultiline(
"##AsmExportText", &
asm_buffer_,
1559 ImGuiInputTextFlags_AllowTabInput);
1561 if (ImGui::Button(
"Copy to Clipboard")) {
1565 if (ImGui::Button(
"Close")) {
1566 ImGui::CloseCurrentPopup();
1572 if (ImGui::BeginPopupModal(
"Import Song ASM",
nullptr,
1573 ImGuiWindowFlags_AlwaysAutoResize)) {
1577 if (song_slot > 0) {
1578 ImGui::Text(
"Target Song: [%02X]", song_slot);
1580 ImGui::TextDisabled(
"Select a song to import into");
1582 ImGui::TextWrapped(
"Paste Oracle of Secrets-compatible ASM here.");
1584 ImGui::InputTextMultiline(
"##AsmImportText", &
asm_buffer_,
1586 ImGuiInputTextFlags_AllowTabInput);
1589 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
1591 ImGui::PopStyleColor();
1596 ImGui::BeginDisabled();
1598 if (ImGui::Button(
"Import")) {
1602 ImGui::CloseCurrentPopup();
1606 ImGui::EndDisabled();
1610 if (ImGui::Button(
"Cancel")) {
1614 ImGui::CloseCurrentPopup();
EditorDependencies dependencies_
void DrawInstrumentEditor()
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_
bool persist_custom_music_
ImVector< int > active_songs_
std::vector< bool > channel_soloed_
int current_segment_index_
void SlowDown(float delta=0.1f)
std::string music_storage_key_
void DrawSongTrackerWindow(int song_index)
emu::Emulator * emulator_
std::unordered_map< int, std::shared_ptr< gui::PanelWindow > > song_cards_
void DrawChannelOverview()
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
bool show_asm_export_popup_
std::vector< UndoState > undo_stack_
void OpenSong(int song_index)
emu::Emulator * emulator() const
void ExportSongToAsm(int song_index)
editor::music::SampleEditorView sample_editor_view_
absl::Status Save() override
absl::Status Cut() override
int current_channel_index_
void OpenSongPianoRoll(int song_index)
absl::Status Load() override
void RestoreState(const UndoState &state)
void ImportSongFromAsm(int song_index)
absl::StatusOr< bool > RestoreMusicState()
absl::Status Copy() override
std::vector< UndoState > redo_stack_
void SpeedUp(float delta=0.1f)
absl::Status PersistMusicState(const char *reason=nullptr)
absl::Status Update() override
void DrawPlaybackControl()
editor::music::InstrumentEditorView instrument_editor_view_
bool song_browser_auto_shown_
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_
int asm_import_target_index_
bool show_asm_import_popup_
std::chrono::steady_clock::time_point last_music_persist_
std::string asm_import_error_
AssemblyEditor assembly_editor_
bool ImportAsmBufferToSong(int song_index)
project::YazeProject * project_
void SeekToSegment(int segment_index)
editor::music::TrackerView tracker_view_
editor::music::SongBrowserView song_browser_view_
editor::music::PianoRollView piano_roll_view_
ImGuiWindowClass song_window_class_
bool * GetVisibilityFlag(size_t session_id, const std::string &base_card_id)
bool ShowPanel(size_t session_id, const std::string &base_card_id)
void RegisterPanel(size_t session_id, const PanelDescriptor &base_info)
std::string GetActiveCategory() const
bool IsPanelVisible(size_t session_id, const std::string &base_card_id) const
void UnregisterPanel(size_t session_id, const std::string &base_card_id)
bool IsPanelPinned(size_t session_id, const std::string &base_card_id) const
void Draw(MusicBank &bank)
Draw the instrument editor.
int GetActiveSegment() const
int GetActiveChannel() const
void SetActiveSegment(int segment)
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 SetActiveChannel(int channel)
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.
int GetSelectedSongIndex() const
void Draw(MusicBank &bank)
Draw the song browser.
void SetSelectedSongIndex(int index)
void Draw(MusicSong *song, const MusicBank *bank=nullptr)
Draw the tracker view for the given song.
A class for emulating and debugging SNES games.
void SetExternalAudioBackend(audio::IAudioBackend *backend)
bool is_snes_initialized() const
audio::IAudioBackend * audio_backend()
static std::unique_ptr< IAudioBackend > Create(BackendType type)
virtual AudioStatus GetStatus() const =0
RAII timer for automatic timing management.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
Exports MusicSong to Oracle of Secrets music_macros.asm format.
absl::StatusOr< std::string > ExportSong(const MusicSong &song, const AsmExportOptions &options)
Export a song to ASM string.
Imports music_macros.asm format files into MusicSong.
absl::StatusOr< AsmParseResult > ImportSong(const std::string &asm_source, const AsmImportOptions &options)
Import a song from ASM string.
bool HasModifications() const
Check if any music data has been modified.
nlohmann::json ToJson() const
absl::Status LoadFromJson(const nlohmann::json &j)
MusicSong * GetSong(int index)
Get a song by index.
size_t GetSongCount() const
Get the number of songs loaded.
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
#define ICON_MD_LIBRARY_MUSIC
#define ICON_MD_VOLUME_UP
#define ICON_MD_PLAY_ARROW
#define ICON_MD_BUG_REPORT
#define ICON_MD_GRAPHIC_EQ
#define ICON_MD_MUSIC_NOTE
#define ICON_MD_PLAY_CIRCLE
#define ICON_MD_OPEN_IN_NEW
#define LOG_DEBUG(category, format,...)
#define LOG_ERROR(category, format,...)
#define LOG_WARN(category, format,...)
#define LOG_INFO(category, format,...)
ImVec4 ConvertColorToImVec4(const Color &color)
bool SliderIntWheel(const char *label, int *v, int v_min, int v_max, const char *format, int wheel_step, ImGuiSliderFlags flags)
bool SliderFloatWheel(const char *label, float *v, float v_min, float v_max, const char *format, float wheel_step, ImGuiSliderFlags flags)
constexpr uint16_t kAuxSongTableAram
constexpr uint16_t kSongTableAram
#define RETURN_IF_ERROR(expr)
PanelManager * panel_manager
std::unique_ptr< editor::music::PianoRollView > view
std::shared_ptr< gui::PanelWindow > card
zelda3::music::MusicSong song_snapshot
Represents the current playback state of the music player.
std::string last_saved_at
bool persist_custom_music
Modern project structure with comprehensive settings consolidation.
std::string MakeStorageKey(absl::string_view suffix) const
struct yaze::project::YazeProject::MusicPersistence music_persistence
Options for ASM export in music_macros.asm format.
bool use_instrument_macros
uint16_t base_aram_address
Options for ASM import from music_macros.asm format.
A complete song composed of segments.
A single event in a music track (note, command, or control).