joeylib2/src/port/amiga/audio.c

256 lines
7.2 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 <proto/exec.h>
#include "ptplayer.h"
#include "hal.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 SFX_VOLUME_MAX 64
// 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
// halAudioFrameTick and clear mt_Enable when seen.
#define PTPLAYER_LOOP_END_MARKER 0xFF
// ----- Module state -----
typedef struct {
UBYTE *samples; // Chip-RAM copy of one SFX sample
UWORD lengthWords; // sample length in 16-bit words
} 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
// ----- Internal helpers -----
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 halAudioInit(void) {
int i;
if (gInstalled) {
return true;
}
if (mt_install() == 0) {
return false;
}
mt_Enable = 0;
gInstalled = true;
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;
}
return true;
}
void halAudioShutdown(void) {
int i;
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;
}
}
}
bool halAudioIsPlayingMod(void) {
return gModPlaying && mt_Enable != 0;
}
void halAudioPlayMod(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 halAudioFrame
// Tick polling sees a clean state.
mt_init((void *)&custom, chip, NULL, 0);
mt_Enable = 1;
gWantLoopOnce = !loop;
gModPlaying = true;
}
void halAudioPlaySfx(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 = -1; // best free channel
sfx.sfx_pri = (BYTE)(slot + 1); // priority 1..N from slot index
mt_playfx((void *)&custom, &sfx);
}
void halAudioStopMod(void) {
if (!gInstalled) {
return;
}
mt_Enable = 0;
mt_end((void *)&custom);
gModPlaying = false;
gWantLoopOnce = false;
}
void halAudioStopSfx(uint8_t slot) {
UBYTE i;
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
// 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
// four channels' SFX state. mt_stopfx is idempotent on idle
// channels, so stopping a quiet one is harmless.
for (i = 0; i < 4; i++) {
mt_stopfx((void *)&custom, i);
}
}
void halAudioFrameTick(void) {
// PTPlayer drives itself off CIA-B; the only 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;
}
}