joeylib2/include/joey/audio.h
2026-10-19 15:46:47 -05:00

326 lines
17 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: 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, mixed by JoeyLib's module player (PTPlayer's
// replay in C) into SB DMA (22 kHz 8-bit, stereo on an
// SB16).
// Atari ST -- raw .MOD, mixed by the module player at 12288 Hz and
// played through the YM2149's channel A volume (PWM); the
// STe build plays it in stereo through DMA sound at
// 12517 Hz.
// X68000 -- raw .MOD, mixed by the module player at 7812 Hz into the
// MSM6258 ADPCM channel.
// DVX -- raw .MOD through DVX's music library.
// hosted -- raw .MOD, mixed by the module player in software.
//
// Game code always calls the same entry points; the per-port HAL hides
// the engine choice. jlInit starts audio (jlConfigT.noAudio leaves it
// off) and jlShutdown stops it. Sound that does not start is non-fatal:
// jlInit still succeeds and audio calls become no-ops.
//
// The mix is the same on every port, within 3 dB: a sound effect at full
// scale, and a module channel at full volume playing a full-scale sample,
// are each as loud as a tone voice at attenuation 0 (a square wave sampled
// at full scale peaks where the voice does), and the noise at attenuation 0
// is as loud as a voice. The ports that mix in software put that peak at a
// quarter of full scale, so the three voices and the noise together, or a
// module's four channels, fit; the plain ST puts it at half of its one
// sound chip channel, so a module whose three or four channels peak at once
// clips there. DOS keeps the mix on the Sound Blaster's mixer, which the
// cards before the SB Pro do not have: there effects and modules play about
// 5 dB over the voices.
#ifndef JOEYLIB_AUDIO_H
#define JOEYLIB_AUDIO_H
#include "platform.h"
#include "types.h"
// SFX slot convention: the IIgs plays tone voices 0-2 on SFX slots 0-2
// and the noise voice on slot 3 (a voice and an effect on one slot cut
// each other off, and a held voice takes its slot back at the next
// jlAudioFrameTick), so applications that also play digital SFX while
// music runs should prefer slot 4. On the Amiga effects, the module, the
// voices, and the noise share Paula's four channels. On the other ports
// the slots are independent of the voice layer.
#define JL_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 < JL_AUDIO_VOICES.
#define JL_AUDIO_VOICES 3
// Begin module playback. data points at the platform-native module
// blob (.MOD on every platform but the IIgs, .NTP there); 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 on every port.
// PTPlayer (Amiga) has no native song-end signal, so the Amiga HAL adds an
// E8F stop marker to the module as it copies it to chip RAM; a module
// with no room for one (all 128 song positions or patterns in use) loops
// there anyway. The `joeymod` tool's .amod output adds the same marker
// ahead of time.
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
// Stop the current module (if any), silent at once on every port (DOS and
// the X68000 take it back out of what they have mixed ahead of the chip).
// 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, once a loop=false song has ended, 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..JL_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, any rate at its pitch (the IIgs above 26 kHz and the
// Amiga above 28.6 kHz skip samples, so a fast one loses its top there). If
// the slot is currently playing, the new sample
// replaces it. Most ports read the sample in place while it plays, so it
// must stay valid and unchanged until it has finished or the slot is
// stopped; the IIgs keeps a copy per slot keyed by address, length, and
// rate, so changed bytes replayed with all three the same are not heard.
//
// 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 in
// program time on every port, from the call that starts the stream and
// then from jlAudioFrameTick (on DVX also while the desktop runs); must
// not block, and should be quick.
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/hosted pull through the
// shared SFX overlay mixer as jlAudioFrameTick refills their sound
// buffers, the IIgs (NTPstreamsound, 7 KB chunks) plays one chunk at a
// time and fires the next from jlAudioFrameTick as the current one runs
// down, and the Amiga (PTPlayer mt_loopfx) loops an 8 KB ring that
// jlAudioFrameTick tops up as Paula plays it -- so the host must be calling
// jlAudioFrameTick for a stream to continue. On the IIgs a seam can carry
// up to one frame of jitter; the Amiga's ring has none, and plays its old
// contents again if the calls stop for longer than it lasts (a third of a
// second at 22 kHz).
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 costs ~7 us a byte, ~50 ms per 7 KB chunk. DVX takes the
// IIgs's format too (its sound library's 8-bit PCM is unsigned) and skips
// its flip. Every other port's raw format is signed PCM, 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 {
JL_AUDIO_PAN_LEFT = 0,
JL_AUDIO_PAN_CENTER,
JL_AUDIO_PAN_RIGHT,
JL_AUDIO_PAN_COUNT
} jlAudioPanE;
// Place a SFX slot in the stereo field. Slots start at JL_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 centered 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). A channel plays on one side only, so a
// centered sound plays on a free channel each side when both sides
// have one, and otherwise on the one channel PTPlayer picks.
// X68000 ignored. Everything is mixed into ONE MSM6258 ADPCM stream, so
// its pan bits would move all audio at once, not one slot.
// DOS all three on an SB16, the module's channels on Paula's sides;
// ignored on the Sound Blasters before it, which play mono.
// ST ignored -- its output is mono. The STe build plays all three
// (its DMA chip plays stereo), the module's as on DOS.
// DVX all three, through DVX's stereo mixer.
// hosted all three (the mix is stereo), except Win16, which 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);
// A single tone at `freqHz` until the next call: jlAudioVoice(0, freqHz,
// 0), tone voice 0 at full volume, on every port. freqHz == 0 silences
// it. 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. It shares voice 0 with jlAudioVoice and jlMusicPlay. On DOS
// without an OPL2, voice 0 (and so this tone) is the PC speaker.
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 `hz` times per second on average, on every port, and may
// fire from an interrupt (DOS, Amiga, ST, X68000): keep fn short, only
// memory reads and direct hardware-register writes, no malloc / printf /
// blocking, and share data with it through jlAudioCritical*. Pass
// fn = NULL to uninstall (always returns true). Returns false when hz is
// 0, audio is off or did not start, or the port cannot take the rate.
//
// 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). The Amiga uses a timer.device E-clock request and a
// software interrupt; the ST and X68000 ride the system's Timer C
// interrupt (200 and 100 Hz), so calls due between its ticks come
// together. The IIgs calls fn in program time from jlAudioFrameTick,
// and DVX and the hosted ports from the mixer, between the samples where
// each tick falls.
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. Sections nest: the outermost pair holds the interrupt off
// (on DOS, the Amiga, the ST, and the X68000, every interrupt, so keep
// it short), and its Exit puts back the interrupt state its Enter found.
// No-ops where the callback runs in program time (IIgs, DVX, hosted).
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, as near as the chip comes: the ST's YM2149 levels are
// ~3.5 dB apart, so within 1.6 dB there). The mapping to underlying
// hardware:
//
// DOS -- AdLib OPL2 melodic channels 0/1/2 (ports 0x388/0x389);
// with no OPL2, voice 0 is the PC speaker, 1 and 2 silent.
// Atari ST -- YM2149 PSG channels A/B/C (plain ST: voice 0 silent while
// digital sound plays, which drives channel A).
// Amiga -- Paula channels 0/1/2 looping a 2-byte square sample.
// IIgs -- Ensoniq DOC oscillators playing a resident square on SFX
// slots 0/1/2.
// X68000 -- YM2151 channels 0/1/2, one sine operator each.
// DVX -- square waves mixed in software into a stream of DVX's
// mixer (DVX's own square tones with only the PC speaker).
// hosted -- square waves mixed in software.
//
// A note sounds until the next call for that voice. The IIgs plays a
// note as a short sample and restarts held notes from jlAudioFrameTick,
// so call that every frame. On the Amiga the voices are the channels the
// MOD engine drives, so while a MOD is playing, voice writes fight the
// player's 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); the noise runs at
// 125000 / pitch Hz (pitch 0 as 1) as far as each chip goes (the Amiga
// tops out at 28.6 kHz and the IIgs at 26 kHz, so pitches 0-4 sound
// alike there). 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 -- the YM2149's 17-bit generator, mixed into the Sound
// Blaster's stream (~22 kHz, so pitches 0-5 sound
// alike). An OPL2 without a Sound Blaster plays its
// rhythm-mode snare drum instead, a tuned drum whose
// pitch spreads over the snare's range.
// 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.
// X68000 -- the YM2151's noise generator, at the rate exactly.
// DVX -- the YM2149's 17-bit generator, mixed in software (a
// noise tone of DVX's sound library with only the PC
// speaker).
// hosted -- the YM2149's 17-bit generator, mixed in software.
//
// The same contract as jlAudioVoice: it sounds until the next call.
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 every port at register-write cost on the
// retro machines' chips (DOS's noise and the DVX and hosted ports mix in
// software), 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.
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 afterward; 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; call it once per frame. Every
// port does sound work here: DOS, ST, X68000, DVX, and hosted mix or
// refill their sound buffers (modules and effects stop or repeat
// without it), the IIgs and Amiga feed streams and notice a module's
// end (and the IIgs holds notes), and every port advances jlMusicPlay.
void jlAudioFrameTick(void);
#endif