// 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 #include #include #include #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_MONO 1u // one channel a frame #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 testRenderHalf(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 (!jlModPlayerStart(module, bytes, TEST_RATE, loop)) { return 0u; } for (n = 1u; n <= TICK_LIMIT; n++) { if (!tick()) { jlModPlayerStop(); return n; } } jlModPlayerStop(); 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 = jlModStopMarkerAdd(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 (jlModPlayerStart(gMod, sizeof(gMod) - (run % 2u) * (RAMP_BYTES / 2u), TEST_RATE, (run & 2u) != 0u)) { started++; (void)jlModPlayerRenderU8(gRenderA, FUZZ_SAMPLES, RENDER_MONO); jlModPlayerStop(); } if (jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true)) { (void)jlModPlayerRenderS16(gRenderS16, FUZZ_SAMPLES / S16_STEREO, (uint8_t)S16_STEREO); jlModPlayerStop(); } } check("fuzz", started > 0u, "mangled modules render without leaving the player's copy"); check("refuses a short module", !jlModPlayerStart(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", !jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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)jlModPlayerStart(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"); jlModPlayerStop(); } // The half unit (jlModPlayerSetU8Shift): one channel at full volume swings half // the range; four in phase pass a byte, so the mono render clamps them where // adding them straight would wrap, and a stereo side's two still fit. static void testRenderHalf(void) { char detail[160]; uint32_t i; uint8_t low; uint8_t high; bool ok; jlModPlayerSetU8Shift(U8_SHIFT_HALF); modNew(1u); modNote(0u, 0u, 0u, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); 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_HALF), U8_BIAS + (SQUARE_HIGH >> U8_SHIFT_HALF)); check("8-bit level, half", low == U8_BIAS + (SQUARE_LOW >> U8_SHIFT_HALF) && high == U8_BIAS + (SQUARE_HIGH >> U8_SHIFT_HALF), detail); for (i = 0u; i < MOD_CHANNELS; i++) { modNote(0u, 0u, (uint8_t)i, SAMPLE_SQUARE, PERIOD_C2, 0x0u, 0x00u); } (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); ok = true; for (i = 0u; i < TEST_RATE; i++) { ok = ok && (gRenderA[i] == 0u || gRenderA[i] == U8_BIAS || gRenderA[i] == UINT8_MAX); } check("8-bit clamp, half", ok, "four channels in phase clamp to 0 and 255 in mono, never wrap"); (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderU8(gRenderB, TEST_RATE / S16_STEREO, (uint8_t)S16_STEREO); ok = true; for (i = 0u; i < TEST_RATE; i++) { ok = ok && (gRenderB[i] == 0u || gRenderB[i] == U8_BIAS || gRenderB[i] == U8_BIAS + 2u * (SQUARE_HIGH >> U8_SHIFT_HALF)); } check("8-bit sides, half", ok, "two channels in phase fill a stereo side, 0 to 254"); jlModPlayerSetU8Shift(U8_SHIFT_QUARTER); } 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)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderU8(gRenderA, TEST_RATE, RENDER_MONO); 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_QUARTER), U8_BIAS + (SQUARE_HIGH >> U8_SHIFT_QUARTER)); check("8-bit level", low == U8_BIAS + (SQUARE_LOW >> U8_SHIFT_QUARTER) && high == U8_BIAS + (SQUARE_HIGH >> U8_SHIFT_QUARTER), detail); // Channel 0 is on the left alone, channel 1 on the right. (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderS16(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)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderU8(gRenderA, RENDER_SAMPLES, RENDER_MONO); (void)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); (void)jlModPlayerRenderS16(gRenderS16, RENDER_SAMPLES, RENDER_MONO); 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)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, true); for (done = 0u; done < RENDER_SAMPLES; done += run) { (void)jlModPlayerRenderU8(gRenderB + done, done + run <= RENDER_SAMPLES ? run : RENDER_SAMPLES - done, RENDER_MONO); } 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)jlModPlayerStart(gMod, sizeof(gMod), TEST_RATE, false); ok = !jlModPlayerRenderU8(gRenderA, RENDER_SAMPLES, RENDER_MONO) && !jlModPlayerActive(); 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(); testRenderHalf(); testTables(argc > 1 ? argv[1] : NULL); testFuzz(); printf("MODPLAYER %u/%u\n", gPassed, gChecks); return gPassed == gChecks ? 0 : 1; }