joeylib2/examples/audpar/audpar.c
2026-10-19 15:46:47 -05:00

533 lines
21 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, 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> <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> <samples>
// a one-shot sine sounding <hz> at <rate>,
// <samples> long (44100 Hz: above the IIgs's
// and the Amiga's chips, which decimate)
// stream <slot> <rate> <hz> <pan> <raw> <samples>
// 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 <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 <string.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 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;
}