joeylib2/src/port/iigs/audio_full.c

394 lines
12 KiB
C

// Apple IIgs audio HAL -- full version (linked only into the AUDIO
// demo via make/iigs.mk's split source set). audio.c keeps the no-op
// stub for every other demo so the monolithic IIgs link budget stays
// safe.
//
// Stage 1 (this file's first cut): load ntpplayer.bin (the Merlin32-
// assembled NinjaTrackerPlus replayer staged by the iigs.mk Merlin
// rule and bundled on the disk image) into a Memory Manager handle.
// halAudioInit reports true if the load succeeds. PlayMod / PlaySfx /
// StopMod still no-op until the JSL trampoline lands -- that's stage
// 2 and gets its own file once the inline-asm syntax is nailed down.
#include <stdio.h>
#include <string.h>
#include <types.h>
#include <memory.h>
#include "hal.h"
#include "joey/audio.h"
// ----- Constants -----
#define NTP_FILENAME "NTPPLAYER.BIN"
#define NTP_BUFFER_BYTES (64L * 1024L)
// Sanity check: NTP source assembles to ~34 KB; reject reads that
// come back too short to plausibly be the replayer.
#define NTP_MIN_BYTES 32000L
// Ensoniq 5503 (DOC) I/O at $E1C03C..$E1C03E. NTP saves its preferred
// sound_control byte at $E100CA so callers that want DOC-register
// access don't have to know NTP's mode bits.
#define DOC_SOUND_CONTROL ((volatile uint8_t *)0xE1C03CL)
#define DOC_SOUND_DATA ((volatile uint8_t *)0xE1C03DL)
#define DOC_SOUND_ADDRESS ((volatile uint8_t *)0xE1C03EL)
#define DOC_IRQ_VOLUME ((volatile uint8_t *)0xE100CAL)
// DOC oscillator control register layout: bit 0 = halt. NTPstreamsound
// itself uses 0x03 (halt + IE) when it wants an osc stopped, so we
// mirror that.
#define DOC_OSC_CTRL_BASE 0xA0
#define DOC_OSC_HALT_BITS 0x03
// SFX: one fixed-bank handle holding all slots' stream structures +
// sample buffers. Per slot: 16-byte struct followed by 4080 bytes of
// sample, total 4 KB. JOEY_AUDIO_SFX_SLOTS slots fit in 16 KB.
#define SFX_SLOT_BYTES (4L * 1024L)
#define SFX_BUFFER_BYTES (SFX_SLOT_BYTES * (long)JOEY_AUDIO_SFX_SLOTS)
// NTP uses oscillators 0..N for music. Each SFX slot reserves 2 oscs
// (1 streamer + 1 player) starting at 14: slot 0 = 14..15, slot 1 =
// 16..17, etc. With 7 max music tracks * 2 oscs = 14 (oscs 0..13)
// and NTP's interrupt osc at 31, this avoids both ranges. Each slot
// also gets its own 512-byte-aligned DOC RAM page so they don't share
// the streaming buffer.
#define SFX_BASE_OSC 14
#define SFX_OSCS_PER_SLOT 2
#define SFX_BASE_DOC_PAGE 0xC0 // 512-aligned (bit 0 must be 0)
#define SFX_DOC_PAGE_STEP 2 // 512 bytes per slot
#define SFX_VOLUME 255
#define SFX_CHANNEL_LEFT 0x00 // Upper 4 bits = output channel
// DOC freq formula: rate_Hz = freq * input_clk / (8 * num_oscs * 65536).
// NTP's max_tracks_available = 31 + 1 interrupt osc => 32 active oscs.
// IIgs DOC input clock = 7158000 Hz; (input_clk / 8 / 32) = 27961.
// Inverted: freq_word = rate * 65536 / 27961. Caps usable rate at
// ~27 kHz (above that freq_word overflows uint16).
#define IIGS_DOC_DIVISOR 27961UL
#define IIGS_MAX_SFX_RATE 27000U
// ----- Module state -----
static Handle gNTPHandle = NULL;
static uint32_t gNTPBase = 0;
static Handle gModuleHandle = NULL;
static uint32_t gModuleBase = 0;
static Handle gSfxHandle = NULL;
static uint32_t gSfxBase = 0;
static bool gNTPReady = false;
static bool gNTPPlaying = false;
// SFX handle layout: stream structure first, sample bytes after.
// Both end up at known 24-bit addresses, side-stepping the small
// memory model's 16-bit pointer issue.
//
// Stream structure consumed by NTPstreamsound (per NTP docs):
// 0..3 : sample pointer (4 bytes, little-endian, 24-bit in low 3)
// 4..7 : sample length (4 bytes)
// 8..9 : freq word (DOC frequency, 2 bytes)
// 10 : DOC RAM page (512-aligned)
// 11 : first oscillator (0..29)
// 12 : playing-osc count (1..30; total oscs used = 1 + count)
// 13 : volume of playing osc 0 (0..255)
// 14 : channel of playing osc 0 (upper 4 bits)
#define SFX_STRUCT_BYTES 16
#define SFX_SAMPLE_OFFSET SFX_STRUCT_BYTES
// Self-modifying call stub. Bakes the X/Y/A register loads AND the
// JSL target into the buffer, so the C-side inline asm only needs
// `jsl gCallStub` -- no global-operand references in the asm block.
//
// (We tried the obvious `lda gAsmGlobal / jsl gJslStub` shape first;
// ORCA's inline assembler accepts the first absolute-global operand
// in a block but rejects the second and third with "invalid operand".
// Folding the loads into the stub side-steps the issue entirely.)
//
// Layout (15 bytes):
// 00: C2 30 REP #$30 ; 16-bit M/X
// 02: A2 lo hi LDX #X
// 05: A0 lo hi LDY #Y
// 08: A9 lo hi LDA #A
// 0B: 22 lo hi bk JSL target
// 0F: 6B RTL
static unsigned char gCallStub[16];
// ----- Internal helpers -----
static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) {
gCallStub[0] = 0xC2;
gCallStub[1] = 0x30;
gCallStub[2] = 0xA2;
gCallStub[3] = (unsigned char)(x & 0xFFu);
gCallStub[4] = (unsigned char)((x >> 8) & 0xFFu);
gCallStub[5] = 0xA0;
gCallStub[6] = (unsigned char)(y & 0xFFu);
gCallStub[7] = (unsigned char)((y >> 8) & 0xFFu);
gCallStub[8] = 0xA9;
gCallStub[9] = (unsigned char)(a & 0xFFu);
gCallStub[10] = (unsigned char)((a >> 8) & 0xFFu);
gCallStub[11] = 0x22;
gCallStub[12] = (unsigned char)(target & 0xFFu);
gCallStub[13] = (unsigned char)((target >> 8) & 0xFFu);
gCallStub[14] = (unsigned char)((target >> 16) & 0xFFu);
gCallStub[15] = 0x6B;
}
static bool loadNTP(void) {
Handle h;
Pointer p;
FILE *fp;
size_t bytesRead;
h = NewHandle(NTP_BUFFER_BYTES, _ownerid,
attrFixed | attrLocked | attrPage | attrNoCross,
NULL);
if (h == NULL || _toolErr != 0) {
return false;
}
HLock(h);
p = *h;
if (p == NULL) {
DisposeHandle(h);
return false;
}
fp = fopen(NTP_FILENAME, "rb");
if (fp == NULL) {
DisposeHandle(h);
return false;
}
bytesRead = fread(p, 1, NTP_BUFFER_BYTES, fp);
fclose(fp);
if (bytesRead < NTP_MIN_BYTES) {
DisposeHandle(h);
return false;
}
gNTPHandle = h;
gNTPBase = (uint32_t)p;
return true;
}
// ----- HAL API (alphabetical) -----
bool halAudioInit(void) {
Handle modHandle;
Handle sfxHandle;
if (gNTPReady) {
return true;
}
if (!loadNTP()) {
return false;
}
// A second fixed-bank handle holds the per-call .NTP module bytes.
// BlockMove copies the caller's data here so we have a known 24-bit
// address to pass to NTPprepare in X/Y.
modHandle = NewHandle(NTP_BUFFER_BYTES, _ownerid,
attrFixed | attrLocked | attrPage | attrNoCross,
NULL);
if (modHandle == NULL || _toolErr != 0) {
DisposeHandle(gNTPHandle);
gNTPHandle = NULL;
return false;
}
HLock(modHandle);
gModuleHandle = modHandle;
gModuleBase = (uint32_t)*modHandle;
// Third fixed-bank handle for SFX sample storage. Same idea as
// the module handle: NTPstreamsound wants a 24-bit pointer, and
// the small-memory-model caller can only hand us a 16-bit one.
sfxHandle = NewHandle(SFX_BUFFER_BYTES, _ownerid,
attrFixed | attrLocked | attrPage | attrNoCross,
NULL);
if (sfxHandle == NULL || _toolErr != 0) {
DisposeHandle(gModuleHandle);
DisposeHandle(gNTPHandle);
gModuleHandle = NULL;
gNTPHandle = NULL;
return false;
}
HLock(sfxHandle);
gSfxHandle = sfxHandle;
gSfxBase = (uint32_t)*sfxHandle;
gNTPReady = true;
return true;
}
void halAudioShutdown(void) {
if (!gNTPReady) {
return;
}
if (gNTPPlaying) {
halAudioStopMod();
}
if (gSfxHandle != NULL) {
DisposeHandle(gSfxHandle);
gSfxHandle = NULL;
gSfxBase = 0;
}
if (gModuleHandle != NULL) {
DisposeHandle(gModuleHandle);
gModuleHandle = NULL;
gModuleBase = 0;
}
if (gNTPHandle != NULL) {
DisposeHandle(gNTPHandle);
gNTPHandle = NULL;
gNTPBase = 0;
}
gNTPReady = false;
}
bool halAudioIsPlayingMod(void) {
return gNTPReady && gNTPPlaying;
}
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
if (!gNTPReady || gModuleHandle == NULL) {
return;
}
if (length == 0 || length > NTP_BUFFER_BYTES) {
return;
}
// Drop any previous song before loading a new one.
if (gNTPPlaying) {
halAudioStopMod();
}
BlockMove((Pointer)data, (Pointer)gModuleBase, length);
// NTPprepare(modPtr in X/Y, doubling in A). modPtr is a 24-bit
// address: low 16 bits in X, bank byte in Y (high byte is don't
// care, low byte is the bank).
buildCallStub(gNTPBase + 0,
(uint16_t)(gModuleBase & 0xFFFFu),
(uint16_t)((gModuleBase >> 16) & 0xFFu),
0);
asm {
jsl gCallStub
}
// NTPplay(loopFlag in A). 0 = loop forever, 1 = play once.
buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1);
asm {
jsl gCallStub
}
gNTPPlaying = true;
}
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
unsigned char *sfx;
uint32_t slotBase;
uint32_t sampleAddr;
uint32_t structAddr;
uint16_t freqWord;
uint16_t clampedRate;
if (!gNTPReady || gSfxHandle == NULL || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
if (length == 0 || length > (SFX_SLOT_BYTES - SFX_SAMPLE_OFFSET)) {
return;
}
clampedRate = (rateHz > IIGS_MAX_SFX_RATE) ? IIGS_MAX_SFX_RATE : rateHz;
freqWord = (uint16_t)(((uint32_t)clampedRate * 65536UL) / IIGS_DOC_DIVISOR);
slotBase = gSfxBase + (uint32_t)slot * (uint32_t)SFX_SLOT_BYTES;
structAddr = slotBase;
sampleAddr = slotBase + SFX_SAMPLE_OFFSET;
// Copy the sample into this slot's fixed-bank region, converting
// signed 8-bit (public API contract) to unsigned 8-bit (DOC RAM
// format) by flipping the sign bit.
{
unsigned char *dst;
uint32_t i;
dst = (unsigned char *)sampleAddr;
for (i = 0; i < length; i++) {
dst[i] = (unsigned char)(sample[i] ^ 0x80);
}
}
// Build the stream structure in this slot's first 16 bytes.
sfx = (unsigned char *)slotBase;
sfx[0] = (unsigned char)(sampleAddr & 0xFFu);
sfx[1] = (unsigned char)((sampleAddr >> 8) & 0xFFu);
sfx[2] = (unsigned char)((sampleAddr >> 16) & 0xFFu);
sfx[3] = 0;
sfx[4] = (unsigned char)(length & 0xFFu);
sfx[5] = (unsigned char)((length >> 8) & 0xFFu);
sfx[6] = (unsigned char)((length >> 16) & 0xFFu);
sfx[7] = (unsigned char)((length >> 24) & 0xFFu);
sfx[8] = (unsigned char)(freqWord & 0xFFu);
sfx[9] = (unsigned char)((freqWord >> 8) & 0xFFu);
sfx[10] = (unsigned char)(SFX_BASE_DOC_PAGE + slot * SFX_DOC_PAGE_STEP);
sfx[11] = (unsigned char)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT);
sfx[12] = 1; // one playing osc
sfx[13] = SFX_VOLUME;
sfx[14] = SFX_CHANNEL_LEFT;
// NTPstreamsound(structPtr in X/Y). Same 24-bit address packing
// pattern as NTPprepare: low 16 in X, bank in Y.
buildCallStub(gNTPBase + 24,
(uint16_t)(structAddr & 0xFFFFu),
(uint16_t)((structAddr >> 16) & 0xFFu),
0);
asm {
jsl gCallStub
}
}
void halAudioStopMod(void) {
if (!gNTPReady || !gNTPPlaying) {
return;
}
buildCallStub(gNTPBase + 6, 0, 0, 0);
asm {
jsl gCallStub
}
gNTPPlaying = false;
}
void halAudioStopSfx(uint8_t slot) {
uint8_t firstOsc;
uint8_t i;
if (!gNTPReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
firstOsc = (uint8_t)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT);
// NTP has no SFX-stop entry point. We poke DOC oscillator control
// registers directly to halt the slot's osc pair. SEI/CLI keeps
// NTP's DOC IRQ from racing our sound_address writes.
asm {
sei
}
*DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME;
for (i = 0; i < SFX_OSCS_PER_SLOT; i++) {
*DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_CTRL_BASE + firstOsc + i);
*DOC_SOUND_DATA = DOC_OSC_HALT_BITS;
}
asm {
cli
}
}
void halAudioFrameTick(void) {
// NTP is DOC-IRQ driven; nothing for the host to pump.
}