// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Audio demo: starts the baked chip-tracker song (demoSong.song -> // demoSong.h via tools/songbake) on entry and exits on ESC. The keys: // SPACE a short digital SFX, test.sfx from the DATA folder // S a falling tone, streamed: a callback writes each chunk as the // engine asks for it, already in the port's own sample format // (jlAudioPlaySfxStreamRaw), so it needs no sample data and could // run as long as it liked // N the noise generator swept through its 32 pitches, bright to // dark (jlAudioNoise); the song pauses for it // M the .MOD loaded from the DATA folder (make/examples.mk says what // each platform stages) // T back to the tracker song // P pause / resume the tracker song in place // The tracker and the MOD share output hardware on the ST/Amiga, so the // demo always stops one before starting the other. // // Built with AUDIO_PROBE (the AUDIOPRB test program) it presses its own // keys on a timetable -- N once, S every 1.5 seconds -- for the IIgs and // X68000 audio gates, which read the sound chips but cannot type. // // On platforms where the audio HAL is still a stub, jlAudioInit // returns false, every audio call is a quiet no-op, and the demo runs // as a silent input loop -- you can confirm the build links and the // input plumbing still works without hearing anything. #include #include #include #include #include "demoSong.h" // Each platform encodes the MOD differently. The IIgs build converts // test.mod -> test.ntp via joeymod at build time and ships the .NTP; // every other platform ships the raw .MOD. test.sfx is the same raw // PCM blob everywhere. The tracker song needs no per-platform form at // all -- the baked JYM1 bytes are identical on every port. The names // are relative to DATA/ (jlDataOpen), which is not always the current // directory's DATA/ (DVX: beside the program). #if defined(JOEYLIB_PLATFORM_IIGS) # define TEST_MOD_NAME "TEST.NTP" #else # define TEST_MOD_NAME "test.mod" #endif #define TEST_SFX_NAME "test.sfx" #define DATA_FILE_MAX (64UL * 1024UL) // Slot 3: the tracker's three voices ride SFX slots 0-2 on the IIgs, // so the demo's one-shot SFX stays out of their way. The stream takes // slot 4, so it never cuts the one-shot off. #define SFX_SLOT 3 #define SFX_RATE_HZ 8000 #define STREAM_SLOT 4 // The streamed tone: STREAM_BLOCKS blocks of STREAM_BLOCK samples, each // at one pitch and one level, falling STREAM_DROP_HZ from its first pitch // while it fades from STREAM_PEAK to STREAM_FLOOR. Each press starts // STREAM_TAKE_HZ higher than the last, over STREAM_TAKES takes, so two // takes in a row never sound alike. The square wave's phase is a 16-bit // accumulator: one cycle per 2^STREAM_PHASE_BITS. #define STREAM_RATE_HZ 8000u #define STREAM_BLOCK 256u #define STREAM_BLOCKS 48u #define STREAM_SAMPLES (STREAM_BLOCK * STREAM_BLOCKS) #define STREAM_START_HZ 330u #define STREAM_DROP_HZ 220u #define STREAM_TAKE_HZ 55u #define STREAM_TAKES 4u #define STREAM_PEAK 96 #define STREAM_FLOOR 16 #define STREAM_PHASE_BITS 16u #define STREAM_PHASE_HALF (1u << (STREAM_PHASE_BITS - 1u)) // Signed sample -> the unsigned form a port that streams unsigned bytes // wants (JL_SFX_RAW_UNSIGNED8_NOZERO). |level| <= STREAM_PEAK keeps every // byte off 0x00, the IIgs DOC's stop byte. #define RAW_BIAS 0x80 // The noise sweep: every pitch in turn for NOISE_STEP_FRAMES frames. #define NOISE_PITCHES 32u #define NOISE_STEP_FRAMES 6u #define NOISE_ATTEN 4u #define ATTEN_SILENT 15u // AUDIO_PROBE's timetable, in frames of the main loop. A new stream starts // about when the last one ends (STREAM_SAMPLES at STREAM_RATE_HZ is 1.5 s), // so whenever a gate looks, one is playing. #define PROBE_NOISE_FRAME 60u #define PROBE_STREAM_FIRST 30u #define PROBE_STREAM_FRAMES 90u #define FLASH_FRAMES 8 #define COLOR_BG 0 #define COLOR_HINT 1 #define COLOR_BAR 2 #define BAR_X 16 #define BAR_Y 88 #define BAR_W (SURFACE_WIDTH - 32) #define BAR_H 16 // The streamed tone's state, which streamFill advances. typedef struct { uint32_t pos; // samples delivered so far uint16_t phase; // square-wave phase accumulator uint16_t startHz; // this take's first pitch } StreamT; static void buildPalette(jlSurfaceT *screen); static void initialPaint(jlSurfaceT *screen, bool audioOk); static bool keyPressed(jlKeyE key, uint16_t frame); static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen); static bool noiseStep(uint16_t sweepFrame); static jlKeyE probeKey(uint16_t frame); static uint8_t rawSample(int16_t level); static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count); static void streamStart(void); static uint8_t gRawFormat = JL_SFX_RAW_SIGNED8; static StreamT gStream; static uint8_t gStreamTake = 0; static void buildPalette(jlSurfaceT *screen) { uint16_t colors[SURFACE_COLORS_PER_PALETTE]; uint16_t i; for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { colors[i] = 0x0000; } colors[COLOR_BG] = 0x0000; colors[COLOR_HINT] = 0x0444; colors[COLOR_BAR] = 0x00F0; jlPaletteSet(screen, 0, colors); } // Visual feedback for "audio is running, but you cannot tell on a // stub HAL": pulse a horizontal bar between the hint color and the // active color whenever a sound has fired this frame. static void initialPaint(jlSurfaceT *screen, bool audioOk) { jlSurfaceClear(screen, COLOR_BG); jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, audioOk ? COLOR_HINT : COLOR_BG); jlStagePresent(); } // True when the player pressed key this frame -- or, in the probe build, // when the timetable presses it. static bool keyPressed(jlKeyE key, uint16_t frame) { return jlKeyPressed(key) || probeKey(frame) == key; } // Read an entire DATA/ file into a freshly allocated buffer. Caller must // free(*outBytes) once the audio engine has consumed the data. // Returns false if the file is missing, empty, or larger than maxLen. static bool loadFile(const char *name, uint32_t maxLen, uint8_t **outBytes, uint32_t *outLen) { FILE *fp; long fileSize; uint8_t *buf; size_t readBytes; fp = jlDataOpen(name, "rb"); if (fp == NULL) { return false; } if (fseek(fp, 0L, SEEK_END) != 0) { fclose(fp); return false; } fileSize = ftell(fp); if (fileSize <= 0 || (uint32_t)fileSize > maxLen) { fclose(fp); return false; } if (fseek(fp, 0L, SEEK_SET) != 0) { fclose(fp); return false; } buf = (uint8_t *)malloc((size_t)fileSize); if (buf == NULL) { fclose(fp); return false; } readBytes = fread(buf, 1, (size_t)fileSize, fp); fclose(fp); if (readBytes != (size_t)fileSize) { free(buf); return false; } *outBytes = buf; *outLen = (uint32_t)fileSize; return true; } // One frame of the noise sweep, sweepFrame frames in: this frame's pitch, // or silence once every pitch has had its turn. Re-asserted every frame, // because the IIgs holds a noise burst for only ~80 ms. Returns false when // the sweep is over. static bool noiseStep(uint16_t sweepFrame) { uint16_t pitch; pitch = (uint16_t)(sweepFrame / NOISE_STEP_FRAMES); if (pitch >= NOISE_PITCHES) { jlAudioNoise(0, ATTEN_SILENT); return false; } jlAudioNoise((uint8_t)pitch, NOISE_ATTEN); return true; } #if defined(AUDIO_PROBE) // The key the probe build presses this frame: N once, then S over and over, // so a gate finds the noise sweep and a stream running. static jlKeyE probeKey(uint16_t frame) { if (frame == PROBE_NOISE_FRAME) { return KEY_N; } if (frame % PROBE_STREAM_FRAMES == PROBE_STREAM_FIRST) { return KEY_S; } return KEY_NONE; } #else static jlKeyE probeKey(uint16_t frame) { (void)frame; return KEY_NONE; } #endif // One sample of the streamed tone in this port's raw stream format. static uint8_t rawSample(int16_t level) { if (gRawFormat == JL_SFX_RAW_UNSIGNED8_NOZERO) { return (uint8_t)(level + RAW_BIAS); } return (uint8_t)(int8_t)level; } // The stream's fill callback: up to count samples of the falling tone, // written straight in the raw format; 0 once it has all been delivered, // which ends the stream. A block's pitch and level are worked out once, // so each sample costs an add and a store -- it may run in an interrupt. static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count) { StreamT *stream; uint8_t *out; uint32_t done; uint16_t run; uint16_t i; uint16_t block; uint16_t hz; uint16_t step; int16_t level; uint8_t hi; uint8_t lo; stream = (StreamT *)ctx; out = (uint8_t *)dst; done = 0; while (done < count && stream->pos < STREAM_SAMPLES) { block = (uint16_t)(stream->pos / STREAM_BLOCK); run = (uint16_t)(STREAM_BLOCK - stream->pos % STREAM_BLOCK); if (run > count - done) { run = (uint16_t)(count - done); } hz = (uint16_t)(stream->startHz - (STREAM_DROP_HZ * block) / STREAM_BLOCKS); step = (uint16_t)(((uint32_t)hz << STREAM_PHASE_BITS) / STREAM_RATE_HZ); level = (int16_t)(STREAM_PEAK - ((STREAM_PEAK - STREAM_FLOOR) * (int16_t)block) / (int16_t)STREAM_BLOCKS); hi = rawSample(level); lo = rawSample((int16_t)-level); for (i = 0; i < run; i++) { stream->phase = (uint16_t)(stream->phase + step); *out++ = (stream->phase & STREAM_PHASE_HALF) ? hi : lo; } done += run; stream->pos += run; } return done; } // Start the falling tone over, a take higher than the last one. static void streamStart(void) { jlAudioStopSfx(STREAM_SLOT); gStream.pos = 0; gStream.phase = 0; gStream.startHz = (uint16_t)(STREAM_START_HZ + STREAM_TAKE_HZ * gStreamTake); gStreamTake = (uint8_t)((gStreamTake + 1u) % STREAM_TAKES); jlAudioPlaySfxStreamRaw(STREAM_SLOT, streamFill, &gStream, STREAM_RATE_HZ); } int main(void) { jlConfigT config; jlSurfaceT *screen; bool audioOk; bool musicPaused; bool noiseOn; bool noiseResumes; uint16_t noiseFrame; uint16_t frame; int16_t flashFrames; uint8_t *modBytes; uint32_t modLen; uint8_t *sfxBytes; uint32_t sfxLen; config.codegenBytes = 8 * 1024; if (!jlInit(&config)) { fprintf(stderr, "jlInit failed: %s\n", jlLastError()); return 1; } screen = jlStageGet(); if (screen == NULL) { fprintf(stderr, "jlStageGet returned NULL\n"); jlShutdown(); return 1; } modBytes = NULL; sfxBytes = NULL; modLen = 0; sfxLen = 0; audioOk = jlAudioInit(); if (audioOk) { // Keep the MOD bytes resident so M can (re)start it later; // jlAudioPlayMod copies them at play time. (void)loadFile(TEST_MOD_NAME, DATA_FILE_MAX, &modBytes, &modLen); (void)loadFile(TEST_SFX_NAME, DATA_FILE_MAX, &sfxBytes, &sfxLen); gRawFormat = jlAudioSfxRawFormat(); } jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); buildPalette(screen); jlScbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0); initialPaint(screen, audioOk); musicPaused = false; noiseOn = false; noiseResumes = false; noiseFrame = 0; flashFrames = 0; for (frame = 0; ; frame++) { jlWaitVBL(); jlInputPoll(); jlAudioFrameTick(); if (keyPressed(KEY_ESCAPE, frame)) { break; } if (keyPressed(KEY_SPACE, frame) && sfxBytes != NULL) { jlAudioPlaySfx(SFX_SLOT, sfxBytes, sfxLen, SFX_RATE_HZ); flashFrames = FLASH_FRAMES; } if (keyPressed(KEY_S, frame) && audioOk) { streamStart(); flashFrames = FLASH_FRAMES; } if (keyPressed(KEY_N, frame) && !noiseOn) { // The song's own drums would fight the sweep for the noise // generator, so it pauses and picks up again afterwards. noiseResumes = jlMusicIsPlaying() && !musicPaused; jlMusicPause(); noiseOn = true; noiseFrame = 0; } if (keyPressed(KEY_M, frame) && modBytes != NULL) { jlMusicStop(); jlAudioPlayMod(modBytes, modLen, true); } if (keyPressed(KEY_T, frame)) { jlAudioStopMod(); jlMusicPlay(gDemoSong, (uint32_t)sizeof(gDemoSong), true); } if (keyPressed(KEY_P, frame)) { if (musicPaused) { jlMusicResume(); } else { jlMusicPause(); } musicPaused = !musicPaused; } if (noiseOn) { noiseOn = noiseStep(noiseFrame); noiseFrame++; if (!noiseOn && noiseResumes && !musicPaused) { jlMusicResume(); } } if (flashFrames > 0) { jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR); jlStagePresent(); flashFrames--; if (flashFrames == 0) { jlFillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT); jlStagePresent(); } } } jlMusicStop(); if (audioOk) { jlAudioStopMod(); } jlAudioShutdown(); if (modBytes != NULL) { free(modBytes); } if (sfxBytes != NULL) { free(sfxBytes); } jlShutdown(); return 0; }