RobSmithDev

Software, Electronics, Tutorials & Solutions for Retro and Modern

Paula's Revenge (Why? Because Amiga!)
Revision 2026 - Wild Compo Entry

"Can you play Audio, through the Amiga Floppy Drive Port!?"

Got 6th Place! Thanks for all the votes!


Revision 2026 Entry Video

Correction to the video, the A2D chip is the LTC2602

What is it?

I built, what I guess you could call, a Sound Card for the Amiga that connects via the Floppy Drive Port. Why? Because I could, and took way more effort than a silly stunt like this really should. However, it has given me an idea for an actual practical use for what I've learnt.

But the floppy drive port does have some advantages going for it. Aside from I'm giving Paula two more audio channels, all of it's high speed data is all done using DMA meaning a very low CPU overhead.

Paula will happily clock out data at 500kbps. Thats right, Paula handles the floppy disk as well as audio, and that data rate is actually quite fast! 500000/8 gives you 62500 bytes/second, and with the help of a logic analser and lots of patience I ended up with the first demo.


AMOS

I started with AMOS because I hadn't done much with C++ on the Amiga. I built a little circuit based around a simple Arduino UNO. The circuit is connected to the floppy drive port on specific connections.

Note in the playback rthe obvious glitches and pops between each buffer as its streamed to the floppy drive port. The audio is a RAW file containing Unsigned 8-bit audio at 62.5khz and has to be loaded into Chip Memory before disabling multitasking.

AMOS Source Code:
Set Buffer 100 Hide On ' Maximum size MFMSIZE_WORDS=$3FFF DSIZE=MFMSIZE_WORDS*2 Open Random 1,"8bit_unsigned.raw" Field 1,DSIZE As D$ Close Workbench BLOCKS=23 Reserve As Chip Work 1,DSIZE Reserve As Chip Work 2,DSIZE Curs Off Paper 0 Pen 5 Cls Print "Precalc...["; For A=1 To BLOCKS Print " "; Next A Print "]" Locate 11,0 ' Encode timing bits Reserve As Work 3,DSIZE*BLOCKS P=Start(3) For A=1 To BLOCKS Get 1,A Print "#"; Copy Varptr(D$),Varptr(D$)+DSIZE To P For G=P To P+DSIZE-1 Step 4 Loke G,Leek(G) or %1000000010000000100000001 Next G Add P,DSIZE Next A Print "" Print "Playing..." _INTREQR=$DFF01E _INTREQ=$DFF09C ' Amiga registers CIAAPRA=$BFE001 CIABPRB=$BFD100 ADKCON=$DFF09E ADKCONR=$DFF010 DSKPTH=$DFF020 DSKLEN=$DFF024 DMACON=$DFF096 DMACONR=$DFF002 DMASTATUS=Deek(DMACONR) Break Off Multi No ' Reset Flags Poke CIABPRB,%11111111 ' Enable MTR Poke CIABPRB,%1111111 ' Select DF1 Poke CIABPRB,%1101111 M=Start(3) Copy M,M+DSIZE To Start(1) Add M,DSIZE BUF=1 Doke _INTREQ,2 For A=1 To BLOCKS ' Setup DMA memory source Doke Start(BUF),%1 Loke DSKPTH,Start(BUF) and $FFFFFFFE ' Setup DMA Transfer: ' 1. Enable DMA and Disk DMA Doke DMACON,%1000001000010000 ' 2. Set DSKLEN to $4000 Doke DSKLEN,$4000 ' 4. Write it again to start dma! Doke DSKLEN,MFMSIZE_WORDS or $C000 Doke DSKLEN,MFMSIZE_WORDS or $C000 ' copy the next bit in while the DMS is happenning If BUF=1 Then BUF=2 Else BUF=1 Copy M,M+DSIZE To Start(BUF) Add M,DSIZE ' wait Repeat D=Deek(_INTREQR) Until Btst(1,D) Doke _INTREQ,2 Doke DSKLEN,$4000 Next A ' Select DF0 Poke CIABPRB,%11101111 ' De-Select DF0 Poke CIABPRB,%11111111 Multi Yes Break On

Arduino Uno Schematic - Click to Enlarge

The strange connection between pin 1 and 21 tricks the Amiga into detecting the floppy drive! The pins on the floppy drive connector that I use are:

PinName
1/READY
7GND
125V
13/SIDE/HEAD
16/WRITE_DATA
17/WRITE_GATE
21/SEL1


Arduino UNO

The Arduino waits for the WRITE_GATE line to drop low, a signal that means data can be written to the disk, and then starts monitoring the WRITE_DATA pin for data.

It knows that data will be arriving at 500kbps, or intervals of 2uS, although, on the two Amigas I tested this on, it was more like 1.98us but thats another story.

To synchronise all of the data, I made a rule that the LSB of each byte MUST be a '1'. I could then use this to keep the byte decoder in sync with the data without having to resort to MFM encoding.

Encoding (bits): ddddddd1 ddddddd1 ddddddd1 ddddddd1 ddddddd1 ddddddd1

I decoded a single byte at a time, and then wrote this out of one of the Arduino Ports, which had a simple R-2R digital to analog converter connected.

The LEDs you see in the photo are connected to READY/SELECT, WRITE_GATE, and WRITE_DATA.

The Arduino

The Arduino source code is actually fairly short and makes use of the hardware Input Capture to precisly measure when the pulses were received from the Amiga, and also one of the hardware timers to keep track of which bit it's using. Because it uses these hardware features it's able to lock on to the bit-stream easily.

Arduino Uno Setup - Click to Enlarge
Arduino Source Code:
#define PIN_READ_DATA 8 // Reads RAW floppy data on this pin. #define PIN_WRITE_GATE 9 // Reads RAW floppy data on this pin. #define PIN_WRITE_GATE_MASK B00000010 // The mask for the port #define PIN_READ_DATA_MASK B00000001 // The mask for the port #define PIN_READ_DATA_PORT PINB // The port the above pin is on #define PIN_WRITE_GATE_PORT PINB void setup() { // Disable all interrupts - we dont want them! Serial.begin(9600); cli(); TIMSK0=0; TIMSK1=0; TIMSK2=0; PCICR = 0; PCIFR = 0; PCMSK0 = 0; PCMSK2 = 0; PCMSK1 = 0; EIMSK&=~B00000011; // disable INT0/1 interrupt mask TCCR1A = 0; TCCR1B = bit(CS10); // Capture from pin 8, falling edge. falling edge input capture, prescaler 1, no output compare TCCR1C = 0; pinMode(PIN_READ_DATA, INPUT_PULLUP); pinMode(PIN_WRITE_GATE, INPUT_PULLUP); pinMode(10,OUTPUT); pinMode(11,OUTPUT); pinMode(12,OUTPUT); // Use timer 0 to count the correct number of ms TCCR0A = 0; // Simple counter TCCR0B = bit(CS00); // No prescaler, ticks = 62.5uSec TCCR0A = bit(WGM01); TCNT0 = 0; // Reset counter; OCR0A = 31; OCR0B = 0; // default timing window pinMode(13,OUTPUT); DDRD = B11111111; } void loop() { PORTB &= ~B1100; while (PIN_WRITE_GATE_PORT&PIN_WRITE_GATE_MASK) {}; PORTB |= B1000; while (!(PIN_WRITE_GATE_PORT&PIN_WRITE_GATE_MASK)) { uint8_t data = 1; // Wait for first pulse to sync with - this is the LSB of the first WORD and it's discarded as a SYNC clock TIFR1|=bit(ICF1); while (!(TIFR1 & bit(ICF1))) {}; TIFR1|=bit(ICF1); TCNT0=0; for (uint8_t b=0; b<7; b++) { data <<= 1; // read a byte // Wait a maximum of 2us TIFR0 |= bit(OCF0A); // PORTB |= B0100; while (!(TIFR0 & bit(OCF0A))) { // Was a pulse detected? if (TIFR1 & bit(ICF1)) { TIFR1|=bit(ICF1); data |= 1; TCNT0=1; // reset maximum timeout for SYNC break; } } } PORTD = data; } }

Amiga Trackdisk.device (OS Legal)

There's not too much to say here, this was an attempt to improve upon the AMOS version by not having to disable multitasking. I wanted to use the Trackdisk.device to actually perform the disk writing directly rather than messing around with hardware registers.

This doesn't work as well as it should because the Amiga is scheduling these writes mixed between other disk activities such as stepping the head back and forth and checking for the presence of disks. This causes a much more noticable glitching/popping in the audio played back.

However there was a massive improvement in the time it took to add the sync bits to the data loaded.

Amiga C++ Trackdisk.device Code Version
#include <proto/exec.h> #include <proto/utility.h> #include <clib/alib_protos.h> #include <exec/types.h> #include <exec/memory.h> #include <devices/trackdisk.h> #include <proto/exec.h> #include <proto/dos.h> #define DRIVE_UNIT 1 /* DF1: */ #define TRACK_SIZE 32766 // Max out the DMA #define CHIP_BUF_SIZE TRACK_SIZE #define NUM_BUFFERS 2 // Allocate CHIP memory buffer UBYTE *chipBuf[NUM_BUFFERS]; struct IOExtTD * diskIO[NUM_BUFFERS]; // Helper to encode and fire a buffer async void sendBuffer(int slot, LONG bytes) { UWORD *wordBuf = (UWORD *)chipBuf[slot]; LONG wordCount = bytes / 2; for (LONG i = 0; i < wordCount; i++) wordBuf[i] = wordBuf[i] | 0x8080; diskIO[slot]->iotd_Req.io_Command = ETD_RAWWRITE; diskIO[slot]->iotd_Req.io_Flags = 0; diskIO[slot]->iotd_Count = 0xFFFFFFFF; diskIO[slot]->iotd_Req.io_Data = chipBuf[slot]; diskIO[slot]->iotd_Req.io_Length = bytes; diskIO[slot]->iotd_Req.io_Offset = 0; SendIO((struct IORequest *)diskIO[slot]); } // Entry of our code int main(int argc, char **argv) { struct MsgPort *diskPort = NULL; BPTR fh = NULL; LONG err = 0; LONG track = 0; LONG bytesRead; LONG totalBytes = 0; struct IOExtTD *diskIOSingle; // Initialize SysBase from absolute address 4 SysBase = *((struct ExecBase **)4); // Open DOS library DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 0); if (!DOSBase) return 1; Printf("FloppyAudio - Raw audio streamer\n"); Printf("Opening trackdisk.device unit %ld...\n", (LONG)DRIVE_UNIT); diskPort = CreateMsgPort(); if (!diskPort) { Printf("ERROR: Failed to create message port\n"); err = 1; goto doneit; } // Open trackdisk.device unit 1 (DF1:) if (OpenDevice(TD_NAME, DRIVE_UNIT, (struct IORequest *)diskIOSingle, 0)) { Printf("ERROR: Failed to open trackdisk.device unit %ld\n", (LONG)DRIVE_UNIT); err = 3; goto doneit; } Printf("trackdisk.device opened OK\n"); // Allocate both buffers and IO requests for (int i = 0; i < NUM_BUFFERS; i++) { chipBuf[i] = AllocMem(CHIP_BUF_SIZE, MEMF_CHIP | MEMF_CLEAR); Printf("2"); diskIO[i] = (struct IOExtTD *)CreateIORequest(diskPort, sizeof(struct IOExtTD)); Printf("3"); diskIO[i]->iotd_Req.io_Device = diskIOSingle->iotd_Req.io_Device; Printf("4"); diskIO[i]->iotd_Req.io_Unit = diskIOSingle->iotd_Req.io_Unit; } Printf("CHIP buffers allocated"); // Open raw audio file Printf("Opening audio.raw...\n"); fh = Open("8bitpcm.raw", MODE_OLDFILE); if (!fh) { Printf("ERROR: Failed to open audio.raw\n"); err = 5; goto doneit; } Printf("File opened OK, streaming...\n"); // Load entire file into chip RAM upfront LONG fileSize = 1024*1024; // 1M of it UBYTE *audioData = AllocMem(fileSize, MEMF_CHIP | MEMF_CLEAR); Read(fh, audioData, fileSize); // Pre-encode all MSB sync bits UWORD *wordBuf = (UWORD *)audioData; LONG wordCount = fileSize / 2; for (LONG i = 0; i < wordCount; i++) wordBuf[i] = wordBuf[i] | 0x0101; // Stream directly from chip RAM UBYTE *ptr = audioData; UBYTE *end = audioData + fileSize; // Prime both buffers before the loop diskIO[0]->iotd_Req.io_Command = ETD_RAWWRITE; diskIO[0]->iotd_Req.io_Flags = 0; diskIO[0]->iotd_Count = 0xFFFFFFFF; diskIO[0]->iotd_Req.io_Data = ptr; diskIO[0]->iotd_Req.io_Length = CHIP_BUF_SIZE; diskIO[0]->iotd_Req.io_Offset = 0; SendIO((struct IORequest *)diskIO[0]); ptr += CHIP_BUF_SIZE; diskIO[1]->iotd_Req.io_Command = ETD_RAWWRITE; diskIO[1]->iotd_Req.io_Flags = 0; diskIO[1]->iotd_Count = 0xFFFFFFFF; diskIO[1]->iotd_Req.io_Data = ptr; diskIO[1]->iotd_Req.io_Length = CHIP_BUF_SIZE; diskIO[1]->iotd_Req.io_Offset = 0; SendIO((struct IORequest *)diskIO[1]); ptr += CHIP_BUF_SIZE; // Prevent DF0 being accessed while we do this struct MsgPort *dummyPort; struct IOExtTD *dummyIO; dummyPort = CreateMsgPort(); dummyIO = (struct IOExtTD *)CreateIORequest(dummyPort, sizeof(struct IOExtTD)); // Open DF0: just to hold it OpenDevice(TD_NAME, 0, (struct IORequest *)dummyIO, 0); // CMD_STOP queues a stop - prevent *any* other use of the floppy bus dummyIO->iotd_Req.io_Command = CMD_STOP; DoIO((struct IORequest *)dummyIO); int cur = 0; int next = 1; while (ptr < end) { LONG remaining = end - ptr; LONG chunkSize = (remaining > CHIP_BUF_SIZE) ? CHIP_BUF_SIZE : remaining; // Wait for oldest request WaitIO((struct IORequest *)diskIO[cur]); if (diskIO[cur]->iotd_Req.io_Error) { Printf("ERROR: %ld\n", (LONG)diskIO[cur]->iotd_Req.io_Error); break; } // Requeue it immediately for the next chunk diskIO[cur]->iotd_Req.io_Command = ETD_RAWWRITE; diskIO[cur]->iotd_Req.io_Flags = 0; diskIO[cur]->iotd_Count = 0xFFFFFFFF; diskIO[cur]->iotd_Req.io_Data = ptr; diskIO[cur]->iotd_Req.io_Length = chunkSize; diskIO[cur]->iotd_Req.io_Offset = 0; SendIO((struct IORequest *)diskIO[cur]); ptr += chunkSize; cur ^= 1; next ^= 1; } WaitIO((struct IORequest *)diskIO[cur]); WaitIO((struct IORequest *)diskIO[next]); FreeMem(audioData, fileSize); // Release DF0: dummyIO->iotd_Req.io_Command = CMD_START; DoIO((struct IORequest *)dummyIO); CloseDevice((struct IORequest *)dummyIO); DeleteIORequest((struct IORequest *)dummyIO); DeleteMsgPort(dummyPort); Printf("\nStreaming complete. %ld tracks, %ld bytes total.\n", track, totalBytes); doneit: if (fh) Close(fh); if (diskIOSingle) { CloseDevice((struct IORequest *)diskIOSingle); DeleteIORequest((struct IORequest *)diskIOSingle); } if (diskPort) DeleteMsgPort(diskPort); if (err) Printf("Exited with error code %ld\n", err); return err; } __attribute__((used)) __attribute__((section(".text.unlikely"))) void _start(int argc, char **argv) { main(argc,argv); }

Amiga Trackdisk.device (Less OS Legal)

The next iteration was an improvement over the previous, although in a slightly less legal way. This version locks out all of the floppy drives to stop the Amiga OS from seeking/checking for disks etc meaning we have exclusive access to them without disabling multitasking.

This means we can go back to controlling the floppy disk DMA ourselves and the result is much better. The only downside is it makes all the drives "busy" while you play back the audio.

Amiga C++ Trackdisk.device Less OS/Safe DMA Version
#include <exec/types.h> #include <exec/memory.h> #include <exec/tasks.h> #include <devices/trackdisk.h> #include <hardware/custom.h> #include <hardware/dmabits.h> #include <hardware/intbits.h> #include <proto/exec.h> #include <proto/dos.h> #include <hardware/cia.h> #define CIABPRB (*(volatile UBYTE *)0xBFD100) #define DSKPTH (*(volatile UWORD *)0xDFF020) #define DSKPTL (*(volatile UWORD *)0xDFF022) #define DSKLEN (*(volatile UWORD *)0xDFF024) #define DMACON (*(volatile UWORD *)0xDFF096) #define INTREQ (*(volatile UWORD *)0xDFF09C) #define INTREQR (*(volatile UWORD *)0xDFF01E) #define CHUNK_SIZE 32766 #define NUM_DRIVES 4 struct ExecBase *SysBase; struct DosLibrary *DOSBase; #define custom (*(struct Custom *)0xDFF000) struct MsgPort *drivePort[NUM_DRIVES]; struct IOExtTD *driveIO[NUM_DRIVES]; BOOL driveOpen[NUM_DRIVES]; void stopAllDrives(void) { int i; for (i = 0; i < NUM_DRIVES; i++) { if (driveOpen[i]) { driveIO[i]->iotd_Req.io_Command = CMD_STOP; DoIO((struct IORequest *)driveIO[i]); } } } void startAllDrives(void) { int i; for (i = 0; i < NUM_DRIVES; i++) { if (driveOpen[i]) { driveIO[i]->iotd_Req.io_Command = CMD_START; DoIO((struct IORequest *)driveIO[i]); } } } void closeAllDrives(void) { int i; for (i = 0; i < NUM_DRIVES; i++) { if (driveOpen[i]) { CloseDevice((struct IORequest *)driveIO[i]); driveOpen[i] = FALSE; } if (driveIO[i]) { DeleteIORequest((struct IORequest *)driveIO[i]); driveIO[i] = NULL; } if (drivePort[i]) { DeleteMsgPort(drivePort[i]); drivePort[i] = NULL; } } } int main(int argc, char **argv) { int i; LONG err = 0; BPTR fh = NULL; UBYTE *audioData = NULL; SysBase = *((struct ExecBase **)4); DOSBase = (struct DosLibrary *)OpenLibrary("dos.library", 0); if (!DOSBase) return 1; for (i = 0; i < NUM_DRIVES; i++) { drivePort[i] = NULL; driveIO[i] = NULL; driveOpen[i] = FALSE; } // Open and CMD_STOP all drives for (i = 0; i < NUM_DRIVES; i++) { drivePort[i] = CreateMsgPort(); if (!drivePort[i]) { Printf("Can't create port %ld\n", (LONG)i); continue; } driveIO[i] = (struct IOExtTD *)CreateIORequest(drivePort[i], sizeof(struct IOExtTD)); if (!driveIO[i]) { Printf("Can't create IO %ld\n", (LONG)i); continue; } if (OpenDevice(TD_NAME, i, (struct IORequest *)driveIO[i], 0) == 0) { driveOpen[i] = TRUE; Printf("Opened DF%ld\n", (LONG)i); } else { Printf("DF%ld not available\n", (LONG)i); } } // Load audio file fh = Open("8bitpcm.raw", MODE_OLDFILE); if (!fh) { Printf("Can't open file\n"); err = 1; goto doneit; } LONG fileSize = 1024 * 1024; audioData = AllocMem(fileSize, MEMF_CHIP | MEMF_CLEAR); if (!audioData) { Printf("Can't alloc chip RAM\n"); err = 2; goto doneit; } Read(fh, audioData, fileSize); Close(fh); fh = NULL; // Pre-encode sync bits UWORD *wordBuf = (UWORD *)audioData; LONG wordCount = fileSize / 2; for (i = 0; i < wordCount; i++) wordBuf[i] = wordBuf[i] | 0x0101; // Boost priority and stop all drives struct Task *myTask = FindTask(NULL); BYTE oldPri = SetTaskPri(myTask, 1); stopAllDrives(); // Stream direct to hardware UBYTE *ptr = audioData; UBYTE *end = audioData + fileSize; // Reset flags Printf("Reset CIA\n"); CIABPRB = 0xFF; // Short delay for drive to settle for (volatile int i = 0; i < 1000; i++); // Enable motor Printf("Reset Enable Motor Line\n"); // Short delay for drive to settle for (volatile int i = 0; i < 1000; i++); // Select DF1 Printf("Select DF1\n"); CIABPRB = 0xFF ^ (CIAF_DSKSEL1); // Short delay for drive to settle for (volatile int i = 0; i < 1000; i++); Printf("Playback starting\n"); DSKLEN = 0x4000; DMACON = DMAF_SETCLR | DMAF_MASTER | DMAF_DISK; DSKLEN = 0x4000; WORD first = 1; while (ptr < end) { LONG remaining = end - ptr; LONG chunkSize = (remaining > CHUNK_SIZE) ? CHUNK_SIZE : remaining; UWORD dsklen = (chunkSize / 2) | 0xC000; ULONG dskptr = ((ULONG)ptr) & 0xFFFFFFFE; // Wait for previous transfer to finish fetching from RAM if (!first) { while (!(INTREQR & INTF_DSKBLK)); INTREQ = INTF_DSKBLK; } else first = 0; // Immediately reload DSKPTH = (UWORD)(dskptr >> 16); DSKPTL = (UWORD)(dskptr & 0xFFFF); DSKLEN = dsklen; DSKLEN = dsklen; ptr += chunkSize; } // Short delay for drive to settle for (volatile int i = 0; i < 1000; i++); CIABPRB = 0x7F; // Short delay for drive to settle for (volatile int i = 0; i < 1000; i++); CIABPRB = 0xFF; // Restore startAllDrives(); SetTaskPri(myTask, oldPri); Printf("Done.\n"); doneit: if (fh) Close(fh); if (audioData) FreeMem(audioData, fileSize); closeAllDrives(); if (DOSBase) CloseLibrary((struct Library *)DOSBase); return err; } __attribute__((used)) __attribute__((section(".text.unlikely"))) void _start(int argc, char **argv) { main(argc,argv); }

V2 - Pi Pico

I wanted to increase the quality and get away from this silly 7-bit playback. I realised that the data being clocked out of Paula would be at a very precise interval, but the Arduino just didnt have the CPU clock speed to measure it properly. So, I switched to the Pi Pico.

The code for this uses a small PIO program to simulate the input capture used by the Arduino so once again I could measure the time between pulses. On the arduino you got 32 clock ticks between each pulse. With the Pi Pico, you end up with around 266! Quite the improvement.

Arduino Uno Schematic - Click to Enlarge

Because of the increase in accuracy I was sure I'd be able to track every single bit without needing additional clock bits. To do this though I would ned to measure the actual clock time, which I did by starting every transfer with two specific WORDs

Encoding (bits): 00000000 00000001 00000000 00000001 dddddddd dddddddd dddddddd dddddddd

By timing how long it takes to get between those two '1's we then know how long 16-bits of data take, so dividing by 16 and we have a good idea of how long a single bit takes. After the second '1' is received, the receiver is also armed to read the rest of the data.

The Pi Pico is actual a dual-core device, so I opted to use the main core to monitor the Amiga floppy disk lines, and the second core to handle the SPI communication to the Digital to Analog converter LTC2602 device, which supports two 16-bit channels!

Now that the data had no clock bits in it, it was simply a matter of extracting every WORD for 16-bit MONO, and every byte pair for 8-bit stereo. The AHI driver however output SIGNED data, not unsigned, so to take the conversion burden (it's not much) off the Amiga, I opted to let the Pi Pico do it.

Arduino Uno Setup - Click to Enlarge
PI Pico Source Code:
#include <stdio.h> #include <SPI.h> #include "hardware/timer.h" #include "hardware/structs/systick.h" #include "pico/stdlib.h" #include "pico/multicore.h" #include "hardware/pio.h" #include "hardware/clocks.h" // Pin definitions #define CS_PIN 17 // SPI CS #define DATA_PIN 15 // Bitstream input #define SPI_SCK 18 #define SPI_MOSI 19 #define PIN_SELECT 11 #define PIN_SIDE 12 // Floppy disk side #define PIN_WRITE_DATA 14 // Reads RAW floppy data on this pin. #define PIN_WRITE_GATE 15 // Enable RAW floppy data on this pin. //#define PIN_LED 25 #define DAC_A 0 #define DAC_B 1 #define DAC_BOTH 15 #define CMD_WRITEUPDATE 48 PIO pio = pio0; uint sm = 0; // Core 1 - handles SPI output to LTC2602 void core1_entry() { // Setup SPI spi_init(spi0, 8000000); spi_set_format(spi0, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); gpio_set_function(SPI_SCK, GPIO_FUNC_SPI); gpio_set_function(SPI_MOSI, GPIO_FUNC_SPI); gpio_init(CS_PIN); gpio_set_dir(CS_PIN, GPIO_OUT); gpio_put(CS_PIN, 1); uint8_t buffer[3] = {CMD_WRITEUPDATE | DAC_BOTH, 0, 0}; while (1) { uint32_t sample = multicore_fifo_pop_blocking(); if (sample & 0x80000) { // 15-bit mono sample ^= 0x8000; // signed/unsigned conversion buffer[0] = CMD_WRITEUPDATE | DAC_BOTH; buffer[1] = sample >> 8; buffer[2] = sample & 0xFF; } else { buffer[0] = CMD_WRITEUPDATE | DAC_A; buffer[1] = ((sample >> 8) & 0xFF) ^ 0x80; buffer[2] = 0; gpio_put(CS_PIN, 0); spi_write_blocking(spi0, buffer, 3); gpio_put(CS_PIN, 1); buffer[0] = CMD_WRITEUPDATE | DAC_B; buffer[1] = (sample & 0xFF) ^ 0x80; buffer[2] = 0; } gpio_put(CS_PIN, 0); spi_write_blocking(spi0, buffer, 3); gpio_put(CS_PIN, 1); } } void setup() { set_sys_clock_khz(133000, true); Serial.begin(115200); gpio_init(PIN_WRITE_GATE); gpio_set_dir(PIN_WRITE_GATE, GPIO_IN); gpio_init(PIN_WRITE_DATA); gpio_set_dir(PIN_WRITE_DATA, GPIO_IN); gpio_init(PIN_SIDE); gpio_set_dir(PIN_SIDE, GPIO_IN); gpio_init(PIN_DIR); gpio_set_dir(PIN_DIR, GPIO_IN); gpio_init(PIN_SELECT); gpio_set_dir(PIN_SELECT, GPIO_IN); gpio_init(PIN_LED); gpio_set_dir(PIN_LED, GPIO_OUT); delay(2000); } // Capture and playback void capture(bool stereo) { while (!pio_sm_is_rx_fifo_empty(pio, sm)) pio_sm_get(pio, sm); // flush anything else // The Sync bits are the LSB of each block as they arrive first // Read initial SYNC - NOTE the value from pio_sm_get_blocking COUNTS DOWN uint32_t lastTime = pio_sm_get_blocking(pio, sm); uint32_t nextTime = pio_sm_get_blocking(pio, sm); // Time between bits - should be around 131 ish uint32_t bitPeriod = (lastTime-nextTime) / 16; if ((bitPeriod < 110) || (bitPeriod > 145)) { // This is wrong and means there was a spurious bit from somewhere or something lastTime = nextTime; nextTime = pio_sm_get_blocking(pio, sm); bitPeriod = (lastTime-nextTime) / 16; } lastTime = nextTime; uint32_t data = 0; const int32_t bias = bitPeriod / 2; uint32_t runningCounter = 0; uint32_t totalTime = 0; uint32_t stereoMarker = stereo ? 0x80000 : 0; uint32_t samplesPlayed = 0; while (runningCounter < 20) { if (!gpio_get(PIN_WRITE_GATE)) runningCounter = 0; else runningCounter++; // Bit detected if (!pio_sm_is_rx_fifo_empty(pio, sm)) { uint32_t curr = pio_sm_get_blocking(pio, sm); // Work out what bit this should be uint32_t diff = lastTime - curr; totalTime += diff; lastTime = curr; uint32_t bit = (totalTime - bias) / bitPeriod; if (bit >= 15) { if (samplesPlayed && samplesPlayed<16380) { multicore_fifo_push_blocking(data | stereoMarker); } samplesPlayed++; // This will go wrong if theres a lot of silence if (bit > 15) { bit &= 0x0F; data = 1 << (15-bit); totalTime = bitPeriod * (bit+1); } else { totalTime = 0; data = 0; } } else { data |= 1 << (15-bit); } } } } // PIO program - counts cycles between falling edges // X decrements every 2 cycles, push value on falling edge static const uint16_t capture_program_instructions[] = { (uint16_t)pio_encode_jmp_x_dec(1), // decrement X, jump to instruction 1 (uint16_t)pio_encode_jmp_pin(0), // if pin HIGH, jump back to 0 (keep waiting) // got falling edge - pin is LOW and fell through (uint16_t)pio_encode_mov(pio_isr, pio_x), (uint16_t)pio_encode_push(false, false), // wait for rising edge (uint16_t)pio_encode_jmp_pin(0), // pin HIGH, jump to top (uint16_t)pio_encode_jmp_x_dec(4), // still LOW, decrement and loop }; static const struct pio_program capture_program = { .instructions = capture_program_instructions, .length = 6, .origin = -1}; void setup_pio(uint pin) { uint offset = pio_add_program(pio, &capture_program); pio_sm_config c = pio_get_default_sm_config(); sm_config_set_wrap(&c, offset, offset + capture_program.length - 1); sm_config_set_in_pins(&c, pin); sm_config_set_jmp_pin(&c, pin); // Set clock divider to 1 for maximum resolution sm_config_set_clkdiv(&c, 1.0f); // Shift ISR left, no autopush sm_config_set_in_shift(&c, false, false, 32); pio_gpio_init(pio, pin); gpio_set_dir(pin, GPIO_IN); pio_sm_init(pio, sm, offset, &c); // Initialise X to 0xFFFFFFFF pio_sm_exec(pio, sm, pio_encode_set(pio_x, 0)); pio_sm_exec(pio, sm, pio_encode_mov_not(pio_x, pio_x)); pio_sm_set_enabled(pio, sm, true); } // Core 0 - handles bitstream input void loop() { multicore_launch_core1(core1_entry); setup_pio(PIN_WRITE_DATA); gpio_put(PIN_LED, 1); while (1) { // Wait for sync pulse if (!gpio_get(PIN_SELECT) && !gpio_get(PIN_WRITE_GATE)) { gpio_put(PIN_LED, 0); capture(gpio_get(PIN_SIDE)); gpio_put(PIN_LED, 1); } } }

AHI Retargetable Audio Driver

Originally I'd wanted to mod the Protracker source code, but 68k Assembly Language is just not something I have much experience with. I downloaded the source and examples for building an AHI driver, and spent *ages* trying to get one to compile.

In the end I borrowed the source code for the ZZ9000ax audio driver as a starting point, ripped out most of the code and replaced it with my own. The result was that it worked. It took me a while to get it stable too, and not just stable but functional.

The only downside is once the driver is active you can't access any floppy drives meaning if you press the VOLUMES button on some of the file selectors the dialog will hang. Oh well, this was just for fun...

AHI Retargetable Audio Driver Source Code:
#include <exec/types.h> #include <exec/exec.h> #include <exec/memory.h> #include <dos/dos.h> #include <dos/dostags.h> #include <exec/interrupts.h> #include <hardware/intbits.h> #include <devices/trackdisk.h> #include <hardware/dmabits.h> #include <hardware/cia.h> #include <exec/tasks.h> #include <dos/dosextens.h> #include <proto/exec.h> #include <proto/dos.h> #include <proto/intuition.h> #include <proto/utility.h> #include <proto/expansion.h> #include <clib/debug_protos.h> #include "floppyaudio.h" #include "ahi_sub.h" #include "ahi_sub_protos.h" #include <math.h> #include <stdint.h> #define STR(s) #s #define XSTR(s) STR(s) #define DEVICE_NAME "floppyaudio.audio" #define DEVICE_DATE "(27.03.2026)" #define DEVICE_VERSION 4 #define DEVICE_REVISION 1 #define DEVICE_ID_STRING "FloppyAudio " XSTR(DEVICE_VERSION) "." XSTR(DEVICE_REVISION) " " DEVICE_DATE #define DEVICE_PRIORITY 0 struct ExecBase *SysBase; struct UtilityBase *UtilityBase; struct Library *AHIsubBase = NULL; struct DosLibrary *DOSBase = NULL; struct DriverBase *driverBase; int __attribute__((no_reorder)) _start() { return -1; } asm("romtag: \n" " dc.w "XSTR(RTC_MATCHWORD)" \n" " dc.l romtag \n" " dc.l endcode \n" " dc.b "XSTR(RTF_AUTOINIT)" \n" " dc.b "XSTR(DEVICE_VERSION)" \n" " dc.b "XSTR(NT_LIBRARY)" \n" " dc.b "XSTR(DEVICE_PRIORITY)" \n" " dc.l _device_name \n" " dc.l _device_id_string \n" " dc.l _auto_init_tables \n" "endcode: \n"); const char device_name[] = DEVICE_NAME; const char device_id_string[] = DEVICE_ID_STRING; static uint32_t __attribute__((used)) init(BPTR seg_list asm("a0"), struct Library *dev asm("d0")) { SysBase = *(struct ExecBase **)4L; if (!(DOSBase = (struct DosLibrary *)OpenLibrary((STRPTR)"dos.library", 0))) return 0; if (!(UtilityBase = (struct UtilityBase *)OpenLibrary((STRPTR)"utility.library", 0))) return 0; struct MsgPort* drivePort = CreateMsgPort(); if (!drivePort) return FALSE; struct IOExtTD* io = (struct IOExtTD *)CreateIORequest(drivePort, sizeof(struct IOExtTD)); if (!io) { DeleteMsgPort(drivePort); return FALSE; } BOOL ret = OpenDevice((CONST_STRPTR)TD_NAME, 1, (struct IORequest *)io, 0) == 0; if (ret) CloseDevice((struct IORequest *)io); DeleteIORequest(io); DeleteMsgPort(drivePort); return ret ? (uint32_t)dev : 0; } static uint8_t* __attribute__((used)) expunge(struct Library *libbase asm("a6")) { if(DOSBase) { CloseLibrary((struct Library *)DOSBase); DOSBase = NULL; } if(UtilityBase) { CloseLibrary((struct Library *)UtilityBase); UtilityBase = NULL; } return 0; } static uint8_t __attribute__((used)) null() { return 0; } static void __attribute__((used)) open(struct Library *dev asm("a6"), struct IORequest *iotd asm("a1"), uint32_t num asm("d0"), uint32_t flags asm("d1")) { iotd->io_Error = 0; dev->lib_OpenCnt++; } static uint8_t* __attribute__((used)) close(struct Library *dev asm("a6"), struct IORequest *iotd asm("a1")) { return 0; } static void __attribute__((used)) begin_io(struct Library *dev asm("a6"), struct IORequest *io asm("a1")) { if (io == NULL) return; if (!(io->io_Flags & IOF_QUICK)) ReplyMsg(&io->io_Message); } static uint32_t __attribute__((used)) abort_io(struct Library *dev asm("a6"), struct IORequest *io asm("a1")) { if (!io) return IOERR_NOCMD; io->io_Error = IOERR_ABORTED; return IOERR_ABORTED; } static uint32_t __attribute__((used)) SoundFunc(struct Hook *hook asm("a0"), struct AHIAudioCtrlDrv *actrl asm("a2"), struct AHISoundMessage *chan asm("a1")) { return 0; } // cleanup! static void MixerCleanup(struct FloppyData* floppyData, struct AHIAudioCtrlDrv* AudioCtrl, struct ExecBase* SysBase) { // Stop Paula DMA DSKLEN = 0x4000; DMACON = DMAF_DISK; Delay(1); // De-Select DF1 CIABPRB = 0xFF; Delay(1); CIABPRB = floppyData->oldCIA; for (UWORD i=0; i<4; i++) { if (floppyData->drives[i].isOpen) { floppyData->drives[i].io->iotd_Req.io_Command = CMD_START; DoIO((struct IORequest *)floppyData->drives[i].io); CloseDevice((struct IORequest *)floppyData->drives[i].io); floppyData->drives[i].isOpen = FALSE; } if (floppyData->drives[i].io) { DeleteIORequest(floppyData->drives[i].io); floppyData->drives[i].io = NULL; } if (floppyData->drives[i].port) { DeleteMsgPort(floppyData->drives[i].port); floppyData->drives[i].port = NULL; } } if (floppyData->mixerAbort != -1) { FreeSignal(floppyData->mixerAbort); floppyData->mixerAbort = -1; } Forbid(); Signal((struct Task*)floppyData->mainTask, 1L << floppyData->mixerReady); floppyData->mixerTask = NULL; } void Mixer() { SysBase = *((struct ExecBase**)4); struct Process* proc = (struct Process *)FindTask(NULL); struct FloppyData* floppyData = (struct FloppyData*)proc->pr_Task.tc_UserData; struct AHIAudioCtrlDrv* AudioCtrl = floppyData->audioCtrl; floppyData->oldCIA = CIABPRB; floppyData->mixerAbort = AllocSignal(-1); if(floppyData->mixerAbort==-1) { MixerCleanup(floppyData, AudioCtrl, SysBase); return; } Signal((struct Task*) floppyData->mainTask, 1L << floppyData->mixerReady); for (UWORD i=0; i<4; i++) { floppyData->drives[i].port = CreateMsgPort(); if (!floppyData->drives[i].port) { MixerCleanup(floppyData, AudioCtrl, SysBase); return; } floppyData->drives[i].io = (struct IOExtTD*)CreateIORequest(floppyData->drives[i].port, sizeof(struct IOExtTD)); if (!floppyData->drives[i].io) { MixerCleanup(floppyData, AudioCtrl, SysBase); return; } floppyData->drives[i].isOpen = OpenDevice((CONST_STRPTR)TD_NAME, i, (struct IORequest *)floppyData->drives[i].io, 0) == 0; if (floppyData->drives[i].isOpen) { floppyData->drives[i].io->iotd_Req.io_Command = CMD_STOP; DoIO((struct IORequest *)floppyData->drives[i].io); } } floppyData->oldCIA = CIABPRB; CIABPRB = 0xFF; Delay(1); UBYTE CIAValue = 0xFF ^ CIAF_DSKMOTOR; CIABPRB = CIAValue; Delay(1); CIAValue&= ~CIAF_DSKSEL1; CIABPRB = CIAValue; Delay(1); if (floppyData->audioid == FLOPPYAUDIO_ID_STEREO) { CIAValue&= ~CIAF_DSKSIDE; CIABPRB = CIAValue; Delay(1); } floppyData->bufferPlaying = 0; floppyData->bufferWritePosition = floppyData->dmaBuffers[1]; floppyData->bufferWritePosition += 2; floppyData->bufferWordsLeft = DMA_BUF_WORDS-2; BOOL firstRun = TRUE; Forbid(); DSKLEN = 0x4000; DMACON = DMAF_SETCLR | DMAF_MASTER | DMAF_DISK; DSKLEN = 0x4000; Permit(); WORD* src; struct MsgPort* timerPort = CreateMsgPort(); struct timerequest* timerIO = (struct timerequest*)CreateIORequest(timerPort, sizeof(struct timerequest)); OpenDevice(TIMERNAME, UNIT_MICROHZ, (struct IORequest*)timerIO, 0); for (;;) { ULONG signals = SetSignal(0L,0L); if (signals & (SIGBREAKF_CTRL_C | (1L<<floppyData->mixerAbort))) break; if (floppyData->playing) { timerIO->tr_node.io_Command = TR_ADDREQUEST; timerIO->tr_time.tv_secs = 0; ULONG playerFreq = AudioCtrl->ahiac_PlayerFreq >> 16; if (playerFreq > 0) { ULONG microSeconds = 1000000 / playerFreq; timerIO->tr_time.tv_micro = (microSeconds * 3) / 4; } else timerIO->tr_time.tv_micro = 15000; DoIO((struct IORequest*)timerIO); CallHookPkt(AudioCtrl->ahiac_PlayerFunc, AudioCtrl, NULL); if (!(*AudioCtrl->ahiac_PreTimer)()) { if (playerFreq > 0) { AudioCtrl->ahiac_BuffSamples = (AudioCtrl->ahiac_MixFreq + playerFreq -1) / playerFreq; } else AudioCtrl->ahiac_BuffSamples = 624; CallHookPkt(AudioCtrl->ahiac_MixerFunc, AudioCtrl, floppyData->mixBuffer); UWORD samplesLeft = (UWORD)AudioCtrl->ahiac_BuffSamples; UWORD* inBuffer = (UWORD*)floppyData->mixBuffer; while (samplesLeft) { UWORD samplesToCopy = samplesLeft > floppyData->bufferWordsLeft ? floppyData->bufferWordsLeft : samplesLeft; samplesLeft -= samplesToCopy; floppyData->bufferWordsLeft -= samplesToCopy; switch (floppyData->audioid) { case FLOPPYAUDIO_ID_MONO: CopyMem(inBuffer, floppyData->bufferWritePosition, samplesToCopy<<1); floppyData->bufferWritePosition += samplesToCopy; inBuffer += samplesToCopy; break; case FLOPPYAUDIO_ID_STEREO: src = (WORD*)inBuffer; while (samplesToCopy--) { UWORD l = (UWORD)*src++ & 0xFF00; UWORD r = (UWORD)*src++ >> 8; *floppyData->bufferWritePosition++ = l | r; } inBuffer = (UWORD*)src; break; default: samplesLeft = 0; break; } if (floppyData->bufferWordsLeft == 0) { ULONG dskptr = (ULONG)floppyData->dmaBuffers[1^floppyData->bufferPlaying]; UWORD dsklen = DMA_BUF_WORDS | 0xC000; floppyData->dmaBuffers[1^floppyData->bufferPlaying][0] = 0x0001; floppyData->dmaBuffers[1^floppyData->bufferPlaying][1] = 0x0001; if (!firstRun) { Forbid(); while (!(INTREQR & INTF_DSKBLK)) {}; INTREQ = INTF_DSKBLK; DSKPTH = (UWORD)(dskptr >> 16); DSKPTL = (UWORD)(dskptr & 0xFFFF); DSKLEN = dsklen; DSKLEN = dsklen; Permit(); } else { firstRun = FALSE; DSKPTH = (UWORD)(dskptr >> 16); DSKPTL = (UWORD)(dskptr & 0xFFFF); DSKLEN = dsklen; DSKLEN = dsklen; } floppyData->bufferWritePosition = &floppyData->dmaBuffers[floppyData->bufferPlaying][2]; floppyData->bufferPlaying ^= 1; floppyData->bufferWordsLeft = DMA_BUF_WORDS-2; } } (*AudioCtrl->ahiac_PostTimer)(); WaitIO((struct IORequest*)timerIO); } else Delay(1); } else Delay(1); } AbortIO((struct IORequest*)timerIO); WaitIO((struct IORequest*)timerIO); CloseDevice((struct IORequest*)timerIO); DeleteIORequest(timerIO); DeleteMsgPort(timerPort); MixerCleanup(floppyData, AudioCtrl, SysBase); } static uint32_t __attribute__((used)) intAHIsub_AllocAudio(struct TagItem *tagList asm("a1"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { ULONG audioid = tagList ? GetTagData(AHIDB_AudioID, FLOPPYAUDIO_ID_MONO, tagList) : FLOPPYAUDIO_ID_MONO; struct FloppyData* floppyData = (struct FloppyData*)AllocVec(sizeof(struct FloppyData), MEMF_CLEAR | MEMF_PUBLIC); if(floppyData == NULL) return AHISF_ERROR; AudioCtrl->ahiac_DriverData = floppyData; AudioCtrl->ahiac_MixFreq = FLOPPYAUDIO_SAMPLE_RATE; floppyData->audioid = audioid; floppyData->mixerReady = AllocSignal(-1); floppyData->mainTask = (struct Process*) FindTask(NULL); floppyData->mixerAbort = -1; floppyData->playing = FALSE; struct TagItem proctags[] = { { NP_Entry, (ULONG) Mixer }, { NP_Name, (ULONG) device_name }, { NP_Priority, 10 }, { TAG_DONE, 0 } }; floppyData->mixBuffer = AllocVec(65536 * 4UL, MEMF_ANY | MEMF_PUBLIC); floppyData->audioCtrl = AudioCtrl; if(!floppyData->mixBuffer) return AHIE_NOMEM; for (UWORD i=0; i<NUM_BUFFERS; i++) { floppyData->dmaBuffers[i] = (UWORD*)AllocMem(DMA_BUF_BYTES, MEMF_CHIP | MEMF_CLEAR); if (!floppyData->dmaBuffers[i]) return AHIE_NOMEM; } Forbid(); floppyData->mixerTask = CreateNewProc(proctags); if (floppyData->mixerTask) floppyData->mixerTask->pr_Task.tc_UserData = floppyData; Permit(); if (!floppyData->mixerTask) return AHIE_NOMEM; Wait(1L << floppyData->mixerReady); if(!floppyData->mixerTask) return AHIE_UNKNOWN; return AHISF_MIXING | (audioid == FLOPPYAUDIO_ID_STEREO ? AHISF_KNOWSTEREO : 0); } static void __attribute__((used)) intAHIsub_FreeAudio(struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { if (AudioCtrl->ahiac_DriverData) { struct FloppyData* floppyData = (struct FloppyData*)AudioCtrl->ahiac_DriverData; if (floppyData->mixerTask) { if (floppyData->mixerAbort!=-1) Signal((struct Task*)floppyData->mixerTask, 1L << floppyData->mixerAbort); Wait(1L << floppyData->mixerReady); floppyData->mixerTask = NULL; } if (floppyData->mixerReady!=-1) FreeSignal(floppyData->mixerReady); if (floppyData->mixBuffer) { FreeVec(floppyData->mixBuffer); floppyData->mixBuffer = NULL; } for (UWORD i=0; i<NUM_BUFFERS; i++) if (floppyData->dmaBuffers[i]) { FreeMem(floppyData->dmaBuffers[i], DMA_BUF_BYTES); floppyData->dmaBuffers[i] = NULL; } FreeVec(AudioCtrl->ahiac_DriverData); AudioCtrl->ahiac_DriverData = NULL; } } static void __attribute__((used)) intAHIsub_Stop(uint32_t Flags asm("d0"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { if(Flags & AHISF_PLAY) { struct FloppyData* floppyData = (struct FloppyData*)AudioCtrl->ahiac_DriverData; if (!floppyData) return; floppyData->playing = FALSE; } } static uint32_t __attribute__((used)) intAHIsub_Start(uint32_t flags asm("d0"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { intAHIsub_Stop(flags, AudioCtrl); if (flags & AHISF_RECORD) return AHIE_UNKNOWN; if (flags & AHISF_PLAY) { struct FloppyData* floppyData = (struct FloppyData*)AudioCtrl->ahiac_DriverData; if (!floppyData) return AHIE_UNKNOWN; floppyData->playing = TRUE; return AHIE_OK; } return AHIE_UNKNOWN; } void __attribute__((used)) cintAHIsub_Enable(struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { // Permit(); // lazy } void __attribute__((used)) cintAHIsub_Disable(struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { // Forbid(); // lazy } static int32_t __attribute__((used)) intAHIsub_GetAttr(uint32_t attr_ asm("d0"), int32_t arg_ asm("d1"), int32_t def_ asm("d2"), struct TagItem *tagList asm("a1"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { uint32_t attr = attr_; int32_t arg = arg_, def = def_; switch(attr) { case AHIDB_Bits: return 16; case AHIDB_Frequencies: return 1; case AHIDB_Frequency: return FLOPPYAUDIO_SAMPLE_RATE; case AHIDB_Index: return 0; case AHIDB_Author: return (int32_t) "RobSmithDev"; case AHIDB_Copyright: return (int32_t) "Public Domain"; case AHIDB_Version: return (int32_t) device_id_string; case AHIDB_Annotation: return (int32_t) "https://robsmithdev.co.uk"; case AHIDB_Record: return FALSE; case AHIDB_FullDuplex: return FALSE; case AHIDB_Realtime: return TRUE; case AHIDB_MaxChannels: { ULONG id = tagList ? GetTagData(AHIDB_AudioID, FLOPPYAUDIO_ID_MONO, tagList) : FLOPPYAUDIO_ID_MONO; return (id == FLOPPYAUDIO_ID_STEREO) ? 2 : 1; } case AHIDB_Outputs: return 1; case AHIDB_Output: return (int32_t)"DF1:DAC/PICO"; default: return def; } } static int32_t __attribute__((used)) intAHIsub_HardwareControl(uint32_t attr asm("d0"), uint32_t arg asm("d1"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { return 0; } static uint32_t __attribute__((used)) intAHIsub_SetEffect(uint8_t *effect asm("a0"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { return AHIS_UNKNOWN; } static uint32_t __attribute__((used)) intAHIsub_LoadSound(uint16_t sound asm("d0"), uint32_t type asm("d1"), struct AHISampleInfo *info asm("a0"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { return AHIS_UNKNOWN; } static uint32_t __attribute__((used)) intAHIsub_UnloadSound(uint16_t sound asm("d0"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2")) { return AHIS_UNKNOWN; } static void __attribute__((used)) intAHIsub_Update(uint32_t flags asm("d0"), struct AHIAudioCtrlDrv *AudioCtrlDrv asm("a2")) { } static uint32_t __attribute__((used)) intAHIsub_SetVol(uint16_t channel asm("d0"), uint32_t volume asm("d1"), uint32_t pan asm("d2"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2"), uint32_t flags asm("d3")) { return AHIS_UNKNOWN; } static uint32_t __attribute__((used)) intAHIsub_SetFreq(uint16_t channel asm("d0"), uint32_t freq asm("d1"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2"), uint32_t flags asm("d2")) { return AHIS_UNKNOWN; } static uint32_t __attribute__((used)) intAHIsub_SetSound(uint16_t channel asm("d0"), uint16_t sound asm("d1"), uint32_t offset asm("d2"), int32_t length asm("d3"), struct AHIAudioCtrlDrv *AudioCtrl asm("a2"), uint32_t flags asm("d4")) { return AHIS_UNKNOWN; } extern void __attribute__((used)) intAHIsub_Enable(struct AHIAudioCtrlDrv *AudioCtrl asm("a2")); extern void __attribute__((used)) intAHIsub_Disable(struct AHIAudioCtrlDrv *AudioCtrl asm("a2")); static uint32_t function_table[] = { (uint32_t)open, (uint32_t)close, (uint32_t)expunge, (uint32_t)null, (uint32_t)intAHIsub_AllocAudio, // AllocAudio (uint32_t)intAHIsub_FreeAudio, // FreeAudio (uint32_t)intAHIsub_Disable, // Disable (uint32_t)intAHIsub_Enable, // Enable (uint32_t)intAHIsub_Start, // Start (uint32_t)intAHIsub_Update, // Update (uint32_t)intAHIsub_Stop, // Stop (uint32_t)intAHIsub_SetVol, // SetVol (uint32_t)intAHIsub_SetFreq, // SetFreq (uint32_t)intAHIsub_SetSound, // SetSound (uint32_t)intAHIsub_SetEffect, // SetEffect (uint32_t)intAHIsub_LoadSound, // LoadSound (uint32_t)intAHIsub_UnloadSound, // UnloadSound (uint32_t)intAHIsub_GetAttr, // GetAttr (uint32_t)intAHIsub_HardwareControl, // HardwareControl (uint32_t)null, (uint32_t)null, (uint32_t)null, -1 }; const uint32_t auto_init_tables[4] = { sizeof(struct Library), (uint32_t)function_table, 0, (uint32_t)init, };

Download It All!

You can download all of the above in a handy ZIP file. Why a ZIP? Well, you may want to try this on real hardware so you'll need to program up a Pi Pico or Arduino. I have to admit though, I'd be suprised if anyone else actually does build this.

Download revision2026.zip