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