joeylib2/include/joey/audio.h

146 lines
6.9 KiB
C

// Audio: 4-channel Protracker .MOD music with digital one-shot SFX.
//
// Authors compose music as .MOD modules. The host-side asset pipeline
// (tools/joeymod) converts each module into the runtime form the
// target platform expects:
//
// Apple IIgs -- NinjaTrackerPlus .NTP, played by Ninjaforce's
// 65816 replayer linked into the binary.
// Amiga -- raw .MOD, played by Frank Wille's PTPlayer.
// DOS -- raw .MOD, played by libxmp-lite over SB DMA.
// Atari ST -- raw .MOD, played by libxmp-lite (parser only) plus
// a hand-rolled 68k 4-channel mixer that outputs via
// YM2149 4-bit PWM at ~12.5 kHz.
//
// Game code always calls the same five entry points; the per-port
// HAL hides the engine choice. A failed audio init is non-fatal --
// jlInit still succeeds and audio calls become no-ops.
#ifndef JOEYLIB_AUDIO_H
#define JOEYLIB_AUDIO_H
#include "platform.h"
#include "types.h"
#define JOEY_AUDIO_SFX_SLOTS 4
// Initialize audio. Returns true if the platform has a working audio
// engine and was able to start it. Returns false silently otherwise;
// the rest of the API stays callable but produces no sound.
bool jlAudioInit(void);
// Tear down the engine. Safe to call when audio is not initialized.
void jlAudioShutdown(void);
// Begin module playback. data points at the platform-native module
// blob (.MOD on most platforms, .NTP on IIgs); the asset pipeline
// produces the right form for each target. If a module is already
// playing, it is replaced.
//
// loop=true plays forever. loop=false stops at song end, but Amiga
// requires the module to contain an E8FF effect at song end for the
// stop to fire (PTPlayer has no native song-end signal). The
// `joeymod` tool's .amod output extension injects that marker
// automatically; ship .amod for Amiga and .mod for the other ports.
// loop=false on a .mod (no E8FF) loops anyway on Amiga.
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
// Stop the current module (if any). The playhead is reset so the next
// jlAudioPlayMod starts from the top.
void jlAudioStopMod(void);
// True if a module is currently producing output (false during silence
// after StopMod or before the first PlayMod, and on platforms where
// audio init failed).
bool jlAudioIsPlayingMod(void);
// Trigger a one-shot digital SFX on the given slot (0..JOEY_AUDIO_SFX_SLOTS-1).
// sample points at raw signed 8-bit PCM. rateHz is the playback rate
// the sample was recorded at; the engine pitches as needed for its
// own output rate. If the slot is currently playing, the new sample
// replaces it.
void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
// Stream-fill callback signature. Called by the mixer to refill a
// streaming slot's prefetch buffer; should write up to `count` signed
// 8-bit samples to `dst` and return the number written. Returning 0
// signals end-of-stream and the slot auto-deactivates. Called from
// the same context as the audio engine's refill (main thread on
// DOS/ST, audio ISR on Amiga/IIgs); must not block or allocate.
typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
// Trigger streaming SFX playback on the given slot. The mixer pulls
// samples from `fill(ctx, ...)` as the slot's internal prefetch
// buffer drains, so SFX can be arbitrarily long with no caller-side
// allocation. `rateHz` is the sample rate `fill` produces; the
// engine resamples (with linear interpolation) to its own output
// rate. If the slot is currently playing, the new stream replaces
// it. No-op on ports whose audio engine doesn't expose the shared
// SFX overlay mixer.
void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
// Stop a SFX slot early. No-op if the slot is already idle.
void jlAudioStopSfx(uint8_t slot);
// Direct hardware tone generator: continuously play a square wave at
// `freqHz` until the next jlAudioTone call. freqHz == 0 silences the
// generator. Designed for AGI-era PSG-style music where the engine
// just programs a divisor and lets the chip play; CPU cost during
// playback is zero. Single voice on platforms with multiple
// hardware oscillators (uses voice 0). On DOS this programs PIT
// counter 2 and the speaker gate (port 0x61). No-op on platforms
// without a reachable tone generator -- callers should fall back to
// jlAudioPlaySfxStream when they need PCM-synth sound on those.
void jlAudioTone(uint16_t freqHz);
// Periodic tick callback for sound schedulers that need better-than-
// frame-rate resolution. Used by PSG-era music drivers (AGI, AdLib
// patches, etc.) to write hardware register changes at exact note
// boundaries -- frame-rate-driven scheduling batches multiple notes
// into one render frame, so the chip only "hears" the last write
// per batch and short notes are dropped.
//
// fn fires from interrupt context at `hz` times per second. Keep
// fn short: only memory reads, direct hardware-register writes,
// no malloc / printf / blocking. Pass fn = NULL to uninstall.
//
// Per-port impl: DOS reprograms PIT counter 0 + hooks INT 8,
// keeping the BIOS tick counter at $0040:$006C advancing at the
// canonical 18.2 Hz via an accumulator (so uclock / time stay
// correct). Other ports return false until a port-appropriate
// timer source is wired.
typedef void (*jlAudioTickFnT)(void);
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
// Critical section against the tick ISR. Callers that mutate
// state read by the registered tick callback must wrap the
// mutation in Enter/Exit so the ISR can't observe a half-written
// state. On DOS this is `cli` / `sti`; on ports without an
// audio ISR these are no-ops.
void jlAudioCriticalEnter(void);
void jlAudioCriticalExit(void);
// 3-voice PSG-style polyphonic tone generator. `voice` selects 0/1/2;
// `freqHz` is the new tone (0 = silence this voice); `atten` is the
// AGI/SN76489-style 4-bit attenuation, 0 = loudest .. 15 = silent
// (2 dB per step). The mapping to underlying hardware:
//
// DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389).
// Atari ST -- YM2149 PSG channels A/B/C.
// Amiga -- Paula channels 0/1/2 playing a 2-byte square sample.
// IIgs -- not wired yet (Ensoniq DOC is wavetable, not square).
//
// Voice state is sticky: a voice keeps playing its last frequency
// until the next jlAudioVoice call on it. CPU cost during sustained
// playback is zero (the hardware oscillates on its own). Safe to
// call without jlAudioInit -- the voice path is direct hardware
// programming, independent of the MOD/SFX mixer.
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
// Hook the engine into the game loop. Most platforms drive their
// engines off a hardware IRQ and ignore this call, but it's safe to
// invoke once per frame regardless and required for any port that
// runs its mixer in user-thread context.
void jlAudioFrameTick(void);
#endif