OXYGENHUB
◆ main enhanced-x16-emulator / src / debug_ui / panels / pcm_panel.cpp
13.2 KiB · C++ · last changed chore(): initial commit 25 days ago source blame history raw
9120e874 Andy McCall 1
// VERA PCM panel — FIFO sample playback debugger for the Commander X16.
2
//
3
// Tabs:
4
//   * Status — side-effect-free decode of AUDIO_CTRL/AUDIO_RATE, FIFO fill,
5
//              AFLOW, current raw samples and playback phase.
6
//   * FIFO   — non-consuming, ring-aware view of queued AUDIO_DATA bytes plus a
7
//              raw waveform preview of what is about to be fetched.
8
//   * Scope  — self-arming oscilloscope of the post-volume PCM output.
9
//
10
// Technique: this panel reads the PCM block through pcm_debug_get_state() and
11
// pcm_debug_peek_fifo() instead of polling VERA register $9F3B/AUDIO_CTRL.
12
// Reading AUDIO_CTRL via video_read(0x1B) runs audio_render() as a side effect,
13
// so a passive debugger panel must not do it. Only the active tab does work
14
// (ImGui::BeginTabItem gates each tab), and pcm_debug_scope_read() is called
15
// only from the Scope tab because that read arms capture for about half a second.
16
#include "imgui.h"
17
#include "debug_ui_panels.h"
18
#include "debug_ui_bridge.h"
19
#include "debug_ui_scope.h"
20
#include "debug_ui_widgets.h"
21
22
#include <stdint.h>
23
#include <stdio.h>
24
25
namespace {
26
27
constexpr double PCM_AUDIO_CLOCK = 25000000.0 / 512.0;
28
constexpr int    MAX_FIFO_BYTES  = PCM_FIFO_SIZE;
29
constexpr int    MAX_FIFO_FRAMES = PCM_FIFO_SIZE;
30
31
const uint8_t VOLUME_LUT[16] = {0, 1, 2, 3, 4, 5, 6, 8, 11, 14, 18, 23, 30, 38, 49, 64};
32
33
struct PcmFormat {
34
    const char *name;
35
    int         bytes_per_frame;
36
    bool        stereo;
37
    bool        bits16;
38
};
39
40
PcmFormat
41
pcm_format(uint8_t ctrl)
42
{
43
    static const PcmFormat formats[4] = {
44
        {"mono 8-bit", 1, false, false},
45
        {"stereo 8-bit", 2, true, false},
46
        {"mono 16-bit", 2, false, true},
47
        {"stereo 16-bit", 4, true, true},
48
    };
49
    return formats[(ctrl >> 4) & 3];
50
}
51
52
int16_t
53
decode_u8(uint8_t v)
54
{
55
    return (int16_t)((uint16_t)v << 8);
56
}
57
58
int16_t
59
decode_s16le(const uint8_t *p)
60
{
61
    uint16_t v = (uint16_t)p[0] | ((uint16_t)p[1] << 8);
62
    return (int16_t)v;
63
}
64
65
void
66
decode_frame(const uint8_t *p, const PcmFormat &fmt, int16_t *left, int16_t *right)
67
{
68
    if (!fmt.bits16) {
69
        *left = decode_u8(p[0]);
70
        *right = fmt.stereo ? decode_u8(p[1]) : *left;
71
        return;
72
    }
73
74
    *left = decode_s16le(p);
75
    *right = fmt.stereo ? decode_s16le(p + 2) : *left;
76
}
77
78
float
79
sample_level(int16_t v)
80
{
81
    int mag = (v < 0) ? -(int)v : (int)v;
82
    return (float)mag / 32768.0f;
83
}
84
85
double
86
effective_rate_hz(uint8_t rate)
87
{
88
    return PCM_AUDIO_CLOCK * (double)rate / 128.0;
89
}
90
91
void
92
text_ctrl_value(uint8_t ctrl)
93
{
94
    char bin[12];
95
    dbgui_format_binary(bin, sizeof bin, ctrl, 8);
96
    ImGui::Text("AUDIO_CTRL stored: $%02X  %s", ctrl, bin);
97
    dbgui_hover_value_tooltip("Stored bits 5-0: format + volume. Bits 7/6 are read-status/reset controls.", ctrl, 1);
98
}
99
100
void
101
draw_status_tab()
102
{
103
    pcm_debug_state s = {};
104
    pcm_debug_get_state(&s);
105
106
    const PcmFormat fmt = pcm_format(s.ctrl);
107
    const int volume = s.ctrl & 0x0f;
108
    const double rate_hz = effective_rate_hz(s.rate);
109
    const unsigned usable = (s.fifo_size > 0) ? (s.fifo_size - 1u) : 0u;
110
    const float fill_frac = (usable > 0) ? (float)s.fifo_cnt / (float)usable : 0.0f;
111
    const unsigned frames = s.fifo_cnt / (unsigned)fmt.bytes_per_frame;
112
    const double buffered_ms = (rate_hz > 0.0) ? ((double)frames * 1000.0 / rate_hz) : 0.0;
113
114
    text_ctrl_value(s.ctrl);
115
    ImGui::Text("Format: %s  (%d byte%s/frame)", fmt.name, fmt.bytes_per_frame,
116
                fmt.bytes_per_frame == 1 ? "" : "s");
117
    ImGui::Text("Volume: %d  LUT=%u/64  linear %.4f", volume, (unsigned)VOLUME_LUT[volume],
118
                (double)VOLUME_LUT[volume] / 64.0);
119
    ImGui::Text("Loop: %s", s.loop ? "enabled" : "disabled");
120
    ImGui::Separator();
121
122
    ImGui::Text("AUDIO_RATE effective divider: %u", (unsigned)s.rate);
123
    ImGui::Text("Playback rate: %s", s.rate == 0 ? "stopped" : "running");
124
    if (s.rate != 0) {
125
        ImGui::SameLine();
126
        ImGui::Text("(%.3f Hz)", rate_hz);
127
    }
128
    ImGui::TextDisabled("AUDIO_RATE writes above 128 are clamped as 256 - value; this is the already-clamped divider.");
129
    ImGui::Separator();
130
131
    char overlay[80];
132
    snprintf(overlay, sizeof overlay, "%u / %u usable bytes", s.fifo_cnt, usable);
133
    ImGui::ProgressBar(fill_frac, ImVec2(-1.0f, 0.0f), overlay);
134
    ImGui::Text("FIFO: %u queued / %u bytes physical (%u usable)", s.fifo_cnt, s.fifo_size, usable);
135
    ImGui::Text("Frames queued: %u   Buffered: %.3f ms", frames, buffered_ms);
136
    ImGui::Text("Read index: %u   Write index: %u", s.fifo_rdidx, s.fifo_wridx);
137
138
    ImVec4 aflow = s.almost_empty ? ImVec4(1.0f, 0.35f, 0.20f, 1.0f)
139
                                  : ImVec4(0.30f, 0.85f, 0.45f, 1.0f);
140
    ImGui::TextColored(aflow, "AFLOW / almost empty IRQ: %s (fifo_cnt < 1024)",
141
                       s.almost_empty ? "ASSERTED" : "clear");
142
    if (s.fifo_cnt > 0 && s.fifo_cnt < (unsigned)fmt.bytes_per_frame) {
143
        ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.20f, 1.0f),
144
                           "Underrun risk: fewer bytes queued than one %s frame; emulator will reset the FIFO mid-frame.",
145
                           fmt.name);
146
    }
147
    ImGui::Separator();
148
149
    ImGui::Text("Current raw fetched sample (pre-volume): L=%d  R=%d", (int)s.cur_l, (int)s.cur_r);
150
    ImGui::TextUnformatted("L");
151
    ImGui::SameLine();
152
    dbgui_level_meter("cur_l", sample_level(s.cur_l), ImVec2(160.0f, 0.0f), IM_COL32(90, 190, 255, 255));
153
    ImGui::SameLine();
154
    ImGui::TextUnformatted("R");
155
    ImGui::SameLine();
156
    dbgui_level_meter("cur_r", sample_level(s.cur_r), ImVec2(160.0f, 0.0f), IM_COL32(255, 175, 80, 255));
157
    ImGui::Text("Phase accumulator: $%02X (%u)", (unsigned)s.phase, (unsigned)s.phase);
158
159
    ImGui::Spacing();
160
    ImGui::TextDisabled("This panel reads the PCM block directly, not VERA $9F3B/AUDIO_CTRL; reading AUDIO_CTRL renders audio.");
161
    ImGui::TextDisabled("cur_l/cur_r are raw fetched samples before volume scaling. Scope traces show post-volume output.");
162
}
163
164
unsigned
165
peek_fifo_bytes(uint8_t *bytes, unsigned fifo_cnt)
166
{
167
    unsigned want = fifo_cnt;
168
    if (want > (unsigned)MAX_FIFO_BYTES) {
169
        want = (unsigned)MAX_FIFO_BYTES;
170
    }
171
    return pcm_debug_peek_fifo(bytes, 0, want);
172
}
173
174
int
175
build_fifo_preview(const uint8_t *bytes, unsigned byte_count, const PcmFormat &fmt,
176
                   int16_t *left, int16_t *right)
177
{
178
    const unsigned frames = byte_count / (unsigned)fmt.bytes_per_frame;
179
    unsigned n = frames;
180
    if (n > (unsigned)MAX_FIFO_FRAMES) {
181
        n = (unsigned)MAX_FIFO_FRAMES;
182
    }
183
    for (unsigned i = 0; i < n; ++i) {
184
        decode_frame(bytes + i * (unsigned)fmt.bytes_per_frame, fmt, &left[i], &right[i]);
185
    }
186
    return (int)n;
187
}
188
189
void
190
draw_fifo_hex_table(const uint8_t *bytes, unsigned byte_count, const PcmFormat &fmt)
191
{
192
    const unsigned rows = (byte_count + (unsigned)fmt.bytes_per_frame - 1u) / (unsigned)fmt.bytes_per_frame;
193
    ImGuiTableFlags flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
194
                            ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY |
195
                            DBGUI_TABLE_FLAGS_RESIZABLE;
196
    if (!ImGui::BeginTable("pcm_fifo_bytes", 4, flags, ImVec2(0.0f, 300.0f))) {
197
        return;
198
    }
199
200
    ImGui::TableSetupScrollFreeze(0, 1);
201
    ImGui::TableSetupColumn("Offset");
202
    ImGui::TableSetupColumn("Hex bytes");
203
    ImGui::TableSetupColumn("Left");
204
    ImGui::TableSetupColumn("Right");
205
    ImGui::TableHeadersRow();
206
207
    for (unsigned row = 0; row < rows; ++row) {
208
        const unsigned off = row * (unsigned)fmt.bytes_per_frame;
209
        const unsigned remain = byte_count - off;
210
        const bool complete = remain >= (unsigned)fmt.bytes_per_frame;
211
212
        ImGui::TableNextRow();
213
        ImGui::TableSetColumnIndex(0);
214
        ImGui::Text("+%04X", off);
215
216
        ImGui::TableSetColumnIndex(1);
217
        char hex[16] = {};
218
        char *out = hex;
219
        size_t left_chars = sizeof hex;
220
        for (int i = 0; i < fmt.bytes_per_frame && off + (unsigned)i < byte_count; ++i) {
221
            int wrote = snprintf(out, left_chars, "%s%02X", i == 0 ? "" : " ", (unsigned)bytes[off + (unsigned)i]);
222
            if (wrote <= 0 || (size_t)wrote >= left_chars) {
223
                break;
224
            }
225
            out += wrote;
226
            left_chars -= (size_t)wrote;
227
        }
228
        ImGui::TextUnformatted(hex);
229
230
        ImGui::TableSetColumnIndex(2);
231
        if (complete) {
232
            int16_t l = 0;
233
            int16_t r = 0;
234
            decode_frame(bytes + off, fmt, &l, &r);
235
            ImGui::Text("%d", (int)l);
236
            ImGui::TableSetColumnIndex(3);
237
            ImGui::Text("%d", (int)r);
238
        } else {
239
            ImGui::TextDisabled("(partial)");
240
            ImGui::TableSetColumnIndex(3);
241
            ImGui::TextDisabled("(partial)");
242
        }
243
    }
244
    ImGui::EndTable();
245
}
246
247
void
248
draw_fifo_tab()
249
{
250
    pcm_debug_state s = {};
251
    pcm_debug_get_state(&s);
252
    const PcmFormat fmt = pcm_format(s.ctrl);
253
254
    ImGui::Text("Queued FIFO bytes from read pointer. Format: %s (%d byte%s/frame).",
255
                fmt.name, fmt.bytes_per_frame, fmt.bytes_per_frame == 1 ? "" : "s");
256
    ImGui::TextDisabled("Preview decodes raw queued data about to be fetched; Scope shows what is playing after volume scaling.");
257
258
    if (s.fifo_cnt == 0) {
259
        ImGui::Spacing();
260
        ImGui::TextDisabled("FIFO is empty.");
261
        return;
262
    }
263
264
    static uint8_t bytes[MAX_FIFO_BYTES];
265
    static int16_t preview_l[MAX_FIFO_FRAMES];
266
    static int16_t preview_r[MAX_FIFO_FRAMES];
267
268
    const unsigned got = peek_fifo_bytes(bytes, s.fifo_cnt);
269
    const int preview_count = build_fifo_preview(bytes, got, fmt, preview_l, preview_r);
270
    const int preview_window = (preview_count < 512) ? preview_count : 512;
271
272
    if (preview_count > 0) {
273
        ImGui::Text("Waveform preview: %d complete frame%s decoded from %u queued byte%s.",
274
                    preview_count, preview_count == 1 ? "" : "s", got, got == 1 ? "" : "s");
275
        dbgui_scope("fifo_preview_l", preview_l, preview_count, ImVec2(0.0f, 90.0f),
276
                    IM_COL32(90, 190, 255, 255), preview_window, DBGUI_SCOPE_AUTO_GAIN, false, "queued L/raw");
277
        if (fmt.stereo) {
278
            dbgui_scope("fifo_preview_r", preview_r, preview_count, ImVec2(0.0f, 90.0f),
279
                        IM_COL32(255, 175, 80, 255), preview_window, DBGUI_SCOPE_AUTO_GAIN, false, "queued R/raw");
280
        }
281
    } else {
282
        ImGui::TextDisabled("Not enough queued bytes for one complete frame.");
283
    }
284
285
    ImGui::Separator();
286
    draw_fifo_hex_table(bytes, got, fmt);
287
}
288
289
void
290
draw_scope_tab()
291
{
292
    static DbgScopeControls ctl;
293
    static int16_t left[PCM_SCOPE_SAMPLES];
294
    static int16_t right[PCM_SCOPE_SAMPLES];
295
    static unsigned left_count = 0;
296
    static unsigned right_count = 0;
297
    static bool captured = false; // buffers hold real audio, not a projection
298
299
    dbgui_scope_controls(&ctl, PCM_SCOPE_SAMPLES);
300
    const bool held = dbgui_audio_hold();
301
    bool       predicted = false;
302
303
    if (!ctl.freeze && !held) {
304
        left_count = pcm_debug_scope_read(PCM_SCOPE_L, left, PCM_SCOPE_SAMPLES);
305
        right_count = pcm_debug_scope_read(PCM_SCOPE_R, right, PCM_SCOPE_SAMPLES);
306
        captured = (left_count > 0 || right_count > 0);
307
    } else if (held && !captured && !ctl.freeze) {
308
        // Nothing captured, but the FIFO already holds the samples that are
309
        // about to play, so show those rather than an empty display.
310
        left_count = right_count = pcm_debug_scope_predict(left, right, PCM_SCOPE_SAMPLES);
311
        predicted = true;
312
    }
313
314
    if (held) {
315
        if (predicted) {
316
            dbgui_audio_predicted_note("the samples queued in the PCM FIFO");
317
        } else {
318
            dbgui_audio_hold_note();
319
        }
320
    }
321
    ImGui::TextDisabled("Reading these traces arms PCM capture; when this tab is hidden, capture self-disarms after about 0.5 s.");
322
323
    int peak_l = dbgui_scope("pcm_scope_l", left, (int)left_count, ImVec2(0.0f, 120.0f),
324
                             IM_COL32(90, 190, 255, 255), ctl.window, ctl.gain, ctl.trigger, "PCM L post-volume");
325
    dbgui_level_meter("pcm_scope_l_meter", (float)peak_l / 32768.0f, ImVec2(-1.0f, 0.0f),
326
                      IM_COL32(90, 190, 255, 255));
327
    int peak_r = dbgui_scope("pcm_scope_r", right, (int)right_count, ImVec2(0.0f, 120.0f),
328
                             IM_COL32(255, 175, 80, 255), ctl.window, ctl.gain, ctl.trigger, "PCM R post-volume");
329
    dbgui_level_meter("pcm_scope_r_meter", (float)peak_r / 32768.0f, ImVec2(-1.0f, 0.0f),
330
                      IM_COL32(255, 175, 80, 255));
331
}
332
333
// ---------------------------------------------------------------------------
334
// Panel entry point
335
// ---------------------------------------------------------------------------
336
void
337
pcm_panel_render(bool *p_open)
338
{
339
    if (ImGui::Begin("PCM", p_open)) {
340
        dbgui_window_zoom("pcm");
341
342
        if (ImGui::BeginTabBar("pcm_tabs", ImGuiTabBarFlags_DrawSelectedOverline)) {
343
            if (ImGui::BeginTabItem("Status")) {
344
                draw_status_tab();
345
                ImGui::EndTabItem();
346
            }
347
            if (ImGui::BeginTabItem("FIFO")) {
348
                draw_fifo_tab();
349
                ImGui::EndTabItem();
350
            }
351
            if (ImGui::BeginTabItem("Scope")) {
352
                draw_scope_tab();
353
                ImGui::EndTabItem();
354
            }
355
            ImGui::EndTabBar();
356
        }
357
    }
358
    dbgui_window_end();
359
}
360
361
} // namespace
362
363
static DebugPanelRegistration s_reg("PCM", pcm_panel_render, true);