275 lines
14 KiB
C
275 lines
14 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"
|
|
|
|
// SFX slot convention: the IIgs maps tracker tone voices onto SFX
|
|
// slots 0-2 and the tracker noise voice onto slot 3, so applications
|
|
// that also play digital SFX while music runs should prefer slot 4.
|
|
// On the other ports the slots are independent of the voice layer.
|
|
#define JOEY_AUDIO_SFX_SLOTS 5
|
|
|
|
// Number of PSG-style polyphonic tone voices (jlAudioVoice selects
|
|
// voice 0/1/2). The core gate range-checks against this so each
|
|
// per-port HAL can assume voice < JOEY_AUDIO_VOICES.
|
|
#define JOEY_AUDIO_VOICES 3
|
|
|
|
// 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.
|
|
//
|
|
// The full -128..127 range is accepted everywhere, but the IIgs plays
|
|
// -128 as -127: its DOC reads 0x00 (which is what -128 becomes in the
|
|
// chip's unsigned format) as a wave-end marker and would halt the voice
|
|
// on that sample. One level out of 256, inaudible in practice.
|
|
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 (see
|
|
// jlAudioPlaySfx for the one clamp the IIgs applies). 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. Implemented on every port: DOS/ST/X68000 pull through the shared
|
|
// SFX overlay mixer, while the IIgs (NTPstreamsound) and Amiga
|
|
// (PTPlayer mt_playfx) play one chunk at a time and fire the next from
|
|
// jlAudioFrameTick as the current one runs down -- so on those two the
|
|
// host must be calling jlAudioFrameTick for a stream to continue past
|
|
// its first chunk, and a seam can carry up to one frame of jitter.
|
|
void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
|
|
|
|
// Raw-format streaming: jlAudioPlaySfxStream, except `fill` delivers
|
|
// bytes already in the port's raw stream format (jlAudioSfxRawFormat),
|
|
// so the port skips its per-byte conversion of every chunk. On the IIgs
|
|
// that conversion is a 65816 byte loop over each 3.5 KB chunk -- it
|
|
// stalled Space Taxi's ride for a third of a second per chunk of speech.
|
|
// Every other port streams signed PCM natively, so there Raw is the
|
|
// plain call. A producer that cannot make the port's format uses
|
|
// jlAudioPlaySfxStream.
|
|
#define JL_SFX_RAW_SIGNED8 0u // int8_t PCM, the jlAudioPlaySfxStream contract
|
|
#define JL_SFX_RAW_UNSIGNED8_NOZERO 1u // uint8_t PCM biased 0x80; 0x00 is forbidden (DOC stop byte)
|
|
uint8_t jlAudioSfxRawFormat(void);
|
|
void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz);
|
|
|
|
// Stereo placement for a SFX slot.
|
|
typedef enum {
|
|
AUDIO_PAN_LEFT = 0,
|
|
AUDIO_PAN_CENTER,
|
|
AUDIO_PAN_RIGHT,
|
|
|
|
AUDIO_PAN_COUNT
|
|
} jlAudioPanE;
|
|
|
|
// Place a SFX slot in the stereo field. Slots start at AUDIO_PAN_CENTER, and
|
|
// the setting sticks to the SLOT, so it survives retriggers -- set it once
|
|
// when a sound is meant to come from one side.
|
|
//
|
|
// What each port can actually do, because the hardware differs:
|
|
// IIgs all three. The DOC pans per OSCILLATOR (control bits 7:4 are the
|
|
// output channel, and per Apple TN #19 odd = left, even = right),
|
|
// so a centred sound is played on two oscillators, one per side.
|
|
// A stock IIgs sums to mono anyway; this only shows up on a stereo
|
|
// card or an emulator.
|
|
// Amiga left and right, by picking the Paula channel side (0 and 3 are
|
|
// left, 1 and 2 right). Paula cannot place one voice in both ears,
|
|
// so AUDIO_PAN_CENTER keeps PTPlayer's own channel choice.
|
|
// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
|
|
// its pan bits would move all audio at once, not one slot.
|
|
// DOS, ST ignored -- their output is mono.
|
|
// Passing an out-of-range pan is ignored.
|
|
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
|
|
|
|
// 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 looping a 2-byte square sample.
|
|
// IIgs -- Ensoniq DOC oscillators playing a resident square,
|
|
// retriggered as an ~80 ms one-shot per call. Needs
|
|
// jlAudioInit (the DOC path rides the NTP engine);
|
|
// callers that hold a note longer than ~80 ms must
|
|
// re-assert it about once a frame (jlMusicPlay's
|
|
// sequencer and AGI's per-frame driver both do).
|
|
//
|
|
// Voice state is sticky: a voice keeps playing its last frequency
|
|
// until the next jlAudioVoice call on it (IIgs excepted, above). CPU
|
|
// cost during sustained playback is zero (the hardware oscillates on
|
|
// its own). Safe to call without jlAudioInit on DOS/ST/Amiga -- the
|
|
// voice path is direct hardware programming, independent of the
|
|
// MOD/SFX mixer. On ST and Amiga the voice channels are the same
|
|
// hardware the MOD engine drives: while a MOD is playing, voice
|
|
// writes fight the player's register writes and lose. Use the voice/
|
|
// tracker layer or the MOD layer per scene, not both at once.
|
|
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
|
|
|
|
// Noise/percussion generator, the 4th logical voice of the tracker
|
|
// layer. `pitch` is 0..31 in YM2149 noise-period style: 0 = the
|
|
// brightest hiss (hats), 31 = the lowest rumble (toms); atten as in
|
|
// jlAudioVoice, and atten >= 15 silences. The per-port mapping:
|
|
//
|
|
// Atari ST -- the YM2149 noise generator (reg 6) mixed onto
|
|
// channel C. Noise shares channel C's volume register
|
|
// with tone voice 2: author noise and voice-2 notes
|
|
// apart (songbake warns on overlap).
|
|
// DOS -- OPL2 rhythm-mode snare drum (the chip's LFSR noise
|
|
// source) on channel 7; the three tone channels are
|
|
// untouched.
|
|
// Amiga -- Paula channel 3 looping a chip-RAM LFSR noise sample
|
|
// (channels 0-2 are the tone voices).
|
|
// IIgs -- a resident LFSR noise sample on SFX slot 3, same
|
|
// ~80 ms retriggered one-shot model as the tone voices
|
|
// (jlMusicPlay's sequencer re-asserts it per frame).
|
|
//
|
|
// Same init contract as jlAudioVoice: direct hardware programming on
|
|
// DOS/ST/Amiga, jlAudioInit required on the IIgs.
|
|
void jlAudioNoise(uint8_t pitch, uint8_t atten);
|
|
|
|
// Portable chip-tracker music: play a JYM1 event stream (baked from
|
|
// tools/songbake text notation) through the jlAudioVoice tone layer.
|
|
// One authored song plays on all four ports at register-write cost --
|
|
// there is no CPU mixing, so unlike jlAudioPlayMod this path is fully
|
|
// viable on the ST and cheap on DOS. The stream is read IN PLACE:
|
|
// data must stay valid until jlMusicStop or song end. Playback is
|
|
// pumped by jlAudioFrameTick (call it once per frame, as usual) and
|
|
// paced by jlMillisElapsed, so tempo holds across 50/60/70 Hz ports.
|
|
// loop=true restarts at the song's authored loop point (or the top
|
|
// when it has none); loop=false silences the voices at song end.
|
|
// Returns false (and plays nothing) on a malformed stream. Call
|
|
// jlAudioInit first for IIgs audibility (see jlAudioVoice).
|
|
bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop);
|
|
|
|
// Stop tracker playback and silence the voices (tones and noise).
|
|
// Safe to call when nothing is playing. The playhead is discarded;
|
|
// use jlMusicPause to keep it.
|
|
void jlMusicStop(void);
|
|
|
|
// True while a jlMusicPlay stream is loaded and active (paused
|
|
// counts as active; stopped/finished does not).
|
|
bool jlMusicIsPlaying(void);
|
|
|
|
// Pause playback in place: voices go silent, the playhead keeps its
|
|
// position. jlMusicResume picks up where the song left off (the
|
|
// current row restarts; wall-clock time spent paused is not
|
|
// fast-forwarded through). Both are safe no-ops when there is no
|
|
// active song.
|
|
void jlMusicPause(void);
|
|
void jlMusicResume(void);
|
|
|
|
// Global music attenuation, added to every authored voice/noise
|
|
// attenuation (0 = as authored .. 15 = silent). Applies immediately
|
|
// to sounding voices and to everything played afterwards; jlMusicPlay
|
|
// does NOT reset it, so a fade-out can carry into the next song.
|
|
// Step it over successive frames for fades.
|
|
void jlMusicSetAtten(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
|