joeylib2/src/atarist/audio.c
2026-10-13 14:01:46 -05:00

822 lines
29 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// Atari ST audio HAL: libxmp-lite Protracker decoder + 68k Timer A IRQ
// running PWM through YM2149 channel-A volume register.
//
// Pipeline:
// * jlpAudioInit creates an xmp_context. Timer A is NOT started here:
// the ISR costs ~half the CPU at 12.3 kHz, so the pump is demand-
// driven -- installed on the first PlayMod/PlaySfx (MFP prescaler
// 200, data 1) and uninstalled once the mix buffer has drained back
// to pure silence with nothing playing.
// * jlpAudioPlayMod loads a .MOD via xmp_load_module_from_memory and
// calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit
// output that the ISR consumes one byte at a time.
// * Each Timer A tick the ISR pops one byte off the buffer, writes
// its high 4 bits to YM register 8 (channel A volume), advances
// the play pointer, raises a refill flag at half/end. The refill
// itself happens in jlpAudioFrameTick on the main thread because
// libxmp-lite is not interrupt-safe.
//
// Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by
// modern standards but period-correct for stock 520ST/1040ST without
// the STe DMA chip.
//
// The STe build (make ste, JOEYLIB_ATARI_STE) plays the same mix buffer
// through the STe's 8-bit DMA sound chip instead, mono at 12517 Hz,
// looping over the whole buffer with no interrupt at all.
// jlpAudioFrameTick reads the chip's frame counter to see which half it
// has finished, converts each refilled half to the chip's signed
// samples, and channel A stays the PSG's: the chiptune keeps all three
// voices while digital sound plays, with no collision to arbitrate. The
// ST build never touches the DMA chip.
#include <string.h>
#include <mint/osbind.h>
#include <libxmp-lite/xmp.h>
#include "port.h"
#include "audioSfxMixInternal.h"
#include "joey/audio.h"
#ifdef JOEYLIB_ATARI_STE
#include "steMachine.h"
#endif
// ----- Constants -----
// YM2149 (sound chip) supervisor-only ports. Index reg 7 (mixer)
// controls per-channel tone + noise enables; reg 8/9/A are volumes
// for channels A/B/C; regs 0/1, 2/3, 4/5 are tone period for those
// channels; reg 6 is noise period.
#define ST_YM_SELECT ((volatile uint8_t *)0xFFFF8800L)
#define ST_YM_DATA ((volatile uint8_t *)0xFFFF8802L)
// YM mixer (reg 7) high bits 6+7 are the I/O port directions TOS
// leaves configured (port A output, port B = centronics printer
// output). Always preserved when we rewrite the mixer.
#define YM_MIXER_IO_DIR 0xC0u
#define YM_REG_MIXER 7u
// Noise generator: reg 6 is the 5-bit noise period (0 = brightest
// hiss .. 31 = lowest rumble -- the portable jlAudioNoise pitch maps
// to it as identity). Noise is mixed onto channel C, whose mixer
// gate is bit 5 (active low, like the tone gates in bits 0-2) and
// whose 4-bit volume reg 0x0A is SHARED with tone voice 2 -- see the
// guards in applyYmVoice/applyYmNoise.
#define YM_REG_NOISE_PERIOD 6u
#define YM_REG_VOL_A 8u
#define YM_REG_VOL_B 9u
#define YM_REG_VOL_C 10u
#define YM_MIX_TONE_A 0x01u
#define YM_MIX_TONE_C 0x04u
#define YM_MIX_NOISE_A 0x08u
#define YM_MIX_NOISE_C 0x20u
#define MIX_BUFFER 2048
#define MIX_HALF (MIX_BUFFER / 2)
// libxmp and the SFX overlay mix unsigned samples centred on $80.
#define MIX_SILENCE_UNSIGNED 0x80u
#ifdef JOEYLIB_ATARI_STE
// STe build: the output stage is the DMA chip, which plays signed
// samples, so the buffer's silence is 0 and a refilled half flips each
// sample's top bit (a long at a time) before the chip reaches it.
#define MIX_RATE 12517 // Hz, the DMA chip's 12.5 kHz rate
#define MIX_SILENCE 0x00u
#define MIX_SIGN_FLIP_LONG 0x80808080ul
// DMA sound (supervisor-only, all byte registers at odd addresses). The
// frame start and end addresses latch when a frame begins, so with
// repeat set the chip loops over [start, end) on its own; the counter
// reads back the address it is playing.
#define STE_DMA_CTRL ((volatile uint8_t *)0xFFFF8901L)
#define STE_DMA_START_HI ((volatile uint8_t *)0xFFFF8903L)
#define STE_DMA_START_MID ((volatile uint8_t *)0xFFFF8905L)
#define STE_DMA_START_LO ((volatile uint8_t *)0xFFFF8907L)
#define STE_DMA_COUNT_HI ((volatile uint8_t *)0xFFFF8909L)
#define STE_DMA_COUNT_MID ((volatile uint8_t *)0xFFFF890BL)
#define STE_DMA_COUNT_LO ((volatile uint8_t *)0xFFFF890DL)
#define STE_DMA_END_HI ((volatile uint8_t *)0xFFFF890FL)
#define STE_DMA_END_MID ((volatile uint8_t *)0xFFFF8911L)
#define STE_DMA_END_LO ((volatile uint8_t *)0xFFFF8913L)
#define STE_DMA_MODE ((volatile uint8_t *)0xFFFF8921L)
#define STE_DMA_STOP 0x00u
#define STE_DMA_PLAY_LOOP 0x03u // bit 0 play, bit 1 repeat
#define STE_DMA_MONO_12K 0x81u // bit 7 mono, rate 1 = 12517 Hz
#else
// ST build: the output stage is PWM through YM channel A's volume, one
// sample per MFP Timer A interrupt.
// MFP Timer A registers and the autovector slot for its IRQ (vector
// $134, Setexc(0x134/4, ...) = Setexc(13, ...)).
#define ST_MFP_TACR ((volatile uint8_t *)0xFFFFFA19L)
#define ST_MFP_TADR ((volatile uint8_t *)0xFFFFFA1FL)
#define ST_MFP_IERA ((volatile uint8_t *)0xFFFFFA07L)
#define ST_MFP_IMRA ((volatile uint8_t *)0xFFFFFA13L)
#define ST_MFP_ISRA ((volatile uint8_t *)0xFFFFFA0FL)
#define MFP_TA_BIT 0x20
#define MFP_TACR_STOP 0x00
#define MFP_TACR_DIV200 0x07
#define VEC_MFP_TA (0x134 / 4)
#define INT_TIMER_A 13 // Jenabint / Jdisint id for Timer A
#define MIX_RATE 12288 // Hz, matches MFP 2.4576 MHz / 200
#define MIX_SILENCE MIX_SILENCE_UNSIGNED
#endif
// ----- Module state -----
// The mix buffer and its refill flags, which the PWM ISR (audio_isr.s)
// raises in the ST build and the frame tick's read of the DMA chip in the
// STe build. Names must match the .extern declarations in audio_isr.s.
// Even-aligned: the DMA chip can only start a frame on an even address
// (2 is also mint a.out's alignment cap).
volatile uint8_t gMixBuf[MIX_BUFFER] __attribute__((aligned(2)));
volatile uint8_t gNeedRefill[2] = { 0, 0 };
#ifdef JOEYLIB_ATARI_STE
// The half the DMA chip was playing at the last frame tick.
static uint8_t gDmaHalf = 0u;
#else
// The PWM ISR's play pointer and its half and end marks (audio_isr.s).
volatile uint8_t *gMixPos = NULL;
volatile uint8_t *gMixMid = NULL;
volatile uint8_t *gMixEnd = NULL;
extern void mfpTimerAIsr(void);
static void (*gOldTimerAVec)(void) = NULL;
// Voice 0's last requested state, latched whether or not it could be applied.
// While the PWM pump owns channel A the voice cannot sound, and this is what
// gets replayed to bring it back when the pump stops. Starts silent.
static volatile uint16_t gVoiceAFreqHz;
static volatile uint8_t gVoiceAAtten = 15u;
#endif
static xmp_context gXmpCtx = NULL;
static bool gReady = false;
static bool gXmpStarted = false;
static bool gXmpLoaded = false;
static int gXmpLoopCount = 0;
// Demand-driven Timer A: the 12.3 kHz ISR eats ~half the CPU, so it
// only runs while there is (or may be) audible output. gHalfSilent[i]
// records that half i was last refilled with pure silence; once both
// halves are silent and nothing is playing, frameTick stops the pump.
static bool gPumpRunning = false;
static bool gHalfSilent[2] = { false, false };
// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
// Mixed in over libxmp's MOD output during jlpAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
// drive the mixer through this one shadow so they share one source of
// truth: 0x3F = all tones and noise off (PWM-DAC default). The YM
// can't be read back, so this shadow IS the authoritative state.
static volatile uint8_t gYmToneMask = 0x3Fu;
// Supexec'd workers take no parameters of their own, so each request is
// stashed here first. Declared up with the module state rather than beside
// applyYmVoice because uninstallTimerA replays a voice through them.
static volatile uint8_t gYmVoice;
static volatile uint16_t gYmFreqHz;
static volatile uint8_t gYmAtten;
static volatile uint8_t gYmNoisePitch;
static volatile uint8_t gYmNoiseAtten;
// ----- Internal helpers -----
// Supexec workers defined further down (with the PSG voice cluster)
// but needed by the early lifecycle functions.
static long ymPsgAllOff(void);
static long applyYmVoice(void);
static bool anySfxActive(void);
static void refillHalf(uint8_t half);
static void silenceMixBuffer(void);
#ifdef JOEYLIB_ATARI_STE
static uint16_t dmaPlayOffset(void);
static void dmaPollRefill(void);
static void dmaSetFrame(volatile uint8_t *hi, volatile uint8_t *mid, volatile uint8_t *lo, uint32_t addr);
static void dmaSignHalf(volatile uint8_t *half);
static long installDma(void);
static long uninstallDma(void);
#endif
// Writing a YM register is a SELECT store then a DATA store, and the PWM ISR
// writes register 8 on every one of its 12288 ticks a second. If it lands
// between the two, the foreground's data byte goes to register 8 -- a click in
// the digital output -- and the register it was aimed at never receives it,
// which is a wrong or missing note. Nothing arbitrated that, so digital plus
// ANY PSG activity corrupted both, on all three channels and not just the one
// they share.
//
// So a foreground write masks interrupts across the pair. It costs a few
// cycles on the handful of writes a frame rather than anything per sample,
// which matters because the ISR already eats about half the CPU. The ISR
// itself needs no such guard: it is the only other writer.
static void ymWrite(uint8_t reg, uint8_t val) {
uint16_t saved;
__asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr"
: "=d" (saved) : : "memory");
*ST_YM_SELECT = reg;
*ST_YM_DATA = val;
__asm__ volatile ("move.w %0,%%sr" : : "d" (saved) : "memory");
}
// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
// TOS I/O-port-direction high bits. Supervisor context required.
static void ymWriteMixer(void) {
ymWrite(YM_REG_MIXER, (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask));
}
#ifndef JOEYLIB_ATARI_STE
static long installTimerA(void) {
// Park the buffer in known-silent state (unsigned 8-bit middle
// = 128) before opening the IRQ gate.
silenceMixBuffer();
gMixPos = (volatile uint8_t *)gMixBuf;
gMixMid = (volatile uint8_t *)(gMixBuf + MIX_HALF);
gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER);
gNeedRefill[0] = 0;
gNeedRefill[1] = 0;
// YM2149 setup for PWM-via-volume on channel A:
// reg 7 (mixer): set bits 0 (tone A off) and 3 (noise A off);
// preserve bits 6+7 (I/O port directions, used
// by TOS for floppy / keyboard / printer).
// reg 8 (channel A volume): start at 0 to avoid a pop at start.
//
// Channel A ONLY. This used to gate every tone and noise off and zero all
// three volumes, so starting a digital sound silenced a playing chiptune
// outright -- including channels B and C, which the DAC never touches.
// Now B and C keep whatever the music left there and play straight
// through, and only voice 0 stands down for the duration.
//
// Without the mixer setup, whatever state TOS left noise A in
// gets gated by our 12 kHz volume writes -- if noise A was on,
// a constant volume = constant hiss. Standard PWM-DAC trick is
// to disable both tone and noise so the volume reg is a pure
// 4-bit amplitude DAC.
//
// We can't reliably read back YM regs on the ST (the data port
// returns last-write, not register contents), so we OR in the
// disable bits over an assumed-safe TOS-default mask. Bit 6 set
// (port A output) matches stock TOS; bit 7 set (port B output)
// matches the centronics-printer direction TOS configures.
gYmToneMask = (uint8_t)(gYmToneMask | YM_MIX_TONE_A | YM_MIX_NOISE_A);
ymWriteMixer(); // channel A gated off: its volume reg is now the DAC
ymWrite(YM_REG_VOL_A, 0); // channel A volume = 0 to avoid a pop at start
// MFP Timer A: stop, install our vector, set prescaler 200 + data
// 1 (= 2.4576 MHz / 200 = 12288 Hz), then start.
*ST_MFP_TACR = MFP_TACR_STOP;
gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)mfpTimerAIsr);
*ST_MFP_TADR = 1;
*ST_MFP_TACR = MFP_TACR_DIV200;
Jenabint(INT_TIMER_A);
return 0;
}
static long uninstallTimerA(void) {
Jdisint(INT_TIMER_A);
*ST_MFP_TACR = MFP_TACR_STOP;
if (gOldTimerAVec != NULL) {
(void)Setexc(VEC_MFP_TA, (long)gOldTimerAVec);
gOldTimerAVec = NULL;
}
// Channel A belongs to the PSG again. Replay voice 0's latched state,
// which both clears the DAC's residual level (so the handoff back to TOS
// leaves no DC on the speaker) and restores a note the music player set
// while the pump owned the channel. Without the replay voice 0 would stay
// dead until its next note EVENT, because src/core/music.c only reprograms
// a voice when one arrives -- a held note would never come back.
gYmVoice = 0u;
gYmFreqHz = gVoiceAFreqHz;
gYmAtten = gVoiceAAtten;
return applyYmVoice();
}
#endif
// Silence in the output stage's own sample format (MIX_SILENCE).
static void silenceMixBuffer(void) {
memset((void *)gMixBuf, MIX_SILENCE, MIX_BUFFER);
}
#ifdef JOEYLIB_ATARI_STE
// Which byte of the buffer the DMA chip is playing. The counter is three
// byte registers read one at a time, so a carry between the reads can
// tear the value: re-read when the high byte moved (a buffer straddling
// a 64 KB boundary would otherwise read 64 KB away). A torn low byte is
// at worst 255 bytes early, which only delays one refill by a tick.
static uint16_t dmaPlayOffset(void) {
uint8_t hi;
uint8_t mid;
uint8_t lo;
do {
hi = *STE_DMA_COUNT_HI;
mid = *STE_DMA_COUNT_MID;
lo = *STE_DMA_COUNT_LO;
} while (hi != *STE_DMA_COUNT_HI);
return (uint16_t)((((uint32_t)hi << 16) | ((uint32_t)mid << 8) | lo) - (uint32_t)gMixBuf);
}
// The DMA path's stand-in for the PWM ISR's refill flags: when the chip
// has moved on to the other half since the last tick, the half it left
// is free to refill. The chip needs no help to loop, so a tick that
// comes a whole buffer late replays a stale buffer once instead of
// tearing it.
static void dmaPollRefill(void) {
uint8_t half;
half = (uint8_t)(dmaPlayOffset() >= MIX_HALF);
if (half != gDmaHalf) {
gNeedRefill[gDmaHalf] = 1;
gDmaHalf = half;
}
}
// Write a frame address into one of the chip's three-byte registers.
static void dmaSetFrame(volatile uint8_t *hi, volatile uint8_t *mid, volatile uint8_t *lo, uint32_t addr) {
*hi = (uint8_t)(addr >> 16);
*mid = (uint8_t)(addr >> 8);
*lo = (uint8_t)addr;
}
// Flip a mixed half's unsigned samples to the chip's signed ones.
static void dmaSignHalf(volatile uint8_t *half) {
uint32_t *flip;
uint16_t i;
flip = (uint32_t *)half;
for (i = 0; i < MIX_HALF / sizeof(uint32_t); i++) {
flip[i] ^= MIX_SIGN_FLIP_LONG;
}
}
// Supexec worker: start the DMA chip looping over the (silent) buffer.
static long installDma(void) {
silenceMixBuffer();
gNeedRefill[0] = 0;
gNeedRefill[1] = 0;
gDmaHalf = 0u;
dmaSetFrame(STE_DMA_START_HI, STE_DMA_START_MID, STE_DMA_START_LO, (uint32_t)gMixBuf);
dmaSetFrame(STE_DMA_END_HI, STE_DMA_END_MID, STE_DMA_END_LO, (uint32_t)(gMixBuf + MIX_BUFFER));
*STE_DMA_MODE = STE_DMA_MONO_12K;
*STE_DMA_CTRL = STE_DMA_PLAY_LOOP;
return 0;
}
// Supexec worker: stop the DMA chip. Channel A was never the DAC here,
// so there is no voice to hand back.
static long uninstallDma(void) {
*STE_DMA_CTRL = STE_DMA_STOP;
return 0;
}
#endif
// Mix one half of the buffer: the MOD (or silence), then the SFX over
// it, then -- for the STe's DMA chip -- into signed samples. silentFill is
// sampled BEFORE the overlay: a slot that exhausts during this overlay
// still wrote its tail into the half, so the half only counts as silent
// when nothing was live going in.
static void refillHalf(uint8_t half) {
volatile uint8_t *dst;
bool silentFill;
dst = gMixBuf + (uint16_t)half * MIX_HALF;
silentFill = !gXmpStarted && !anySfxActive();
if (gXmpStarted) {
xmp_play_buffer(gXmpCtx, (void *)dst, MIX_HALF, gXmpLoopCount);
} else {
memset((void *)dst, MIX_SILENCE_UNSIGNED, MIX_HALF);
}
audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
#ifdef JOEYLIB_ATARI_STE
dmaSignHalf(dst);
#endif
gHalfSilent[half] = silentFill;
gNeedRefill[half] = 0;
}
static bool anySfxActive(void) {
uint8_t i;
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
if (gSfxSlots[i].active) {
return true;
}
}
return false;
}
static void pumpStart(void) {
if (gPumpRunning) {
return;
}
#ifdef JOEYLIB_ATARI_STE
Supexec(installDma);
#else
Supexec(installTimerA);
#endif
gPumpRunning = true;
gHalfSilent[0] = false;
gHalfSilent[1] = false;
}
static void pumpStop(void) {
if (!gPumpRunning) {
return;
}
// Clear the flag BEFORE the Supexec, not after: uninstallTimerA replays
// voice 0 through applyYmVoice, and applyYmVoice stands that voice down
// for as long as the flag says the DAC owns channel A. Stopping the timer
// is the first thing uninstallTimerA does, so the ISR cannot fire in
// between and the early clear costs nothing.
gPumpRunning = false;
#ifdef JOEYLIB_ATARI_STE
Supexec(uninstallDma);
#else
Supexec(uninstallTimerA);
#endif
}
// ----- HAL API (alphabetical) -----
bool jlpAudioInit(void) {
if (gReady) {
return true;
}
#ifdef JOEYLIB_ATARI_STE
// The STe build's output stage is the DMA chip: no chip, no audio.
if (!steMachineCheck()) {
return false;
}
#endif
gXmpCtx = xmp_create_context();
if (gXmpCtx == NULL) {
return false;
}
gReady = true;
return true;
}
void jlpAudioShutdown(void) {
// Pump first, then the PSG: stopping the pump hands channel A back and
// replays voice 0, so the all-off has to come after it or a replayed note
// would outlive the program. (The all-off still runs independent of
// gReady: the tone/noise voice layer is usable without jlpAudioInit, and
// the YM latches -- a live voice keeps sounding after exit.)
if (gReady) {
pumpStop();
}
Supexec(ymPsgAllOff);
if (!gReady) {
return;
}
silenceMixBuffer();
if (gXmpCtx != NULL) {
if (gXmpStarted) {
xmp_end_player(gXmpCtx);
gXmpStarted = false;
}
if (gXmpLoaded) {
xmp_release_module(gXmpCtx);
gXmpLoaded = false;
}
xmp_free_context(gXmpCtx);
gXmpCtx = NULL;
}
gReady = false;
}
bool jlpAudioIsPlayingMod(void) {
return gReady && gXmpStarted;
}
void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gReady || gXmpCtx == NULL) {
return;
}
if (gXmpStarted) {
xmp_end_player(gXmpCtx);
gXmpStarted = false;
}
if (gXmpLoaded) {
xmp_release_module(gXmpCtx);
gXmpLoaded = false;
}
if (xmp_load_module_from_memory(gXmpCtx, (void *)data, (long)length) != 0) {
return;
}
gXmpLoaded = true;
if (xmp_start_player(gXmpCtx, MIX_RATE,
XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
xmp_release_module(gXmpCtx);
gXmpLoaded = false;
return;
}
gXmpLoopCount = loop ? -1 : 0;
gXmpStarted = true;
pumpStart();
}
void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
pumpStart();
}
void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
pumpStart();
}
// YM2149 has three PSG voices; a proper tone-mode driver would
// program voice A here. Not wired yet -- AGI-on-ST will be silent
// until this is filled in.
void jlpAudioTone(uint16_t freqHz) {
(void)freqHz;
}
// YM2149 PSG: three tone channels (A/B/C) addressed via reg 0..5
// (12-bit tone period each) and reg 8/9/A (4-bit volume each).
// Reg 7 (mixer) gates tone + noise per channel; bits 0..2 = tone
// disable A/B/C (0 = enabled), 3..5 = noise disable, 6..7 = I/O
// port direction (preserve TOS defaults).
//
// Tone period units: 2 MHz / 16 / period = output Hz. So period =
// 125000 / freqHz. 12-bit range covers 30 Hz - 62 kHz.
//
// Volume mapping: AGI atten 0..15 (0=loud) -> YM vol 15..0 (15=loud).
// YM also accepts bit 4 = envelope-controlled volume; we never set
// it (no envelope on AGI tones).
//
// YM regs are supervisor-only on the ST, so this is a Supexec block.
// We stash the request in static globals (Supexec'd functions have
// no parameters of their own).
static long applyYmVoice(void) {
uint16_t period;
uint8_t vol;
uint8_t toneReg;
uint8_t volReg;
toneReg = (uint8_t)(gYmVoice * 2u); // reg 0/2/4
volReg = (uint8_t)(8u + gYmVoice); // reg 8/9/A
// Voice 0 and the PWM DAC are the same register (reg 8) and the same mixer
// gate (bit 0), and they cannot be shared: enabling tone A chops the DAC's
// output with a square wave, and the DAC's 12 kHz writes trample the
// voice's level. While the pump runs the DAC wins and the voice waits --
// jlpAudioVoice has latched its state, and uninstallTimerA replays it.
// (The STe build's DMA chip is not the PSG at all, so there it never
// waits.)
#ifndef JOEYLIB_ATARI_STE
if (gPumpRunning && gYmVoice == 0u) {
return 0;
}
#endif
if (gYmFreqHz == 0u || gYmAtten >= 15u) {
// Silence: disable tone on this channel (mixer bit 0/1/2 set),
// zero volume. Don't touch other channels' bits. Channel C's
// volume reg is shared with the noise generator -- leave the
// level alone while noise is live or a voice-2 note-off would
// kill active percussion.
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
ymWriteMixer();
if (gYmVoice != 2u || (gYmToneMask & YM_MIX_NOISE_C) != 0u) {
ymWrite(volReg, 0);
}
return 0;
}
period = (uint16_t)(125000u / (uint32_t)gYmFreqHz);
if (period == 0u) {
period = 1u;
}
if (period > 0x0FFFu) {
period = 0x0FFFu;
}
ymWrite(toneReg, (uint8_t)(period & 0xFFu));
ymWrite((uint8_t)(toneReg + 1u), (uint8_t)((period >> 8) & 0x0Fu));
vol = (uint8_t)(15u - gYmAtten);
ymWrite(volReg, (uint8_t)(vol & 0x0Fu));
// Enable tone on this channel (clear its bit in mixer).
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
ymWriteMixer();
return 0;
}
// Supexec worker for jlpAudioNoise; request stashed in gYmNoisePitch/
// gYmNoiseAtten like the applyYmVoice cluster. Channel C's volume reg
// is one 4-bit level shared by tone voice 2 and noise: the last
// on-call wins the level, off-calls preserve it while the other
// consumer is live and zero it only when both gates are closed
// (a nonzero level with both gates closed is a DC offset).
static long applyYmNoise(void) {
if (gYmNoiseAtten >= 15u) {
gYmToneMask = (uint8_t)(gYmToneMask | YM_MIX_NOISE_C);
ymWriteMixer();
if ((gYmToneMask & YM_MIX_TONE_C) != 0u) {
ymWrite(YM_REG_VOL_C, 0);
}
return 0;
}
// Program period + volume before opening the mixer gate so the
// gate never opens onto stale settings.
ymWrite(YM_REG_NOISE_PERIOD, (uint8_t)(gYmNoisePitch & 0x1Fu));
ymWrite(YM_REG_VOL_C, (uint8_t)((uint8_t)(15u - gYmNoiseAtten) & 0x0Fu));
gYmToneMask = (uint8_t)(gYmToneMask & (uint8_t)~YM_MIX_NOISE_C);
ymWriteMixer();
return 0;
}
// Supexec worker: gate every tone and the noise off and zero all
// three volumes. Used by jlpAudioShutdown so a latched PSG voice
// (the chip free-runs with zero CPU) cannot keep sounding after the
// program exits.
static long ymPsgAllOff(void) {
gYmToneMask = 0x3Fu;
ymWriteMixer();
ymWrite(YM_REG_VOL_A, 0);
ymWrite(YM_REG_VOL_B, 0);
ymWrite(YM_REG_VOL_C, 0);
#ifndef JOEYLIB_ATARI_STE
// Drop the latched voice-0 note too, or a later pump stop would replay it.
gVoiceAAtten = 15u;
#endif
return 0;
}
// PIT-style tick ISR not wired on ST yet: Timer A is already busy
// driving 12 kHz PWM for MOD output (see installTimerA). A future
// AGI-on-ST sound path could lift the YM2149 tone driver onto
// Timer B or C and register it here.
bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
(void)fn;
(void)hz;
return false;
}
void jlpAudioCriticalEnter(void) {
// No tick-callback ISR is registered (jlpAudioTickRegister returns
// false). The Timer A MOD ISR synchronizes with the producer via
// the gNeedRefill flags, not this critical section, so masking
// here would only starve that ~12 kHz audio ISR for no benefit.
}
void jlpAudioCriticalExit(void) {
}
void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
// Same Supexec-with-stashed-args shape as jlpAudioVoice. Noise plays
// through channel C, so it keeps playing while the PWM pump owns
// channel A (installTimerA takes channel A only).
gYmNoisePitch = pitch;
gYmNoiseAtten = atten;
Supexec(applyYmNoise);
}
void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
if (voice >= JOEY_AUDIO_VOICES) {
return;
}
// Digital output and chip music now coexist. The PWM engine holds channel
// A (reg 8) as its DAC for as long as it runs, so voice 0 is latched here
// and applied when the pump stops; voices 1 and 2 play throughout. Writes
// are atomic against the ISR (see ymWrite), which is what makes the two
// safe to run together at all. (The STe build's DMA chip leaves channel
// A alone, so there is nothing to latch.)
#ifndef JOEYLIB_ATARI_STE
if (voice == 0u) {
gVoiceAFreqHz = freqHz;
gVoiceAAtten = atten;
}
#endif
gYmVoice = voice;
gYmFreqHz = freqHz;
gYmAtten = atten;
Supexec(applyYmVoice);
}
void jlpAudioStopMod(void) {
if (!gReady || gXmpCtx == NULL) {
return;
}
if (gXmpStarted) {
xmp_end_player(gXmpCtx);
gXmpStarted = false;
}
if (gXmpLoaded) {
xmp_release_module(gXmpCtx);
gXmpLoaded = false;
}
silenceMixBuffer();
// StopMod discards the mixed tail by design (buffer just went
// silent), so the pump can stop immediately -- unless an SFX is
// still live, in which case frameTick's drain path owns the stop.
if (!anySfxActive()) {
pumpStop();
}
}
void jlpAudioStopSfx(uint8_t slot) {
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
gSfxSlots[slot].active = false;
}
// Drains ISR-raised refill flags by re-running libxmp's mixer into
// the consumed half then overlaying any active SFX. Called from the
// game loop, never from IRQ. The STe build has no ISR: the flags come
// from reading the DMA chip's play position here.
//
// Soft-real-time constraint: the producer (this function) must refill
// a consumed half within MIX_HALF / MIX_RATE = 1024 / 12288 ~= 83 ms
// of the ISR raising its flag, or the ISR drains into the half still
// being rewritten (audible tearing). A full MOD-frame mix plus
// audioSfxOverlayMix can approach that budget under load, so callers
// should run the game loop comfortably faster than ~12 Hz.
void jlpAudioFrameTick(void) {
if (!gReady || !gPumpRunning) {
return;
}
#ifdef JOEYLIB_ATARI_STE
dmaPollRefill();
#endif
// Underrun guard: both flags set means the producer fell a full
// buffer behind, so the ISR is now draining a half this tick is
// about to rewrite. Silence first so the in-flight torn read plays
// as silence rather than garbage, then refill both halves below.
if (gNeedRefill[0] && gNeedRefill[1]) {
silenceMixBuffer();
}
if (gNeedRefill[0]) {
refillHalf(0u);
}
if (gNeedRefill[1]) {
refillHalf(1u);
}
// Drain-to-silence stop: both halves hold pure silence and nothing
// is playing, so the ISR would stream silence forever -- park it.
if (!gXmpStarted && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) {
pumpStop();
}
}