Files
picogus/sw/sbdsp.cpp
T
2025-06-12 23:10:52 -06:00

677 lines
23 KiB
C++

#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "pico_pic.h"
/*
Title : SoundBlaster DSP Emulation
Date : 2023-12-30
Author : Kevin Moonlight <me@yyzkevin.com>
Copyright (C) 2023 Kevin Moonlight
Copyright (C) 2024 Ian Scott
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SPDX-License-Identifier: MIT
*/
extern uint LED_PIN;
#include "isa_dma.h"
#ifndef MAX
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
#endif
static irq_handler_t SBDSP_DMA_isr_pt;
static dma_inst_t dma_config;
#define DMA_PIO_SM 2
#define DSP_VERSION_MAJOR 2
#define DSP_VERSION_MINOR 1
// Sound Blaster DSP I/O port offsets
#define DSP_RESET 0x6
#define DSP_READ 0xA
#define DSP_WRITE 0xC
#define DSP_WRITE_STATUS 0xC
#define DSP_READ_STATUS 0xE
#define OUTPUT_SAMPLERATE 49716ul
// Sound Blaster DSP commands.
#define DSP_DMA_HS_SINGLE 0x91
#define DSP_DMA_HS_AUTO 0x90
#define DSP_DMA_ADPCM 0x7F //creative ADPCM 8bit to 3bit
#define DSP_DMA_SINGLE 0x14 //follosed by length
#define DSP_DMA_AUTO 0X1C //length based on 48h
#define DSP_DMA_BLOCK_SIZE 0x48 //block size for highspeed/dma
//#define DSP_DMA_DAC 0x14
#define DSP_DIRECT_DAC 0x10
#define DSP_DIRECT_ADC 0x20
#define DSP_MIDI_READ_POLL 0x30
#define DSP_MIDI_WRITE_POLL 0x38
#define DSP_SET_TIME_CONSTANT 0x40
#define DSP_DMA_PAUSE 0xD0
#define DSP_DAC_PAUSE_DURATION 0x80 // Pause DAC for a duration, then generate an interrupt. Used by Tyrian.
#define DSP_ENABLE_SPEAKER 0xD1
#define DSP_DISABLE_SPEAKER 0xD3
#define DSP_DMA_RESUME 0xD4
#define DSP_SPEAKER_STATUS 0xD8
#define DSP_IDENT 0xE0
#define DSP_VERSION 0xE1
#define DSP_WRITETEST 0xE4
#define DSP_READTEST 0xE8
#define DSP_SINE 0xF0
#define DSP_IRQ 0xF2
#define DSP_CHECKSUM 0xF4
// #define DSP_DMA_FIFO_SIZE 256
// constexpr uint16_t DSP_DMA_FIFO_BITS = DSP_DMA_FIFO_SIZE - 1;
#include "audio_fifo.h"
#define DSP_UNUSED_STATUS_BITS_PULLED_HIGH 0x7F
typedef struct sbdsp_t {
uint8_t inbox;
uint8_t outbox;
uint8_t test_register;
uint8_t current_command;
uint8_t current_command_index;
uint16_t dma_interval;
int16_t dma_interval_trim;
uint8_t dma_transfer_size;
audio_fifo_t audio_fifo;
// uint16_t dma_buffer[DSP_DMA_FIFO_SIZE];
// volatile uint16_t dma_buffer_tail;
// volatile uint16_t dma_buffer_head;
uint16_t dac_pause_duration;
uint8_t dac_pause_duration_low;
uint16_t dma_block_size;
uint32_t dma_sample_count;
uint32_t dma_sample_count_rx;
uint8_t time_constant;
uint16_t sample_rate;
// uint32_t sample_step;
// uint64_t cycle_us;
// uint64_t sample_offset;
// uint8_t sample_factor;
bool autoinit;
bool dma_enabled;
bool speaker_on;
volatile bool dav_pc;
volatile bool dav_dsp;
volatile bool dsp_busy;
bool dac_resume_pending;
uint8_t reset_state;
volatile int16_t cur_sample;
} sbdsp_t;
static sbdsp_t sbdsp;
// uint16_t sbdsp_fifo_level() {
// if(sbdsp.dma_buffer_tail < sbdsp.dma_buffer_head) return DSP_DMA_FIFO_SIZE - (sbdsp.dma_buffer_head - sbdsp.dma_buffer_tail);
// return sbdsp.dma_buffer_tail - sbdsp.dma_buffer_head;
// }
/*
bool __force_inline sbdsp_fifo_half() {
if(sbdsp_fifo_level() >= (DSP_DMA_FIFO_SIZE/2)) return true;
return false;
}
*/
static int16_t __force_inline dma_interval_trim() {
uint32_t level = fifo_level(&sbdsp.audio_fifo);
// if (level >= (AUDIO_FIFO_SIZE >> 1)) {
/*
if (level >= 768) {
// putchar('+');
return sbdsp.dma_interval_trim;
// return sbdsp.dma_interval_trim;
} else {
// putchar('-');
return -sbdsp.dma_interval_trim;
}
*/
// return (int32_t)(level >> 4) - 32;
// if(sbdsp_fifo_level() >= (DSP_DMA_FIFO_SIZE/2)) return true;
// return false;
}
static int16_t __force_inline dma_interval_calc() {
uint32_t level = fifo_level(&sbdsp.audio_fifo);
if (level < 512) {
return sbdsp.dma_interval - 5;
} else {
return sbdsp.dma_interval + 5;
}
// if (level >= (AUDIO_FIFO_SIZE >> 1)) {
/*
if (level >= 768) {
// putchar('+');
return sbdsp.dma_interval_trim;
// return sbdsp.dma_interval_trim;
} else {
// putchar('-');
return -sbdsp.dma_interval_trim;
}
*/
// return (int32_t)(level >> 4) - 32;
// if(sbdsp_fifo_level() >= (DSP_DMA_FIFO_SIZE/2)) return true;
// return false;
}
void __force_inline sbdsp_fifo_rx(uint8_t byte) {
if (!fifo_add_sample(&sbdsp.audio_fifo, (int16_t)(byte ^ 0x80) << 8)) {
putchar('O');
// printf("%d", dma_interval_trim());
// printf("OVERRUN");
}
/*
if(sbdsp_fifo_level()+1 == DSP_DMA_FIFO_SIZE) printf("OVERRRUN");
sbdsp.dma_buffer[sbdsp.dma_buffer_tail] = (int16_t)(byte ^ 0x80) << 8;
sbdsp.dma_buffer_tail++;
if(sbdsp.dma_buffer_tail == DSP_DMA_FIFO_SIZE) sbdsp.dma_buffer_tail=0;
*/
}
void __force_inline sbdsp_fifo_clear() {
fifo_set_state(&sbdsp.audio_fifo, FIFO_STATE_STOPPED);
fifo_reset(&sbdsp.audio_fifo);
// sbdsp.dma_buffer_head=sbdsp.dma_buffer_tail;
}
audio_fifo_t* sbdsp_fifo_peek() {
return &sbdsp.audio_fifo;
}
/*
int16_t sbdsp_fifo_take(uint16_t len) {
if (len > sbdsp_fifo_level() || sbdsp.dac_resume_pending) return -1;
uint16_t ret = sbdsp.dma_buffer_head;
sbdsp.dma_buffer_head = (sbdsp.dma_buffer_head + len) & DSP_DMA_FIFO_BITS;
if (!sbdsp.speaker_on) return -1;
return ret;
}
*/
/*
uint16_t sbdsp_fifo_tx(uint8_t *buffer, uint16_t len) {
uint16_t level = sbdsp_fifo_level();
if (!level) return 0;
if (!len) return 0;
if (len > level) len=level;
// Calculate the number of bytes until the end of the fifo
size_t bytes_until_end = DSP_DMA_FIFO_SIZE - sbdsp.dma_buffer_head;
if (bytes_until_end >= len) {
// Simple case - no wrap-around needed
memcpy(buffer, sbdsp.dma_buffer + sbdsp.dma_buffer_head, len);
if (bytes_until_end == len) {
sbdsp.dma_buffer_head = 0;
} else {
sbdsp.dma_buffer_head += len;
}
} else {
// Handle wrap-around: first copy until the end of the fifo
memcpy(buffer, sbdsp.dma_buffer + sbdsp.dma_buffer_head, bytes_until_end);
// Then copy the remaining data from the beginning of the fifo
// memcpy(fifo->data, data + bytes_until_end, data_size - bytes_until_end);
memcpy(buffer + bytes_until_end, sbdsp.dma_buffer, len - bytes_until_end);
sbdsp.dma_buffer_head = len - bytes_until_end;
}
// printf("%u %u|", len, sbdsp.dma_buffer_head);
return len;
}
*/
/*
uint8_t sbdsp_fifo_tx() {
uint16_t level = sbdsp_fifo_level();
if (level < 64) return 128;
uint8_t ret = sbdsp.dma_buffer[sbdsp.dma_buffer_head];
sbdsp.dma_buffer_head = (sbdsp.dma_buffer_head + 1) & 0x1ff;
// printf("%x ", sbdsp.dma_buffer_head);
return ret;
}
*/
/*
uint16_t sbdsp_fifo_tx(char *buffer, uint16_t len) {
uint16_t level = sbdsp_fifo_level();
if (!level) return 0;
if (!len) return 0;
if (len > level) len=level;
if (sbdsp.dma_buffer_head < sbdsp.dma_buffer_tail || ((sbdsp.dma_buffer_head+len) < DSP_DMA_FIFO_SIZE)) {
memcpy(buffer, sbdsp.dma_buffer+sbdsp.dma_buffer_head, len);
sbdsp.dma_buffer_head += len;
return len;
}
else {
memcpy(buffer, sbdsp.dma_buffer+sbdsp.dma_buffer_head, DSP_DMA_FIFO_SIZE-sbdsp.dma_buffer_head);
memcpy(buffer+256-sbdsp.dma_buffer_head, sbdsp.dma_buffer,len-(DSP_DMA_FIFO_SIZE-sbdsp.dma_buffer_head));
sbdsp.dma_buffer_head += (len-DSP_DMA_FIFO_SIZE);
return len;
}
return 0;
}
*/
static uint32_t DSP_DMA_EventHandler(Bitu val);
static PIC_TimerEvent DSP_DMA_Event = {
.handler = DSP_DMA_EventHandler,
};
static __force_inline void sbdsp_dma_disable() {
sbdsp.dma_enabled=false;
PIC_RemoveEvent(&DSP_DMA_Event);
// fifo_set_state(&sbdsp.audio_fifo, FIFO_STATE_STOPPED);
sbdsp.cur_sample = 0; // zero current sample
// putchar('S');
}
static __force_inline void sbdsp_dma_enable() {
if(!sbdsp.dma_enabled) {
// sbdsp_fifo_clear();
sbdsp.dma_enabled=true;
PIC_AddEvent(&DSP_DMA_Event, sbdsp.dma_interval, 0);
fifo_set_state(&sbdsp.audio_fifo, FIFO_STATE_RUNNING);
}
// else {
// printf("INFO: DMA Already Enabled");
// }
}
static uint32_t DSP_DMA_EventHandler(Bitu val) {
DMA_Start_Write(&dma_config);
uint32_t current_interval;
sbdsp.dma_sample_count_rx++;
// current_interval = sbdsp.dma_interval + dma_interval_trim();
current_interval = dma_interval_calc();
// printf("%u\n", current_interval);
if(sbdsp.dma_sample_count_rx <= sbdsp.dma_sample_count) {
return current_interval;
} else {
PIC_ActivateIRQ();
if(sbdsp.autoinit) {
sbdsp.dma_sample_count_rx=0;
return current_interval;
}
else {
sbdsp_dma_disable();
}
}
return 0;
}
static void sbdsp_dma_isr(void) {
const uint32_t dma_data = DMA_Complete_Write(&dma_config);
// sbdsp.cur_sample = (int16_t)((dma_data & 0xFF) ^ 0x80) << 8;
sbdsp_fifo_rx(dma_data & 0xFF);
}
static uint32_t DSP_DAC_Resume_eventHandler(Bitu val) {
PIC_ActivateIRQ();
sbdsp.dac_resume_pending = false;
return 0;
}
static PIC_TimerEvent DSP_DAC_Resume_event = {
.handler = DSP_DAC_Resume_eventHandler,
};
int16_t sbdsp_sample() {
return (sbdsp.speaker_on & ~sbdsp.dac_resume_pending) ? sbdsp.cur_sample : 0;
}
int16_t sbdsp_muted() {
return (!sbdsp.speaker_on || sbdsp.dac_resume_pending);
}
uint16_t sbdsp_sample_rate() {
return sbdsp.sample_rate;
}
void sbdsp_init() {
// uint8_t x,y,z;
// char buffer[32];
puts("Initing ISA DMA PIO...");
SBDSP_DMA_isr_pt = sbdsp_dma_isr;
sbdsp.outbox = 0xAA;
dma_config = DMA_init(pio0, DMA_PIO_SM, SBDSP_DMA_isr_pt);
fifo_init(&sbdsp.audio_fifo);
}
static __force_inline void sbdsp_output(uint8_t value) {
sbdsp.outbox=value;
sbdsp.dav_pc=1;
}
void sbdsp_process(void) {
if(sbdsp.reset_state) return;
sbdsp.dsp_busy=1;
if(sbdsp.dav_dsp) {
if(!sbdsp.current_command) {
sbdsp.current_command = sbdsp.inbox;
sbdsp.current_command_index=0;
sbdsp.dav_dsp=0;
}
}
switch(sbdsp.current_command) {
case DSP_DMA_PAUSE:
sbdsp.current_command=0;
sbdsp_dma_disable();
//printf("(0xD0)DMA PAUSE\n\r");
break;
case DSP_DMA_RESUME:
sbdsp.current_command=0;
sbdsp_dma_enable();
//printf("(0xD4)DMA RESUME\n\r");
break;
case DSP_DMA_AUTO:
// printf("(0x1C)DMA_AUTO\n\r");
sbdsp.autoinit=1;
sbdsp.dma_sample_count = sbdsp.dma_block_size;
sbdsp.dma_sample_count_rx=0;
sbdsp_dma_enable();
sbdsp.current_command=0;
break;
case DSP_DMA_HS_AUTO:
// printf("(0x90) DMA_HS_AUTO\n\r");
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
sbdsp.autoinit=1;
sbdsp.dma_sample_count = sbdsp.dma_block_size;
sbdsp.dma_sample_count_rx=0;
sbdsp_dma_enable();
break;
case DSP_SET_TIME_CONSTANT:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
//printf("(0x40) DSP_SET_TIME_CONSTANT\n\r");
sbdsp.time_constant = sbdsp.inbox;
/*
sbdsp.sample_rate = 1000000ul / (256 - sbdsp.time_constant);
sbdsp.dma_interval = 1000000ul / sbdsp.sample_rate; // redundant.
*/
sbdsp.dma_interval = 256 - sbdsp.time_constant;
sbdsp.sample_rate = 1000000ul / sbdsp.dma_interval;
// sbdsp.dma_interval_trim = MAX(1, sbdsp.dma_interval >> 3);
sbdsp.dma_interval_trim = MAX(1, sbdsp.dma_interval >> 1);
// printf("interval: %u rate: %u, trim: %u\n", sbdsp.dma_interval, sbdsp.sample_rate, sbdsp.dma_interval_trim);
// sbdsp.sample_step = sbdsp.sample_rate * 65535ul / OUTPUT_SAMPLERATE;
// sbdsp.sample_factor = (OUTPUT_SAMPLERATE / sbdsp.sample_rate)+5; //Estimate
// sbdsp.dma_transfer_size = 4;
//sbdsp.i2s_buffer_size = ((OUTPUT_SAMPLERATE * 65535ul) / sbdsp.sample_rate * sbdsp.dma_buffer_size) >> 16;
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
}
sbdsp.current_command_index++;
}
break;
case DSP_DMA_BLOCK_SIZE:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
sbdsp.dma_block_size=sbdsp.inbox;
sbdsp.dav_dsp=0;
}
else if(sbdsp.current_command_index==2) {
sbdsp.dma_block_size += (sbdsp.inbox << 8);
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
//printf("(0x48) Set BlockSize:%u\n\r",sbdsp.dma_block_size);
}
sbdsp.current_command_index++;
}
break;
case DSP_DMA_HS_SINGLE:
// printf("(0x91) DMA_HS_SINGLE\n\r");
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
sbdsp.autoinit=0;
sbdsp.dma_sample_count = sbdsp.dma_block_size;
sbdsp.dma_sample_count_rx=0;
sbdsp_dma_enable();
break;
case DSP_DMA_SINGLE:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
sbdsp.dma_sample_count = sbdsp.inbox;
sbdsp.dav_dsp=0;
}
else if(sbdsp.current_command_index==2) {
// printf("(0x14)DMA_SINGLE\n\r");
sbdsp.dma_sample_count += (sbdsp.inbox << 8);
sbdsp.dma_sample_count_rx=0;
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
sbdsp.autoinit=0;
//printf("Sample Count:%u\n",sbdsp.dma_sample_count);
sbdsp_dma_enable();
}
sbdsp.current_command_index++;
}
break;
case DSP_IRQ:
sbdsp.current_command=0;
PIC_ActivateIRQ();
break;
case DSP_VERSION:
if(sbdsp.current_command_index==0) {
sbdsp.current_command_index=1;
sbdsp_output(DSP_VERSION_MAJOR);
}
else {
if(!sbdsp.dav_pc) {
sbdsp.current_command=0;
sbdsp_output(DSP_VERSION_MINOR);
}
}
break;
case DSP_IDENT:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
//printf("(0xE0) DSP_IDENT\n\r");
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
sbdsp_output(~sbdsp.inbox);
}
sbdsp.current_command_index++;
}
break;
case DSP_ENABLE_SPEAKER:
//printf("ENABLE SPEAKER\n");
sbdsp.speaker_on = true;
sbdsp.current_command=0;
break;
case DSP_DISABLE_SPEAKER:
//printf("DISABLE SPEAKER\n");
sbdsp.speaker_on = false;
sbdsp.current_command=0;
break;
case DSP_SPEAKER_STATUS:
if(sbdsp.current_command_index==0) {
sbdsp.current_command=0;
sbdsp_output(sbdsp.speaker_on ? 0xff : 0x00);
}
break;
case DSP_DIRECT_DAC:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
sbdsp.cur_sample=(int16_t)(sbdsp.inbox) - 0x80 << 5;
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
}
sbdsp.current_command_index++;
}
break;
//case DSP_DIRECT_ADC:
//case DSP_MIDI_READ_POLL:
//case DSP_MIDI_WRITE_POLL:
//case DSP_HALT_DMA:
case DSP_WRITETEST:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
//printf("(0xE4) DSP_WRITETEST\n\r");
sbdsp.test_register = sbdsp.inbox;
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
}
sbdsp.current_command_index++;
}
break;
case DSP_READTEST:
if(sbdsp.current_command_index==0) {
sbdsp.current_command=0;
sbdsp_output(sbdsp.test_register);
}
break;
case DSP_DAC_PAUSE_DURATION:
if(sbdsp.dav_dsp) {
if(sbdsp.current_command_index==1) {
sbdsp.dac_pause_duration_low=sbdsp.inbox;
sbdsp.dav_dsp=0;
}
else if(sbdsp.current_command_index==2) {
sbdsp.dac_pause_duration = sbdsp.dac_pause_duration_low + (sbdsp.inbox << 8);
sbdsp.dac_resume_pending = true;
// When the specified duration elapses, the DSP generates an interrupt.
PIC_AddEvent(&DSP_DAC_Resume_event, sbdsp.dma_interval * sbdsp.dac_pause_duration, 0);
sbdsp.dav_dsp=0;
sbdsp.current_command=0;
//printf("(0x80) Pause Duration:%u\n\r",sbdsp.dac_pause_duration);
}
sbdsp.current_command_index++;
}
break;
//case DSP_SINE:
//case DSP_CHECKSUM:
case 0:
//not in a command
break;
default:
printf("Unknown Command: %x\n",sbdsp.current_command);
sbdsp.current_command=0;
break;
}
sbdsp.dsp_busy=0;
}
static uint32_t DSP_Reset_EventHandler(Bitu val) {
sbdsp.reset_state=0;
sbdsp.outbox = 0xAA;
sbdsp.dav_pc=1;
sbdsp.current_command=0;
sbdsp.current_command_index=0;
sbdsp.dma_block_size=0x7FF; //default per 2.01
sbdsp.dma_sample_count=0;
sbdsp.dma_sample_count_rx=0;
sbdsp.speaker_on = false;
sbdsp.dac_resume_pending = false;
return 0;
}
static PIC_TimerEvent DSP_Reset_Event = {
.handler = DSP_Reset_EventHandler,
};
static __force_inline void sbdsp_reset(uint8_t value) {
//TODO: COLDBOOT ? WARMBOOT ?
value &= 1; // Some games may write unknown data for bits other than the LSB.
switch(value) {
case 1:
PIC_RemoveEvent(&DSP_Reset_Event);
sbdsp.autoinit=0;
sbdsp_dma_disable();
sbdsp_fifo_clear();
sbdsp.reset_state=1;
break;
case 0:
if(sbdsp.reset_state==1) {
sbdsp.dav_pc=0;
// sbdsp.outbox = 0xAA;
PIC_RemoveEvent(&DSP_Reset_Event);
PIC_AddEvent(&DSP_Reset_Event, 100, 0);
sbdsp.reset_state = 2;
}
break;
default:
break;
}
}
uint8_t sbdsp_read(uint8_t address) {
uint8_t x;
switch(address) {
case DSP_READ:
sbdsp.dav_pc=0;
return sbdsp.outbox;
case DSP_READ_STATUS: //e
PIC_DeActivateIRQ();
return sbdsp.dav_pc << 7 | DSP_UNUSED_STATUS_BITS_PULLED_HIGH;
case DSP_WRITE_STATUS://c
return (sbdsp.dav_dsp | sbdsp.dsp_busy | sbdsp.dac_resume_pending) << 7 | DSP_UNUSED_STATUS_BITS_PULLED_HIGH;
default:
//printf("SB READ: %x\n\r",address);
return 0xFF;
}
}
void sbdsp_write(uint8_t address, uint8_t value) {
switch(address) {
case DSP_WRITE://c
if(sbdsp.dav_dsp) printf("WARN - DAV_DSP OVERWRITE\n");
sbdsp.inbox = value;
sbdsp.dav_dsp = 1;
break;
case DSP_RESET:
sbdsp_reset(value);
break;
default:
//printf("SB WRITE: %x => %x \n\r",value,address);
break;
}
}