// 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, a module that does not loop ending (and jlAudioIsPlayingMod saying // so), 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 // mod start / mod end jlAudioPlayMod with loop false, and the // first jlAudioIsPlayingMod false 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 #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 SFX_SLOT 0u #define SFX_SAMPLES 4000u #define SFX_PERIOD 8u // samples a cycle: the tone is rate / 8 #define SFX_AMPLITUDE 100 #define MS_PER_SECOND 1000u #define EVENTS_MAX 128u typedef enum { STEP_SYNC, STEP_TONE, STEP_VOICE, STEP_NOISE, STEP_SFX, STEP_MOD, 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; // 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; } 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 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, 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 }, // A module that does not loop, to its end. { STEP_MOD, 0u, 0u, 0u, MOD_WAIT_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 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; 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: jlLogF("AUDPAR %lu sfx %u %u %u %u", (unsigned long)event->ms, (unsigned)SFX_SLOT, (unsigned)event->value, (unsigned)(event->value / SFX_PERIOD), (unsigned)event->channel); 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_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) { 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: jlAudioSetPan(SFX_SLOT, (jlAudioPanE)step->channel); jlAudioPlaySfx(SFX_SLOT, (const uint8_t *)gSine, SFX_SAMPLES, step->value); (void)waitMs((uint16_t)((uint32_t)SFX_SAMPLES * 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_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); } 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; 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; } for (i = 0u; i < SFX_SAMPLES; i++) { gSine[i] = kSineCycle[i % SFX_PERIOD]; } 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); jlShutdown(); return 0; }