yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tracker_view.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <vector>
6
7#include "absl/strings/str_format.h"
9#include "imgui/imgui.h"
10
11namespace yaze {
12namespace editor {
13namespace music {
14
15using namespace yaze::zelda3::music;
16
17namespace {
18// Theme-aware color helpers
19ImU32 GetColorNote() {
20 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
21 return ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.success));
22}
23
25 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
26 return ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.info));
27}
28
30 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
31 return ImGui::GetColorU32(gui::ConvertColorToImVec4(theme.warning));
32}
33
35 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
36 auto color = theme.active_selection;
37 color.alpha = 0.15f; // Low opacity for subtle highlight
38 return ImGui::GetColorU32(gui::ConvertColorToImVec4(color));
39}
40
41ImU32 GetColorSelection(bool is_range) {
42 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
43 auto color = theme.editor_selection;
44 color.alpha = is_range ? 0.4f : 0.7f;
45 return ImGui::GetColorU32(gui::ConvertColorToImVec4(color));
46}
47std::string DescribeCommand(uint8_t opcode) {
48 switch (opcode) {
49 case 0xE0:
50 return "Set Instrument";
51 case 0xE1:
52 return "Set Pan";
53 case 0xE5:
54 return "Master Volume";
55 case 0xE6:
56 return "Master Volume Fade";
57 case 0xE7:
58 return "Set Tempo";
59 case 0xE8:
60 return "Tempo Fade";
61 case 0xE9:
62 return "Global Transpose";
63 case 0xEA:
64 return "Channel Transpose";
65 case 0xEB:
66 return "Tremolo On";
67 case 0xEC:
68 return "Tremolo Off";
69 case 0xED:
70 return "Channel Volume";
71 case 0xEE:
72 return "Channel Volume Fade";
73 case 0xEF:
74 return "Call Subroutine";
75 case 0xF0:
76 return "Vibrato Fade";
77 case 0xF1:
78 return "Pitch Env To";
79 case 0xF2:
80 return "Pitch Env From";
81 case 0xF3:
82 return "Pitch Env Off";
83 case 0xF4:
84 return "Tuning";
85 case 0xF5:
86 return "Echo Bits";
87 case 0xF6:
88 return "Echo Off";
89 case 0xF7:
90 return "Echo Params";
91 case 0xF8:
92 return "Echo Vol Fade";
93 case 0xF9:
94 return "Pitch Slide";
95 case 0xFA:
96 return "Percussion Patch";
97 default:
98 return "Command";
99 }
100}
101
102// Command options for combo box
104 const char* name;
105 uint8_t opcode;
106};
107
109 {"Set Instrument", 0xE0}, {"Set Pan", 0xE1},
110 {"Vibrato On", 0xE3}, {"Vibrato Off", 0xE4},
111 {"Master Volume", 0xE5}, {"Master Volume Fade", 0xE6},
112 {"Set Tempo", 0xE7}, {"Tempo Fade", 0xE8},
113 {"Global Transpose", 0xE9}, {"Channel Transpose", 0xEA},
114 {"Tremolo On", 0xEB}, {"Tremolo Off", 0xEC},
115 {"Channel Volume", 0xED}, {"Channel Volume Fade", 0xEE},
116 {"Call Subroutine", 0xEF}, {"Vibrato Fade", 0xF0},
117 {"Pitch Env To", 0xF1}, {"Pitch Env From", 0xF2},
118 {"Pitch Env Off", 0xF3}, {"Tuning", 0xF4},
119 {"Echo Bits", 0xF5}, {"Echo Off", 0xF6},
120 {"Echo Params", 0xF7}, {"Echo Vol Fade", 0xF8},
121 {"Pitch Slide", 0xF9}, {"Percussion Patch", 0xFA},
122};
123} // namespace
124
125void TrackerView::Draw(MusicSong* song, const MusicBank* bank) {
126 if (!song) {
127 ImGui::TextDisabled(tr("No song loaded"));
128 return;
129 }
130
131 // Handle input before drawing to avoid 1-frame lag
132 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
136 }
137
138 DrawToolbar(song);
139 ImGui::Separator();
140
141 ImGui::BeginChild("TrackerGrid", ImVec2(0, 0), true);
142 DrawGrid(song, bank);
143 ImGui::EndChild();
144}
145
147 ImGui::Text("%s", song->name.empty() ? "Untitled" : song->name.c_str());
148 ImGui::SameLine();
149 ImGui::TextDisabled(tr("(%d ticks)"), song->GetTotalDuration());
150
151 ImGui::SameLine();
152 ImGui::Text(tr("| Bank: %s"),
153 song->bank == 0 ? "Overworld"
154 : (song->bank == 1 ? "Dungeon" : "Credits"));
155
156 ImGui::SameLine();
157 ImGui::PushItemWidth(100);
158 if (ImGui::DragInt(tr("Ticks/Row"), &ticks_per_row_, 1, 1, 96)) {
159 if (ticks_per_row_ < 1)
160 ticks_per_row_ = 1;
161 }
162 ImGui::PopItemWidth();
163}
164
165void TrackerView::DrawGrid(MusicSong* song, const MusicBank* bank) {
166 if (song->segments.empty())
167 return;
168
169 // Use the first segment for now (TODO: Handle multiple segments)
170 // Non-const reference to allow editing
171 auto& segment = song->segments[0];
172 uint32_t duration = segment.GetDuration();
173
174 // Table setup: Row number + 8 Channels
175 if (ImGui::BeginTable("TrackerTable", 9,
176 ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY |
177 ImGuiTableFlags_RowBg |
178 ImGuiTableFlags_Resizable)) {
179
180 // Header
181 ImGui::TableSetupColumn("Tick", ImGuiTableColumnFlags_WidthFixed, 50.0f);
182 for (int i = 0; i < 8; ++i) {
183 ImGui::TableSetupColumn(absl::StrFormat("Ch %d", i + 1).c_str());
184 }
185 ImGui::TableHeadersRow();
186
187 // Rows
188 int total_rows = (duration + ticks_per_row_ - 1) / ticks_per_row_;
189 ImGuiListClipper clipper;
190 clipper.Begin(total_rows, row_height_);
191
192 while (clipper.Step()) {
193 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
194 int tick_start = row * ticks_per_row_;
195 int tick_end = tick_start + ticks_per_row_;
196
197 ImGui::TableNextRow();
198
199 // Highlight every 4th row (beat)
200 if (row % 4 == 0) {
201 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
202 GetColorBeatHighlight());
203 }
204
205 // Tick Number Column
206 ImGui::TableSetColumnIndex(0);
207 ImGui::TextDisabled("%04X", tick_start);
208
209 // Channel Columns
210 for (int ch = 0; ch < 8; ++ch) {
211 ImGui::TableSetColumnIndex(ch + 1);
212
213 // Selection drawing
214 bool is_selected =
215 (row == selected_row_ && (ch + 1) == selected_col_);
216
217 // Calculate range selection
218 if (selection_anchor_row_ != -1 && selection_anchor_col_ != -1) {
219 int row_start = std::min(selected_row_, selection_anchor_row_);
220 int row_end = std::max(selected_row_, selection_anchor_row_);
221 int col_start = std::min(selected_col_, selection_anchor_col_);
222 int col_end = std::max(selected_col_, selection_anchor_col_);
223
224 if (row >= row_start && row <= row_end && (ch + 1) >= col_start &&
225 (ch + 1) <= col_end) {
226 ImGui::TableSetBgColor(
227 ImGuiTableBgTarget_CellBg,
228 GetColorSelection(true)); // Range selection
229 }
230 } else if (is_selected) {
231 ImGui::TableSetBgColor(
232 ImGuiTableBgTarget_CellBg,
233 GetColorSelection(false)); // Single selection
234
235 // Auto-scroll to selection
236 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
237 // Simple scroll-into-view logic could go here if needed,
238 // but ImGui handles focused items usually well enough if using Selectable
239 }
240 }
241
242 // Find event in this time range
243 int event_index = -1;
244 auto& track = segment.tracks[ch];
245 for (size_t idx = 0; idx < track.events.size(); ++idx) {
246 const auto& evt = track.events[idx];
247 if (evt.tick >= tick_start && evt.tick < tick_end) {
248 event_index = static_cast<int>(idx);
249 break;
250 }
251 if (evt.tick >= tick_end)
252 break;
253 }
254
255 DrawEventCell(track, event_index, ch, tick_start, bank);
256
257 // Handle cell click for selection
258 // Invisible button to capture clicks
259 ImGui::PushID(row * 100 + ch);
260 if (ImGui::Selectable("##cell", false,
261 ImGuiSelectableFlags_SpanAllColumns |
262 ImGuiSelectableFlags_AllowOverlap,
263 ImVec2(0, row_height_))) {
264 if (ImGui::GetIO().KeyShift) {
265 if (selection_anchor_row_ == -1) {
268 }
269 } else {
272 }
273 selected_row_ = row;
274 selected_col_ = ch + 1;
275 }
276 ImGui::PopID();
277 }
278 }
279 }
280 ImGui::EndTable();
281 }
282}
283
284void TrackerView::DrawEventCell(MusicTrack& track, int event_index,
285 int channel_idx, uint16_t tick,
286 const MusicBank* bank) {
287 TrackEvent* event_ptr =
288 (event_index >= 0 && event_index < static_cast<int>(track.events.size()))
289 ? &track.events[event_index]
290 : nullptr;
291
292 bool has_event = event_ptr != nullptr;
293
294 if (!has_event) {
295 ImGui::TextDisabled("...");
296 } else {
297 auto& event = *event_ptr;
298 switch (event.type) {
299 case TrackEvent::Type::Note:
300 ImGui::TextColored(ImColor(GetColorNote()), "%s",
301 event.note.GetNoteName().c_str());
302 if (ImGui::IsItemHovered()) {
303 ImGui::SetTooltip(tr("Note: %s\nDuration: %d\nVelocity: %d"),
304 event.note.GetNoteName().c_str(),
305 event.note.duration, event.note.velocity);
306 }
307 break;
308
309 case TrackEvent::Type::Command: {
310 ImU32 color = GetColorCommand();
311 std::string label = absl::StrFormat("CMD %02X", event.command.opcode);
312 std::string tooltip = DescribeCommand(event.command.opcode);
313
314 // Improved display for common commands
315 if (event.command.opcode == 0xE0) { // Set Instrument
316 if (bank) {
317 const auto* inst = bank->GetInstrument(event.command.params[0]);
318 if (inst) {
319 label = absl::StrFormat("Instr: %s", inst->name.c_str());
320 tooltip =
321 absl::StrFormat("Set Instrument: %s (ID %02X)",
322 inst->name.c_str(), event.command.params[0]);
323 } else {
324 label = absl::StrFormat("Instr: %02X", event.command.params[0]);
325 }
326 } else {
327 label = absl::StrFormat("Instr: %02X", event.command.params[0]);
328 }
329 } else if (event.command.opcode == 0xE1) { // Set Pan
330 int pan = event.command.params[0];
331 if (pan == 0x0A)
332 label = "Pan: Center";
333 else if (pan < 0x0A)
334 label = absl::StrFormat("Pan: L%d", 0x0A - pan);
335 else
336 label = absl::StrFormat("Pan: R%d", pan - 0x0A);
337 } else if (event.command.opcode == 0xE7) { // Set Tempo
338 label = absl::StrFormat("Tempo: %d", event.command.params[0]);
339 } else if (event.command.opcode == 0xED) { // Channel Volume
340 label = absl::StrFormat("Vol: %d", event.command.params[0]);
341 } else if (event.command.opcode == 0xE5) { // Master Volume
342 label = absl::StrFormat("M.Vol: %d", event.command.params[0]);
343 }
344
345 ImGui::TextColored(ImColor(color), "%s", label.c_str());
346 if (ImGui::IsItemHovered()) {
347 ImGui::SetTooltip(tr("%s\nOpcode: %02X\nParams: %02X %02X %02X"),
348 tooltip.c_str(), event.command.opcode,
349 event.command.params[0], event.command.params[1],
350 event.command.params[2]);
351 }
352 break;
353 }
354
355 case TrackEvent::Type::SubroutineCall:
356 ImGui::TextColored(ImColor(GetColorSubroutine()), tr("CALL"));
357 break;
358
359 case TrackEvent::Type::End:
360 ImGui::TextDisabled(tr("END"));
361 break;
362 }
363 }
364
365 bool hovered = ImGui::IsItemHovered();
366 const bool double_clicked =
367 hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
368 const bool right_clicked =
369 hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
370
371 // If empty and double-clicked, insert a default note
372 if (!has_event && double_clicked) {
373 if (on_edit_) {
374 on_edit_();
375 }
377 tick, static_cast<uint8_t>(kNoteMinPitch + 36), kDurationSixteenth);
378 track.InsertEvent(evt);
379 return;
380 }
381
382 const std::string popup_id =
383 absl::StrFormat("EditEvent##%d_%d_%d", channel_idx, tick, event_index);
384 if ((double_clicked || right_clicked) && has_event) {
385 ImGui::OpenPopup(popup_id.c_str());
386 }
387
388 if (ImGui::BeginPopup(popup_id.c_str())) {
389 if (!has_event) {
390 ImGui::TextDisabled(tr("Empty"));
391 ImGui::EndPopup();
392 return;
393 }
394
395 auto& event = *event_ptr;
396 if (event.type == TrackEvent::Type::Note) {
397 static int edit_pitch = kNoteMinPitch + 36;
398 static int edit_duration = kDurationSixteenth;
399 edit_pitch = event.note.pitch;
400 edit_duration = event.note.duration;
401
402 static std::vector<std::string> note_labels;
403 if (note_labels.empty()) {
404 for (int p = kNoteMinPitch; p <= kNoteMaxPitch; ++p) {
405 Note n;
406 n.pitch = static_cast<uint8_t>(p);
407 note_labels.push_back(n.GetNoteName());
408 }
409 }
410 int idx = edit_pitch - kNoteMinPitch;
411 idx = std::clamp(idx, 0, static_cast<int>(note_labels.size()) - 1);
412
413 ImGui::Text(tr("Edit Note"));
414 if (ImGui::BeginCombo(tr("Pitch"), note_labels[idx].c_str())) {
415 for (int i = 0; i < static_cast<int>(note_labels.size()); ++i) {
416 bool sel = (i == idx);
417 if (ImGui::Selectable(note_labels[i].c_str(), sel)) {
418 idx = i;
419 edit_pitch = kNoteMinPitch + i;
420 }
421 if (sel)
422 ImGui::SetItemDefaultFocus();
423 }
424 ImGui::EndCombo();
425 }
426
427 if (ImGui::SliderInt(tr("Duration"), &edit_duration, 1, 0xFF)) {
428 edit_duration = std::clamp(edit_duration, 1, 0xFF);
429 }
430
431 if (ImGui::Button(tr("Apply"))) {
432 if (on_edit_) {
433 on_edit_();
434 }
435 event.note.pitch = static_cast<uint8_t>(edit_pitch);
436 event.note.duration = static_cast<uint8_t>(edit_duration);
437 ImGui::CloseCurrentPopup();
438 }
439 } else if (event.type == TrackEvent::Type::Command) {
440 int current_cmd_idx = 0;
441 for (size_t i = 0; i < IM_ARRAYSIZE(kCommandOptions); ++i) {
442 if (kCommandOptions[i].opcode == event.command.opcode) {
443 current_cmd_idx = static_cast<int>(i);
444 break;
445 }
446 }
447 ImGui::Text(tr("Edit Command"));
448 if (ImGui::BeginCombo(tr("Opcode"),
449 kCommandOptions[current_cmd_idx].name)) {
450 for (size_t i = 0; i < IM_ARRAYSIZE(kCommandOptions); ++i) {
451 bool sel = (static_cast<int>(i) == current_cmd_idx);
452 if (ImGui::Selectable(kCommandOptions[i].name, sel)) {
453 current_cmd_idx = static_cast<int>(i);
454 }
455 if (sel)
456 ImGui::SetItemDefaultFocus();
457 }
458 ImGui::EndCombo();
459 }
460
461 int p0 = event.command.params[0];
462 int p1 = event.command.params[1];
463 int p2 = event.command.params[2];
464
465 uint8_t opcode = kCommandOptions[current_cmd_idx].opcode;
466
467 if (opcode == 0xE0 && bank) { // Set Instrument
468 // Instrument selector
469 const auto* inst = bank->GetInstrument(p0);
470 std::string preview =
471 inst ? absl::StrFormat("%02X: %s", p0, inst->name.c_str())
472 : absl::StrFormat("%02X", p0);
473 if (ImGui::BeginCombo(tr("Instrument"), preview.c_str())) {
474 for (size_t i = 0; i < bank->GetInstrumentCount(); ++i) {
475 const auto* item = bank->GetInstrument(i);
476 bool is_selected = (static_cast<int>(i) == p0);
477 if (ImGui::Selectable(
478 absl::StrFormat("%02X: %s", i, item->name.c_str()).c_str(),
479 is_selected)) {
480 p0 = static_cast<int>(i);
481 }
482 if (is_selected)
483 ImGui::SetItemDefaultFocus();
484 }
485 ImGui::EndCombo();
486 }
487 } else {
488 ImGui::InputInt(tr("Param 0 (hex)"), &p0, 1, 4,
489 ImGuiInputTextFlags_CharsHexadecimal);
490 }
491
492 ImGui::InputInt(tr("Param 1 (hex)"), &p1, 1, 4,
493 ImGuiInputTextFlags_CharsHexadecimal);
494 ImGui::InputInt(tr("Param 2 (hex)"), &p2, 1, 4,
495 ImGuiInputTextFlags_CharsHexadecimal);
496
497 if (ImGui::Button(tr("Apply"))) {
498 if (on_edit_) {
499 on_edit_();
500 }
501 event.command.opcode = opcode;
502 event.command.params[0] = static_cast<uint8_t>(p0 & 0xFF);
503 event.command.params[1] = static_cast<uint8_t>(p1 & 0xFF);
504 event.command.params[2] = static_cast<uint8_t>(p2 & 0xFF);
505 ImGui::CloseCurrentPopup();
506 }
507 ImGui::SameLine();
508 ImGui::TextDisabled("%s", DescribeCommand(event.command.opcode).c_str());
509 } else {
510 ImGui::TextDisabled(tr("Unsupported edit type"));
511 }
512
513 ImGui::EndPopup();
514 }
515}
516
518 if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
519 if (ImGui::GetIO().KeyShift && selection_anchor_row_ == -1) {
522 }
523 if (!ImGui::GetIO().KeyShift && selection_anchor_row_ != -1) {
526 }
527 selected_row_ = std::max(0, selected_row_ - 1);
528 }
529 if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
530 if (ImGui::GetIO().KeyShift && selection_anchor_row_ == -1) {
533 }
534 if (!ImGui::GetIO().KeyShift && selection_anchor_row_ != -1) {
537 }
538 selected_row_++; // Limit checked against song length later
539 }
540 if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
541 if (ImGui::GetIO().KeyShift && selection_anchor_row_ == -1) {
544 }
545 if (!ImGui::GetIO().KeyShift && selection_anchor_row_ != -1) {
548 }
549 selected_col_ = std::max(1, selected_col_ - 1);
550 }
551 if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
552 if (ImGui::GetIO().KeyShift && selection_anchor_row_ == -1) {
555 }
556 if (!ImGui::GetIO().KeyShift && selection_anchor_row_ != -1) {
559 }
560 selected_col_ = std::min(8, selected_col_ + 1);
561 }
562
563 if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
564 selected_row_ = std::max(0, selected_row_ - 16);
565 }
566 if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
567 selected_row_ += 16;
568 }
569 if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
570 selected_row_ = 0;
571 }
572}
573
575 if (!song || song->segments.empty())
576 return;
577
578 if (selected_row_ < 0 || selected_col_ < 1 || selected_col_ > 8)
579 return;
580 int ch = selected_col_ - 1;
581
582 auto& track = song->segments[0].tracks[ch];
583 int tick = selected_row_ * ticks_per_row_;
584
585 // Helper to trigger undo
586 auto TriggerEdit = [this]() {
587 if (on_edit_)
588 on_edit_();
589 };
590
591 // Handle Note Entry
592 // Mapping: Z=C, S=C#, X=D, D=D#, C=E, V=F, G=F#, B=G, H=G#, N=A, J=A#, M=B
593 // Octave +1: Q, 2, W, 3, E, R, 5, T, 6, Y, 7, U
594 struct KeyNote {
595 ImGuiKey key;
596 int semitone;
597 int octave_offset;
598 };
599 static const KeyNote key_map[] = {
600 {ImGuiKey_Z, 0, 0}, {ImGuiKey_S, 1, 0}, {ImGuiKey_X, 2, 0},
601 {ImGuiKey_D, 3, 0}, {ImGuiKey_C, 4, 0}, {ImGuiKey_V, 5, 0},
602 {ImGuiKey_G, 6, 0}, {ImGuiKey_B, 7, 0}, {ImGuiKey_H, 8, 0},
603 {ImGuiKey_N, 9, 0}, {ImGuiKey_J, 10, 0}, {ImGuiKey_M, 11, 0},
604 {ImGuiKey_Q, 0, 1}, {ImGuiKey_2, 1, 1}, {ImGuiKey_W, 2, 1},
605 {ImGuiKey_3, 3, 1}, {ImGuiKey_E, 4, 1}, {ImGuiKey_R, 5, 1},
606 {ImGuiKey_5, 6, 1}, {ImGuiKey_T, 7, 1}, {ImGuiKey_6, 8, 1},
607 {ImGuiKey_Y, 9, 1}, {ImGuiKey_7, 10, 1}, {ImGuiKey_U, 11, 1}};
608
609 static int base_octave = 4; // Default octave
610
611 // Octave Control
612 if (ImGui::IsKeyPressed(ImGuiKey_F1))
613 base_octave = std::max(1, base_octave - 1);
614 if (ImGui::IsKeyPressed(ImGuiKey_F2))
615 base_octave = std::min(6, base_octave + 1);
616
617 // Check note keys
618 for (const auto& kn : key_map) {
619 if (ImGui::IsKeyPressed(kn.key)) {
620 TriggerEdit();
621
622 int octave = base_octave + kn.octave_offset;
623 if (octave > 6)
624 octave = 6;
625
626 uint8_t pitch = kNoteMinPitch + (octave - 1) * 12 + kn.semitone;
627
628 // Check for existing event at this tick
629 bool found = false;
630 for (auto& evt : track.events) {
631 if (evt.tick == tick) {
632 if (evt.type == TrackEvent::Type::Note) {
633 evt.note.pitch = pitch; // Update pitch, keep duration
634 } else {
635 // Replace command/other with note
636 evt = TrackEvent::MakeNote(tick, pitch, kDurationSixteenth);
637 }
638 found = true;
639 break;
640 }
641 }
642
643 if (!found) {
644 track.InsertEvent(
646 }
647
648 // Auto-advance
650 return;
651 }
652 }
653
654 // Deletion
655 if (ImGui::IsKeyPressed(ImGuiKey_Delete) ||
656 ImGui::IsKeyPressed(ImGuiKey_Backspace)) {
657 bool changed = false;
658
659 // Handle range deletion if selected
660 if (selection_anchor_row_ != -1) {
661 TriggerEdit();
662
663 int row_start = std::min(selected_row_, selection_anchor_row_);
664 int row_end = std::max(selected_row_, selection_anchor_row_);
665 int col_start = std::min(selected_col_, selection_anchor_col_);
666 int col_end = std::max(selected_col_, selection_anchor_col_);
667
668 int tick_start = row_start * ticks_per_row_;
669 int tick_end = (row_end + 1) * ticks_per_row_;
670
671 // Delete events in each selected channel within the tick range
672 auto& segment = song->segments[0];
673 for (int c = col_start; c <= col_end; ++c) {
674 int ch_idx = c - 1;
675 if (ch_idx < 0 || ch_idx >= 8)
676 continue;
677
678 auto& ch_track = segment.tracks[ch_idx];
679 // Remove events in reverse to preserve indices
680 for (int i = static_cast<int>(ch_track.events.size()) - 1; i >= 0;
681 --i) {
682 int evt_tick = ch_track.events[i].tick;
683 if (evt_tick >= tick_start && evt_tick < tick_end) {
684 ch_track.RemoveEvent(static_cast<size_t>(i));
685 }
686 }
687 }
688
689 // Clear selection range
692 changed = true;
693 } else {
694 // Single cell deletion
695 for (size_t i = 0; i < track.events.size(); ++i) {
696 if (track.events[i].tick == tick) {
697 TriggerEdit();
698 track.RemoveEvent(i);
699 changed = true;
700 break;
701 }
702 }
703 }
704
705 if (changed) {
707 }
708 }
709
710 // Special keys
711 if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
712 // Insert Key Off / Rest
713 TriggerEdit();
714
715 // Check if an event already exists at this tick
716 bool found = false;
717 for (auto& evt : track.events) {
718 if (evt.tick == tick) {
719 // Convert existing event to a rest
720 evt.type = TrackEvent::Type::Note;
721 evt.note.pitch = kNoteRest;
722 evt.note.duration = kDurationSixteenth;
723 evt.note.velocity = 0;
724 found = true;
725 break;
726 }
727 }
728
729 if (!found) {
730 // Insert a new rest event
731 track.InsertEvent(
733 }
734
735 // Auto-advance cursor
737 }
738}
739
741 if (!song || song->segments.empty())
742 return;
743 if (selected_row_ < 0 || selected_col_ < 1 || selected_col_ > 8)
744 return;
745
746 int ch = selected_col_ - 1;
747 auto& track = song->segments[0].tracks[ch];
748 int tick = selected_row_ * ticks_per_row_;
749
750 auto TriggerEdit = [this]() {
751 if (on_edit_)
752 on_edit_();
753 };
754
755 // Insert simple SetInstrument command (Cmd+I / Ctrl+I)
756 if (ImGui::IsKeyPressed(ImGuiKey_I) &&
757 (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) {
758 TriggerEdit();
759 // default instrument 0
760 TrackEvent cmd = TrackEvent::MakeCommand(tick, 0xE0, 0x00);
761 track.InsertEvent(cmd);
762 }
763
764 // Insert SetPan (Cmd+P)
765 if (ImGui::IsKeyPressed(ImGuiKey_P) &&
766 (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) {
767 TriggerEdit();
768 TrackEvent cmd = TrackEvent::MakeCommand(tick, 0xE1, 0x10); // center pan
769 track.InsertEvent(cmd);
770 }
771
772 // Insert Channel Volume (Cmd+V)
773 if (ImGui::IsKeyPressed(ImGuiKey_V) &&
774 (ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeySuper)) {
775 TriggerEdit();
776 TrackEvent cmd = TrackEvent::MakeCommand(tick, 0xED, 0x7F);
777 track.InsertEvent(cmd);
778 }
779
780 // Quick duration tweak for the note at this tick (Alt+[ / Alt+])
781 if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket) && ImGui::GetIO().KeyAlt) {
782 for (auto& evt : track.events) {
783 if (evt.tick == tick && evt.type == TrackEvent::Type::Note) {
784 TriggerEdit();
785 evt.note.duration = std::max<uint16_t>(1, evt.note.duration - 6);
786 break;
787 }
788 }
789 }
790 if (ImGui::IsKeyPressed(ImGuiKey_RightBracket) && ImGui::GetIO().KeyAlt) {
791 for (auto& evt : track.events) {
792 if (evt.tick == tick && evt.type == TrackEvent::Type::Note) {
793 TriggerEdit();
794 evt.note.duration = evt.note.duration + 6;
795 break;
796 }
797 }
798 }
799}
800
801} // namespace music
802} // namespace editor
803} // namespace yaze
void HandleKeyboardInput(MusicSong *song)
std::function< void()> on_edit_
void HandleEditShortcuts(MusicSong *song)
void DrawEventCell(MusicTrack &track, int event_index, int channel_idx, uint16_t tick, const MusicBank *bank)
void DrawGrid(MusicSong *song, const MusicBank *bank)
void DrawToolbar(MusicSong *song)
void Draw(MusicSong *song, const MusicBank *bank=nullptr)
Draw the tracker view for the given song.
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
Manages the collection of songs, instruments, and samples from a ROM.
Definition music_bank.h:27
MusicInstrument * GetInstrument(int index)
Get an instrument by index.
size_t GetInstrumentCount() const
Get the number of instruments.
Definition music_bank.h:176
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
Contains classes and functions for handling music data in Zelda 3.
constexpr uint8_t kNoteRest
Definition song_data.h:58
constexpr uint8_t kDurationSixteenth
Definition song_data.h:68
constexpr uint8_t kNoteMinPitch
Definition song_data.h:55
constexpr uint8_t kNoteMaxPitch
Definition song_data.h:56
float alpha
Definition color.h:20
A complete song composed of segments.
Definition song_data.h:334
std::vector< MusicSegment > segments
Definition song_data.h:336
uint32_t GetTotalDuration() const
Definition song_data.h:343
One of 8 channels in a music segment.
Definition song_data.h:291
void InsertEvent(TrackEvent event)
Definition song_data.h:467
std::vector< TrackEvent > events
Definition song_data.h:292
Represents a single musical note.
Definition song_data.h:192
std::string GetNoteName() const
Definition song_data.h:433
A single event in a music track (note, command, or control).
Definition song_data.h:247
static TrackEvent MakeNote(uint16_t tick, uint8_t pitch, uint8_t duration, uint8_t velocity=0)
Definition song_data.h:259
static TrackEvent MakeCommand(uint16_t tick, uint8_t opcode, uint8_t p1=0, uint8_t p2=0, uint8_t p3=0)
Definition song_data.h:270