1151 lines
42 KiB
C
1151 lines
42 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: 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 <string.h>
|
|
|
|
#include <mint/osbind.h>
|
|
|
|
#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
|
|
}
|
|
}
|