581 lines
25 KiB
C
581 lines
25 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 behaviors 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_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;
|
|
}
|