| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
#include "imgui.h" |
| 17 |
#include "debug_ui_panels.h" |
| 18 |
#include "debug_ui_bridge.h" |
| 19 |
#include "debug_ui_widgets.h" |
| 20 |
#include "debug_ui_scope.h" |
| 21 |
|
| 22 |
#include <math.h> |
| 23 |
#include <stdint.h> |
| 24 |
#include <stdio.h> |
| 25 |
#include <string.h> |
| 26 |
|
| 27 |
namespace { |
| 28 |
|
| 29 |
constexpr double YM_CLOCK_HZ = 3579545.0; |
| 30 |
constexpr double YM_SAMPLE_HZ = YM_CLOCK_HZ / 64.0; |
| 31 |
constexpr int OP_COUNT = 4; |
| 32 |
|
| 33 |
static int s_selected_ch = 0; |
| 34 |
|
| 35 |
struct AlgorithmInfo { |
| 36 |
uint8_t inputs[4]; |
| 37 |
uint8_t carriers_mask; |
| 38 |
const char *desc; |
| 39 |
}; |
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
|
| 44 |
|
| 45 |
|
| 46 |
|
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
|
| 51 |
|
| 52 |
static const AlgorithmInfo s_alg[8] = { |
| 53 |
{{0, 1, 2, 3}, 0x8, "M1 -> C1 -> M2 -> C2 -> out"}, |
| 54 |
{{0, 0, 5, 3}, 0x8, "(M1 + C1) -> M2 -> C2 -> out"}, |
| 55 |
{{0, 0, 2, 6}, 0x8, "(M1 + (C1 -> M2)) -> C2 -> out"}, |
| 56 |
{{0, 1, 0, 7}, 0x8, "((M1 -> C1) + M2) -> C2 -> out"}, |
| 57 |
{{0, 1, 0, 3}, 0xA, "(M1 -> C1) + (M2 -> C2) -> out"}, |
| 58 |
{{0, 1, 1, 1}, 0xE, "M1 modulates C1, M2 and C2; all three out"}, |
| 59 |
{{0, 1, 0, 0}, 0xE, "(M1 -> C1) + M2 + C2 -> out"}, |
| 60 |
{{0, 0, 0, 0}, 0xF, "M1 + M2 + C1 + C2 -> out"}, |
| 61 |
}; |
| 62 |
|
| 63 |
|
| 64 |
static const int kOpToSlot[OP_COUNT] = { 0, 2, 1, 3 }; |
| 65 |
|
| 66 |
|
| 67 |
inline uint8_t op_mask_to_slot_mask(uint8_t op_mask) |
| 68 |
{ |
| 69 |
uint8_t slots = 0; |
| 70 |
for (int o = 0; o < OP_COUNT; ++o) { |
| 71 |
if ((op_mask & (1u << o)) != 0) slots |= (uint8_t)(1u << kOpToSlot[o]); |
| 72 |
} |
| 73 |
return slots; |
| 74 |
} |
| 75 |
|
| 76 |
static const char *s_slot_names[OP_COUNT] = { "M1", "M2", "C1", "C2" }; |
| 77 |
static const char *s_eg_names[5] = { "-", "Attack", "Decay", "Sustain", "Release" }; |
| 78 |
static const char *s_lfo_waves[4] = { "saw", "square", "triangle", "noise" }; |
| 79 |
|
| 80 |
inline uint8_t ym_reg(int reg) { return YM_debug_get_reg((uint8_t)reg); } |
| 81 |
inline uint8_t ch_reg(int base, int ch) { return ym_reg(base + ch); } |
| 82 |
inline uint8_t op_reg(int base, int slot, int ch) { return ym_reg(base + slot * 8 + ch); } |
| 83 |
inline int op_index(int slot, int ch) { return slot * 8 + ch; } |
| 84 |
|
| 85 |
struct NoteInfo { |
| 86 |
bool valid; |
| 87 |
char text[48]; |
| 88 |
double hz; |
| 89 |
}; |
| 90 |
|
| 91 |
float clamp01(float v) |
| 92 |
{ |
| 93 |
if (v < 0.0f) return 0.0f; |
| 94 |
if (v > 1.0f) return 1.0f; |
| 95 |
return v; |
| 96 |
} |
| 97 |
|
| 98 |
uint8_t alg_for_ch(int ch) { return (uint8_t)(ch_reg(0x20, ch) & 0x07); } |
| 99 |
|
| 100 |
|
| 101 |
|
| 102 |
uint8_t carriers_for_alg(int alg) { return op_mask_to_slot_mask(s_alg[alg & 7].carriers_mask); } |
| 103 |
|
| 104 |
float operator_level(int slot, int ch) |
| 105 |
{ |
| 106 |
ym_debug_operator op = {}; |
| 107 |
if (!YM_debug_get_operator(op_index(slot, ch), &op)) { |
| 108 |
return 0.0f; |
| 109 |
} |
| 110 |
return clamp01(1.0f - (float)op.eg_attenuation / 1023.0f); |
| 111 |
} |
| 112 |
|
| 113 |
float channel_carrier_level(int ch) |
| 114 |
{ |
| 115 |
const uint8_t carriers = carriers_for_alg(alg_for_ch(ch)); |
| 116 |
float level = 0.0f; |
| 117 |
for (int slot = 0; slot < OP_COUNT; ++slot) { |
| 118 |
if ((carriers & (1u << slot)) != 0) { |
| 119 |
const float l = operator_level(slot, ch); |
| 120 |
if (l > level) level = l; |
| 121 |
} |
| 122 |
} |
| 123 |
return level; |
| 124 |
} |
| 125 |
|
| 126 |
bool channel_effectively_silent(int ch) |
| 127 |
{ |
| 128 |
for (int slot = 0; slot < OP_COUNT; ++slot) { |
| 129 |
ym_debug_operator op = {}; |
| 130 |
if (YM_debug_get_operator(op_index(slot, ch), &op) && op.eg_attenuation < 0x3f0) { |
| 131 |
return false; |
| 132 |
} |
| 133 |
} |
| 134 |
return true; |
| 135 |
} |
| 136 |
|
| 137 |
ImU32 eg_color(uint8_t state) |
| 138 |
{ |
| 139 |
switch (state) { |
| 140 |
case YM_EG_ATTACK: return IM_COL32(105, 220, 125, 255); |
| 141 |
case YM_EG_DECAY: return IM_COL32(235, 205, 90, 255); |
| 142 |
case YM_EG_SUSTAIN: return IM_COL32(105, 170, 255, 255); |
| 143 |
case YM_EG_RELEASE: return IM_COL32(195, 130, 230, 255); |
| 144 |
default: return IM_COL32(120, 126, 136, 255); |
| 145 |
} |
| 146 |
} |
| 147 |
|
| 148 |
void note_from_kc_kf(uint8_t kc, uint8_t kf, NoteInfo *out) |
| 149 |
{ |
| 150 |
static const char *names[12] = { "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "C" }; |
| 151 |
const int oct = kc >> 4; |
| 152 |
const int low = kc & 0x0f; |
| 153 |
const int semi = (kc & 3) + 3 * ((kc >> 2) & 3); |
| 154 |
out->valid = (low != 3 && low != 7 && low != 11 && low != 15); |
| 155 |
if (!out->valid) { |
| 156 |
out->hz = 0.0; |
| 157 |
snprintf(out->text, sizeof(out->text), "invalid KC %02X", kc); |
| 158 |
return; |
| 159 |
} |
| 160 |
const double fine_semi = (double)(kf >> 2) / 64.0; |
| 161 |
const double midi = 12.0 * (double)(oct + 1) + 1.0 + (double)semi + fine_semi; |
| 162 |
out->hz = 440.0 * pow(2.0, (midi - 69.0) / 12.0); |
| 163 |
snprintf(out->text, sizeof(out->text), "%s%d %+d c %.2f Hz", names[semi], oct, (int)(fine_semi * 100.0 + 0.5), out->hz); |
| 164 |
} |
| 165 |
|
| 166 |
void draw_key_mask(uint8_t mask) |
| 167 |
{ |
| 168 |
|
| 169 |
|
| 170 |
for (int slot = 0; slot < OP_COUNT; ++slot) { |
| 171 |
if (slot != 0) ImGui::SameLine(0.0f, 4.0f); |
| 172 |
ImGui::TextColored((mask & (1u << slot)) ? ImVec4(0.45f, 1.0f, 0.55f, 1.0f) |
| 173 |
: ImVec4(0.35f, 0.36f, 0.38f, 1.0f), |
| 174 |
"%s", s_slot_names[slot]); |
| 175 |
} |
| 176 |
} |
| 177 |
|
| 178 |
void draw_eg_summary(int ch) |
| 179 |
{ |
| 180 |
for (int slot = 0; slot < OP_COUNT; ++slot) { |
| 181 |
ym_debug_operator op = {}; |
| 182 |
uint8_t state = 0; |
| 183 |
if (YM_debug_get_operator(op_index(slot, ch), &op)) { |
| 184 |
state = op.eg_state; |
| 185 |
} |
| 186 |
const char c = (state == YM_EG_ATTACK) ? 'A' : (state == YM_EG_DECAY) ? 'D' : |
| 187 |
(state == YM_EG_SUSTAIN) ? 'S' : (state == YM_EG_RELEASE) ? 'R' : '-'; |
| 188 |
if (slot != 0) ImGui::SameLine(0.0f, 4.0f); |
| 189 |
ImGui::TextColored(ImColor(eg_color(state)), "%c", c); |
| 190 |
} |
| 191 |
} |
| 192 |
|
| 193 |
void draw_channels_tab() |
| 194 |
{ |
| 195 |
|
| 196 |
|
| 197 |
|
| 198 |
|
| 199 |
ImGui::TextDisabled("Channels are 0-7, matching the $20+ch / $28+ch register offsets " |
| 200 |
"and the $08 key-on channel field."); |
| 201 |
if (ImGui::BeginTable("ym_channels", 11, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) { |
| 202 |
ImGui::TableSetupColumn("Ch", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Ch", "00")); |
| 203 |
ImGui::TableSetupColumn("KC / note"); |
| 204 |
ImGui::TableSetupColumn("KF", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("KF", "$FF")); |
| 205 |
ImGui::TableSetupColumn("ALG", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("ALG", "000")); |
| 206 |
ImGui::TableSetupColumn("FB", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("FB", "00")); |
| 207 |
ImGui::TableSetupColumn("Pan", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Pan", "L+R")); |
| 208 |
ImGui::TableSetupColumn("PMS", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("PMS", "000")); |
| 209 |
ImGui::TableSetupColumn("AMS", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("AMS", "000")); |
| 210 |
ImGui::TableSetupColumn("Key-on"); |
| 211 |
ImGui::TableSetupColumn("EG"); |
| 212 |
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Level", "-00.0 dB")); |
| 213 |
ImGui::TableHeadersRow(); |
| 214 |
for (int ch = 0; ch < YM_NUM_CHANNELS; ++ch) { |
| 215 |
const uint8_t r20 = ch_reg(0x20, ch); |
| 216 |
const uint8_t kc = ch_reg(0x28, ch) & 0x7f; |
| 217 |
const uint8_t kf = ch_reg(0x30, ch) & 0xfc; |
| 218 |
const uint8_t r38 = ch_reg(0x38, ch); |
| 219 |
NoteInfo note; |
| 220 |
note_from_kc_kf(kc, kf, ¬e); |
| 221 |
const bool silent = channel_effectively_silent(ch); |
| 222 |
|
| 223 |
ImGui::TableNextRow(); |
| 224 |
if (silent) ImGui::BeginDisabled(); |
| 225 |
ImGui::TableSetColumnIndex(0); |
| 226 |
char id[16]; |
| 227 |
snprintf(id, sizeof(id), "Ch %d", ch); |
| 228 |
if (ImGui::Selectable(id, s_selected_ch == ch, ImGuiSelectableFlags_SpanAllColumns)) { |
| 229 |
s_selected_ch = ch; |
| 230 |
} |
| 231 |
ImGui::TableSetColumnIndex(1); |
| 232 |
ImGui::Text("$%02X %s", kc, note.text); |
| 233 |
if (!note.valid) ImGui::SameLine(), ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "!"); |
| 234 |
ImGui::TableSetColumnIndex(2); ImGui::Text("%2u", (unsigned)(kf >> 2)); |
| 235 |
ImGui::TableSetColumnIndex(3); ImGui::Text("%u", (unsigned)(r20 & 7)); |
| 236 |
ImGui::TableSetColumnIndex(4); ImGui::Text("%u", (unsigned)((r20 >> 3) & 7)); |
| 237 |
ImGui::TableSetColumnIndex(5); ImGui::Text("%c/%c", (r20 & 0x40) ? 'L' : '-', (r20 & 0x80) ? 'R' : '-'); |
| 238 |
ImGui::TableSetColumnIndex(6); ImGui::Text("%u", (unsigned)((r38 >> 4) & 7)); |
| 239 |
ImGui::TableSetColumnIndex(7); ImGui::Text("%u", (unsigned)(r38 & 3)); |
| 240 |
ImGui::TableSetColumnIndex(8); draw_key_mask(YM_debug_get_keyon(ch)); |
| 241 |
ImGui::TableSetColumnIndex(9); draw_eg_summary(ch); |
| 242 |
ImGui::TableSetColumnIndex(10); |
| 243 |
snprintf(id, sizeof(id), "##lev%d", ch); |
| 244 |
dbgui_level_meter(id, channel_carrier_level(ch), ImVec2(80.0f, 0.0f), IM_COL32(80, 190, 110, 255)); |
| 245 |
if (silent) ImGui::EndDisabled(); |
| 246 |
} |
| 247 |
ImGui::EndTable(); |
| 248 |
} |
| 249 |
} |
| 250 |
|
| 251 |
void draw_env_shape(int slot, int ch, ImVec2 size) |
| 252 |
{ |
| 253 |
ImGui::PushID(slot * 16 + ch); |
| 254 |
ImGui::InvisibleButton("##env", size); |
| 255 |
const ImVec2 p0 = ImGui::GetItemRectMin(); |
| 256 |
const ImVec2 p1 = ImGui::GetItemRectMax(); |
| 257 |
ImGui::PopID(); |
| 258 |
ImDrawList *dl = ImGui::GetWindowDrawList(); |
| 259 |
dl->AddRectFilled(p0, p1, IM_COL32(16, 18, 22, 255)); |
| 260 |
dl->AddRect(p0, p1, IM_COL32(70, 78, 90, 255)); |
| 261 |
|
| 262 |
const uint8_t tl = op_reg(0x60, slot, ch) & 0x7f; |
| 263 |
const uint8_t ar = op_reg(0x80, slot, ch) & 0x1f; |
| 264 |
const uint8_t d1r = op_reg(0xa0, slot, ch) & 0x1f; |
| 265 |
const uint8_t d2r = op_reg(0xc0, slot, ch) & 0x1f; |
| 266 |
const uint8_t d1l = (op_reg(0xe0, slot, ch) >> 4) & 0x0f; |
| 267 |
const uint8_t rr = op_reg(0xe0, slot, ch) & 0x0f; |
| 268 |
|
| 269 |
const float w = p1.x - p0.x; |
| 270 |
const float h = p1.y - p0.y; |
| 271 |
const float loud = (float)tl / 127.0f; |
| 272 |
const float sustain = (float)d1l / 15.0f; |
| 273 |
const float x1 = p0.x + w * (0.12f + (31 - ar) * 0.0025f); |
| 274 |
const float x2 = x1 + w * (0.14f + (31 - d1r) * 0.003f); |
| 275 |
const float x3 = x2 + w * (0.18f + (31 - d2r) * 0.002f); |
| 276 |
const float x4 = p1.x - w * (0.08f + (15 - rr) * 0.004f); |
| 277 |
const float y0 = p1.y - 2.0f; |
| 278 |
const float y_peak = p0.y + 2.0f + h * loud; |
| 279 |
const float y_sus = p0.y + 2.0f + h * (loud + (1.0f - loud) * sustain); |
| 280 |
ImVec2 pts[5] = { ImVec2(p0.x + 2.0f, y0), ImVec2(x1, y_peak), ImVec2(x2, y_sus), ImVec2(x3, y_sus), ImVec2(x4, y0) }; |
| 281 |
dl->AddPolyline(pts, 5, IM_COL32(120, 190, 255, 255), false, 1.5f); |
| 282 |
} |
| 283 |
|
| 284 |
void draw_operator_row(int ch, int slot) |
| 285 |
{ |
| 286 |
const uint8_t r40 = op_reg(0x40, slot, ch); |
| 287 |
const uint8_t r60 = op_reg(0x60, slot, ch); |
| 288 |
const uint8_t r80 = op_reg(0x80, slot, ch); |
| 289 |
const uint8_t ra0 = op_reg(0xa0, slot, ch); |
| 290 |
const uint8_t rc0 = op_reg(0xc0, slot, ch); |
| 291 |
const uint8_t re0 = op_reg(0xe0, slot, ch); |
| 292 |
ym_debug_operator op = {}; |
| 293 |
YM_debug_get_operator(op_index(slot, ch), &op); |
| 294 |
|
| 295 |
ImGui::TableNextRow(); |
| 296 |
ImGui::TableSetColumnIndex(0); ImGui::Text("Ch %d %s", ch, s_slot_names[slot]); |
| 297 |
ImGui::TableSetColumnIndex(1); ImGui::Text("%u", (unsigned)((r40 >> 4) & 7)); |
| 298 |
ImGui::TableSetColumnIndex(2); ImGui::Text("%u", (unsigned)(r40 & 0x0f)); |
| 299 |
ImGui::TableSetColumnIndex(3); ImGui::Text("%u", (unsigned)(r60 & 0x7f)); |
| 300 |
ImGui::TableSetColumnIndex(4); ImGui::Text("%u", (unsigned)((r80 >> 6) & 3)); |
| 301 |
ImGui::TableSetColumnIndex(5); ImGui::Text("%u", (unsigned)(r80 & 0x1f)); |
| 302 |
ImGui::TableSetColumnIndex(6); ImGui::Text("%u", (unsigned)(ra0 & 0x1f)); |
| 303 |
ImGui::TableSetColumnIndex(7); ImGui::Text("%u", (unsigned)((rc0 >> 6) & 3)); |
| 304 |
ImGui::TableSetColumnIndex(8); ImGui::Text("%u", (unsigned)(rc0 & 0x1f)); |
| 305 |
ImGui::TableSetColumnIndex(9); ImGui::Text("%u", (unsigned)((re0 >> 4) & 0x0f)); |
| 306 |
ImGui::TableSetColumnIndex(10); ImGui::Text("%u", (unsigned)(re0 & 0x0f)); |
| 307 |
ImGui::TableSetColumnIndex(11); ImGui::TextUnformatted((ra0 & 0x80) ? "yes" : "no"); |
| 308 |
ImGui::TableSetColumnIndex(12); ImGui::TextColored(ImColor(eg_color(op.eg_state)), "%s", s_eg_names[(op.eg_state <= YM_EG_RELEASE) ? op.eg_state : 0]); |
| 309 |
ImGui::TableSetColumnIndex(13); |
| 310 |
char id[32]; |
| 311 |
snprintf(id, sizeof(id), "##att%d_%d", ch, slot); |
| 312 |
dbgui_level_meter(id, operator_level(slot, ch), ImVec2(90.0f, 0.0f), IM_COL32(90, 170, 240, 255)); |
| 313 |
ImGui::TableSetColumnIndex(14); |
| 314 |
draw_env_shape(slot, ch, ImVec2(110.0f, 28.0f)); |
| 315 |
} |
| 316 |
|
| 317 |
void draw_operators_tab() |
| 318 |
{ |
| 319 |
static bool show_all = false; |
| 320 |
ImGui::Checkbox("Show all 32 operators grouped by channel", &show_all); |
| 321 |
ImGui::TextDisabled("Envelope sketches are qualitative ADSR/TL shapes, not cycle-accurate ymfm EG traces."); |
| 322 |
const int first_ch = show_all ? 0 : s_selected_ch; |
| 323 |
const int last_ch = show_all ? YM_NUM_CHANNELS : (s_selected_ch + 1); |
| 324 |
if (ImGui::BeginTable("ym_ops", 15, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0.0f, 0.0f))) { |
| 325 |
const char *cols[15] = { "Slot", "DT1", "MUL", "TL", "KS", "AR", "D1R", "DT2", "D2R", "D1L", "RR", "AMS-EN", "EG state", "Atten", "ADSR sketch" }; |
| 326 |
for (int i = 0; i < 15; ++i) ImGui::TableSetupColumn(cols[i]); |
| 327 |
ImGui::TableHeadersRow(); |
| 328 |
for (int ch = first_ch; ch < last_ch; ++ch) { |
| 329 |
for (int slot = 0; slot < OP_COUNT; ++slot) draw_operator_row(ch, slot); |
| 330 |
} |
| 331 |
ImGui::EndTable(); |
| 332 |
} |
| 333 |
} |
| 334 |
|
| 335 |
uint8_t source_mask(uint8_t selector) |
| 336 |
{ |
| 337 |
switch (selector) { |
| 338 |
case 1: return 0x1; |
| 339 |
case 2: return 0x2; |
| 340 |
case 3: return 0x4; |
| 341 |
case 5: return 0x3; |
| 342 |
case 6: return 0x5; |
| 343 |
case 7: return 0x6; |
| 344 |
default: return 0x0; |
| 345 |
} |
| 346 |
} |
| 347 |
|
| 348 |
void draw_arrow(ImDrawList *dl, ImVec2 a, ImVec2 b, ImU32 color) |
| 349 |
{ |
| 350 |
dl->AddLine(a, b, color, 2.0f); |
| 351 |
const float ang = atan2f(b.y - a.y, b.x - a.x); |
| 352 |
const float len = 8.0f; |
| 353 |
const ImVec2 p1(b.x - cosf(ang - 0.45f) * len, b.y - sinf(ang - 0.45f) * len); |
| 354 |
const ImVec2 p2(b.x - cosf(ang + 0.45f) * len, b.y - sinf(ang + 0.45f) * len); |
| 355 |
dl->AddTriangleFilled(b, p1, p2, color); |
| 356 |
} |
| 357 |
|
| 358 |
void draw_algorithm_tab() |
| 359 |
{ |
| 360 |
const int ch = s_selected_ch; |
| 361 |
const int alg = alg_for_ch(ch); |
| 362 |
const uint8_t fb = (ch_reg(0x20, ch) >> 3) & 7; |
| 363 |
ImGui::Text("Channel %d ALG %d: %s", ch, alg, s_alg[alg].desc); |
| 364 |
ImGui::TextDisabled("Algorithm table is derived from ymfm_fm.ipp output_4op(). Box brightness follows live EG attenuation."); |
| 365 |
|
| 366 |
const ImVec2 canvas = ImVec2(ImGui::GetContentRegionAvail().x, 330.0f); |
| 367 |
ImGui::InvisibleButton("alg_canvas", canvas); |
| 368 |
const ImVec2 p0 = ImGui::GetItemRectMin(); |
| 369 |
const ImVec2 p1 = ImGui::GetItemRectMax(); |
| 370 |
ImDrawList *dl = ImGui::GetWindowDrawList(); |
| 371 |
dl->AddRectFilled(p0, p1, IM_COL32(14, 16, 20, 255)); |
| 372 |
dl->AddRect(p0, p1, IM_COL32(70, 78, 90, 255)); |
| 373 |
|
| 374 |
ImVec2 c[OP_COUNT] = { |
| 375 |
ImVec2(p0.x + canvas.x * 0.16f, p0.y + canvas.y * 0.42f), |
| 376 |
ImVec2(p0.x + canvas.x * 0.38f, p0.y + canvas.y * 0.25f), |
| 377 |
ImVec2(p0.x + canvas.x * 0.38f, p0.y + canvas.y * 0.62f), |
| 378 |
ImVec2(p0.x + canvas.x * 0.64f, p0.y + canvas.y * 0.42f), |
| 379 |
}; |
| 380 |
const ImVec2 box(70.0f, 42.0f); |
| 381 |
const uint8_t carriers = carriers_for_alg(alg); |
| 382 |
const ImU32 mod_col = IM_COL32(140, 170, 220, 255); |
| 383 |
const ImU32 out_col = IM_COL32(110, 230, 130, 255); |
| 384 |
|
| 385 |
|
| 386 |
|
| 387 |
|
| 388 |
for (int dst_op = 1; dst_op < OP_COUNT; ++dst_op) { |
| 389 |
const uint8_t srcs = source_mask(s_alg[alg].inputs[dst_op]); |
| 390 |
const int dst = kOpToSlot[dst_op]; |
| 391 |
for (int src_op = 0; src_op < OP_COUNT; ++src_op) { |
| 392 |
if ((srcs & (1u << src_op)) != 0) { |
| 393 |
const int src = kOpToSlot[src_op]; |
| 394 |
draw_arrow(dl, ImVec2(c[src].x + box.x * 0.5f, c[src].y), ImVec2(c[dst].x - box.x * 0.5f, c[dst].y), mod_col); |
| 395 |
} |
| 396 |
} |
| 397 |
} |
| 398 |
if (fb != 0) { |
| 399 |
const ImVec2 a(c[0].x - 18.0f, c[0].y - box.y * 0.5f); |
| 400 |
const ImVec2 b(c[0].x + 18.0f, c[0].y - box.y * 0.5f); |
| 401 |
dl->AddBezierCubic(a, ImVec2(a.x - 45.0f, a.y - 60.0f), ImVec2(b.x + 45.0f, b.y - 60.0f), b, IM_COL32(235, 170, 80, 255), 2.0f); |
| 402 |
dl->AddText(ImVec2(c[0].x - 18.0f, a.y - 58.0f), IM_COL32(235, 185, 100, 255), "FB"); |
| 403 |
} |
| 404 |
const ImVec2 out(p0.x + canvas.x * 0.86f, p0.y + canvas.y * 0.42f); |
| 405 |
for (int slot = 0; slot < OP_COUNT; ++slot) { |
| 406 |
if ((carriers & (1u << slot)) != 0) { |
| 407 |
draw_arrow(dl, ImVec2(c[slot].x + box.x * 0.5f, c[slot].y), out, out_col); |
| 408 |
} |
| 409 |
} |
| 410 |
dl->AddText(ImVec2(out.x + 8.0f, out.y - 8.0f), out_col, "OUT"); |
| 411 |
|
| 412 |
for (int slot = 0; slot < OP_COUNT; ++slot) { |
| 413 |
const float lvl = operator_level(slot, ch); |
| 414 |
const int base = (int)(55.0f + lvl * 150.0f); |
| 415 |
const bool carrier = (carriers & (1u << slot)) != 0; |
| 416 |
const ImU32 fill = carrier ? IM_COL32(35, base, 65, 255) : IM_COL32(35, 55, base, 255); |
| 417 |
const ImVec2 b0(c[slot].x - box.x * 0.5f, c[slot].y - box.y * 0.5f); |
| 418 |
const ImVec2 b1(c[slot].x + box.x * 0.5f, c[slot].y + box.y * 0.5f); |
| 419 |
dl->AddRectFilled(b0, b1, fill, 5.0f); |
| 420 |
dl->AddRect(b0, b1, carrier ? out_col : mod_col, 5.0f, 0, 2.0f); |
| 421 |
char label[32]; |
| 422 |
snprintf(label, sizeof(label), "%s\n%.0f%%", s_slot_names[slot], lvl * 100.0f); |
| 423 |
dl->AddText(ImVec2(b0.x + 8.0f, b0.y + 5.0f), IM_COL32(235, 240, 245, 255), label); |
| 424 |
} |
| 425 |
} |
| 426 |
|
| 427 |
void draw_global_tab() |
| 428 |
{ |
| 429 |
const uint8_t r0f = ym_reg(0x0f); |
| 430 |
const uint8_t r14 = ym_reg(0x14); |
| 431 |
const uint8_t r18 = ym_reg(0x18); |
| 432 |
const uint8_t r19 = ym_reg(0x19); |
| 433 |
const uint8_t r1a = ym_reg(0x1a); |
| 434 |
const uint8_t r1b = ym_reg(0x1b); |
| 435 |
const uint16_t ta = (uint16_t)((ym_reg(0x10) << 2) | (ym_reg(0x11) & 3)); |
| 436 |
const uint8_t tb = ym_reg(0x12); |
| 437 |
const double ta_ms = (1024.0 - (double)ta) / YM_SAMPLE_HZ * 1000.0; |
| 438 |
const double tb_ms = (256.0 - (double)tb) * 16.0 / YM_SAMPLE_HZ * 1000.0; |
| 439 |
const uint8_t status = YM_debug_get_status(); |
| 440 |
|
| 441 |
ImGui::Text("Clock: %.0f Hz ymfm sample rate: %.3f Hz", YM_CLOCK_HZ, YM_SAMPLE_HZ); |
| 442 |
ImGui::Separator(); |
| 443 |
ImGui::Text("LFO: rate $%02X, waveform %s, AM depth %u, PM depth %u", r18, s_lfo_waves[r1b & 3], (unsigned)(r19 & 0x7f), (unsigned)(r1a & 0x7f)); |
| 444 |
ImGui::TextDisabled("$19 writes select AM (bit7=0) or PM (bit7=1); PM is shown from the shadowed internal $1A."); |
| 445 |
ImGui::Text("Noise: %s, frequency %u", (r0f & 0x80) ? "enabled" : "disabled", (unsigned)(r0f & 0x1f)); |
| 446 |
ImGui::Text("CT pins: CT1=%u CT2=%u", (unsigned)((r1b >> 6) & 1), (unsigned)((r1b >> 7) & 1)); |
| 447 |
ImGui::Separator(); |
| 448 |
ImGui::Text("Timer A: %u approx %.3f ms", (unsigned)ta, ta_ms); |
| 449 |
ImGui::Text("Timer B: %u approx %.3f ms", (unsigned)tb, tb_ms); |
| 450 |
ImGui::Text("Mode $14: CSM=%u reset TB/TA=%u/%u enable TB/TA=%u/%u load TB/TA=%u/%u", |
| 451 |
(unsigned)((r14 >> 7) & 1), (unsigned)((r14 >> 5) & 1), (unsigned)((r14 >> 4) & 1), |
| 452 |
(unsigned)((r14 >> 3) & 1), (unsigned)((r14 >> 2) & 1), (unsigned)((r14 >> 1) & 1), (unsigned)(r14 & 1)); |
| 453 |
ImGui::Separator(); |
| 454 |
ImGui::Text("Status $%02X: Timer A ovf=%u Timer B ovf=%u busy bit=%u", status, (unsigned)(status & 1), (unsigned)((status >> 1) & 1), (unsigned)((status >> 7) & 1)); |
| 455 |
ImGui::Text("YM_debug_is_busy(): %s", YM_debug_is_busy() ? "true" : "false"); |
| 456 |
} |
| 457 |
|
| 458 |
const char *system_name(int reg) |
| 459 |
{ |
| 460 |
switch (reg) { |
| 461 |
case 0x01: return "TEST / LFO reset"; |
| 462 |
case 0x08: return "Key-on"; |
| 463 |
case 0x0f: return "Noise"; |
| 464 |
case 0x10: return "Timer A high"; |
| 465 |
case 0x11: return "Timer A low"; |
| 466 |
case 0x12: return "Timer B"; |
| 467 |
case 0x14: return "Timer control"; |
| 468 |
case 0x18: return "LFO frequency"; |
| 469 |
case 0x19: return "AM/PM depth write"; |
| 470 |
case 0x1a: return "PM depth shadow"; |
| 471 |
case 0x1b: return "CT / LFO wave"; |
| 472 |
default: return "System"; |
| 473 |
} |
| 474 |
} |
| 475 |
|
| 476 |
void decode_reg(int reg, uint8_t v, char *out, size_t outsz) |
| 477 |
{ |
| 478 |
if (reg < 0x20) { |
| 479 |
switch (reg) { |
| 480 |
case 0x01: snprintf(out, outsz, "test=$%02X LFO reset=%u", v, (unsigned)((v >> 1) & 1)); break; |
| 481 |
case 0x08: snprintf(out, outsz, "ch=%u raw op bits b3-b6=M1,C1,M2,C2", (unsigned)(v & 7)); break; |
| 482 |
case 0x0f: snprintf(out, outsz, "noise %s freq=%u", (v & 0x80) ? "on" : "off", (unsigned)(v & 0x1f)); break; |
| 483 |
case 0x14: snprintf(out, outsz, "CSM=%u en TB/TA=%u/%u load TB/TA=%u/%u", (unsigned)(v >> 7), (unsigned)((v >> 3) & 1), (unsigned)((v >> 2) & 1), (unsigned)((v >> 1) & 1), (unsigned)(v & 1)); break; |
| 484 |
case 0x18: snprintf(out, outsz, "LFO freq=%u", (unsigned)v); break; |
| 485 |
case 0x19: snprintf(out, outsz, "%s depth write=%u", (v & 0x80) ? "PM" : "AM", (unsigned)(v & 0x7f)); break; |
| 486 |
case 0x1b: snprintf(out, outsz, "CT=%u wave=%s", (unsigned)((v >> 6) & 3), s_lfo_waves[v & 3]); break; |
| 487 |
default: snprintf(out, outsz, "%s", system_name(reg)); break; |
| 488 |
} |
| 489 |
} else if (reg < 0x40) { |
| 490 |
const int ch = reg & 7; |
| 491 |
const int base = reg & 0xf8; |
| 492 |
if (base == 0x20) snprintf(out, outsz, "Ch%d pan L/R=%u/%u FB=%u ALG=%u", ch, (unsigned)((v >> 6) & 1), (unsigned)((v >> 7) & 1), (unsigned)((v >> 3) & 7), (unsigned)(v & 7)); |
| 493 |
else if (base == 0x28) snprintf(out, outsz, "Ch%d KC=%u", ch, (unsigned)(v & 0x7f)); |
| 494 |
else if (base == 0x30) snprintf(out, outsz, "Ch%d KF=%u", ch, (unsigned)(v >> 2)); |
| 495 |
else if (base == 0x38) snprintf(out, outsz, "Ch%d PMS=%u AMS=%u", ch, (unsigned)((v >> 4) & 7), (unsigned)(v & 3)); |
| 496 |
else snprintf(out, outsz, "Ch%d unused/shadow", ch); |
| 497 |
} else { |
| 498 |
const int off = reg - 0x40; |
| 499 |
const int group = reg & 0xe0; |
| 500 |
const int slot = (off >> 3) & 3; |
| 501 |
const int ch = off & 7; |
| 502 |
if (group == 0x40) snprintf(out, outsz, "%s Ch%d DT1=%u MUL=%u", s_slot_names[slot], ch, (unsigned)((v >> 4) & 7), (unsigned)(v & 0x0f)); |
| 503 |
else if (group == 0x60) snprintf(out, outsz, "%s Ch%d TL=%u", s_slot_names[slot], ch, (unsigned)(v & 0x7f)); |
| 504 |
else if (group == 0x80) snprintf(out, outsz, "%s Ch%d KS=%u AR=%u", s_slot_names[slot], ch, (unsigned)((v >> 6) & 3), (unsigned)(v & 0x1f)); |
| 505 |
else if (group == 0xa0) snprintf(out, outsz, "%s Ch%d AMS-EN=%u D1R=%u", s_slot_names[slot], ch, (unsigned)((v >> 7) & 1), (unsigned)(v & 0x1f)); |
| 506 |
else if (group == 0xc0) snprintf(out, outsz, "%s Ch%d DT2=%u D2R=%u", s_slot_names[slot], ch, (unsigned)((v >> 6) & 3), (unsigned)(v & 0x1f)); |
| 507 |
else snprintf(out, outsz, "%s Ch%d D1L=%u RR=%u", s_slot_names[slot], ch, (unsigned)((v >> 4) & 0x0f), (unsigned)(v & 0x0f)); |
| 508 |
} |
| 509 |
} |
| 510 |
|
| 511 |
void draw_register_range(const char *label, int first, int last) |
| 512 |
{ |
| 513 |
ImGui::TextUnformatted(label); |
| 514 |
if (ImGui::BeginTable(label, 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) { |
| 515 |
ImGui::TableSetupColumn("Reg", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Reg", "$FF")); |
| 516 |
ImGui::TableSetupColumn("Layout", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Layout", "1111 1111")); |
| 517 |
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Name", "AMS-EN / D1R")); |
| 518 |
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, dbgui_col_width("Value", "$FF (255)")); |
| 519 |
ImGui::TableSetupColumn("Decode"); |
| 520 |
ImGui::TableHeadersRow(); |
| 521 |
for (int reg = first; reg <= last; ++reg) { |
| 522 |
const uint8_t v = ym_reg(reg); |
| 523 |
char bin[16]; |
| 524 |
char layout[32]; |
| 525 |
char name[64]; |
| 526 |
char dec[160]; |
| 527 |
dbgui_format_binary(bin, sizeof(bin), v, 8); |
| 528 |
if (reg < 0x20) snprintf(layout, sizeof(layout), "system"); |
| 529 |
else if (reg < 0x40) snprintf(layout, sizeof(layout), "ch %d", (reg & 7)); |
| 530 |
else snprintf(layout, sizeof(layout), "%s ch %d", s_slot_names[((reg - 0x40) >> 3) & 3], (reg & 7)); |
| 531 |
if (reg < 0x20) snprintf(name, sizeof(name), "%s", system_name(reg)); |
| 532 |
else if (reg < 0x40) snprintf(name, sizeof(name), "%s", ((reg & 0xf8) == 0x20) ? "Pan/FB/ALG" : ((reg & 0xf8) == 0x28) ? "KC" : ((reg & 0xf8) == 0x30) ? "KF" : ((reg & 0xf8) == 0x38) ? "PMS/AMS" : "unused"); |
| 533 |
else snprintf(name, sizeof(name), "%s", ((reg & 0xe0) == 0x40) ? "DT1/MUL" : ((reg & 0xe0) == 0x60) ? "TL" : ((reg & 0xe0) == 0x80) ? "KS/AR" : ((reg & 0xe0) == 0xa0) ? "AMS-EN/D1R" : ((reg & 0xe0) == 0xc0) ? "DT2/D2R" : "D1L/RR"); |
| 534 |
decode_reg(reg, v, dec, sizeof(dec)); |
| 535 |
ImGui::TableNextRow(); |
| 536 |
ImGui::TableSetColumnIndex(0); ImGui::Text("$%02X", reg); dbgui_hover_value_tooltip(dec, v, 1); |
| 537 |
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(layout); |
| 538 |
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(name); |
| 539 |
ImGui::TableSetColumnIndex(3); ImGui::Text("$%02X %s", v, bin); dbgui_hover_value_tooltip(dec, v, 1); |
| 540 |
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(dec); |
| 541 |
} |
| 542 |
ImGui::EndTable(); |
| 543 |
} |
| 544 |
} |
| 545 |
|
| 546 |
void draw_registers_tab() |
| 547 |
{ |
| 548 |
ImGui::TextDisabled("Shadowed registers show only writes the chip accepted; YM_write_reg drops writes while the chip is busy."); |
| 549 |
ImGui::TextDisabled("Per-operator address layout is base + (slot << 3) + channel; slots are M1, M2, C1, C2. Raw $08 key-on bits are M1,C1,M2,C2, but live key masks are normalized."); |
| 550 |
if (ImGui::CollapsingHeader("$00-$1F system", ImGuiTreeNodeFlags_DefaultOpen)) draw_register_range("system_regs", 0x00, 0x1f); |
| 551 |
if (ImGui::CollapsingHeader("$20-$3F per channel", ImGuiTreeNodeFlags_DefaultOpen)) draw_register_range("channel_regs", 0x20, 0x3f); |
| 552 |
if (ImGui::CollapsingHeader("$40-$FF per operator", ImGuiTreeNodeFlags_DefaultOpen)) draw_register_range("operator_regs", 0x40, 0xff); |
| 553 |
} |
| 554 |
|
| 555 |
void draw_scope_tab() |
| 556 |
{ |
| 557 |
static DbgScopeControls ctl; |
| 558 |
static int16_t samples[YM_SCOPE_STREAMS][YM_SCOPE_SAMPLES]; |
| 559 |
static unsigned counts[YM_SCOPE_STREAMS] = {}; |
| 560 |
static bool captured = false; |
| 561 |
|
| 562 |
dbgui_scope_controls(&ctl, YM_SCOPE_SAMPLES); |
| 563 |
const bool held = dbgui_audio_hold(); |
| 564 |
bool predicted = false; |
| 565 |
|
| 566 |
if (!ctl.freeze && !held) { |
| 567 |
captured = false; |
| 568 |
for (int stream = 0; stream < YM_SCOPE_STREAMS; ++stream) { |
| 569 |
counts[stream] = YM_debug_scope_read(stream, samples[stream], YM_SCOPE_SAMPLES); |
| 570 |
if (counts[stream] > 0) captured = true; |
| 571 |
} |
| 572 |
} else if (held && !captured && !ctl.freeze) { |
| 573 |
|
| 574 |
|
| 575 |
const unsigned n = YM_debug_scope_predict(samples, YM_SCOPE_SAMPLES); |
| 576 |
for (int stream = 0; stream < YM_SCOPE_STREAMS; ++stream) { |
| 577 |
counts[stream] = n; |
| 578 |
} |
| 579 |
predicted = true; |
| 580 |
} |
| 581 |
|
| 582 |
if (held) { |
| 583 |
if (predicted) { |
| 584 |
dbgui_audio_predicted_note("the YM2151's current envelope and phase state"); |
| 585 |
} else { |
| 586 |
dbgui_audio_hold_note(); |
| 587 |
} |
| 588 |
} |
| 589 |
ImGui::TextDisabled("Per-channel capture is self-armed only while this Scope tab reads it; hidden tabs let it disarm."); |
| 590 |
|
| 591 |
if (ImGui::BeginTable("ym_scope_grid", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg)) { |
| 592 |
for (int ch = 0; ch < YM_NUM_CHANNELS; ++ch) { |
| 593 |
if ((ch % 2) == 0) ImGui::TableNextRow(); |
| 594 |
ImGui::TableSetColumnIndex(ch % 2); |
| 595 |
NoteInfo note; |
| 596 |
note_from_kc_kf(ch_reg(0x28, ch) & 0x7f, ch_reg(0x30, ch) & 0xfc, ¬e); |
| 597 |
char overlay[96]; |
| 598 |
snprintf(overlay, sizeof(overlay), "Ch %d ALG %u %s", ch, (unsigned)alg_for_ch(ch), note.text); |
| 599 |
char id[32]; |
| 600 |
snprintf(id, sizeof(id), "mini%d", ch); |
| 601 |
dbgui_scope(id, samples[ch], (int)counts[ch], ImVec2(0.0f, 58.0f), IM_COL32(90, 190, 245, 255), ctl.window, ctl.gain, ctl.trigger, overlay); |
| 602 |
if (ImGui::IsItemClicked()) s_selected_ch = ch; |
| 603 |
} |
| 604 |
ImGui::EndTable(); |
| 605 |
} |
| 606 |
ImGui::Separator(); |
| 607 |
char overlay[96]; |
| 608 |
NoteInfo note; |
| 609 |
note_from_kc_kf(ch_reg(0x28, s_selected_ch) & 0x7f, ch_reg(0x30, s_selected_ch) & 0xfc, ¬e); |
| 610 |
snprintf(overlay, sizeof(overlay), "Selected Ch %d ALG %u %s", s_selected_ch, (unsigned)alg_for_ch(s_selected_ch), note.text); |
| 611 |
dbgui_scope("selected", samples[s_selected_ch], (int)counts[s_selected_ch], ImVec2(0.0f, 120.0f), IM_COL32(105, 225, 140, 255), ctl.window, ctl.gain, ctl.trigger, overlay); |
| 612 |
ImGui::TextUnformatted("Summed YM output"); |
| 613 |
dbgui_scope("mix_l", samples[YM_SCOPE_MIX_L], (int)counts[YM_SCOPE_MIX_L], ImVec2(0.0f, 84.0f), IM_COL32(245, 185, 90, 255), ctl.window, ctl.gain, ctl.trigger, "Mix L"); |
| 614 |
dbgui_scope("mix_r", samples[YM_SCOPE_MIX_R], (int)counts[YM_SCOPE_MIX_R], ImVec2(0.0f, 84.0f), IM_COL32(245, 120, 160, 255), ctl.window, ctl.gain, ctl.trigger, "Mix R"); |
| 615 |
} |
| 616 |
|
| 617 |
void fm_panel_render(bool *p_open) |
| 618 |
{ |
| 619 |
if (ImGui::Begin("YM2151", p_open)) { |
| 620 |
dbgui_window_zoom("ym2151"); |
| 621 |
if (s_selected_ch < 0 || s_selected_ch >= YM_NUM_CHANNELS) s_selected_ch = 0; |
| 622 |
if (ImGui::BeginTabBar("ym2151_tabs", ImGuiTabBarFlags_DrawSelectedOverline)) { |
| 623 |
if (ImGui::BeginTabItem("Channels")) { |
| 624 |
draw_channels_tab(); |
| 625 |
ImGui::EndTabItem(); |
| 626 |
} |
| 627 |
if (ImGui::BeginTabItem("Operators")) { |
| 628 |
draw_operators_tab(); |
| 629 |
ImGui::EndTabItem(); |
| 630 |
} |
| 631 |
if (ImGui::BeginTabItem("Algorithm")) { |
| 632 |
draw_algorithm_tab(); |
| 633 |
ImGui::EndTabItem(); |
| 634 |
} |
| 635 |
if (ImGui::BeginTabItem("Global")) { |
| 636 |
draw_global_tab(); |
| 637 |
ImGui::EndTabItem(); |
| 638 |
} |
| 639 |
if (ImGui::BeginTabItem("Registers")) { |
| 640 |
draw_registers_tab(); |
| 641 |
ImGui::EndTabItem(); |
| 642 |
} |
| 643 |
if (ImGui::BeginTabItem("Scope")) { |
| 644 |
draw_scope_tab(); |
| 645 |
ImGui::EndTabItem(); |
| 646 |
} |
| 647 |
ImGui::EndTabBar(); |
| 648 |
} |
| 649 |
} |
| 650 |
dbgui_window_end(); |
| 651 |
} |
| 652 |
|
| 653 |
} |
| 654 |
|
| 655 |
static DebugPanelRegistration s_reg("YM2151", fm_panel_render, true); |