256 lines
7.2 KiB
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;
|
|
}
|
|
}
|