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