601 lines
20 KiB
C
601 lines
20 KiB
C
// Amiga audio HAL: PTPlayer (Frank Wille, public domain) drives Paula
|
|
// for both module playback and one-shot SFX overlay.
|
|
//
|
|
// We build PTPlayer in OSCOMPAT mode so it cooperates with audio.device
|
|
// rather than taking Paula away from the OS -- matches our cooperative-
|
|
// with-Intuition graphics HAL. mt_install allocates all four Paula
|
|
// channels via audio.device; mt_init loads a .MOD; mt_Enable=1 starts
|
|
// playback; mt_playfx triggers a SFX on a free channel (or the worst
|
|
// channel if all are busy) with priority arbitration.
|
|
//
|
|
// PTPlayer expects modules and SFX samples to live in Chip RAM. The
|
|
// asset pipeline does not yet partition into Chip vs. Fast, so we
|
|
// AllocMem(MEMF_CHIP) a copy at PlayMod / PlaySfx time and free it at
|
|
// StopMod / StopSfx (or shutdown).
|
|
|
|
#include <string.h>
|
|
|
|
#include <exec/types.h>
|
|
#include <exec/memory.h>
|
|
#include <hardware/custom.h>
|
|
#include <hardware/dmabits.h>
|
|
|
|
#include <proto/exec.h>
|
|
|
|
#include "ptplayer.h"
|
|
#include "port.h"
|
|
#include "joey/audio.h"
|
|
|
|
extern struct Custom custom;
|
|
extern UBYTE mt_Enable;
|
|
extern UBYTE mt_E8Trigger;
|
|
|
|
// ----- Constants -----
|
|
|
|
// Paula's NTSC PAL period for middle-C-ish playback. PTPlayer's SFX
|
|
// API takes a hardware period (cycles per sample) rather than a
|
|
// frequency. To convert from a sample's authored Hz: period = clock /
|
|
// rate, where clock is ~3546895 (PAL) or ~3579545 (NTSC). We default
|
|
// PAL since that's what our screen mode requests, and clamp to legal
|
|
// Paula period limits.
|
|
#define PAULA_CLOCK_PAL 3546895UL
|
|
#define PAULA_PERIOD_MIN 124 // Paula HW limit
|
|
#define PAULA_PERIOD_MAX 65535
|
|
#define PAULA_CHANNELS 4 // hardware audio channel count
|
|
|
|
#define SFX_VOLUME_MAX 64
|
|
|
|
// PSG-style tone voices: Paula loops a 2-byte full-swing square per
|
|
// channel (DMA auto-reloads the location/length latches at length
|
|
// exhaustion, so a started voice sustains with zero CPU -- the sticky
|
|
// contract falls straight out of the hardware). Pitch is the period
|
|
// register: toneHz = PAULA_CLOCK_PAL / (period * 2). Loudness is the
|
|
// channel's 0..64 volume. The square must live in chip RAM; it is
|
|
// AllocMem'd on first use because jlAudioVoice is callable without
|
|
// jlAudioInit (jlpAudioShutdown frees it independently of gInstalled).
|
|
#define VOICE_SQUARE_BYTES 2
|
|
#define VOICE_VOLUME_MAX 64
|
|
#define VOICE_ATTEN_OFF 15
|
|
|
|
// Noise voice: Paula has no noise generator, so channel 3 loops a
|
|
// chip-RAM sample of 1-bit LFSR noise (full-swing +127/-128 per LFSR
|
|
// bit -- the YM2149 timbre, so the portable pitch semantic carries
|
|
// over: spectral edge = sample rate). 1024 bytes puts the worst-case
|
|
// loop-repeat rate (~28 Hz at pitch 0) below tonal salience; 512
|
|
// would buzz at ~56 Hz. Generated once with a fixed seed so output
|
|
// is deterministic.
|
|
#define NOISE_BYTES 1024
|
|
#define NOISE_CHANNEL 3
|
|
#define NOISE_PITCH_MAX 31
|
|
#define NOISE_LFSR_SEED 0xACE1u
|
|
#define NOISE_LFSR_TAPS 0xB400u // 16-bit maximal Galois LFSR
|
|
|
|
// PTPlayer has no built-in "play once" mode -- songs loop forever
|
|
// unless the author placed an `E8FF` effect at song end. mt_E8Trigger
|
|
// reflects the most-recently-seen E8 value; mt_init resets it to 0.
|
|
// When loop=false on PlayMod, we poll for this sentinel in
|
|
// jlpAudioFrameTick and clear mt_Enable when seen.
|
|
#define PTPLAYER_LOOP_END_MARKER 0xFF
|
|
|
|
// ----- Module state -----
|
|
|
|
// Streaming SFX chunk size, in bytes. PTPlayer's mt_playfx plays a
|
|
// one-shot sample and does not queue, so a stream is played one chunk at
|
|
// a time with the next one fired from jlpAudioFrameTick when the current
|
|
// is due to end. Bigger chunks mean fewer seams; 4 KB is ~0.8 s at 5 kHz,
|
|
// which covers a whole speech sample with no seam at all.
|
|
#define SFX_STREAM_CHUNK 4096u
|
|
|
|
// The host calls jlpAudioFrameTick once per display frame, and this port
|
|
// is PAL throughout (see PAULA_CLOCK_PAL in periodForRate), so chunk
|
|
// lifetimes are counted in 50 Hz frames.
|
|
#define AMIGA_FRAME_HZ 50u
|
|
|
|
typedef struct {
|
|
UBYTE *samples; // Chip-RAM copy of one SFX sample
|
|
UWORD lengthWords; // sample length in 16-bit words
|
|
// ---- streaming (jlpAudioPlaySfxStream) ----
|
|
UBYTE *chunk; // Chip-RAM chunk buffer, SFX_STREAM_CHUNK
|
|
BYTE cha; // Paula channel, -1 = let PTPlayer pick
|
|
jlAudioStreamFillT fill;
|
|
void *ctx;
|
|
uint16_t rateHz;
|
|
uint16_t framesLeft; // until the playing chunk runs out
|
|
bool active;
|
|
} SfxSlotT;
|
|
|
|
static UBYTE *gModuleChip = NULL; // Chip-RAM copy of the playing module
|
|
static ULONG gModuleLength = 0; // bytes to FreeMem on swap / shutdown
|
|
static SfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
|
static bool gInstalled = false;
|
|
static bool gModPlaying = false;
|
|
static bool gWantLoopOnce = false; // honor loop=false via E8 marker
|
|
static BYTE *gVoiceChip = NULL; // chip-RAM square shared by all voices
|
|
static BYTE *gNoiseChip = NULL; // chip-RAM LFSR noise for channel 3
|
|
|
|
// pitch 0..31 -> Paula period, geometric 124..1773 (~9%/step, about
|
|
// 1.5 semitones). Effective sample rate = 3546895/period: pitch 0 =
|
|
// ~28.6 kHz bright hiss, pitch 31 = ~2.0 kHz sample-and-hold rumble.
|
|
// Every entry is inside Paula's legal 124..65535 range by
|
|
// construction. Derivation: round(124 * (1773/124)^(i/31)).
|
|
// Paula periods for noise pitch 0..31, chosen so the sample plays at the
|
|
// SAME rate the other ports give that pitch: rate = 26000 - pitch * 750,
|
|
// period = 3546895 / rate (PAL colour clock). That is the IIgs curve
|
|
// (NOISE_RATE_MAX / NOISE_RATE_STEP in src/iigs/audio_full.c), and it
|
|
// also lands within about a tenth of the ST's YM2149 at the top of the
|
|
// range, so all three agree on what a given pitch sounds like.
|
|
//
|
|
// The old table was geometric, an even ratio per step from 124 to 1773.
|
|
// It matched at pitch 0 and drifted ever darker above that: at the
|
|
// pitches Space Taxi's level-intro tune actually uses -- 28, 29 and 30,
|
|
// its only noise notes -- it ran 2587/2376/2180 Hz against the IIgs's
|
|
// 5000/4250/3500 and the ST's 4464/4310/4167. An octave too dark, which
|
|
// is why that one screen's music sounded wrong on the Amiga while every
|
|
// other song, none of which touches the noise voice, sounded right.
|
|
// Smallest period here is 136, comfortably above Paula's 124 floor.
|
|
static const UWORD kNoisePeriod[NOISE_PITCH_MAX + 1] = {
|
|
136, 140, 145, 149, 154, 159, 165, 171,
|
|
177, 184, 192, 200, 209, 218, 229, 240,
|
|
253, 268, 284, 302, 322, 346, 373, 405,
|
|
443, 489, 546, 617, 709, 835, 1013, 1290
|
|
};
|
|
|
|
// ----- Internal helpers -----
|
|
|
|
// Keep PTPlayer's automatic SFX channel selection (sfx_cha = -1) off
|
|
// the noise channel. mt_end clears the per-channel music-only flags
|
|
// (its resetch does so on every channel), and mt_end runs in the
|
|
// PlayMod/StopMod/Shutdown paths -- so the mask must be re-applied
|
|
// after every mt_end, not set once at install.
|
|
static void maskNoiseChannel(void) {
|
|
if (gInstalled) {
|
|
mt_musicmask((void *)&custom, (UBYTE)(1u << NOISE_CHANNEL));
|
|
}
|
|
}
|
|
|
|
|
|
static UWORD periodForRate(uint16_t rateHz) {
|
|
unsigned long period;
|
|
|
|
if (rateHz == 0) {
|
|
return 428; // ~middle C, sane default
|
|
}
|
|
period = PAULA_CLOCK_PAL / (unsigned long)rateHz;
|
|
if (period < PAULA_PERIOD_MIN) {
|
|
period = PAULA_PERIOD_MIN;
|
|
}
|
|
if (period > PAULA_PERIOD_MAX) {
|
|
period = PAULA_PERIOD_MAX;
|
|
}
|
|
return (UWORD)period;
|
|
}
|
|
|
|
|
|
// ----- HAL API (alphabetical) -----
|
|
|
|
bool jlpAudioInit(void) {
|
|
int i;
|
|
|
|
if (gInstalled) {
|
|
return true;
|
|
}
|
|
if (mt_install() == 0) {
|
|
return false;
|
|
}
|
|
mt_Enable = 0;
|
|
gInstalled = true;
|
|
maskNoiseChannel();
|
|
gModPlaying = false;
|
|
gWantLoopOnce = false;
|
|
gModuleChip = NULL;
|
|
gModuleLength = 0;
|
|
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
|
|
gSfxSlots[i].samples = NULL;
|
|
gSfxSlots[i].lengthWords = 0;
|
|
// -1, not 0: zero is Paula channel 0, which would silently pin every
|
|
// slot hard left instead of letting PTPlayer choose.
|
|
gSfxSlots[i].cha = -1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void jlpAudioShutdown(void) {
|
|
int i;
|
|
|
|
// Voice-layer teardown first, independent of gInstalled: the
|
|
// tone square and noise sample are allocated on first use, which
|
|
// does not require jlpAudioInit. DMA must stop before the frees
|
|
// or Paula keeps looping freed chip RAM.
|
|
custom.dmacon = DMAF_AUD0 | DMAF_AUD1 | DMAF_AUD2 | DMAF_AUD3;
|
|
for (i = 0; i < PAULA_CHANNELS; i++) {
|
|
custom.aud[i].ac_vol = 0;
|
|
}
|
|
if (gVoiceChip != NULL) {
|
|
FreeMem(gVoiceChip, VOICE_SQUARE_BYTES);
|
|
gVoiceChip = NULL;
|
|
}
|
|
if (gNoiseChip != NULL) {
|
|
FreeMem(gNoiseChip, NOISE_BYTES);
|
|
gNoiseChip = NULL;
|
|
}
|
|
|
|
if (!gInstalled) {
|
|
return;
|
|
}
|
|
mt_Enable = 0;
|
|
mt_end((void *)&custom);
|
|
mt_remove();
|
|
gInstalled = false;
|
|
gModPlaying = false;
|
|
|
|
if (gModuleChip != NULL) {
|
|
FreeMem(gModuleChip, gModuleLength);
|
|
gModuleChip = NULL;
|
|
gModuleLength = 0;
|
|
}
|
|
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
|
|
if (gSfxSlots[i].samples != NULL) {
|
|
FreeMem(gSfxSlots[i].samples, (ULONG)gSfxSlots[i].lengthWords * 2UL);
|
|
gSfxSlots[i].samples = NULL;
|
|
gSfxSlots[i].lengthWords = 0;
|
|
}
|
|
gSfxSlots[i].active = false;
|
|
if (gSfxSlots[i].chunk != NULL) {
|
|
FreeMem(gSfxSlots[i].chunk, (ULONG)SFX_STREAM_CHUNK);
|
|
gSfxSlots[i].chunk = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool jlpAudioIsPlayingMod(void) {
|
|
return gModPlaying && mt_Enable != 0;
|
|
}
|
|
|
|
|
|
void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
|
UBYTE *chip;
|
|
|
|
if (!gInstalled) {
|
|
return;
|
|
}
|
|
mt_Enable = 0;
|
|
mt_end((void *)&custom);
|
|
gModPlaying = false;
|
|
gWantLoopOnce = false;
|
|
if (gModuleChip != NULL) {
|
|
FreeMem(gModuleChip, gModuleLength);
|
|
gModuleChip = NULL;
|
|
gModuleLength = 0;
|
|
}
|
|
|
|
chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP);
|
|
if (chip == NULL) {
|
|
return;
|
|
}
|
|
memcpy(chip, data, length);
|
|
gModuleChip = chip;
|
|
gModuleLength = (ULONG)length;
|
|
|
|
// PTPlayer takes the module pointer in a0, sample-data pointer in
|
|
// a1 (NULL = samples follow the module header), song position in
|
|
// d0. mt_init resets mt_E8Trigger to 0 so subsequent jlpAudioFrame
|
|
// Tick polling sees a clean state.
|
|
mt_init((void *)&custom, chip, NULL, 0);
|
|
maskNoiseChannel(); // mt_end above cleared the mask
|
|
mt_Enable = 1;
|
|
gWantLoopOnce = !loop;
|
|
gModPlaying = true;
|
|
}
|
|
|
|
|
|
// Pull one chunk from a streaming slot and hand it to PTPlayer.
|
|
// Returns false when the stream has ended (fill returned 0).
|
|
static bool streamChunk(uint8_t slot) {
|
|
SfxSlotT *s = &gSfxSlots[slot];
|
|
SfxStructure sfx;
|
|
uint32_t got;
|
|
|
|
// The fill contract is SIGNED 8-bit, which is exactly what Paula
|
|
// wants, so the callback writes straight into Chip RAM.
|
|
got = s->fill(s->ctx, (int8_t *)s->chunk, (uint32_t)SFX_STREAM_CHUNK);
|
|
if (got < 2u || got > (uint32_t)SFX_STREAM_CHUNK) {
|
|
return false;
|
|
}
|
|
got &= ~1u; // Paula counts 16-bit words
|
|
|
|
sfx.sfx_ptr = s->chunk;
|
|
sfx.sfx_len = (UWORD)(got >> 1);
|
|
sfx.sfx_per = periodForRate(s->rateHz);
|
|
sfx.sfx_vol = SFX_VOLUME_MAX;
|
|
sfx.sfx_cha = s->cha;
|
|
sfx.sfx_pri = (BYTE)(slot + 1);
|
|
mt_playfx((void *)&custom, &sfx);
|
|
|
|
// Fire the next chunk a frame early rather than leaving a gap.
|
|
{
|
|
uint32_t frames = (got * (uint32_t)AMIGA_FRAME_HZ) / (uint32_t)s->rateHz;
|
|
|
|
if (frames > 1u) {
|
|
frames--;
|
|
}
|
|
s->framesLeft = (uint16_t)frames;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
|
SfxStructure sfx;
|
|
SfxSlotT *s;
|
|
UBYTE *chip;
|
|
UWORD words;
|
|
|
|
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
|
return;
|
|
}
|
|
if (length < 2 || length > 0x1FFFEUL) {
|
|
return; // Paula sample length is 16 bits of words = 128KB max
|
|
}
|
|
|
|
s = &gSfxSlots[slot];
|
|
if (s->samples != NULL) {
|
|
FreeMem(s->samples, (ULONG)s->lengthWords * 2UL);
|
|
s->samples = NULL;
|
|
s->lengthWords = 0;
|
|
}
|
|
|
|
chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP);
|
|
if (chip == NULL) {
|
|
return;
|
|
}
|
|
memcpy(chip, sample, length);
|
|
words = (UWORD)(length >> 1);
|
|
|
|
s->samples = chip;
|
|
s->lengthWords = words;
|
|
|
|
sfx.sfx_ptr = chip;
|
|
sfx.sfx_len = words;
|
|
sfx.sfx_per = periodForRate(rateHz);
|
|
sfx.sfx_vol = SFX_VOLUME_MAX;
|
|
sfx.sfx_cha = s->cha; // -1 = best free channel
|
|
sfx.sfx_pri = (BYTE)(slot + 1); // priority 1..N from slot index
|
|
mt_playfx((void *)&custom, &sfx);
|
|
}
|
|
|
|
|
|
// Streaming SFX. PTPlayer's mt_playfx wants a complete sample at fire
|
|
// time, so the stream is chunked rather than refilled mid-playback:
|
|
// arm the slot, push the first chunk now, and let jlpAudioFrameTick
|
|
// push each following one as the previous is due to run out. The seam
|
|
// therefore quantises to a 50 Hz frame, which is the jitter the public
|
|
// contract documents. Each slot gets one SFX_STREAM_CHUNK Chip-RAM
|
|
// buffer, allocated on first use and freed in jlpAudioShutdown.
|
|
void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz) {
|
|
SfxSlotT *s;
|
|
|
|
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
|
return;
|
|
}
|
|
if (fill == (jlAudioStreamFillT)0 || rateHz == 0) {
|
|
return;
|
|
}
|
|
s = &gSfxSlots[slot];
|
|
if (s->chunk == NULL) {
|
|
s->chunk = (UBYTE *)AllocMem((ULONG)SFX_STREAM_CHUNK, MEMF_CHIP);
|
|
if (s->chunk == NULL) {
|
|
return;
|
|
}
|
|
}
|
|
s->fill = fill;
|
|
s->ctx = ctx;
|
|
s->rateHz = rateHz;
|
|
s->active = true;
|
|
if (!streamChunk(slot)) {
|
|
s->active = false;
|
|
}
|
|
}
|
|
|
|
|
|
// Noise voice on Paula channel 3: a looped chip-RAM LFSR sample.
|
|
// DMA auto-reload sustains it with zero CPU, exactly like the tone
|
|
// squares; while a MOD plays, PTPlayer's interrupt owns the channel
|
|
// and noise writes lose -- the same exclusivity as the tone voices.
|
|
void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
|
|
UWORD vol;
|
|
|
|
if (atten >= VOICE_ATTEN_OFF) {
|
|
custom.dmacon = DMAF_AUD3;
|
|
custom.aud[NOISE_CHANNEL].ac_vol = 0;
|
|
return;
|
|
}
|
|
|
|
if (gNoiseChip == NULL) {
|
|
uint16_t lfsr;
|
|
int i;
|
|
|
|
gNoiseChip = (BYTE *)AllocMem(NOISE_BYTES, MEMF_CHIP);
|
|
if (gNoiseChip == NULL) {
|
|
return;
|
|
}
|
|
lfsr = NOISE_LFSR_SEED;
|
|
for (i = 0; i < NOISE_BYTES; i++) {
|
|
gNoiseChip[i] = (BYTE)((lfsr & 1u) ? 127 : -128);
|
|
lfsr = (uint16_t)((lfsr >> 1) ^ ((lfsr & 1u) ? NOISE_LFSR_TAPS : 0u));
|
|
}
|
|
}
|
|
|
|
if (pitch > NOISE_PITCH_MAX) {
|
|
pitch = NOISE_PITCH_MAX;
|
|
}
|
|
vol = (UWORD)(((UWORD)(VOICE_ATTEN_OFF - atten) * VOICE_VOLUME_MAX) / VOICE_ATTEN_OFF);
|
|
|
|
// ptr/len writes latch at the next loop reload; per/vol take
|
|
// effect immediately -- a live retune is click-free and the
|
|
// redundant DMA set is harmless.
|
|
custom.aud[NOISE_CHANNEL].ac_ptr = (UWORD *)gNoiseChip;
|
|
custom.aud[NOISE_CHANNEL].ac_len = NOISE_BYTES / 2; // Paula length is in words
|
|
custom.aud[NOISE_CHANNEL].ac_per = kNoisePeriod[pitch];
|
|
custom.aud[NOISE_CHANNEL].ac_vol = vol;
|
|
custom.dmacon = (UWORD)(DMAF_SETCLR | DMAF_MASTER | DMAF_AUD3);
|
|
}
|
|
|
|
|
|
void jlpAudioTone(uint16_t freqHz) {
|
|
jlpAudioVoice(0u, freqHz, 0u); // single full-volume tone on voice 0
|
|
}
|
|
|
|
|
|
// 3-voice PSG-style tone on Paula channels 0-2, programmed directly
|
|
// (no audio.device AllocAudio -- consistent with the ST's direct YM
|
|
// pokes; a JoeyLib game owns the machine while it runs). Caveats:
|
|
// while a MOD is playing, PTPlayer rewrites all four channels' regs
|
|
// from its CIA interrupt and voice writes lose -- same voices-vs-MOD
|
|
// exclusivity as the ST's YM. mt_playfx SFX can also land on a busy
|
|
// voice channel (it picks any free-looking channel); prefer the
|
|
// tracker/voice layer OR the MOD/SFX layer per scene, not both.
|
|
void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
|
|
ULONG period;
|
|
UWORD vol;
|
|
|
|
if (voice >= JOEY_AUDIO_VOICES) {
|
|
return;
|
|
}
|
|
if (freqHz == 0u || atten >= VOICE_ATTEN_OFF) {
|
|
// Silence: stop the channel's DMA and zero its volume. The
|
|
// dmacon write without DMAF_SETCLR clears the named bit only.
|
|
custom.dmacon = (UWORD)(DMAF_AUD0 << voice);
|
|
custom.aud[voice].ac_vol = 0;
|
|
return;
|
|
}
|
|
|
|
if (gVoiceChip == NULL) {
|
|
gVoiceChip = (BYTE *)AllocMem(VOICE_SQUARE_BYTES, MEMF_CHIP);
|
|
if (gVoiceChip == NULL) {
|
|
return;
|
|
}
|
|
gVoiceChip[0] = 127;
|
|
gVoiceChip[1] = -128;
|
|
}
|
|
|
|
period = PAULA_CLOCK_PAL / ((ULONG)freqHz * 2UL);
|
|
if (period < PAULA_PERIOD_MIN) {
|
|
period = PAULA_PERIOD_MIN;
|
|
}
|
|
if (period > PAULA_PERIOD_MAX) {
|
|
period = PAULA_PERIOD_MAX;
|
|
}
|
|
|
|
// AGI atten 0..14 (0 = loud) -> Paula volume 64..4, linear.
|
|
vol = (UWORD)(((UWORD)(VOICE_ATTEN_OFF - atten) * VOICE_VOLUME_MAX) / VOICE_ATTEN_OFF);
|
|
|
|
custom.aud[voice].ac_ptr = (UWORD *)gVoiceChip;
|
|
custom.aud[voice].ac_len = VOICE_SQUARE_BYTES / 2; // Paula length is in words
|
|
custom.aud[voice].ac_per = (UWORD)period;
|
|
custom.aud[voice].ac_vol = vol;
|
|
custom.dmacon = (UWORD)(DMAF_SETCLR | DMAF_MASTER | (DMAF_AUD0 << voice));
|
|
}
|
|
|
|
|
|
bool jlpAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
|
|
// CIA-B Timer B at hz could drive this; not wired until the
|
|
// Paula voice path lands.
|
|
(void)fn;
|
|
(void)hz;
|
|
return false;
|
|
}
|
|
|
|
|
|
void jlpAudioCriticalEnter(void) {
|
|
}
|
|
|
|
|
|
void jlpAudioCriticalExit(void) {
|
|
}
|
|
|
|
|
|
void jlpAudioStopMod(void) {
|
|
if (!gInstalled) {
|
|
return;
|
|
}
|
|
mt_Enable = 0;
|
|
mt_end((void *)&custom);
|
|
maskNoiseChannel(); // mt_end cleared the mask
|
|
gModPlaying = false;
|
|
gWantLoopOnce = false;
|
|
}
|
|
|
|
|
|
// Paula pans per CHANNEL: 0 and 3 feed the left output, 1 and 2 the right.
|
|
// One voice cannot reach both ears, so AUDIO_PAN_CENTER keeps PTPlayer's own
|
|
// "best free channel" pick (sfx_cha = -1) -- which is what every slot did
|
|
// before this call existed, so the default behaviour is unchanged. A panned
|
|
// slot pins its SFX to a channel on that side instead.
|
|
void jlpAudioSetPan(uint8_t slot, uint8_t pan) {
|
|
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
|
|
return;
|
|
}
|
|
switch (pan) {
|
|
case AUDIO_PAN_LEFT:
|
|
gSfxSlots[slot].cha = 3;
|
|
break;
|
|
case AUDIO_PAN_RIGHT:
|
|
gSfxSlots[slot].cha = 2;
|
|
break;
|
|
default:
|
|
gSfxSlots[slot].cha = -1; // PTPlayer picks
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void jlpAudioStopSfx(uint8_t slot) {
|
|
UBYTE i;
|
|
|
|
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
|
return;
|
|
}
|
|
gSfxSlots[slot].active = false;
|
|
// PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
|
|
// picks a channel dynamically and does not return which one. We
|
|
// can't reliably map slot -> channel after the fact, so stop all
|
|
// PAULA_CHANNELS channels' SFX state. mt_stopfx is idempotent on
|
|
// idle channels, so stopping a quiet one is harmless.
|
|
for (i = 0; i < PAULA_CHANNELS; i++) {
|
|
mt_stopfx((void *)&custom, i);
|
|
}
|
|
}
|
|
|
|
|
|
void jlpAudioFrameTick(void) {
|
|
// Streaming slots: mt_playfx plays one chunk and stops, so the next
|
|
// chunk goes out here as the previous one runs down.
|
|
{
|
|
uint8_t slot;
|
|
|
|
for (slot = 0u; slot < JOEY_AUDIO_SFX_SLOTS; slot++) {
|
|
if (!gSfxSlots[slot].active) {
|
|
continue;
|
|
}
|
|
if (gSfxSlots[slot].framesLeft != 0u) {
|
|
gSfxSlots[slot].framesLeft--;
|
|
continue;
|
|
}
|
|
if (!streamChunk(slot)) {
|
|
gSfxSlots[slot].active = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// PTPlayer drives itself off CIA-B; the only other host-loop work is
|
|
// the play-once watchdog. When the song author placed an `E8FF` at
|
|
// song end, mt_E8Trigger latches to 0xFF -- if the caller passed
|
|
// loop=false to PlayMod, that's our cue to stop the song.
|
|
if (gWantLoopOnce && gModPlaying && mt_E8Trigger == PTPLAYER_LOOP_END_MARKER) {
|
|
mt_Enable = 0;
|
|
gModPlaying = false;
|
|
gWantLoopOnce = false;
|
|
}
|
|
}
|