// 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 #include #include #include #include #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; } }