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