joeylib2/tests/gate/stAudioGrade.c

728 lines
26 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// stAudioGrade.c - grade an Atari ST audio run from Hatari's traces, for
// scripts/verify-atarist-audio.sh.
//
// stAudioGrade LOG BASELINE PROG TAPS MACHINE
//
// LOG is the whole run's trace (psg_write, and on the STe dmasound),
// BASELINE the part before any sound effect; PROG names the program and
// TAPS how many effects it played (both only printed); MACHINE is "ste"
// for the STe (DMA sound), anything else for the plain ST (the PWM pump).
// The PSG writes Hatari traces look like
//
// ym write data reg=0x8 val=0xc video_cyc=8696 60@17 pc=e1197e ...
//
// and video_cyc restarting marks a new frame; writes from TOS (pc=e...)
// are not the program's and are left out.
//
// Plain ST: the tracker ran before the effects; the pump writes volume A
// (register 8) 150 to 280 times a frame while an effect plays; inside each
// pump run one writer (the timer interrupt) owns register 8 in all but 5%
// of frames; and the tracker keeps writing its own channel B. STe: the DMA
// chip is set to stereo 12517 Hz and play + repeat, each run pulls the
// effect's samples (signed, not unsigned silence), there is no pump, and
// the tracker keeps channels A and B while the chip plays.
//
// Prints a summary line, then each failure on stderr, then PASS (exit 0)
// or FAIL (exit 1).
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gateArg.h"
#include "toolFile.h"
#define TOOL_NAME "stAudioGrade"
#define GATE_NAME "verify-atarist-audio"
#define LINE_MAX_CHARS 4096u
#define PC_MAX 24u
#define HEX 16
#define DECIMAL 10
#define PERCENT 100.0
#define FAILS_MAX 16u
#define FAIL_MAX_CHARS 1024u
#define SUMMARY_MAX_CHARS 2048u
#define WRITERS_SHOWN 3u
#define LIST_START 16u
// The PSG's registers.
#define REG_TONE_A_FINE 0u
#define REG_TONE_A_COARSE 1u
#define REG_TONE_B_FINE 2u
#define REG_TONE_B_COARSE 3u
#define REG_MIXER 7u
#define REG_VOL_A 8u
#define REGS 16u
// The pump: the tracker writes register 8 at most once a frame, the pump
// 205 (12288 Hz at 60 fps) to 246 (at 50); a slower or faster rate means
// Timer A's divider is wrong. Start and stop are the only frames another
// writer may share with it; a tracker fighting it would every frame.
#define PUMP_PER_FRAME_MIN 100u
#define PUMP_RATE_LO 150u
#define PUMP_RATE_HI 280u
#define STRAY_FRAME_PCT_MAX 5.0
// STe DMA sound: mode 0x01 is stereo at 12517 Hz, control 0x03 play +
// repeat; $80 is the mixer's unsigned silence read as signed; test.sfx is
// about 390 non-silent samples peaking near half scale (61 measured).
#define DMA_MODE_STEREO_12K 0x01u
#define DMA_CTRL_PLAY_LOOP 0x03u
#define DMA_UNCONVERTED_SILENCE (-128)
#define SFX_SAMPLES_MIN 100u
#define SFX_PEAK_MIN 32
#define UNSIGNED_PCT_MAX 5.0
#define SAMPLE_SIGN 128
#define SAMPLE_RANGE 256
// One of the program's PSG writes: its frame, its register, and the pc
// that made it.
typedef struct WriteT {
uint32_t frame;
uint32_t reg;
char pc[PC_MAX];
} WriteT;
// A log's writes, and its frames: each frame's first write and the one
// past its last.
typedef struct TraceT {
WriteT *writes;
size_t count;
size_t cap;
uint32_t *frameStart;
uint32_t *frameEnd;
uint32_t frames;
} TraceT;
// The pcs that wrote register 8, and how often, in the order first seen.
typedef struct WriterT {
char pc[PC_MAX];
uint32_t count;
} WriterT;
// One DMA run: the samples the chip pulled and the PSG writes made meanwhile.
typedef struct DmaRunT {
int8_t *samples;
size_t count;
size_t cap;
uint32_t ym[REGS];
} DmaRunT;
// A growing list of numbers.
typedef struct ListT {
uint32_t *values;
size_t count;
size_t cap;
} ListT;
// The grade so far: the summary and the failures.
typedef struct GradeT {
char summary[SUMMARY_MAX_CHARS];
char fails[FAILS_MAX][FAIL_MAX_CHARS];
uint32_t failCount;
} GradeT;
// The PSG registers of channel A's voice, the mixer, and channel B's tone.
// The PSG registers of channel A's voice, and of channel B's tone.
static const uint32_t kVoiceA[] = { REG_TONE_A_FINE, REG_TONE_A_COARSE, REG_VOL_A };
static const uint32_t kMixer[] = { REG_MIXER };
static const uint32_t kToneB[] = { REG_TONE_B_FINE, REG_TONE_B_COARSE };
// How Hatari's trace starts a PSG data write.
static const char kHead[] = "ym write data reg=0x";
static uint32_t countRegs(const TraceT *trace, const uint32_t *frames, uint32_t frameCount, const uint32_t *regs, uint32_t regCount);
static bool dmaWrite(const char *line, uint32_t *value, char *pc);
static void fail(GradeT *grade, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void gradeDma(GradeT *grade, const TraceT *full, const char *path, uint32_t taps);
static void gradePump(GradeT *grade, const TraceT *full, uint32_t taps);
static void listAdd(ListT *list, uint32_t value);
static void listPrint(char *out, size_t size, const uint32_t *values, size_t count, bool hex, bool distinct);
static bool pcAfter(const char *line, char *pc);
static uint32_t peakVolA(const TraceT *trace);
static bool psgWrite(const char *line, uint32_t *reg, uint32_t *cycle, char *pc);
static void summaryAdd(GradeT *grade, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static void traceRead(const char *path, TraceT *trace);
static uint32_t volAWrites(const TraceT *trace, uint32_t frame);
// How many writes the frames hold to the registers.
static uint32_t countRegs(const TraceT *trace, const uint32_t *frames, uint32_t frameCount, const uint32_t *regs, uint32_t regCount) {
uint32_t total;
uint32_t f;
uint32_t w;
uint32_t r;
total = 0;
for (f = 0; f < frameCount; f++) {
for (w = trace->frameStart[frames[f]]; w < trace->frameEnd[frames[f]]; w++) {
for (r = 0; r < regCount; r++) {
total += trace->writes[w].reg == regs[r] ? 1u : 0u;
}
}
}
return total;
}
// A "DMA snd mode|control write: 0xNN ... pc=PC" line's value and pc.
static bool dmaWrite(const char *line, uint32_t *value, char *pc) {
const char *at;
char *stop;
at = strstr(line, "write: 0x");
if (at == NULL) {
return false;
}
at += strlen("write: 0x");
*value = (uint32_t)strtoul(at, &stop, HEX);
return stop != at && *stop == ' ' && pcAfter(stop, pc);
}
static void fail(GradeT *grade, const char *fmt, ...) {
va_list args;
if (grade->failCount == FAILS_MAX) {
return;
}
va_start(args, fmt);
vsnprintf(grade->fails[grade->failCount++], FAIL_MAX_CHARS, fmt, args);
va_end(args);
}
// STe: the DMA chip plays the digital sound and the PSG keeps channel A.
static void gradeDma(GradeT *grade, const TraceT *full, const char *path, uint32_t taps) {
FILE *f;
char line[LINE_MAX_CHARS];
char pc[PC_MAX];
char list[FAIL_MAX_CHARS];
char list2[FAIL_MAX_CHARS];
DmaRunT *runs;
DmaRunT *run;
ListT modes;
ListT controls;
uint32_t *played;
uint32_t *peaks;
uint32_t runCount;
uint32_t value;
uint32_t reg;
uint32_t cycle;
uint32_t minPlayed;
uint32_t maxPlayed;
uint32_t minPeak;
uint32_t maxPeak;
uint32_t peak;
uint32_t voiceA;
uint32_t toneB;
size_t pulled;
size_t unsignedSilence;
size_t i;
uint32_t r;
uint32_t k;
int sample;
bool wrongMode;
bool playLoop;
memset(&modes, 0, sizeof(modes));
memset(&controls, 0, sizeof(controls));
runs = NULL;
runCount = 0;
run = NULL;
f = fopen(path, "rb");
if (f == NULL) {
toolFail("cannot read %s", path);
}
while (fgets(line, sizeof(line), f) != NULL) {
if (strncmp(line, "DMA snd mode write:", strlen("DMA snd mode write:")) == 0) {
if (dmaWrite(line, &value, pc) && pc[0] != 'e') {
listAdd(&modes, value);
}
} else if (strncmp(line, "DMA snd control write: starting", strlen("DMA snd control write: starting")) == 0) {
runs = (DmaRunT *)realloc(runs, (runCount + 1u) * sizeof(DmaRunT));
if (runs == NULL) {
toolFail("out of memory");
}
run = &runs[runCount++];
memset(run, 0, sizeof(*run));
} else if (strncmp(line, "DMA snd control write: stopping", strlen("DMA snd control write: stopping")) == 0) {
run = NULL;
} else if (strncmp(line, "DMA snd control write:", strlen("DMA snd control write:")) == 0) {
if (dmaWrite(line, &value, pc) && pc[0] != 'e') {
listAdd(&controls, value);
}
} else if (run == NULL) {
continue;
} else if (strncmp(line, "DMA snd fifo pull", strlen("DMA snd fifo pull")) == 0) {
for (i = strlen(line); i > 0 && (line[i - 1u] == ' ' || line[i - 1u] == '\t' || line[i - 1u] == '\r' || line[i - 1u] == '\n'); i--) {
}
line[i] = 0;
for (; i > 0 && line[i - 1u] != ' ' && line[i - 1u] != '\t'; i--) {
}
value = (uint32_t)strtoul(line + i, NULL, HEX);
if (run->count == run->cap) {
run->cap = run->cap == 0 ? LINE_MAX_CHARS : run->cap * 2u;
run->samples = (int8_t *)realloc(run->samples, run->cap);
if (run->samples == NULL) {
toolFail("out of memory");
}
}
run->samples[run->count++] = (int8_t)(value >= SAMPLE_SIGN ? (int)value - SAMPLE_RANGE : (int)value);
} else if (psgWrite(line, &reg, &cycle, pc) && pc[0] != 'e' && reg < REGS) {
run->ym[reg]++;
}
}
fclose(f);
// 2. The chip ran, programmed as the HAL programs it.
if (runCount == 0) {
fail(grade, "the DMA sound chip never started: jlAudioPlaySfx did not reach it, so every digital sound on the STe is silent");
}
wrongMode = modes.count == 0;
for (i = 0; i < modes.count; i++) {
wrongMode = wrongMode || modes.values[i] != DMA_MODE_STEREO_12K;
}
if (wrongMode) {
list[0] = 0;
listPrint(list, sizeof(list), modes.values, modes.count, true, false);
fail(grade, "DMA mode writes %s, want 0x%x (stereo, 12517 Hz) -- the mix is made for that rate and layout", list, DMA_MODE_STEREO_12K);
}
playLoop = false;
for (i = 0; i < controls.count; i++) {
playLoop = playLoop || controls.values[i] == DMA_CTRL_PLAY_LOOP;
}
if (!playLoop) {
list[0] = 0;
listPrint(list, sizeof(list), controls.values, controls.count, true, false);
fail(grade, "DMA control writes %s never set play + repeat (0x%x), so the chip cannot loop over the mix buffer", list, DMA_CTRL_PLAY_LOOP);
}
// 3. Each run played the effect.
played = (uint32_t *)calloc(runCount + 1u, sizeof(uint32_t));
peaks = (uint32_t *)calloc(runCount + 1u, sizeof(uint32_t));
pulled = 0;
unsignedSilence = 0;
if (played == NULL || peaks == NULL) {
toolFail("out of memory");
}
for (r = 0; r < runCount; r++) {
for (i = 0; i < runs[r].count; i++) {
sample = runs[r].samples[i];
played[r] += sample != 0 ? 1u : 0u;
peaks[r] = (uint32_t)abs(sample) > peaks[r] ? (uint32_t)abs(sample) : peaks[r];
unsignedSilence += sample == DMA_UNCONVERTED_SILENCE ? 1u : 0u;
}
pulled += runs[r].count;
}
minPlayed = 0;
maxPlayed = 0;
minPeak = 0;
maxPeak = 0;
for (r = 0; r < runCount; r++) {
minPlayed = r == 0 || played[r] < minPlayed ? played[r] : minPlayed;
maxPlayed = played[r] > maxPlayed ? played[r] : maxPlayed;
minPeak = r == 0 || peaks[r] < minPeak ? peaks[r] : minPeak;
maxPeak = peaks[r] > maxPeak ? peaks[r] : maxPeak;
}
if (runCount > 0 && (minPlayed < SFX_SAMPLES_MIN || minPeak < (uint32_t)SFX_PEAK_MIN)) {
list[0] = 0;
list2[0] = 0;
listPrint(list, sizeof(list), played, runCount, false, false);
listPrint(list2, sizeof(list2), peaks, runCount, false, false);
fail(grade, "a DMA run played no SFX: non-silent samples per run %s (want >= %u), peaks %s (want >= %d). The mix never reached the buffer the chip plays", list, SFX_SAMPLES_MIN, list2, SFX_PEAK_MIN);
}
if (pulled > 0 && PERCENT * (double)unsignedSilence / (double)pulled > UNSIGNED_PCT_MAX) {
fail(grade, "%zu of %zu samples are -128: unsigned silence the HAL never converted to the chip's signed format, a DC step rather than a wave", unsignedSilence, pulled);
}
// 4. No PWM pump on this machine.
peak = peakVolA(full);
if (peak >= PUMP_PER_FRAME_MIN) {
fail(grade, "reg 8 was written %u times in one frame: the PWM pump ran on a machine with DMA sound, so channel A is still the DAC", peak);
}
// 5. The tracker keeps channel A (and B) while the chip plays.
voiceA = 0;
toneB = 0;
for (r = 0; r < runCount; r++) {
for (k = 0; k < sizeof(kVoiceA) / sizeof(kVoiceA[0]); k++) {
voiceA += runs[r].ym[kVoiceA[k]];
}
for (k = 0; k < sizeof(kToneB) / sizeof(kToneB[0]); k++) {
toneB += runs[r].ym[kToneB[k]];
}
}
if (runCount > 0 && voiceA == 0) {
fail(grade, "the tracker wrote none of voice 0's registers (0, 1, 8) while the DMA chip played: channel A is still being given up to digital sound");
}
if (runCount > 0 && toneB == 0) {
fail(grade, "the tracker stopped writing its own channel B while the DMA chip played, so the SFX silenced the music instead of mixing with it");
}
list[0] = 0;
listPrint(list, sizeof(list), modes.values, modes.count, true, true);
summaryAdd(grade, "; DMA %u runs (expected %u), mode %s, non-silent samples/run %u..%u, peak %u, peak reg8/frame %u, tracker voice A %u / toneB %u writes during DMA", runCount, taps, list, minPlayed, maxPlayed, maxPeak, peak, voiceA, toneB);
for (r = 0; r < runCount; r++) {
free(runs[r].samples);
}
free(runs);
free(played);
free(peaks);
free(modes.values);
free(controls.values);
}
// Plain ST: the PWM pump through channel A's volume.
static void gradePump(GradeT *grade, const TraceT *full, uint32_t taps) {
WriterT *writers;
WriterT held;
uint32_t *interior;
uint32_t *runStart;
uint32_t writerCount;
uint32_t interiorCount;
uint32_t runCount;
uint32_t runLen;
uint32_t peak;
uint32_t strayFrames;
uint32_t toneDuring;
uint32_t f;
uint32_t w;
uint32_t i;
uint32_t r;
uint32_t isr;
double strayPct;
bool pumping;
bool stray;
char others[FAIL_MAX_CHARS];
// 2. The pump ran, at the right rate. Pump frames are grouped into runs
// (consecutive frames that have writes) so each effect can be told
// from the next.
runStart = (uint32_t *)calloc(full->frames + 1u, sizeof(uint32_t));
interior = (uint32_t *)calloc(full->frames + 1u, sizeof(uint32_t));
writers = (WriterT *)calloc(full->count + 1u, sizeof(WriterT));
runCount = 0;
interiorCount = 0;
runLen = 0;
if (runStart == NULL || interior == NULL || writers == NULL) {
toolFail("out of memory");
}
for (f = 0; f <= full->frames; f++) {
pumping = f < full->frames && volAWrites(full, f) >= PUMP_PER_FRAME_MIN;
if (pumping) {
runStart[runCount] = runLen == 0 ? f : runStart[runCount];
runLen++;
continue;
}
if (runLen > 0) {
// 3 + 4 look inside each run, minus its first and last frame.
for (i = 1; runLen > 2u && i + 1u < runLen; i++) {
interior[interiorCount++] = runStart[runCount] + i;
}
runCount++;
runLen = 0;
}
}
peak = peakVolA(full);
if (runCount == 0) {
fail(grade, "the PWM pump never ran: peak %u reg-8 writes in a frame, want >= %u. jlAudioPlaySfx did not start Timer A, so every digital sound on this port is silent", peak, PUMP_PER_FRAME_MIN);
} else if (!(peak >= PUMP_RATE_LO && peak <= PUMP_RATE_HI)) {
fail(grade, "the pump ran at %u reg-8 writes/frame, want %u-%u (12288 Hz / 50-60 fps). The MFP Timer A divider is wrong, so sampled sound plays at the wrong pitch", peak, PUMP_RATE_LO, PUMP_RATE_HI);
}
if (runCount == 0) {
summaryAdd(grade, "; pump never ran (peak %u reg8/frame)", peak);
free(runStart);
free(interior);
free(writers);
return;
}
writerCount = 0;
for (i = 0; i < interiorCount; i++) {
for (w = full->frameStart[interior[i]]; w < full->frameEnd[interior[i]]; w++) {
if (full->writes[w].reg != REG_VOL_A) {
continue;
}
for (r = 0; r < writerCount && strcmp(writers[r].pc, full->writes[w].pc) != 0; r++) {
}
if (r == writerCount) {
memcpy(writers[writerCount++].pc, full->writes[w].pc, PC_MAX);
}
writers[r].count++;
}
}
// Most writes first; equal ones in the order first seen.
for (i = 1; i < writerCount; i++) {
held = writers[i];
for (r = i; r > 0 && writers[r - 1u].count < held.count; r--) {
writers[r] = writers[r - 1u];
}
writers[r] = held;
}
strayFrames = 0;
for (i = 0; i < interiorCount && writerCount > 0; i++) {
stray = false;
for (w = full->frameStart[interior[i]]; w < full->frameEnd[interior[i]]; w++) {
stray = stray || (full->writes[w].reg == REG_VOL_A && strcmp(full->writes[w].pc, writers[0].pc) != 0);
}
strayFrames += stray ? 1u : 0u;
}
strayPct = interiorCount > 0 ? PERCENT * (double)strayFrames / (double)interiorCount : 0.0;
if (strayPct > STRAY_FRAME_PCT_MAX) {
others[0] = 0;
for (isr = 1; isr < writerCount && isr <= WRITERS_SHOWN; isr++) {
snprintf(others + strlen(others), sizeof(others) - strlen(others), "%spc=%s x%u", isr > 1 ? ", " : "", writers[isr].pc, writers[isr].count);
}
fail(grade, "reg 8 (volume A) has %u writers in %.1f%% of the pump's frames: ISR pc=%s plus %s. The tracker is overwriting the PWM sample - digital and chiptune are colliding on channel A", writerCount, strayPct, writers[0].pc, others);
}
toneDuring = countRegs(full, interior, interiorCount, kToneB, sizeof(kToneB) / sizeof(kToneB[0]));
if (toneDuring == 0) {
fail(grade, "the tracker stopped writing its own channel B while the pump ran, so the SFX silenced the music instead of mixing with it");
}
summaryAdd(grade, "; pump %u runs (expected %u) peak %u reg8/frame, %u reg8 writer(s), stray frames %.1f%%, tracker toneB during pump %u", runCount, taps, peak, writerCount, strayPct, toneDuring);
free(runStart);
free(interior);
free(writers);
}
static void listAdd(ListT *list, uint32_t value) {
if (list->count == list->cap) {
list->cap = list->cap == 0 ? LIST_START : list->cap * 2u;
list->values = (uint32_t *)realloc(list->values, list->cap * sizeof(uint32_t));
if (list->values == NULL) {
toolFail("out of memory");
}
}
list->values[list->count++] = value;
}
// Values appended to out, ", " between them, as 0x hex or decimal; with
// distinct, each once, smallest first.
static void listPrint(char *out, size_t size, const uint32_t *values, size_t count, bool hex, bool distinct) {
uint32_t *kept;
size_t n;
size_t i;
size_t j;
kept = (uint32_t *)calloc(count + 1u, sizeof(uint32_t));
if (kept == NULL) {
toolFail("out of memory");
}
n = 0;
for (i = 0; i < count; i++) {
for (j = n; distinct && j > 0 && kept[j - 1u] > values[i]; j--) {
}
if (distinct && j > 0 && kept[j - 1u] == values[i]) {
continue;
}
memmove(kept + j + 1u, kept + j, (n - j) * sizeof(uint32_t));
kept[j] = values[i];
n++;
}
for (i = 0; i < n; i++) {
snprintf(out + strlen(out), size - strlen(out), hex ? "%s0x%x" : "%s%u", i > 0 ? ", " : "", kept[i]);
}
free(kept);
}
// The hex digits after a line's last "pc=", if any follow it.
static bool pcAfter(const char *line, char *pc) {
const char *at;
const char *next;
size_t n;
at = strstr(line, "pc=");
if (at == NULL) {
return false;
}
while ((next = strstr(at + 1, "pc=")) != NULL) {
at = next;
}
at += strlen("pc=");
for (n = 0; n + 1u < PC_MAX && ((at[n] >= '0' && at[n] <= '9') || (at[n] >= 'a' && at[n] <= 'f')); n++) {
pc[n] = at[n];
}
pc[n] = 0;
return n > 0;
}
// The most register-8 writes any frame holds.
static uint32_t peakVolA(const TraceT *trace) {
uint32_t peak;
uint32_t f;
peak = 0;
for (f = 0; f < trace->frames; f++) {
peak = volAWrites(trace, f) > peak ? volAWrites(trace, f) : peak;
}
return peak;
}
// A "ym write data reg=0xR val=0xV video_cyc=C ... pc=PC" line's register,
// cycle, and pc.
static bool psgWrite(const char *line, uint32_t *reg, uint32_t *cycle, char *pc) {
const char *at;
char *stop;
if (strncmp(line, kHead, sizeof(kHead) - 1u) != 0) {
return false;
}
at = line + sizeof(kHead) - 1u;
*reg = (uint32_t)strtoul(at, &stop, HEX);
if (stop == at || strncmp(stop, " val=0x", strlen(" val=0x")) != 0) {
return false;
}
at = stop + strlen(" val=0x");
strtoul(at, &stop, HEX);
if (stop == at || strncmp(stop, " video_cyc=", strlen(" video_cyc=")) != 0) {
return false;
}
at = stop + strlen(" video_cyc=");
*cycle = (uint32_t)strtoul(at, &stop, DECIMAL);
return stop != at && pcAfter(stop, pc);
}
static void summaryAdd(GradeT *grade, const char *fmt, ...) {
va_list args;
size_t used;
used = strlen(grade->summary);
va_start(args, fmt);
vsnprintf(grade->summary + used, sizeof(grade->summary) - used, fmt, args);
va_end(args);
}
// A log's program writes, frame by frame (video_cyc going back starts a
// frame; a frame with no program write is not one of them).
static void traceRead(const char *path, TraceT *trace) {
FILE *f;
char line[LINE_MAX_CHARS];
char pc[PC_MAX];
uint32_t reg;
uint32_t cycle;
uint32_t frame;
uint32_t prev;
bool started;
size_t i;
memset(trace, 0, sizeof(*trace));
f = fopen(path, "rb");
if (f == NULL) {
toolFail("cannot read %s", path);
}
frame = 0;
prev = 0;
started = false;
while (fgets(line, sizeof(line), f) != NULL) {
if (!psgWrite(line, &reg, &cycle, pc)) {
continue;
}
frame += started && cycle < prev ? 1u : 0u;
prev = cycle;
started = true;
if (pc[0] == 'e') {
continue;
}
if (trace->count == trace->cap) {
trace->cap = trace->cap == 0 ? LINE_MAX_CHARS : trace->cap * 2u;
trace->writes = (WriteT *)realloc(trace->writes, trace->cap * sizeof(WriteT));
if (trace->writes == NULL) {
toolFail("out of memory");
}
}
trace->writes[trace->count].frame = frame;
trace->writes[trace->count].reg = reg;
memcpy(trace->writes[trace->count].pc, pc, PC_MAX);
trace->count++;
}
fclose(f);
trace->frameStart = (uint32_t *)calloc(trace->count + 1u, sizeof(uint32_t));
trace->frameEnd = (uint32_t *)calloc(trace->count + 1u, sizeof(uint32_t));
if (trace->frameStart == NULL || trace->frameEnd == NULL) {
toolFail("out of memory");
}
for (i = 0; i < trace->count; i++) {
if (i == 0 || trace->writes[i].frame != trace->writes[i - 1u].frame) {
trace->frameStart[trace->frames++] = (uint32_t)i;
}
trace->frameEnd[trace->frames - 1u] = (uint32_t)i + 1u;
}
}
// How many register-8 writes a frame holds.
static uint32_t volAWrites(const TraceT *trace, uint32_t frame) {
uint32_t total;
uint32_t w;
total = 0;
for (w = trace->frameStart[frame]; w < trace->frameEnd[frame]; w++) {
total += trace->writes[w].reg == REG_VOL_A ? 1u : 0u;
}
return total;
}
int main(int argc, char **argv) {
TraceT full;
TraceT base;
GradeT grade;
uint32_t *frames;
uint32_t baseTone;
uint32_t baseMixer;
uint32_t f;
long taps;
toolFileName(TOOL_NAME);
if (argc != 6) {
fprintf(stderr, "usage: %s LOG BASELINE PROG TAPS MACHINE\n", TOOL_NAME);
return GATE_USAGE_EXIT;
}
taps = gateArgLong("taps", argv[4]);
traceRead(argv[1], &full);
traceRead(argv[2], &base);
if (full.count == 0) {
fprintf(stderr, "%s: FAIL - no application PSG writes at all\n", GATE_NAME);
return 1;
}
memset(&grade, 0, sizeof(grade));
// 1. The tracker ran before any effect overlapped it.
frames = (uint32_t *)calloc(base.frames + 1u, sizeof(uint32_t));
if (frames == NULL) {
toolFail("out of memory");
}
for (f = 0; f < base.frames; f++) {
frames[f] = f;
}
baseTone = countRegs(&base, frames, base.frames, kToneB, sizeof(kToneB) / sizeof(kToneB[0]));
baseMixer = countRegs(&base, frames, base.frames, kMixer, sizeof(kMixer) / sizeof(kMixer[0]));
if (baseTone == 0 || baseMixer == 0) {
fail(&grade, "the tracker never played: %u channel-B period writes and %u mixer writes before the SFX. jlMusicPlay did not drive the PSG", baseTone, baseMixer);
}
summaryAdd(&grade, "%s tracker baseline %u toneB/%u mixer", argv[3], baseTone, baseMixer);
if (strcmp(argv[5], "ste") == 0) {
gradeDma(&grade, &full, argv[1], (uint32_t)taps);
} else {
gradePump(&grade, &full, (uint32_t)taps);
}
printf("%s: %s\n", GATE_NAME, grade.summary);
fflush(stdout);
for (f = 0; f < grade.failCount; f++) {
fprintf(stderr, "%s: %s\n", GATE_NAME, grade.fails[f]);
}
if (grade.failCount > 0) {
fprintf(stderr, "%s: FAIL\n", GATE_NAME);
return 1;
}
printf("%s: PASS\n", GATE_NAME);
return 0;
}