394 lines
12 KiB
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.
|
|
}
|