323 lines
17 KiB
C
323 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, while the IIgs (NTPstreamsound, 7 KB chunks) and Amiga
|
|
// (PTPlayer mt_playfx, 4 KB chunks) play one chunk at a time and fire the
|
|
// next from jlAudioFrameTick as the current one runs down -- so the host
|
|
// must be calling jlAudioFrameTick for a stream to continue, and on those
|
|
// two 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 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 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). A channel plays on one side only, so a
|
|
// centred 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 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; 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
|