// 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: the shared module player (src/core/modPlayer.c) + 68k // Timer A IRQ running PWM through YM2149 channel-A volume register. // // Pipeline: // * Timer A is NOT started at init: the ISR costs ~40% of 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 held pure silence, with nothing playing, for // PWM_IDLE_MS, after a ramp down to level 0 (and it starts with one // up to silence), so neither end clicks. // * jlpAudioPlayMod starts the module 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 // YM level (gYmLevel: the YM's sixteen levels are logarithmic, so each // sample plays as the level nearest it on a straight line) to YM // register 8 (channel A volume), and advances the play pointer; the // frame tick sees which half it has left. The refill itself happens in // jlpAudioFrameTick on the main thread: the mix is too long for an // interrupt. // // Quality budget: sixteen logarithmic levels 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 mix buffer, in // left and right byte pairs, through the STe's 8-bit DMA sound chip // instead, stereo 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 #include #include "m68kIrq.h" #include "port.h" #include "audioSfxMixInternal.h" #include "modPlayer.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 // The buffer is two halves of MIX_HALF_FRAMES frames: bytes on the plain // ST, left and right byte pairs on the STe, whose DMA chip plays stereo. #ifdef JOEYLIB_ATARI_STE #define MIX_CHANNELS 2u #else #define MIX_CHANNELS 1u #endif #define MIX_HALF_FRAMES 1024u #define MIX_HALF (MIX_HALF_FRAMES * MIX_CHANNELS) #define MIX_BUFFER (MIX_HALF * 2u) // Frames ahead of the play position where a new sound's start-now overlay // begins (sfxStartNow), so the write always lands before the read: ~5 ms. // The overlay's mix (src/m68k/sfxMix.s) outruns the play position once // started, at about twice the rate the plain ST's PWM interrupt plays even // with the interrupt's share of the CPU gone, so the margin only covers the // start. #define SFX_START_MARGIN 64u // The module player and the SFX overlay mix unsigned samples centred on $80. #define MIX_SILENCE_UNSIGNED MOD_PLAYER_SILENCE_U8 // The 8-bit mix's unit (joey/audio.h's mix): a module channel at full volume // and a full-scale sound effect each swing half a byte (modPlayerSetU8Shift, // audioSfxSetShift). On the STe a side's two channels fill it; the plain // ST's mono sums four, which clip when three or four peak together. The // voices come down to the same level (kYmVolume). #define MIX_UNIT_SHIFT 1u #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 #define MIX_SIGN_FLIP_BYTE 0x80u // 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_STEREO_12K 0x01u // bit 7 clear: stereo; 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 // The YM's channel-A volume is the DAC, and its sixteen levels are // logarithmic, about 3 dB apart: each sample is played as the level nearest // its value on a straight line from level 0 to level 15 (gYmLevel), so the // waveform keeps its shape and silence sits near the middle, level 13. The // levels' amplitudes, as the YM2149 puts them out (ST-Sound's measured // table). #define YM_LEVELS 16u #define SAMPLE_VALUES 256u #define YM_SAMPLE_MAX 255u static const uint16_t kYmAmplitude[YM_LEVELS] = { 62u, 161u, 265u, 377u, 580u, 774u, 1155u, 1575u, 2260u, 3088u, 4570u, 6233u, 9330u, 12481u, 18663u, 25000u }; // So the step from channel A's level 0 to silence's level is no click, the // pump starts with a ramp up to silence and, once everything has played, // ends with one down to 0 before it stops (the drain): PWM_RAMP_SAMPLES, // ~10 ms, from PWM_DRAIN_MARGIN samples ahead of the ISR. #define PWM_RAMP_SAMPLES 128u #define PWM_DRAIN_MARGIN 64u // The drain waits for this long of silence, so sounds close together keep // the level at silence between them rather than ramp down and up again. #define PWM_IDLE_MS 200u #endif // jlAudioTickRegister rides TOS's 200 Hz Timer C (both builds: the plain // ST's Timer A is the PWM pump's, B the per-line palettes', D the serial // port's), chained in front of TOS's own handler by stTickIsr // (audio_isr.s). A carry paces the program's rate against the 200 ticks a // second, so the average is exact at any rate; at most 200 a second come // one to an interrupt, faster rates several to one. The callback runs in // the interrupt, as the contract allows (joey/audio.h). #define VEC_MFP_TC (0x114 / 4) #define ST_TIMER_C_HZ 200u #define SETEXC_READ (-1L) // Setexc's vector argument that only reads // ----- Module state ----- // The mix buffer, the ST's PWM ISR's (audio_isr.s, whose .extern names // these must match) or the STe's DMA chip'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))); // The halves to refill: a frame tick that finds the play position has left // a half since the last one raises its flag (pollRefill), and refills it. static uint8_t gNeedRefill[2] = { 0, 0 }; // The half the play position was in at the last frame tick. static uint8_t gPlayHalf = 0u; #ifndef JOEYLIB_ATARI_STE // The PWM ISR's play pointer and the buffer's end (audio_isr.s). volatile uint8_t *gMixPos = NULL; volatile uint8_t *gMixEnd = NULL; // Each sample's YM level (kYmAmplitude), built at jlpAudioInit: the ISR // plays gYmLevel[sample], from beside its own code (audio_isr.s). extern uint8_t gYmLevel[SAMPLE_VALUES]; // The drain (PWM_RAMP_SAMPLES): the ramp down is written, from // PWM_DRAIN_MARGIN past gDrainFrom, where the ISR was; and the silence // before it (PWM_IDLE_MS), since gIdleSinceMs. static bool gDraining = false; static uint16_t gDrainFrom = 0u; static bool gIdle = false; static uint32_t gIdleSinceMs = 0u; 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 // Read by stTickIsr (audio_isr.s), so not static. void (*gStTickOldVec)(void) = NULL; static volatile jlAudioTickFnT gStTickFn = NULL; static volatile uint32_t gStTickHz = 0u; static volatile uint32_t gStTickCarry = 0u; static bool gStTickHooked = false; // The status register at the outermost jlAudioCriticalEnter. static uint16_t gCriticalSr = 0u; extern void stTickIsr(void); void stTickRun(void); static bool gReady = false; // 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 }; // A song that has ended still sounds from the half its end is in, until // that half has played: jlAudioIsPlayingMod says true until then. static bool gModTail = false; static uint8_t gModTailHalf = 0; // SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c). // Mixed in over the module's output during jlpAudioFrameTick. static AudioSfxSlotT gSfxSlots[JL_AUDIO_SFX_SLOTS]; #ifdef JOEYLIB_ATARI_STE static uint8_t gSfxPan[JL_AUDIO_SFX_SLOTS]; // jlAudioSetPan's #endif // 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 uint16_t playOffset(void); static void pollRefill(void); static bool pumpStart(void); static void pumpStop(void); static void refillHalf(uint8_t half); static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot); static void sfxStartNow(uint8_t slot, bool fresh); static void silenceMixBuffer(void); static void tickHookUpdate(void); #ifndef JOEYLIB_ATARI_STE static void drainCancel(void); static bool drainDone(void); static void drainStart(void); static void rampWrite(uint16_t at, uint8_t from, uint8_t to); static void ymLevelsBuild(void); #endif #ifdef JOEYLIB_ATARI_STE static uint16_t dmaPlayOffset(void); static void dmaSetFrame(volatile uint8_t *hi, volatile uint8_t *mid, volatile uint8_t *lo, uint32_t addr); static void dmaSignBytes(volatile uint8_t *bytes, uint16_t len); 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; saved = m68kInterruptsOff(); *ST_YM_SELECT = reg; *ST_YM_DATA = val; m68kInterruptsRestore(saved); } // 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(); rampWrite(0u, 0u, MIX_SILENCE); gMixPos = (volatile uint8_t *)gMixBuf; gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER); gNeedRefill[0] = 0; gNeedRefill[1] = 0; gPlayHalf = 0u; gDraining = false; gIdle = false; // 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); } // 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 mixed samples between unsigned (the mixer's) and the chip's signed // ones, a long at a time from the first even byte (the 68000 reads a long // from any even address). static void dmaSignBytes(volatile uint8_t *bytes, uint16_t len) { uint32_t *flip; uint16_t i; if (((uintptr_t)bytes & 1u) != 0u && len != 0u) { *bytes++ ^= MIX_SIGN_FLIP_BYTE; len--; } flip = (uint32_t *)bytes; for (i = 0; i < len / sizeof(uint32_t); i++) { flip[i] ^= MIX_SIGN_FLIP_LONG; } for (i = (uint16_t)(len & ~(sizeof(uint32_t) - 1u)); i < len; i++) { bytes[i] ^= MIX_SIGN_FLIP_BYTE; } } // Supexec worker: start the DMA chip looping over the (silent) buffer. static long installDma(void) { silenceMixBuffer(); gNeedRefill[0] = 0; gNeedRefill[1] = 0; gPlayHalf = 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_STEREO_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 // Which byte of the buffer is playing: the PWM ISR's pointer, or the DMA // chip's counter. A long read, so the ISR cannot tear it. static uint16_t playOffset(void) { #ifdef JOEYLIB_ATARI_STE return dmaPlayOffset(); #else return (uint16_t)(gMixPos - gMixBuf); #endif } // The refill flags: when the play position has moved on to the other half // since the last tick, the half it left is free to refill. The buffer loops // by itself, so a tick that comes a whole buffer late replays a stale half // once instead of tearing it. static void pollRefill(void) { uint8_t half; half = (uint8_t)(playOffset() >= MIX_HALF); if (half != gPlayHalf) { gNeedRefill[gPlayHalf] = 1u; gPlayHalf = half; } } // 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 = !modPlayerActive() && !anySfxActive(); if (modPlayerActive()) { if (!modPlayerRenderU8((uint8_t *)dst, MIX_HALF_FRAMES, MIX_CHANNELS)) { gModTail = true; gModTailHalf = half; } } else { memset((void *)dst, MIX_SILENCE_UNSIGNED, MIX_HALF); } #ifdef JOEYLIB_ATARI_STE audioSfxOverlayMixStereo(dst, MIX_HALF_FRAMES, gSfxSlots, JL_AUDIO_SFX_SLOTS, gSfxPan, AUDIO_SFX_DST_UNSIGNED); #else audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JL_AUDIO_SFX_SLOTS); #endif #ifdef JOEYLIB_ATARI_STE dmaSignBytes(dst, MIX_HALF); #endif gHalfSilent[half] = silentFill; gNeedRefill[half] = 0; } // Mix a slot over len bytes of the buffer from at that are mixed already, // in the output's own format: the STe's are signed stereo pairs, the slot // on its pan's side. The chip may reach them while this runs, so they are // mixed as they stand, in order, never flipped to unsigned and back. static void sfxOverlayAt(uint16_t at, uint16_t len, uint8_t slot) { #ifdef JOEYLIB_ATARI_STE audioSfxOverlayMixStereo(gMixBuf + at, (uint16_t)(len / MIX_CHANNELS), &gSfxSlots[slot], 1u, &gSfxPan[slot], AUDIO_SFX_DST_SIGNED); #else audioSfxOverlayMix(gMixBuf + at, len, &gSfxSlots[slot], 1u); #endif } // Start a just-armed slot now. A refill mixes it, but the halves ahead of // the play position are mixed already, so on its own a sound waited for // the next refill: up to two halves, ~80-170 ms. So its first samples go on // top of what is mixed ahead -- the rest of the half playing and, if it has // been refilled already, the half after it -- from SFX_START_MARGIN past the // play position, which is in the next half when the play position is that // near its half's end; and the refills carry on from where that leaves the // slot. static void sfxStartNow(uint8_t slot, bool fresh) { uint16_t pos; uint16_t start; uint16_t halfEnd; uint16_t aheadEnd; uint8_t half; // A pump just started plays from the buffer's start; the STe's frame // counter may still say where the chip stopped last time until its new // frame begins. pos = fresh ? 0u : playOffset(); if (pos >= MIX_BUFFER) { return; } half = (uint8_t)(pos >= MIX_HALF); // The play position may have moved on since the frame tick last looked, // leaving a half behind that waits for its refill: that refill places // the sound there. Offsets here run on past the buffer's end into the // other half, unwrapped. halfEnd = (uint16_t)((half + 1u) * MIX_HALF); aheadEnd = (uint16_t)(halfEnd + (half == gPlayHalf ? MIX_HALF : 0u)); // A whole frame, so the sides keep their places. start = (uint16_t)((pos / MIX_CHANNELS + SFX_START_MARGIN) * MIX_CHANNELS); if (start >= aheadEnd) { return; } if (start < halfEnd) { sfxOverlayAt(start, (uint16_t)(halfEnd - start), slot); gHalfSilent[half] = false; start = halfEnd; } if (start < aheadEnd) { sfxOverlayAt((uint16_t)((half ^ 1u) * MIX_HALF + (start - halfEnd)), (uint16_t)(aheadEnd - start), slot); gHalfSilent[half ^ 1u] = false; } } static bool anySfxActive(void) { uint8_t i; for (i = 0; i < JL_AUDIO_SFX_SLOTS; i++) { if (gSfxSlots[i].active) { return true; } } return false; } #ifndef JOEYLIB_ATARI_STE // A sound starts while the drain is under way: the ramp down and the zeros // after it, ahead of the ISR, become a ramp back up to silence and silence, // the baseline the start-now overlay mixes onto. static void drainCancel(void) { uint16_t at; uint16_t i; if (!gDraining) { return; } gDraining = false; at = (uint16_t)((playOffset() + PWM_DRAIN_MARGIN) % MIX_BUFFER); for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) { gMixBuf[(at + i) % MIX_BUFFER] = MIX_SILENCE; } rampWrite(at, 0u, MIX_SILENCE); gHalfSilent[0] = false; gHalfSilent[1] = false; } // The ISR has played the ramp down and is into the zeros after it: as far // from where it was as the ramp's end, and not back round to it. static bool drainDone(void) { return (uint16_t)((playOffset() + MIX_BUFFER - gDrainFrom) % MIX_BUFFER) >= PWM_DRAIN_MARGIN + PWM_RAMP_SAMPLES; } // Everything has played: from just ahead of the ISR, a ramp down to level // 0, then zeros round the buffer to just behind it; the pump stops once // the ISR is past the ramp (drainDone). static void drainStart(void) { uint16_t at; uint16_t i; gDrainFrom = playOffset(); at = (uint16_t)((gDrainFrom + PWM_DRAIN_MARGIN) % MIX_BUFFER); for (i = 0u; i < MIX_BUFFER - 2u * PWM_DRAIN_MARGIN; i++) { gMixBuf[(at + i) % MIX_BUFFER] = 0u; } rampWrite(at, MIX_SILENCE, 0u); gDraining = true; } // PWM_RAMP_SAMPLES of a straight line from sample value from to to, into // the buffer from at on (round its end). static void rampWrite(uint16_t at, uint8_t from, uint8_t to) { uint16_t i; for (i = 0u; i < PWM_RAMP_SAMPLES; i++) { gMixBuf[(at + i) % MIX_BUFFER] = (uint8_t)((int16_t)from + ((int16_t)to - (int16_t)from) * (int16_t)i / (int16_t)PWM_RAMP_SAMPLES); } } // gYmLevel: for each sample value, the level whose amplitude is nearest // the value's place on the line from level 0's amplitude to level 15's. static void ymLevelsBuild(void) { uint32_t target; uint32_t gap; uint32_t best; uint16_t s; uint8_t v; for (s = 0u; s < SAMPLE_VALUES; s++) { target = kYmAmplitude[0] + ((uint32_t)(kYmAmplitude[YM_LEVELS - 1u] - kYmAmplitude[0]) * s) / YM_SAMPLE_MAX; best = UINT32_MAX; gYmLevel[s] = 0u; for (v = 0u; v < YM_LEVELS; v++) { gap = kYmAmplitude[v] > target ? kYmAmplitude[v] - target : target - kYmAmplitude[v]; if (gap < best) { best = gap; gYmLevel[s] = v; } } } } #endif // True when the pump was idle: it now plays from the buffer's start. static bool pumpStart(void) { if (gPumpRunning) { #ifndef JOEYLIB_ATARI_STE drainCancel(); #endif return false; } #ifdef JOEYLIB_ATARI_STE Supexec(installDma); #else Supexec(installTimerA); #endif gPumpRunning = true; gHalfSilent[0] = false; gHalfSilent[1] = false; tickHookUpdate(); return true; } 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 tickHookUpdate(); } // ----- 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; } #else ymLevelsBuild(); #endif #ifdef JOEYLIB_ATARI_STE memset(gSfxPan, JL_AUDIO_PAN_CENTER, sizeof(gSfxPan)); #endif modPlayerSetU8Shift(MIX_UNIT_SHIFT); audioSfxSetShift(MIX_UNIT_SHIFT); gReady = true; return true; } #ifdef JOEYLIB_ATARI_STE // A slot's side, for the sound it plays and the next (the DMA chip plays // stereo; the plain ST's one DAC cannot pan). void jlpAudioSetPan(uint8_t slot, uint8_t pan) { if (slot < JL_AUDIO_SFX_SLOTS && pan < JL_AUDIO_PAN_COUNT) { gSfxPan[slot] = pan; } } #endif 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.) (void)jlpAudioTickRegister(NULL, 0u); if (gReady) { pumpStop(); } Supexec(ymPsgAllOff); if (!gReady) { return; } silenceMixBuffer(); modPlayerStop(); gReady = false; } bool jlpAudioIsPlayingMod(void) { return gReady && (modPlayerActive() || gModTail); } void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) { if (!gReady) { return; } gModTail = false; if (!modPlayerStart(data, length, MIX_RATE, loop)) { return; } (void)pumpStart(); } void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) { if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE); sfxStartNow(slot, pumpStart()); } void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) { if (!gReady || slot >= JL_AUDIO_SFX_SLOTS) { return; } audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE); sfxStartNow(slot, pumpStart()); } // 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, rounded. 12-bit range covers 30 Hz - 62 kHz. // // Volume mapping: attenuation is 2 dB a step on every port (joey/audio.h), // and the YM's sixteen levels are about 3.5 dB apart (measured: 0, 3.5, // 7.4, 10.4, 13.8, 16.5, 19.9, 22.7, 26.0, 28.8, 32.3, 35.3, 39.1, 42.8 and // 49.4 dB below level 15), so each attenuation takes the level nearest 2 dB // a step below attenuation 0's. That top level is where the mix puts a // voice, as loud as a full-scale effect at MIX_UNIT_SHIFT: the plain ST's // level 13 (its PWM plays the digital sound through a channel like these), // within 1.4 dB a step; the STe's level 14 (its DMA sound is louder than a // YM channel), within 1.9 dB, half the 3.9 dB from level 14 to 13. YM also // accepts bit 4 = envelope-controlled volume; we never set it. // // 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). #define YM_TONE_HZ 125000u // 2 MHz / 16: Hz = this / period #define YM_PERIOD_MAX 0x0FFFu #ifdef JOEYLIB_ATARI_STE static const uint8_t kYmVolume[15] = { 14u, 13u, 13u, 12u, 12u, 11u, 10u, 10u, 9u, 8u, 8u, 7u, 6u, 6u, 5u }; #else static const uint8_t kYmVolume[15] = { 13u, 12u, 12u, 11u, 10u, 10u, 9u, 8u, 8u, 7u, 7u, 6u, 5u, 5u, 4u }; #endif static long applyYmVoice(void) { uint16_t period; 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)((YM_TONE_HZ + (uint32_t)gYmFreqHz / 2u) / (uint32_t)gYmFreqHz); if (period == 0u) { period = 1u; } if (period > YM_PERIOD_MAX) { period = YM_PERIOD_MAX; } ymWrite(toneReg, (uint8_t)(period & 0xFFu)); ymWrite((uint8_t)(toneReg + 1u), (uint8_t)((period >> 8) & 0x0Fu)); ymWrite(volReg, kYmVolume[gYmAtten]); // 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, kYmVolume[gYmNoiseAtten]); 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; } // The tick rides TOS's 200 Hz Timer C (stTickIsr chains to TOS's handler), // counting the rate against it: see the Timer C notes at the top. The hook // may stay in place for the pump (tickHookUpdate), so the tick is switched // off while its fields change -- at rate 0 stTickRun's carry never comes // due -- and its rate goes in last. Each store is one instruction, so this // needs no interrupts held off, and runs before jlInit's supervisor mode. bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) { gStTickHz = 0u; gStTickCarry = 0u; gStTickFn = fn; if (fn != NULL) { gStTickHz = hz; } tickHookUpdate(); return true; } // The Timer C hook in place while something needs it: the program's tick, // or the plain ST's PWM pump, whose Timer A the hook lets in during TOS's // handler (tickIsr.s). TOS's vector is read before the hook goes in, so an // interrupt between the two never chains to a vector not yet saved. static void tickHookUpdate(void) { bool want; #ifdef JOEYLIB_ATARI_STE want = gStTickFn != NULL; #else want = gStTickFn != NULL || gPumpRunning; #endif if (want == gStTickHooked) { return; } if (want) { gStTickOldVec = (void (*)(void))Setexc(VEC_MFP_TC, SETEXC_READ); (void)Setexc(VEC_MFP_TC, (long)stTickIsr); } else { (void)Setexc(VEC_MFP_TC, (long)gStTickOldVec); } gStTickHooked = want; } // Called from stTickIsr (audio_isr.s) at each Timer C interrupt: the // program's tick as many times as its rate has come due since the last. void stTickRun(void) { gStTickCarry += gStTickHz; while (gStTickCarry >= ST_TIMER_C_HZ) { gStTickCarry -= ST_TIMER_C_HZ; gStTickFn(); } } // The tick callback runs from Timer C (stTickIsr), so a section holds // interrupts off, as ymWrite does; keep it short, as the PWM pump's Timer A // is held off with it. The core calls these for the outermost section only, // and Exit puts back the status register Enter found. void jlpAudioCriticalEnter(void) { uint16_t saved; saved = m68kInterruptsOff(); gCriticalSr = saved; } void jlpAudioCriticalExit(void) { uint16_t saved; saved = gCriticalSr; m68kInterruptsRestore(saved); } 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 >= JL_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) { return; } modPlayerStop(); gModTail = 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 >= JL_AUDIO_SFX_SLOTS) { return; } gSfxSlots[slot].active = false; } // Refills the half the play position has left since the last tick by // re-running the module player into it, then overlaying any active SFX. // Called from the game loop, never from IRQ. // // Soft-real-time constraint: a half must be refilled within MIX_HALF / // MIX_RATE = 1024 / 12288 ~= 83 ms of being left, or the play position // comes back round to it stale: a tick that comes a whole buffer late // replays the old half once. 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; } #ifndef JOEYLIB_ATARI_STE // The drain owns the buffer until the ISR is past its ramp. if (gDraining) { if (drainDone()) { pumpStop(); } return; } #endif pollRefill(); // An ended song's last half, flagged for refill, has played. if (gModTail && gNeedRefill[gModTailHalf]) { gModTail = false; } 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 (on // the plain ST once PWM_IDLE_MS of it have gone by, after a ramp down // to level 0: drainStart). if (!modPlayerActive() && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) { #ifdef JOEYLIB_ATARI_STE pumpStop(); #else if (!gIdle) { gIdle = true; gIdleSinceMs = jlpMillisElapsed(); } else if (jlpMillisElapsed() - gIdleSinceMs >= PWM_IDLE_MS) { drainStart(); } } else { gIdle = false; #endif } }