401 lines
15 KiB
C
401 lines
15 KiB
C
// 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> <event> <values>", ms counted
|
|
// from the first sync tone's start:
|
|
// sync <hz> a sync tone: the timeline starts and ends
|
|
// with one, so a recording can be aligned
|
|
// tone <hz> jlAudioTone
|
|
// voice <v> <hz> <atten> jlAudioVoice
|
|
// noise <pitch> <atten> jlAudioNoise
|
|
// sfx <slot> <rate> <hz> <pan> a one-shot sine sounding <hz> at <rate>
|
|
// mod start / mod end jlAudioPlayMod with loop false, and the
|
|
// first jlAudioIsPlayingMod false after it
|
|
// tick <hz> <ok> <calls> <ms> jlAudioTickRegister's result, the calls
|
|
// that came, and the time they had
|
|
// silence everything stopped
|
|
// done
|
|
// Escape ends it early.
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include <joey/joey.h>
|
|
#include <joey/file.h>
|
|
|
|
#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;
|
|
}
|