// 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 #include #include #include #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; }