172 uint32_t duration = segment.GetDuration();
175 if (ImGui::BeginTable(
"TrackerTable", 9,
176 ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollY |
177 ImGuiTableFlags_RowBg |
178 ImGuiTableFlags_Resizable)) {
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());
185 ImGui::TableHeadersRow();
189 ImGuiListClipper clipper;
192 while (clipper.Step()) {
193 for (
int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
197 ImGui::TableNextRow();
201 ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0,
202 GetColorBeatHighlight());
206 ImGui::TableSetColumnIndex(0);
207 ImGui::TextDisabled(
"%04X", tick_start);
210 for (
int ch = 0; ch < 8; ++ch) {
211 ImGui::TableSetColumnIndex(ch + 1);
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));
230 }
else if (is_selected) {
231 ImGui::TableSetBgColor(
232 ImGuiTableBgTarget_CellBg,
233 GetColorSelection(
false));
236 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
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);
251 if (evt.tick >= tick_end)
259 ImGui::PushID(row * 100 + ch);
260 if (ImGui::Selectable(
"##cell",
false,
261 ImGuiSelectableFlags_SpanAllColumns |
262 ImGuiSelectableFlags_AllowOverlap,
264 if (ImGui::GetIO().KeyShift) {
285 int channel_idx, uint16_t tick,
288 (event_index >= 0 && event_index < static_cast<int>(track.
events.size()))
289 ? &track.
events[event_index]
292 bool has_event = event_ptr !=
nullptr;
295 ImGui::TextDisabled(
"...");
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);
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);
315 if (event.command.opcode == 0xE0) {
317 const auto* inst = bank->
GetInstrument(event.command.params[0]);
319 label = absl::StrFormat(
"Instr: %s", inst->name.c_str());
321 absl::StrFormat(
"Set Instrument: %s (ID %02X)",
322 inst->name.c_str(), event.command.params[0]);
324 label = absl::StrFormat(
"Instr: %02X", event.command.params[0]);
327 label = absl::StrFormat(
"Instr: %02X", event.command.params[0]);
329 }
else if (event.command.opcode == 0xE1) {
330 int pan =
event.command.params[0];
332 label =
"Pan: Center";
334 label = absl::StrFormat(
"Pan: L%d", 0x0A - pan);
336 label = absl::StrFormat(
"Pan: R%d", pan - 0x0A);
337 }
else if (event.command.opcode == 0xE7) {
338 label = absl::StrFormat(
"Tempo: %d", event.command.params[0]);
339 }
else if (event.command.opcode == 0xED) {
340 label = absl::StrFormat(
"Vol: %d", event.command.params[0]);
341 }
else if (event.command.opcode == 0xE5) {
342 label = absl::StrFormat(
"M.Vol: %d", event.command.params[0]);
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]);
355 case TrackEvent::Type::SubroutineCall:
356 ImGui::TextColored(ImColor(GetColorSubroutine()), tr(
"CALL"));
359 case TrackEvent::Type::End:
360 ImGui::TextDisabled(tr(
"END"));
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);
372 if (!has_event && double_clicked) {
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());
388 if (ImGui::BeginPopup(popup_id.c_str())) {
390 ImGui::TextDisabled(tr(
"Empty"));
395 auto&
event = *event_ptr;
396 if (event.type == TrackEvent::Type::Note) {
400 edit_duration =
event.note.duration;
402 static std::vector<std::string> note_labels;
403 if (note_labels.empty()) {
406 n.
pitch =
static_cast<uint8_t
>(p);
411 idx = std::clamp(idx, 0,
static_cast<int>(note_labels.size()) - 1);
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)) {
422 ImGui::SetItemDefaultFocus();
427 if (ImGui::SliderInt(tr(
"Duration"), &edit_duration, 1, 0xFF)) {
428 edit_duration = std::clamp(edit_duration, 1, 0xFF);
431 if (ImGui::Button(tr(
"Apply"))) {
435 event.note.pitch =
static_cast<uint8_t
>(edit_pitch);
436 event.note.duration =
static_cast<uint8_t
>(edit_duration);
437 ImGui::CloseCurrentPopup();
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);
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);
456 ImGui::SetItemDefaultFocus();
461 int p0 =
event.command.params[0];
462 int p1 =
event.command.params[1];
463 int p2 =
event.command.params[2];
465 uint8_t opcode = kCommandOptions[current_cmd_idx].
opcode;
467 if (opcode == 0xE0 && bank) {
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())) {
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(),
480 p0 =
static_cast<int>(i);
483 ImGui::SetItemDefaultFocus();
488 ImGui::InputInt(tr(
"Param 0 (hex)"), &p0, 1, 4,
489 ImGuiInputTextFlags_CharsHexadecimal);
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);
497 if (ImGui::Button(tr(
"Apply"))) {
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();
508 ImGui::TextDisabled(
"%s", DescribeCommand(event.command.opcode).c_str());
510 ImGui::TextDisabled(tr(
"Unsupported edit type"));
575 if (!song || song->
segments.empty())
578 if (
selected_row_ < 0 || selected_col_ < 1 || selected_col_ > 8)
582 auto& track = song->
segments[0].tracks[ch];
586 auto TriggerEdit = [
this]() {
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}};
609 static int base_octave = 4;
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);
618 for (
const auto& kn : key_map) {
619 if (ImGui::IsKeyPressed(kn.key)) {
622 int octave = base_octave + kn.octave_offset;
626 uint8_t pitch =
kNoteMinPitch + (octave - 1) * 12 + kn.semitone;
630 for (
auto& evt : track.events) {
631 if (evt.tick == tick) {
632 if (evt.type == TrackEvent::Type::Note) {
633 evt.note.pitch = pitch;
655 if (ImGui::IsKeyPressed(ImGuiKey_Delete) ||
656 ImGui::IsKeyPressed(ImGuiKey_Backspace)) {
657 bool changed =
false;
673 for (
int c = col_start; c <= col_end; ++c) {
675 if (ch_idx < 0 || ch_idx >= 8)
678 auto& ch_track = segment.tracks[ch_idx];
680 for (
int i =
static_cast<int>(ch_track.events.size()) - 1; i >= 0;
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));
695 for (
size_t i = 0; i < track.events.size(); ++i) {
696 if (track.events[i].tick == tick) {
698 track.RemoveEvent(i);
711 if (ImGui::IsKeyPressed(ImGuiKey_Space)) {
717 for (
auto& evt : track.events) {
718 if (evt.tick == tick) {
720 evt.type = TrackEvent::Type::Note;
723 evt.note.velocity = 0;