joeylib2/tests/host/modPlayerHost.c

534 lines
22 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Host harness for src/core/modPlayer.c (scripts/check-modplayer.sh). It
// includes the player, so it can tick the replay and read the emulated Paula
// directly, and builds its own small modules in memory:
// protracker the Protracker 2.3 behaviours the player keeps: a sample
// offset applied twice, 6xy's vibrato, an arpeggio past B-3,
// a tremolo twice a vibrato's depth, glissando, pattern loops
// and delays, a sample number without a note taking effect
// once some channel starts a sample;
// ends a song that does not loop ends at its end, at a jump back,
// at an F00 and at an E8F, not at a jump forward; one that
// loops never ends; the Amiga's stop marker (modStopMarker.c)
// ends a song that jumps back at the same tick;
// render the 8-bit and 16-bit outputs: their levels, sides and
// agreement, any call size giving the same stream, silence
// after the end, a restart playing the song again;
// tables the periods and vibrato waves against PTPlayer's own, when
// the script hands over ptplayer.asm's tables;
// fuzz mangled modules play without reading outside the copy (the
// script builds this with the address sanitizer).
//
// Usage: modPlayerHost [PTPLAYER_TABLES.txt]
// Prints one line per check and "MODPLAYER n/m"; exit 0 only if all pass.
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "modPlayer.c"
#include "modStopMarker.c"
// ----- Constants -----
#define TEST_RATE 12288u
#define TEST_PATTERNS 4u
#define SQUARE_BYTES 64u // sample 1: a looping square, full volume
#define RAMP_BYTES 4096u // sample 2: a one-shot ramp, volume 48
#define SAMPLE_SQUARE 1u
#define SAMPLE_RAMP 2u
#define RAMP_VOLUME 48u
#define SQUARE_HIGH 127
#define SQUARE_LOW (-128)
#define MODULE_BYTES (MOD_HEADER_BYTES + TEST_PATTERNS * MOD_PATTERN_BYTES + SQUARE_BYTES + RAMP_BYTES)
#define LEAD_TICKS (DEFAULT_SPEED - 1u) // ticks before the first row
#define PERIOD_C1 856u
#define PERIOD_C2 428u
#define PERIOD_C3 214u
#define PERIOD_B3 113u
#define RENDER_SECONDS 3u
#define RENDER_SAMPLES (TEST_RATE * RENDER_SECONDS)
#define FUZZ_RUNS 300u
#define FUZZ_SAMPLES (TEST_RATE * 2u)
#define TICK_LIMIT 1000u
#define CORRELATION_MIN 0.9
#define VIBRATO_TABLES 3u
// ----- Prototypes -----
static void check(const char *name, bool ok, const char *detail);
static void modNew(uint8_t songLength);
static void modNote(uint8_t pattern, uint8_t row, uint8_t ch, uint8_t sample, uint16_t period, uint8_t cmd, uint8_t param);
static uint32_t ticksUntilEnd(const uint8_t *module, uint32_t bytes, bool loop);
static void testEnds(void);
static void testFuzz(void);
static void testProtracker(void);
static void testRender(void);
static void testTables(const char *path);
// ----- Module state -----
static uint8_t gMod[MODULE_BYTES];
static uint8_t gMarked[MODULE_BYTES + MOD_STOP_MARKER_BYTES];
static uint8_t gRenderA[RENDER_SAMPLES];
static uint8_t gRenderB[RENDER_SAMPLES];
static int16_t gRenderS16[RENDER_SAMPLES * S16_STEREO];
static uint32_t gChecks = 0u;
static uint32_t gPassed = 0u;
static uint32_t gRandom = 12345u;
void *jlpGenericBigAlloc(uint32_t bytes) {
return malloc(bytes);
}
void jlpGenericBigFree(void *p) {
free(p);
}
static void check(const char *name, bool ok, const char *detail) {
gChecks++;
if (ok) {
gPassed++;
}
printf("%s %s: %s\n", ok ? "PASS" : "FAIL", name, detail);
}
// A module of TEST_PATTERNS empty patterns, songLength positions playing
// patterns 0, 1, 2... and the two test samples.
static void modNew(uint8_t songLength) {
uint8_t *info;
int8_t *data;
uint32_t i;
memset(gMod, 0, sizeof(gMod));
info = gMod + MOD_SAMPLE_INFO_OFFSET;
info[MOD_SAMPLE_LENGTH_OFFSET + 1u] = (uint8_t)(SQUARE_BYTES / WORD_BYTES);
info[MOD_SAMPLE_VOLUME_OFFSET] = (uint8_t)VOLUME_MAX;
info[MOD_SAMPLE_REPLEN_OFFSET + 1u] = (uint8_t)(SQUARE_BYTES / WORD_BYTES);
info += MOD_SAMPLE_INFO_BYTES;
info[MOD_SAMPLE_LENGTH_OFFSET] = (uint8_t)((RAMP_BYTES / WORD_BYTES) >> BYTE_SHIFT);
info[MOD_SAMPLE_LENGTH_OFFSET + 1u] = (uint8_t)((RAMP_BYTES / WORD_BYTES) & 0xFFu);
info[MOD_SAMPLE_VOLUME_OFFSET] = (uint8_t)RAMP_VOLUME;
info[MOD_SAMPLE_REPLEN_OFFSET + 1u] = (uint8_t)IDLE_WORDS;
gMod[MOD_SONG_LENGTH_OFFSET] = songLength;
for (i = 0u; i < songLength; i++) {
gMod[MOD_ORDER_TABLE_OFFSET + i] = (uint8_t)(i % TEST_PATTERNS);
}
// Every pattern stored, whatever the song plays.
gMod[MOD_ORDER_TABLE_OFFSET + MOD_ORDER_TABLE_LEN - 1u] = (uint8_t)(TEST_PATTERNS - 1u);
memcpy(gMod + MOD_MAGIC_OFFSET, "M.K.", MOD_MAGIC_LEN);
data = (int8_t *)(gMod + MOD_HEADER_BYTES + TEST_PATTERNS * MOD_PATTERN_BYTES);
for (i = 0u; i < SQUARE_BYTES; i++) {
data[i] = (int8_t)(i < SQUARE_BYTES / 2u ? SQUARE_HIGH : SQUARE_LOW);
}
for (i = 0u; i < RAMP_BYTES; i++) {
data[SQUARE_BYTES + i] = (int8_t)(i & 0xFFu);
}
}
static void modNote(uint8_t pattern, uint8_t row, uint8_t ch, uint8_t sample, uint16_t period, uint8_t cmd, uint8_t param) {
uint8_t *note;
note = gMod + MOD_HEADER_BYTES + (uint32_t)pattern * MOD_PATTERN_BYTES + (uint32_t)row * MOD_ROW_BYTES + (uint32_t)ch * MOD_NOTE_BYTES;
note[0] = (uint8_t)((sample & SAMPLE_HIGH_MASK) | ((period >> BYTE_SHIFT) & NIBBLE_MASK));
note[1] = (uint8_t)(period & 0xFFu);
note[2] = (uint8_t)(((sample & NIBBLE_MASK) << NIBBLE_SHIFT) | (cmd & NIBBLE_MASK));
note[3] = param;
}
// The tick() call (counting from 1) that ends the song; 0 if none within
// TICK_LIMIT.
static uint32_t ticksUntilEnd(const uint8_t *module, uint32_t bytes, bool loop) {
uint32_t n;
if (!modPlayerStart(module, bytes, TEST_RATE, loop)) {
return 0u;
}
for (n = 1u; n <= TICK_LIMIT; n++) {
if (!tick()) {
modPlayerStop();
return n;
}
}
modPlayerStop();
return 0u;
}
static void testEnds(void) {
char detail[128];
uint32_t n;
bool marked;
// F01 makes every tick a row: row r is tick LEAD_TICKS + 1 + r, and the
// tick after a song's last row ends it.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u);
n = ticksUntilEnd(gMod, sizeof(gMod), false);
snprintf(detail, sizeof(detail), "a 64-row song ends on tick %u (expected %u)", n, LEAD_TICKS + MOD_ROWS + 1u);
check("ends at its end", n == LEAD_TICKS + MOD_ROWS + 1u, detail);
n = ticksUntilEnd(gMod, sizeof(gMod), true);
check("loops for ever", n == 0u, n == 0u ? "no end in 1000 ticks" : "it ended");
// Position 1's B00 goes back to the first row.
modNew(2u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u);
modNote(1u, 0u, 0u, 0u, 0u, 0xBu, 0x00u);
n = ticksUntilEnd(gMod, sizeof(gMod), false);
snprintf(detail, sizeof(detail), "B00 on position 1 ends it on tick %u (expected %u)", n, LEAD_TICKS + MOD_ROWS + 2u);
check("ends at a jump back", n == LEAD_TICKS + MOD_ROWS + 2u, detail);
// Marked, the jump goes to the marker, whose E8F ends it on the same tick.
marked = modStopMarkerAdd(gMod, sizeof(gMod), gMarked);
n = marked ? ticksUntilEnd(gMarked, sizeof(gMarked), false) : 0u;
snprintf(detail, sizeof(detail), "marked, it ends on tick %u (expected %u)", n, LEAD_TICKS + MOD_ROWS + 2u);
check("stop marker", n == LEAD_TICKS + MOD_ROWS + 2u, detail);
n = marked ? ticksUntilEnd(gMarked, sizeof(gMarked), true) : 1u;
check("stop marker loops", n == 0u, n == 0u ? "marked, a looping song plays on" : "it ended");
// B00 with D63 on row 1 goes forward, to row 63 of the same position.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u);
modNote(0u, 1u, 0u, 0u, 0u, 0xBu, 0x00u);
modNote(0u, 1u, 1u, 0u, 0u, 0xDu, 0x63u);
n = ticksUntilEnd(gMod, sizeof(gMod), false);
snprintf(detail, sizeof(detail), "rows 0, 1, 63 then the end: tick %u (expected %u)", n, LEAD_TICKS + 4u);
check("plays on past a jump forward", n == LEAD_TICKS + 4u, detail);
// F00 stops it on its row.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u);
modNote(0u, 2u, 0u, 0u, 0u, 0xFu, 0x00u);
n = ticksUntilEnd(gMod, sizeof(gMod), true);
snprintf(detail, sizeof(detail), "F00 on row 2 stops it on tick %u (expected %u)", n, LEAD_TICKS + 3u);
check("ends at F00", n == LEAD_TICKS + 3u, detail);
// E8F is the Amiga's stop marker, for a song that does not loop.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u);
modNote(0u, 2u, 0u, 0u, 0u, 0xEu, 0x80u | MOD_STOP_MARKER_VALUE);
n = ticksUntilEnd(gMod, sizeof(gMod), false);
snprintf(detail, sizeof(detail), "E8F on row 2 stops it on tick %u (expected %u)", n, LEAD_TICKS + 3u);
check("ends at E8F", n == LEAD_TICKS + 3u, detail);
n = ticksUntilEnd(gMod, sizeof(gMod), true);
check("loops past E8F", n == 0u, n == 0u ? "a looping song ignores the marker" : "it ended");
}
// Mangled modules: whatever they ask for, the player stays in its copy.
static void testFuzz(void) {
uint32_t run;
uint32_t i;
uint32_t started;
uint32_t at;
started = 0u;
for (run = 0u; run < FUZZ_RUNS; run++) {
modNew(3u);
for (i = 0u; i < 64u; i++) {
gRandom = gRandom * 1103515245u + 12345u;
at = (gRandom >> 8) % (MOD_HEADER_BYTES + TEST_PATTERNS * MOD_PATTERN_BYTES);
gRandom = gRandom * 1103515245u + 12345u;
if (at < MOD_MAGIC_OFFSET || at >= MOD_MAGIC_OFFSET + MOD_MAGIC_LEN) {
gMod[at] = (uint8_t)(gRandom >> 16);
}
}
if (gMod[MOD_SONG_LENGTH_OFFSET] == 0u || gMod[MOD_SONG_LENGTH_OFFSET] > MOD_ORDER_TABLE_LEN) {
gMod[MOD_SONG_LENGTH_OFFSET] = 3u;
}
for (i = 0u; i < MOD_ORDER_TABLE_LEN; i++) {
gMod[MOD_ORDER_TABLE_OFFSET + i] = (uint8_t)(gMod[MOD_ORDER_TABLE_OFFSET + i] % TEST_PATTERNS);
}
// Lengths, loops and the file's own length cut short as they come.
if (modPlayerStart(gMod, sizeof(gMod) - (run % 2u) * (RAMP_BYTES / 2u), TEST_RATE, (run & 2u) != 0u)) {
started++;
(void)modPlayerRenderU8(gRenderA, FUZZ_SAMPLES);
modPlayerStop();
}
if (modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true)) {
(void)modPlayerRenderS16(gRenderS16, FUZZ_SAMPLES / S16_STEREO, (uint8_t)S16_STEREO);
modPlayerStop();
}
}
check("fuzz", started > 0u, "mangled modules render without leaving the player's copy");
check("refuses a short module", !modPlayerStart(gMod, MOD_HEADER_BYTES - 1u, TEST_RATE, false), "fewer bytes than a header");
modNew(1u);
memcpy(gMod + MOD_MAGIC_OFFSET, "8CHN", MOD_MAGIC_LEN);
check("refuses eight channels", !modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false), "an 8CHN module");
}
static void testProtracker(void) {
PaulaT *a;
char detail[160];
uint32_t i;
uint32_t swings;
bool ok;
uint16_t played[7];
a = &gPlay.paula[0];
// 9xx: 256 bytes in for the note, the channel's start 512 in after it.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_RAMP, PERIOD_C2, 0x9u, 0x01u);
modNote(0u, 1u, 0u, 0u, PERIOD_C2, 0x0u, 0x00u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i <= LEAD_TICKS; i++) {
(void)tick();
}
ok = a->cur == gPlay.sampleStart[1] + 256 && gPlay.chan[0].start == gPlay.sampleStart[1] + 512;
for (i = 0u; i < DEFAULT_SPEED; i++) {
(void)tick();
}
ok = ok && a->cur == gPlay.sampleStart[1] + 512;
check("9xx twice", ok, "the note starts 256 bytes in, the next note without a sample number 512");
// 6xy keeps the vibrato 4xy set up (PTPlayer's volume slide loses it).
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x4u, 0x48u);
modNote(0u, 1u, 0u, 0u, 0u, 0x6u, 0x00u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED; i++) {
(void)tick();
}
swings = 0u;
for (i = 1u; i < DEFAULT_SPEED; i++) {
(void)tick();
swings += a->period != PERIOD_C2 ? 1u : 0u;
}
snprintf(detail, sizeof(detail), "%u of row 1's 5 ticks off the note's period", swings);
check("6xy vibrato", swings >= 3u, detail);
// 0xy past B-3 reads on into the next finetune's periods.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_B3, 0x0u, 0x0Cu);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i <= LEAD_TICKS + 2u; i++) {
(void)tick();
}
snprintf(detail, sizeof(detail), "B-3 plus 12 semitones plays %u (expected %u)", a->period, kPeriods[1][11]);
check("arpeggio past B-3", a->period == kPeriods[1][11], detail);
// 7xy: a sine tremolo of depth 4 at speed 8 adds 180 * 4 / 64 on its
// second tick and 255 * 4 / 64 on its third.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xCu, 0x20u);
modNote(0u, 1u, 0u, 0u, 0u, 0x7u, 0x84u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED + 2u; i++) {
(void)tick();
}
ok = a->volume == 0x20u + 11u;
(void)tick();
ok = ok && a->volume == 0x20u + 15u;
snprintf(detail, sizeof(detail), "volume 32 swings to 43, then 47 (now %u)", a->volume);
check("tremolo depth", ok, detail);
// E31: a tone portamento plays only the notes it passes.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C1, 0x0u, 0x00u);
modNote(0u, 1u, 0u, 0u, PERIOD_C3, 0x3u, 0x08u);
modNote(0u, 2u, 0u, 0u, 0u, 0xEu, 0x31u);
modNote(0u, 3u, 0u, 0u, 0u, 0x3u, 0x00u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED * 3u; i++) {
(void)tick();
}
ok = true;
for (i = 1u; i < DEFAULT_SPEED; i++) {
(void)tick();
ok = ok && a->period == kPeriods[0][noteIndex(kPeriods[0], a->period)];
}
check("glissando", ok, "each tick of the slide plays a note's period");
// E60/E62 plays rows 0 and 1 three times, then row 2; EE2 holds row 0
// for three rows' time. Channel 3's notes say which row played.
modNew(1u);
modNote(0u, 0u, 0u, 0u, 0u, 0xFu, 0x01u);
modNote(0u, 0u, 1u, 0u, 0u, 0xEu, 0x60u);
modNote(0u, 1u, 1u, 0u, 0u, 0xEu, 0x62u);
for (i = 0u; i < 3u; i++) {
modNote(0u, (uint8_t)i, 3u, SAMPLE_SQUARE, kPeriods[0][i], 0x0u, 0x00u);
}
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i < LEAD_TICKS; i++) {
(void)tick();
}
for (i = 0u; i < 7u; i++) {
(void)tick();
played[i] = gPlay.chan[3].period;
}
ok = played[0] == kPeriods[0][0] && played[1] == kPeriods[0][1] && played[2] == kPeriods[0][0] && played[3] == kPeriods[0][1] && played[4] == kPeriods[0][0] && played[5] == kPeriods[0][1] && played[6] == kPeriods[0][2];
check("pattern loop", ok, "rows 0, 1, 0, 1, 0, 1, 2");
modNote(0u, 0u, 1u, 0u, 0u, 0xEu, 0xE2u);
modNote(0u, 1u, 1u, 0u, 0u, 0x0u, 0x00u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i < LEAD_TICKS; i++) {
(void)tick();
}
for (i = 0u; i < 4u; i++) {
(void)tick();
played[i] = gPlay.chan[3].period;
}
ok = played[0] == kPeriods[0][0] && played[2] == kPeriods[0][0] && played[3] == kPeriods[0][1];
check("pattern delay", ok, "row 0 three times as long, then row 1");
// A sample number without a note changes the loop once a channel starts a sample.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u);
modNote(0u, 1u, 0u, SAMPLE_RAMP, 0u, 0x0u, 0x00u);
modNote(0u, 2u, 1u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
for (i = 0u; i <= LEAD_TICKS + DEFAULT_SPEED; i++) {
(void)tick();
}
ok = a->latch == gPlay.sampleStart[0] && a->volume == RAMP_VOLUME;
for (i = 0u; i < DEFAULT_SPEED; i++) {
(void)tick();
}
ok = ok && a->latch == NULL;
check("sample swap", ok, "the volume at once, the loop at the next start of a sample");
modPlayerStop();
}
static void testRender(void) {
char detail[160];
uint32_t i;
uint32_t done;
uint32_t run;
uint8_t low;
uint8_t high;
bool ok;
double dot;
double xx;
double yy;
// One channel at full volume: a quarter of the 8-bit range each way.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true);
(void)modPlayerRenderU8(gRenderA, TEST_RATE);
low = 0xFFu;
high = 0u;
for (i = 0u; i < TEST_RATE; i++) {
low = gRenderA[i] < low ? gRenderA[i] : low;
high = gRenderA[i] > high ? gRenderA[i] : high;
}
snprintf(detail, sizeof(detail), "a full-volume square swings %u..%u (expected %u..%u)", low, high, U8_BIAS + (SQUARE_LOW >> U8_SHIFT), U8_BIAS + (SQUARE_HIGH >> U8_SHIFT));
check("8-bit level", low == U8_BIAS + (SQUARE_LOW >> U8_SHIFT) && high == U8_BIAS + (SQUARE_HIGH >> U8_SHIFT), detail);
// Channel 0 is on the left alone, channel 1 on the right.
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true);
(void)modPlayerRenderS16(gRenderS16, TEST_RATE, (uint8_t)S16_STEREO);
ok = true;
for (i = 0u; i < TEST_RATE; i++) {
ok = ok && gRenderS16[i * S16_STEREO + 1u] == 0;
}
xx = 0.0;
for (i = 0u; i < TEST_RATE; i++) {
xx = fabs((double)gRenderS16[i * S16_STEREO]) > xx ? fabs((double)gRenderS16[i * S16_STEREO]) : xx;
}
snprintf(detail, sizeof(detail), "channel 0 left only, peaking at %.0f of 32767", xx);
check("16-bit sides", ok && xx > 15000.0 && xx < 17000.0, detail);
// All four channels, the two outputs side by side.
for (i = 0u; i < MOD_CHANNELS; i++) {
modNote(0u, 0u, (uint8_t)i, (uint8_t)(i % 2u ? SAMPLE_RAMP : SAMPLE_SQUARE), kPeriods[0][i * 5u], (uint8_t)(i == 2u ? 0x4u : 0x0u), (uint8_t)(i == 2u ? 0x46u : 0x00u));
}
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true);
(void)modPlayerRenderU8(gRenderA, RENDER_SAMPLES);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true);
(void)modPlayerRenderS16(gRenderS16, RENDER_SAMPLES, 1u);
dot = 0.0;
xx = 0.0;
yy = 0.0;
for (i = 0u; i < RENDER_SAMPLES; i++) {
dot += ((double)gRenderA[i] - U8_BIAS) * gRenderS16[i];
xx += ((double)gRenderA[i] - U8_BIAS) * ((double)gRenderA[i] - U8_BIAS);
yy += (double)gRenderS16[i] * gRenderS16[i];
}
snprintf(detail, sizeof(detail), "the 8-bit and 16-bit mixes correlate %.3f", dot / sqrt(xx * yy + 1.0));
check("outputs agree", dot / sqrt(xx * yy + 1.0) > CORRELATION_MIN, detail);
// The same stream whatever the calls' sizes: 1, 7, then 1000 at a time.
ok = true;
for (run = 1u; run <= 1000u; run = run == 1u ? 7u : 1000u) {
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, true);
for (done = 0u; done < RENDER_SAMPLES; done += run) {
(void)modPlayerRenderU8(gRenderB + done, done + run <= RENDER_SAMPLES ? run : RENDER_SAMPLES - done);
}
ok = ok && memcmp(gRenderA, gRenderB, RENDER_SAMPLES) == 0;
if (run == 1000u) {
break;
}
}
check("call sizes", ok, "1, 7 and 1000 samples a call give the same stream, and a restart the same song");
// A song that ends: false from the call it ends in, silence after.
modNew(1u);
modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0xFu, 0x01u);
(void)modPlayerStart(gMod, sizeof(gMod), TEST_RATE, false);
ok = !modPlayerRenderU8(gRenderA, RENDER_SAMPLES) && !modPlayerActive();
for (i = RENDER_SAMPLES - TEST_RATE; i < RENDER_SAMPLES; i++) {
ok = ok && gRenderA[i] == MOD_PLAYER_SILENCE_U8;
}
check("silence after the end", ok, "a 64-tick song returns false, stops, and pads with silence");
}
// PTPlayer's tables, from ptplayer.asm by the script: "periods" and 576
// values, "vibrato" and the sine, saw and square tables' 3 * 1024 values.
static void testTables(const char *path) {
FILE *fp;
char word[32];
long value;
uint32_t i;
uint32_t bad;
uint32_t table;
uint32_t amp;
uint32_t pos;
int16_t ours;
if (path == NULL) {
printf("SKIP tables: no ptplayer.asm (the Amiga's PTPlayer is not installed)\n");
return;
}
fp = fopen(path, "r");
if (fp == NULL) {
check("tables", false, "cannot read the PTPlayer tables");
return;
}
bad = 0u;
if (fscanf(fp, "%31s", word) != 1 || strcmp(word, "periods") != 0) {
bad++;
}
for (i = 0u; i < PERIOD_ENTRIES; i++) {
if (fscanf(fp, "%ld", &value) != 1 || value != (&kPeriods[0][0])[i]) {
bad++;
}
}
check("periods", bad == 0u, "the 16 finetunes' periods are PTPlayer's mt_PeriodTable");
bad = 0u;
if (fscanf(fp, "%31s", word) != 1 || strcmp(word, "vibrato") != 0) {
bad++;
}
for (table = 0u; table < VIBRATO_TABLES; table++) {
for (amp = 0u; amp < FINETUNE_COUNT; amp++) {
for (pos = 0u; pos < WAVE_NEGATIVE * 2u; pos++) {
ours = (int16_t)((waveBase((uint8_t)table, (uint8_t)pos, (uint8_t)pos) * amp) >> VIBRATO_SHIFT);
ours = (pos & WAVE_NEGATIVE) != 0u ? (int16_t)-ours : ours;
if (fscanf(fp, "%ld", &value) != 1 || value != ours) {
bad++;
}
}
}
}
check("vibrato waves", bad == 0u, "sine, saw and square match PTPlayer's mt_Vibrato*Table");
fclose(fp);
}
int main(int argc, char **argv) {
testProtracker();
testEnds();
testRender();
testTables(argc > 1 ? argv[1] : NULL);
testFuzz();
printf("MODPLAYER %u/%u\n", gPassed, gChecks);
return gPassed == gChecks ? 0 : 1;
}