// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Audio conformance probe (AUDPAR, a gate's program, not an example): one // fixed timeline of what every port's audio must do alike, each step logged // as it starts, so scripts/verify-audio-conformance.sh can record what a // port played and tools/gradeAudioConformance.py can measure it against the // API's contract: tone and voice pitch, 2 dB attenuation steps, every voice, // the noise generator's rate and steps, sound effects at their rates and // pans, streamed ones (signed, and in the port's raw format), a module that // does not loop ending (and jlAudioIsPlayingMod saying so), one that loops // stopping when it is told, and tick callbacks at their rates. // // The log, one line per event, "AUDPAR ", ms counted // from the first sync tone's start: // sync a sync tone: the timeline starts and ends // with one, so a recording can be aligned // tone jlAudioTone // voice jlAudioVoice // noise jlAudioNoise // sfx // a one-shot sine sounding at , // long (44100 Hz: above the IIgs's // and the Amiga's chips, which decimate) // stream // a sine streamed (raw 1: through // jlAudioPlaySfxStreamRaw) // mod start / mod end jlAudioPlayMod with loop false, and the // first jlAudioIsPlayingMod false after it // mod loop / mod stop jlAudioPlayMod with loop true, and the // jlAudioStopMod after it // tick jlAudioTickRegister's result, the calls // that came, and the time they had // silence everything stopped // done // Escape ends it early. #include #include #include #include #include #include #include #if defined(JOEYLIB_PLATFORM_IIGS) # define TEST_MOD_NAME "TEST.NTP" #else # define TEST_MOD_NAME "test.mod" #endif #define DATA_FILE_MAX (64UL * 1024UL) #define SYNC_HZ 1000u #define SYNC_MS 300u #define GAP_MS 100u #define PAUSE_MS 200u #define ATTEN_STEPS 15u // 0..14 audible, 15 silent #define ATTEN_SILENT 15u #define TONE_MS 400u #define STEP_MS 300u #define NOISE_MS 400u #define NOISE_STEP_MS 200u #define NOISE_STEP_PITCH 8u #define LEVEL_HZ 440u #define VOICE_HZ 660u #define TICK_MS 2000u #define MOD_WAIT_MS 12000u #define MOD_STOP_MS 1500u // a looping module plays this long, then stops #define SFX_SLOT 0u #define SFX_SAMPLES 4000u #define SFX_PERIOD 8u // samples a cycle: the tone is rate / 8 // Faster than the IIgs's DOC and the Amiga's Paula play, so they decimate: // the tone rate / 16, as the 22050 Hz step's, as long as that step. #define SFX_FAST_RATE 44100u #define SFX_FAST_PERIOD 16u #define SFX_FAST_SAMPLES 8000u #define SFX_AMPLITUDE 100 #define RAW_BIAS 0x80 // JL_SFX_RAW_UNSIGNED8_NOZERO's zero #define MS_PER_SECOND 1000u #define EVENTS_MAX 128u typedef enum { STEP_SYNC, STEP_TONE, STEP_VOICE, STEP_NOISE, STEP_SFX, STEP_SFX_FAST, STEP_STREAM, STEP_STREAM_RAW, STEP_STREAM_FAST, STEP_MOD, STEP_MOD_STOP, STEP_TICK, STEP_SILENCE } StepKindE; typedef struct { StepKindE kind; uint8_t channel; // voice, or the sfx pan uint16_t value; // Hz, noise pitch, or sfx rate uint8_t level; // attenuation uint16_t ms; // how long it sounds (sfx: from its length) } StepT; // A sine a sound step plays: count samples, period of them a cycle. typedef struct { const int8_t *samples; uint32_t count; uint16_t period; } SineT; // A stream step's sine and its progress through it. typedef struct { const SineT *sine; uint32_t pos; } StreamT; // What the log will say of one step, kept in memory until the timeline is // over: a log line can take a disk write (a fifth of a second on the ST), // and written while it plays it would stall the program's sound. typedef struct { uint32_t ms; StepKindE kind; uint16_t value; uint8_t channel; uint8_t level; uint32_t count; // mod: its length, then playing; tick: calls uint32_t span; // tick: the time the calls had bool ok; // tick: registered; mod end: still playing bool modEnd; // mod: its end; mod stop: the stop } EventT; static EventT *eventAdd(uint32_t ms, const StepT *step); static void eventsLog(void); static void loadMod(uint8_t **bytes, uint32_t *length); static bool pump(void); static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength); static uint32_t since(void); static void silenceAll(void); static const SineT *stepSine(StepKindE kind); static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count); static void tickCount(void); static bool waitMs(uint16_t ms); static const StepT kTimeline[] = { { STEP_SYNC, 0u, SYNC_HZ, 0u, SYNC_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // jlAudioTone across the octaves every port can play. { STEP_TONE, 0u, 110u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_TONE, 0u, 220u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_TONE, 0u, 440u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_TONE, 0u, 880u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_TONE, 0u, 1760u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_TONE, 0u, 3136u, 0u, TONE_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // Voice 0's attenuation, one step after another: 2 dB apart. { STEP_VOICE, 0u, LEVEL_HZ, 0u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 1u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 2u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 3u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 4u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 5u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 6u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 7u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 8u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 9u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 10u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 11u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 12u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 13u, STEP_MS }, { STEP_VOICE, 0u, LEVEL_HZ, 14u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // The other two voices. { STEP_VOICE, 1u, VOICE_HZ, 0u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_VOICE, 2u, VOICE_HZ, 0u, STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // The noise generator's rate, bright to dark. { STEP_NOISE, 0u, 1u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_NOISE, 0u, 2u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_NOISE, 0u, 4u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_NOISE, 0u, 8u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_NOISE, 0u, 16u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_NOISE, 0u, 31u, 0u, NOISE_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // The noise's attenuation steps. { STEP_NOISE, 0u, NOISE_STEP_PITCH, 0u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 2u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 4u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 6u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 8u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 10u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 12u, NOISE_STEP_MS }, { STEP_NOISE, 0u, NOISE_STEP_PITCH, 14u, NOISE_STEP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // Sound effects at three rates, then panned each way. { STEP_SFX, JL_AUDIO_PAN_CENTER, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_CENTER, 16000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_CENTER, 22050u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX_FAST, JL_AUDIO_PAN_CENTER, SFX_FAST_RATE, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_LEFT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_SFX, JL_AUDIO_PAN_RIGHT, 8000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // The same sound streamed, streamed raw, and streamed fast. { STEP_STREAM, JL_AUDIO_PAN_CENTER, 11025u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_STREAM_RAW, JL_AUDIO_PAN_CENTER, 12000u, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, GAP_MS }, { STEP_STREAM_FAST, JL_AUDIO_PAN_CENTER, SFX_FAST_RATE, 0u, 0u }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // A module that does not loop, to its end. { STEP_MOD, 0u, 0u, 0u, MOD_WAIT_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // A module that loops, stopped while it plays. { STEP_MOD_STOP, 0u, 0u, 0u, MOD_STOP_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, // Tick callbacks, slow and at a frame's rate. { STEP_TICK, 0u, 10u, 0u, TICK_MS }, { STEP_TICK, 0u, 60u, 0u, TICK_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS }, { STEP_SYNC, 0u, SYNC_HZ, 0u, SYNC_MS }, { STEP_SILENCE, 0u, 0u, 0u, PAUSE_MS } }; // One cycle of a sine, SFX_PERIOD samples, so a sound of it is rate / 8 Hz. static const int8_t kSineCycle[SFX_PERIOD] = { 0, 71, SFX_AMPLITUDE, 71, 0, -71, -SFX_AMPLITUDE, -71 }; static EventT gEvents[EVENTS_MAX]; static uint16_t gEventCount = 0u; static int8_t gSine[SFX_SAMPLES]; static int8_t gSineRaw[SFX_SAMPLES]; // gSine in the port's raw stream format // The sines the steps play (stepSine); the fast one's samples are allocated // at the start, as a static buffer that size would crowd the IIgs's bank. static SineT gSineSlow = { gSine, SFX_SAMPLES, SFX_PERIOD }; static SineT gSineSlowRaw = { gSineRaw, SFX_SAMPLES, SFX_PERIOD }; static SineT gSineFast = { NULL, SFX_FAST_SAMPLES, SFX_FAST_PERIOD }; static StreamT gStream; static uint8_t gRawFormat = JL_SFX_RAW_SIGNED8; static uint32_t gStartMs = 0u; static volatile uint32_t gTickCalls = 0u; // A new event for step at ms; NULL once EVENTS_MAX are taken. static EventT *eventAdd(uint32_t ms, const StepT *step) { EventT *event; if (gEventCount >= EVENTS_MAX) { return NULL; } event = &gEvents[gEventCount++]; event->ms = ms; event->kind = step->kind; event->value = step->value; event->channel = step->channel; event->level = step->level; event->count = 0u; event->span = 0u; event->ok = false; event->modEnd = false; return event; } // The log, once the timeline is over (the format in the header). static void eventsLog(void) { const EventT *event; const SineT *sine; uint16_t i; for (i = 0u; i < gEventCount; i++) { event = &gEvents[i]; switch (event->kind) { case STEP_SYNC: jlLogF("AUDPAR %lu sync %u", (unsigned long)event->ms, (unsigned)event->value); break; case STEP_TONE: jlLogF("AUDPAR %lu tone %u", (unsigned long)event->ms, (unsigned)event->value); break; case STEP_VOICE: jlLogF("AUDPAR %lu voice %u %u %u", (unsigned long)event->ms, (unsigned)event->channel, (unsigned)event->value, (unsigned)event->level); break; case STEP_NOISE: jlLogF("AUDPAR %lu noise %u %u", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->level); break; case STEP_SFX: case STEP_SFX_FAST: sine = stepSine(event->kind); jlLogF("AUDPAR %lu sfx %u %u %u %u %lu", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / sine->period), (unsigned)event->channel, (unsigned long)sine->count); break; case STEP_STREAM: case STEP_STREAM_RAW: case STEP_STREAM_FAST: sine = stepSine(event->kind); jlLogF("AUDPAR %lu stream %u %u %u %u %u %lu", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / sine->period), (unsigned)event->channel, (unsigned)(event->kind == STEP_STREAM_RAW), (unsigned long)sine->count); break; case STEP_MOD: if (event->modEnd) { jlLogF("AUDPAR %lu mod end %u", (unsigned long)event->ms, (unsigned)event->ok); } else { jlLogF("AUDPAR %lu mod start %lu", (unsigned long)event->ms, (unsigned long)event->count); } break; case STEP_MOD_STOP: jlLogF("AUDPAR %lu mod %s", (unsigned long)event->ms, event->modEnd ? "stop" : "loop"); break; case STEP_TICK: jlLogF("AUDPAR %lu tick %u %u %lu %lu", (unsigned long)event->ms, (unsigned)event->value, (unsigned)event->ok, (unsigned long)event->count, (unsigned long)event->span); break; case STEP_SILENCE: jlLogF("AUDPAR %lu silence", (unsigned long)event->ms); break; } } } // DATA's module, whole; none (NULL, 0) when it is missing or too big. static void loadMod(uint8_t **bytes, uint32_t *length) { FILE *fp; long size; *bytes = NULL; *length = 0u; fp = jlDataOpen(TEST_MOD_NAME, "rb"); if (fp == NULL) { return; } size = -1L; if (fseek(fp, 0L, SEEK_END) == 0) { size = ftell(fp); } if (size > 0L && (uint32_t)size <= DATA_FILE_MAX && fseek(fp, 0L, SEEK_SET) == 0) { *bytes = (uint8_t *)malloc((size_t)size); if (*bytes != NULL && fread(*bytes, 1, (size_t)size, fp) == (size_t)size) { *length = (uint32_t)size; } } fclose(fp); } // One frame: the engine's pump, the display's pace, and input. False when // Escape asks to stop. static bool pump(void) { jlAudioFrameTick(); jlWaitVBL(); jlInputPoll(); return !jlKeyPressed(JL_KEY_ESCAPE); } // Each step's time is taken as it starts. static void runStep(const StepT *step, const uint8_t *mod, uint32_t modLength) { const SineT *sine; uint32_t start; EventT *event; start = since(); event = eventAdd(start, step); switch (step->kind) { case STEP_SYNC: case STEP_TONE: jlAudioTone(step->value); (void)waitMs(step->ms); break; case STEP_VOICE: jlAudioVoice(step->channel, step->value, step->level); (void)waitMs(step->ms); break; case STEP_NOISE: jlAudioNoise((uint8_t)step->value, step->level); (void)waitMs(step->ms); break; case STEP_SFX: case STEP_SFX_FAST: sine = stepSine(step->kind); jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel); jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)sine->samples, sine->count, step->value); (void)waitMs((uint16_t)(sine->count * MS_PER_SECOND / step->value)); break; case STEP_STREAM: case STEP_STREAM_RAW: case STEP_STREAM_FAST: jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel); gStream.sine = stepSine(step->kind); gStream.pos = 0u; if (step->kind == STEP_STREAM_RAW) { jlAudioPlaySfxStreamRaw(SFX_SLOT, streamFill, &gStream, step->value); } else { jlAudioPlaySfxStream(SFX_SLOT, streamFill, &gStream, step->value); } (void)waitMs((uint16_t)(gStream.sine->count * MS_PER_SECOND / step->value)); break; case STEP_MOD: if (mod != NULL) { jlAudioPlayMod(mod, modLength, false); } if (event != NULL) { event->count = modLength; } while (jlAudioIsPlayingMod() && since() - start < step->ms) { if (!pump()) { break; } } event = eventAdd(since(), step); if (event != NULL) { event->modEnd = true; event->ok = jlAudioIsPlayingMod(); } jlAudioStopMod(); break; case STEP_MOD_STOP: if (mod != NULL) { jlAudioPlayMod(mod, modLength, true); } (void)waitMs(step->ms); event = eventAdd(since(), step); if (event != NULL) { event->modEnd = true; } jlAudioStopMod(); break; case STEP_TICK: gTickCalls = 0u; if (event != NULL) { event->ok = jlAudioTickRegister(tickCount, step->value); } (void)waitMs(step->ms); (void)jlAudioTickRegister(NULL, 0u); if (event != NULL) { event->count = gTickCalls; event->span = since() - start; } break; case STEP_SILENCE: silenceAll(); (void)waitMs(step->ms); break; } } static uint32_t since(void) { return jlMillisElapsed() - gStartMs; } static void silenceAll(void) { uint8_t voice; for (voice = 0u; voice < JL_AUDIO_VOICES; voice++) { jlAudioVoice(voice, 0u, ATTEN_SILENT); } jlAudioNoise(0u, ATTEN_SILENT); jlAudioStopSfx(SFX_SLOT); } // The sine a sound step plays: the fast steps' (SFX_FAST_RATE), the raw // stream's in the port's raw format, the rest the one-shots' signed bytes. static const SineT *stepSine(StepKindE kind) { if (kind == STEP_SFX_FAST || kind == STEP_STREAM_FAST) { return &gSineFast; } return kind == STEP_STREAM_RAW ? &gSineSlowRaw : &gSineSlow; } // A stream step's fill: its sine, as stepSine chose it; 0 once it has all // gone, which ends the stream. A copy, so the time the fill takes is the // port's, not this program's. static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count) { StreamT *stream; stream = (StreamT *)ctx; if (count > stream->sine->count - stream->pos) { count = stream->sine->count - stream->pos; } memcpy(dst, stream->sine->samples + stream->pos, count); stream->pos += count; return count; } static void tickCount(void) { gTickCalls++; } static bool waitMs(uint16_t ms) { uint32_t start; start = since(); while (since() - start < ms) { if (!pump()) { return false; } } return true; } int main(void) { jlConfigT config; uint8_t *mod; int8_t *fast; uint32_t modLength; uint16_t i; config.codegenBytes = 8 * 1024; config.noAudio = false; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return 1; } gRawFormat = jlAudioSfxRawFormat(); for (i = 0u; i < SFX_SAMPLES; i++) { gSine[i] = kSineCycle[i % SFX_PERIOD]; gSineRaw[i] = gRawFormat == JL_SFX_RAW_UNSIGNED8_NOZERO ? (int8_t)(uint8_t)(gSine[i] + RAW_BIAS) : gSine[i]; } // The fast sine: each cycle step of the slow one held for two samples. fast = (int8_t *)malloc(SFX_FAST_SAMPLES); if (fast == NULL) { fprintf(stderr, "AUDPAR: no memory for the fast sine\n"); jlShutdown(); return 1; } for (i = 0u; i < SFX_FAST_SAMPLES; i++) { fast[i] = kSineCycle[(i / (SFX_FAST_PERIOD / SFX_PERIOD)) % SFX_PERIOD]; } gSineFast.samples = fast; loadMod(&mod, &modLength); jlSurfaceClear(jlStageGet(), 0u); jlStagePresent(); gStartMs = jlMillisElapsed(); for (i = 0u; i < (uint16_t)(sizeof(kTimeline) / sizeof(kTimeline[0])); i++) { runStep(&kTimeline[i], mod, modLength); if (jlKeyDown(JL_KEY_ESCAPE)) { break; } } silenceAll(); eventsLog(); jlLogF("AUDPAR %lu done", (unsigned long)since()); jlLogFlush(); free(mod); free(fast); jlShutdown(); return 0; }