728 lines
26 KiB
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, ®, &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, ®, &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;
|
|
}
|