Files
picogus/sw/sbdsp/sbdsp.cpp
T

1418 lines
54 KiB
C++

#include <inttypes.h>
#include <stdbool.h>
#include "../include/pg_debug.h"
#include <string.h>
#include "system/pico_pic.h"
#include "audio/volctrl.h"
#include "sbdsp.h"
#ifdef SOUND_MPU
#include "mpu401/export.h"
#endif
/*
Title : SoundBlaster DSP Emulation
Date : 2023-12-30
Author : Kevin Moonlight <me@yyzkevin.com>
Copyright (C) 2023 Kevin Moonlight
Copyright (C) 2024-2026 Ian Scott
Copyright (C) 2026 Artem Vasilev
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/isa_dma.h"
#ifdef INTERP_SB_LINEAR
#include "hardware/interp.h"
#endif
#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 SB_TYPE_SB1 0x1
#define SB_TYPE_SBPRO1 0x2
#define SB_TYPE_SB2 0x3
#define SB_TYPE_SBPRO2 0x4
#define SB_TYPE_SBPRO2MCA 0x5
#define SB_TYPE_SB16 0x6
// 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 DSP_IRQ_16_STATUS 0xF
#define MIXER_COMMAND 0x4
#define MIXER_DATA 0x5
#define OUTPUT_SAMPLERATE 49716ul
// Sound Blaster DSP commands.
#define DSP_DMA_HS_SINGLE 0x91
#define DSP_DMA_HS_AUTO 0x90
#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
// SB16 DSP commands
#define DSP_DMA_IO_START 0xB0
#define DSP_DMA_IO_END 0xCF
#define DSP_PAUSE_DMA_16 0xD5
#define DSP_CONTINUE_DMA_16 0xD6
#define DSP_CONTINUE_AUTOINIT_8 0x45
#define DSP_CONTINUE_AUTOINIT_16 0x47
#define DSP_EXIT_AUTOINIT_16 0xD9
#define DSP_EXIT_AUTOINIT_8 0xDA
//#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_SET_SAMPLING_RATE 0x41
#define DSP_PAUSE_DMA_8 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_CONTINUE_DMA_8 0xD4
#define DSP_SPEAKER_STATUS 0xD8
// SB16 ASP commands
#define DSP_ASP_SET_CODEC_PARAM 0x05
#define DSP_ASP_SET_REGISTER 0x0E
#define DSP_ASP_GET_REGISTER 0x0F
#define DSP_IDENT 0xE0
#define DSP_VERSION 0xE1
#define DSP_IDENT_E2 0xE2
#define DSP_COPYRIGHT 0xE3
#define DSP_WRITETEST 0xE4
#define DSP_READTEST 0xE8
#define DSP_SINE 0xF0
#define DSP_IRQ_8 0xF2
#define DSP_IRQ_16 0xF3
#define DSP_CHECKSUM 0xF4
#define DSP_8051_READ 0xF9
#define DSP_8051_WRITE 0xFA
#define MIXER_INTERRUPT 0x80
#define MIXER_DMA 0x81
#define MIXER_IRQ_STATUS 0x82
#define MIXER_STEREO 0xE
#define MIXER_VOL_VOICE 0x04
#define MIXER_VOL_MASTER 0x22
#define MIXER_VOL_MIDI 0x26
#define MIXER_VOL_CD 0x28
#define MIXER_VOL_LINE 0x2E
#define MIXER_VOL_MASTER_L 0x30 // SB16
#define MIXER_VOL_MASTER_R 0x31 // SB16
#define MIXER_VOL_VOICE_L 0x32 // SB16
#define MIXER_VOL_VOICE_R 0x33 // SB16
#define MIXER_VOL_MIDI_L 0x34 // SB16
#define MIXER_VOL_MIDI_R 0x35 // SB16
#define MIXER_VOL_CD_L 0x36 // SB16
#define MIXER_VOL_CD_R 0x37 // SB16
#define MIXER_VOL_LINE_L 0x38 // SB16
#define MIXER_VOL_LINE_R 0x39 // SB16
#define MIXER_OUT_SWITCH 0x3C // SB16
#define DSP_UNUSED_STATUS_BITS_PULLED_HIGH 0x7F
// mixer locking settings
#define MIXER_LOCK_NONE 0 // none, all registers writeable
#define MIXER_LOCK_ALL_BUT_SBP_VOICE 1 // lock all but SBPro Voice (for stereo FX like in Wolf3D)
#define MIXER_LOCK_ALL 2 // lock all registers
static char const copyright_string[] = "COPYRIGHT (C) CREATIVE TECHNOLOGY LTD, 1992.";
union sample32 {
uint32_t data32;
int16_t data16[2];
uint8_t data8[4];
};
sbdsp_t sbdsp;
// Creative ADPCM decoders. Each sub-sample is decoded as:
// delta = magnitude * accumulator + accumulator/2
// reference += +/-delta (clamped to 0..255, sign from the sub-sample's sign bit)
// The accumulator doubles on max-magnitude sub-samples (up to a per-format
// cap) and halves on zero-magnitude sub-samples (down to 1).
//
// Algorithm derived from the SB v2.02 DSP firmware disassembly (TubeTime,
// https://github.com/schlae/sb-firmware) and VocTool (MIT, Torsten Stremlau).
// Not derived from DOSBox/dosbox-x or 86Box.
// 4-bit: nibble is sign (bit 3) + 3 magnitude bits (bits 2..0).
// Accumulator range [1..8].
// Structure adapted from VocTool's CreativeAdpcmDecoder4Bit (MIT, Torsten
// Stremlau); the sign formula below is inverted relative to VocTool's so that
// bit 3 set means subtract, matching the SB DSP firmware convention.
static inline uint8_t decode_ADPCM_4_sample(uint8_t bits) {
int32_t sign = 1 - ((bits & 8) >> 2); // bit 3 is the sign bit (0 -> +1, 1 -> -1)
int32_t data = bits & 7; // the lower 3 bits are the sample data
int32_t delta =
(data * sbdsp.adpcm.accum) +
(sbdsp.adpcm.accum >> 1); // scale sample data using accumulator value
int32_t result = sbdsp.adpcm.reference + sign * delta; // calculate the next value
if (result > 0xff) result = 0xff; // limit value to 0..255
else if (result < 0) result = 0;
sbdsp.adpcm.reference = (uint8_t)result;
if ((data == 0) && (sbdsp.adpcm.accum > 1)) // if input value is 0, and accumulator is
sbdsp.adpcm.accum >>= 1; // larger than 1, then halve accumulator.
if ((data >= 5) && (sbdsp.adpcm.accum < 8)) // if input value larger than 5, and accumulator is
sbdsp.adpcm.accum <<= 1; // lower than 8, then double accumulator.
return sbdsp.adpcm.reference;
}
// 2.6-bit: 3 samples per byte (3 bits, 3 bits, 2 bits). Accumulator range [1..16].
// Caller passes magnitude bits (0..3 for the first two samples, 0..1 for the third)
// and sign separately since sign-bit position varies per sub-sample.
static inline uint8_t decode_ADPCM_3_sample(uint8_t bits, bool negative) {
int32_t sign = negative ? -1 : 1; // sign bit extracted by caller
int32_t data = bits; // 0..3, or 0..1 for the final sample
int32_t delta =
(data * sbdsp.adpcm.accum) +
(sbdsp.adpcm.accum >> 1); // scale sample data using accumulator value
int32_t result = sbdsp.adpcm.reference + sign * delta; // calculate the next value
if (result > 0xff) result = 0xff; // limit value to 0..255
else if (result < 0) result = 0;
sbdsp.adpcm.reference = (uint8_t)result;
if ((data == 0) && (sbdsp.adpcm.accum > 1)) // if input value is 0, and accumulator is
sbdsp.adpcm.accum >>= 1; // larger than 1, then halve accumulator.
if ((data >= 3) && (sbdsp.adpcm.accum < 0x10)) // if input value is 3, and accumulator is
sbdsp.adpcm.accum <<= 1; // lower than 0x10, then double accumulator.
return sbdsp.adpcm.reference;
}
// 2-bit: sign (bit 1) + 1 magnitude bit (bit 0). Accumulator range [1..32].
static inline uint8_t decode_ADPCM_2_sample(uint8_t bits) {
int32_t sign = 1 - (bits & 2); // bit 1 is the sign bit (0 -> +1, 1 -> -1)
int32_t data = bits & 1; // the lower bit is the sample data
int32_t delta =
(data * sbdsp.adpcm.accum) +
(sbdsp.adpcm.accum >> 1); // scale sample data using accumulator value
int32_t result = sbdsp.adpcm.reference + sign * delta; // calculate the next value
if (result > 0xff) result = 0xff; // limit value to 0..255
else if (result < 0) result = 0;
sbdsp.adpcm.reference = (uint8_t)result;
if ((data == 0) && (sbdsp.adpcm.accum > 1)) // if input value is 0, and accumulator is
sbdsp.adpcm.accum >>= 1; // larger than 1, then halve accumulator.
if ((data >= 1) && (sbdsp.adpcm.accum < 0x20)) // if input value is 1, and accumulator is
sbdsp.adpcm.accum <<= 1; // lower than 0x20, then double accumulator.
return sbdsp.adpcm.reference;
}
// Convert decoded 8-bit unsigned mono sample to packed signed stereo
static inline uint32_t adpcm_to_stereo(uint8_t ref) {
int16_t s = ((int16_t)(int8_t)(ref ^ 0x80)) << 8;
return (uint32_t)(uint16_t)s | ((uint32_t)(uint16_t)s << 16);
}
// Ring buffer helpers
static inline uint8_t ring_count() {
return (uint8_t)(sbdsp.rs.ring_head - sbdsp.rs.ring_tail);
}
static inline uint8_t ring_free() {
return SB_RING_SIZE - ring_count();
}
static inline bool ring_empty() {
return sbdsp.rs.ring_head == sbdsp.rs.ring_tail;
}
static inline void ring_push(uint32_t sample) {
sbdsp.rs.ring[sbdsp.rs.ring_head & (SB_RING_SIZE - 1)] = sample;
sbdsp.rs.ring_head++;
}
static inline uint32_t ring_pop() {
uint32_t s = sbdsp.rs.ring[sbdsp.rs.ring_tail & (SB_RING_SIZE - 1)];
sbdsp.rs.ring_tail++;
return s;
}
static inline uint8_t samples_per_transfer() {
switch (sbdsp.adpcm.format) {
case 4: return 2;
case 3: return 3;
case 2: return 4;
default: return 1;
}
}
static uint8_t mixer_state[256] = { 0 };
static uint8_t sb_8051_ram[256] = { 0 };
// Wavetable volume pass-through state
static uint8_t *wt_volume_ptr = NULL;
static sbmixer_wtvol_cb_t wtvol_cb = NULL;
void sbdsp_set_wtvol_passthrough(uint8_t *wt_volume, sbmixer_wtvol_cb_t cb) {
wt_volume_ptr = wt_volume;
wtvol_cb = cb;
}
// Fake ADC (recording) - just fire IRQ after the expected transfer time so
// programs don't hang, but they will get garbage data
static uint32_t DSP_ADC_eventHandler(Bitu val);
static PIC_TimerEvent DSP_ADC_event = {
.handler = DSP_ADC_eventHandler,
};
static uint32_t DSP_ADC_eventHandler(Bitu val) {
if (!sbdsp.adc_active) return 0;
if (sbdsp.dma_16bit) {
sbdsp.irq_16_pending = true;
} else {
sbdsp.irq_8_pending = true;
}
PIC_ActivateIRQ();
// for auto-init, reschedule the next block
if (sbdsp.autoinit) {
PIC_AddEvent(&DSP_ADC_event, sbdsp.dma_interval * (sbdsp.dma_block_size + 1), 0);
}
return 0;
}
static __force_inline void sbdsp_dma_disable(bool pause) {
if (sbdsp.adc_active) {
PIC_RemoveEvent(&DSP_ADC_event);
if (!pause) sbdsp.adc_active = false;
return;
}
sbdsp.dma_enabled = false;
sbdsp.rs.dma_pending = false;
// sbdsp.cur_sample = 0; // hold last sample to prevent pops!
if (!pause) {
sbdsp.rs.ring_head = sbdsp.rs.ring_tail = 0;
sbdsp.adpcm.format = 0;
sbdsp.dma_16bit = false;
sbdsp.dma_signed = false;
sbdsp.dma_stereo = false;
}
}
uint32_t sbdsp_generate_sample() {
sbdsp.rs.phase_acc += sbdsp.rateratio;
while (sbdsp.rs.phase_acc >= (1 << SB_RSM_FRAC)) {
sbdsp.rs.phase_acc -= (1 << SB_RSM_FRAC);
// advance interpolation buffer state
sbdsp.rs.interp[1] = sbdsp.rs.interp[0];
if (!ring_empty()) {
sbdsp.rs.interp[0] = ring_pop();
}
// else: hold last sample (graceful degradation)
}
// Restart DMA chain if ring drained and DMA still active
if (sbdsp.dma_enabled && !sbdsp.rs.dma_pending) {
uint8_t samples_per_xfer = samples_per_transfer();
if (ring_free() >= samples_per_xfer) {
sbdsp.rs.dma_pending = true;
DMA_Multi_Start_Write(&dma_config, sbdsp.dma_bytes_per_frame);
}
}
// interpolate sample
#ifdef INTERP_SB_LINEAR
// interp0 blend: BASE0 + alpha * (BASE1 - BASE0) >> 8
// base01 packs older sample (low 16) and newer sample (high 16), sign-extended
interp0->accum[1] = sbdsp.rs.phase_acc;
// left channel: older_L in low 16, newer_L in high 16
interp0->base01 = (sbdsp.rs.interp[1] & 0xFFFF)
| ((sbdsp.rs.interp[0] & 0xFFFF) << 16);
int32_t out_l = interp0->peek[1];
// right channel: older_R in low 16, newer_R in high 16
interp0->base01 = (sbdsp.rs.interp[1] >> 16)
| (sbdsp.rs.interp[0] & 0xFFFF0000);
int32_t out_r = interp0->peek[1];
return (uint32_t)((out_l & 0xFFFF) | (out_r << 16));
#else
int32_t l0 = (int16_t)(sbdsp.rs.interp[0] & 0xFFFF);
int32_t r0 = (int16_t)(sbdsp.rs.interp[0] >> 16);
int32_t l1 = (int16_t)(sbdsp.rs.interp[1] & 0xFFFF);
int32_t r1 = (int16_t)(sbdsp.rs.interp[1] >> 16);
int32_t phase_frac = sbdsp.rs.phase_acc & ((1 << SB_RSM_FRAC) - 1);
int32_t out_l = ((l0 * phase_frac) + (l1 * ((1 << SB_RSM_FRAC) - phase_frac))) >> SB_RSM_FRAC;
int32_t out_r = ((r0 * phase_frac) + (r1 * ((1 << SB_RSM_FRAC) - phase_frac))) >> SB_RSM_FRAC;
return (uint32_t)((out_l & 0xFFFF) | (out_r << 16));
#endif
}
static void sbdsp_handle_time_constant(uint8_t tc) {
if (sbdsp.options.fixTC) {
// round time constants to 11/22/44 KHz sample rates
switch (tc) {
case (256 - 62): // 16129 Hz
case (256 - 63): // 15873 Hz
sbdsp.time_constant = 0; sbdsp.sample_rate = 16000; return;
case (256 - 31): // 32258 Hz
sbdsp.time_constant = 0; sbdsp.sample_rate = 32000; return;
case (256 - 91): // 10989 Hz
case (256 - 90): // 11111 Hz
sbdsp.time_constant = 0; sbdsp.sample_rate = 11025; return;
case (256 - 45): // 22222 Hz
case (256 - 46): // 21739 Hz
sbdsp.time_constant = 0; sbdsp.sample_rate = 22050; return;
case (256 - 22): // 45454 Hz
case (256 - 23): // 43478 Hz
sbdsp.time_constant = 0; sbdsp.sample_rate = 44100; return;
default: break;
}
}
sbdsp.time_constant = tc;
sbdsp.sample_rate = 0; // Rate of 0 indicates time constant drives DMA timing
}
static __force_inline void sbdsp_set_dma_interval() {
if (sbdsp.sample_rate) {
uint32_t actual_sr = (sbdsp.sample_rate >> (sbdsp.dma_stereo_sbpro ? 1 : 0));
sbdsp.dma_interval = 1000000000ul / actual_sr / 1000;
sbdsp.rateratio = (actual_sr << SB_RSM_FRAC) / 44100;
} else {
sbdsp.dma_interval = (256 - sbdsp.time_constant) << (sbdsp.dma_stereo_sbpro ? 1 : 0);
sbdsp.rateratio = ((1000000ul / sbdsp.dma_interval) << SB_RSM_FRAC) / 44100;
}
// No halving for stereo/16-bit (unless SBPro stereo): dma_write_multi transfers a complete
// frame per event, so the interval is always one sample period.
}
static __force_inline void sbdsp_dma_enable() {
if (sbdsp.adc_active) {
// resume fake ADC
PIC_AddEvent(&DSP_ADC_event, sbdsp.dma_interval * (sbdsp.dma_block_size + 1), 0);
return;
}
if (!sbdsp.dma_enabled) {
sbdsp.dma_enabled = true;
// Set autopush bits to number of bits per audio frame.
// 32 will get masked to 0 by this operation which is correct behavior.
DMA_Multi_Set_Push_Threshold(&dma_config, sbdsp.dma_bytes_per_frame << 3);
sbdsp.rs.dma_pending = true;
DMA_Multi_Start_Write(&dma_config, sbdsp.dma_bytes_per_frame);
}
}
static void sbdsp_dma_isr(void) {
// dma_write_multi uses right-shift: bytes fill from MSB down.
// Complete frame arrives as one push thanks to autopush threshold.
const uint32_t dma_data = DMA_Complete_Write(&dma_config);
sbdsp.rs.dma_pending = false;
// Decode and push to ring buffer.
// Reference byte case: consumes a DMA byte but pushes no samples.
if (sbdsp.adpcm.have_ref) {
sbdsp.adpcm.reference = dma_data >> 24;
sbdsp.adpcm.accum = 1; // accumulator starts at 1 in SB DSP firmware
sbdsp.adpcm.have_ref = false;
} else if (sbdsp.adpcm.format == 0) {
// PCM decode (existing logic), push 1 sample
uint32_t sample;
if (sbdsp.dma_stereo) {
if (sbdsp.dma_16bit) {
sample = dma_data;
} else {
// 8-bit stereo: L at [23:16], R at [31:24] to MSB of each 16-bit half
sample = (dma_data & 0xFF000000) | ((dma_data >> 8) & 0x0000FF00);
}
} else {
if (sbdsp.dma_16bit) {
// 16-bit mono: duplicate upper 16 to both halves
sample = (dma_data & 0xFFFF0000) | (dma_data >> 16);
} else {
// 8-bit mono: byte at [31:24], duplicate to MSB of each half
sample = (dma_data & 0xFF000000) | ((dma_data >> 16) & 0x0000FF00);
}
}
if (!sbdsp.dma_signed) sample ^= 0x80008000;
ring_push(sample);
} else {
// ADPCM decode: extract samples from byte, push each
uint8_t byte = dma_data >> 24; // ADPCM is always 1-byte mono
switch (sbdsp.adpcm.format) {
case 4: // 4-bit: 2 samples per byte
ring_push(adpcm_to_stereo(decode_ADPCM_4_sample(byte >> 4)));
ring_push(adpcm_to_stereo(decode_ADPCM_4_sample(byte & 0xf)));
break;
case 3: // 2.6-bit: 3 samples per byte (3 bits, 3 bits, 2 bits)
ring_push(adpcm_to_stereo(decode_ADPCM_3_sample((byte >> 5) & 3, byte & 0x80)));
ring_push(adpcm_to_stereo(decode_ADPCM_3_sample((byte >> 2) & 3, byte & 0x10)));
ring_push(adpcm_to_stereo(decode_ADPCM_3_sample( byte & 1, byte & 0x02)));
break;
case 2: // 2-bit: 4 samples per byte
ring_push(adpcm_to_stereo(decode_ADPCM_2_sample((byte >> 6) & 3)));
ring_push(adpcm_to_stereo(decode_ADPCM_2_sample((byte >> 4) & 3)));
ring_push(adpcm_to_stereo(decode_ADPCM_2_sample((byte >> 2) & 3)));
ring_push(adpcm_to_stereo(decode_ADPCM_2_sample(byte & 3)));
break;
}
}
// Transfer counting (runs for all cases including reference byte)
sbdsp.dma_xfer_count_left--;
if (!sbdsp.dma_xfer_count_left) {
sbdsp.dma_done = true;
if (sbdsp.dma_16bit) {
sbdsp.irq_16_pending = true;
} else {
sbdsp.irq_8_pending = true;
}
PIC_ActivateIRQ();
if (sbdsp.autoinit) {
sbdsp.dma_xfer_count_left = sbdsp.dma_xfer_count;
} else {
sbdsp.dma_enabled = false;
return;
}
}
// Chain next DMA if ring has space
uint8_t samples_per_xfer = samples_per_transfer();
if (sbdsp.dma_enabled && ring_free() >= samples_per_xfer) {
sbdsp.rs.dma_pending = true;
DMA_Multi_Start_Write(&dma_config, sbdsp.dma_bytes_per_frame);
}
}
static uint32_t DSP_DAC_Resume_eventHandler(Bitu val) {
if (sbdsp.dma_16bit) {
sbdsp.irq_16_pending = true;
} else {
sbdsp.irq_8_pending = true;
}
PIC_ActivateIRQ();
sbdsp.dac_resume_pending = false;
return 0;
}
static PIC_TimerEvent DSP_DAC_Resume_event = {
.handler = DSP_DAC_Resume_eventHandler,
};
int16_t sbdsp_muted() {
return (!sbdsp.speaker_on || sbdsp.dac_resume_pending);
}
uint16_t sbdsp_sample_rate() {
return sbdsp.sample_rate;
}
void sbdsp_set_options(uint8_t options) {
sbdsp.options.b = options;
// TODO if any to apply
}
void sbdsp_set_type(uint8_t type) {
if ((type == 0) && (type > SB_TYPE_SB16)) type = SB_TYPE_SB16;
switch (type) {
default:
case SB_TYPE_SB1: sbdsp.dsp_version.major = 1; sbdsp.dsp_version.minor = 5; break;
case SB_TYPE_SB2: sbdsp.dsp_version.major = 2; sbdsp.dsp_version.minor = 1; break;
case SB_TYPE_SBPRO1: sbdsp.dsp_version.major = 3; sbdsp.dsp_version.minor = 0; break;
case SB_TYPE_SBPRO2:
case SB_TYPE_SBPRO2MCA: sbdsp.dsp_version.major = 3; sbdsp.dsp_version.minor = 1; break;
case SB_TYPE_SB16: sbdsp.dsp_version.major = 4; sbdsp.dsp_version.minor = 5; break;
}
// SB16 hardware ignores speaker on/off commands; speaker is always on
sbdsp.speaker_on = (sbdsp.dsp_version.major >= 4);
}
void sbdsp_set_irq(uint8_t irq) {
sbdsp.resources.irq = irq;
int mixer_irq;
switch (irq) {
case 2:
case 9: mixer_irq = 1; break;
case 7: mixer_irq = 4; break;
case 10: mixer_irq = 8; break;
case 5:
default: mixer_irq = 2; break;
}
mixer_state[MIXER_INTERRUPT] = mixer_irq | 0xF0; // as seen on real SB16
}
void sbdsp_set_dma(uint8_t dma) {
sbdsp.resources.dma = dma;
mixer_state[MIXER_DMA] = dma < 4 ? (1 << dma) : 0; // 8-bit DMA channel only
}
static void sbmixer_reset(void);
void sbdsp_init() {
DBG_PUTS("Initing ISA DMA PIO...");
SBDSP_DMA_isr_pt = sbdsp_dma_isr;
sbdsp.outbox = 0xAA;
dma_config = DMA_multi_init(pio0, DMA_PIO_SM, SBDSP_DMA_isr_pt);
// Initialize 8051 RAM with SB16 default values (per DOSBox-X)
sb_8051_ram[0x0e] = 0xff;
sb_8051_ram[0x0f] = 0x07;
sb_8051_ram[0x37] = 0x38;
// Initialize buffer for the E2 command
sbdsp.ident_e2[0] = 0xAA;
sbdsp.ident_e2[1] = 0x96;
// Reset mixer
sbmixer_reset();
#ifdef INTERP_SB_LINEAR
// Configure interp0 blend mode for SB DSP PCM linear interpolation.
// Lane 0: blend=true. Lane 1: shift maps 12-bit phase fraction to 8-bit alpha.
{
interp_config cfg = interp_default_config();
interp_config_set_blend(&cfg, true);
interp_set_config(interp0, 0, &cfg);
cfg = interp_default_config();
interp_config_set_shift(&cfg, SB_RSM_FRAC - 8);
interp_config_set_signed(&cfg, true);
interp_set_config(interp0, 1, &cfg);
}
#endif
}
static __force_inline void sbdsp_output(uint8_t value) {
sbdsp.outbox = value;
sbdsp.dav_pc = 1;
}
// start ADPCM DMA transfer
static void sbdsp_start_adpcm_dma(int autoinit, uint16_t xfer_size, uint8_t format, bool with_ref) {
sbdsp.autoinit = autoinit;
sbdsp.adpcm.format = format;
sbdsp.adpcm.have_ref = with_ref;
sbdsp.adpcm.accum = 1; // SB DSP firmware initializes accumulator to 1 at DMA start
// (for have_ref it's overwritten again when the ref byte arrives)
sbdsp.dma_16bit = false;
sbdsp.dma_signed = false;
sbdsp.dma_stereo = false;
sbdsp.dma_stereo_sbpro = false;
sbdsp.dma_bytes_per_frame = 1; // ADPCM always 1 byte at a time
sbdsp.dma_xfer_count = xfer_size + 1; // byte count
sbdsp.dma_xfer_count_left = sbdsp.dma_xfer_count;
sbdsp_set_dma_interval();
sbdsp.dma_done = false;
sbdsp_dma_enable();
}
// start pre-SB16 PCM DMA transfer
static void sbdsp_start_pcm_dma(int autoinit, uint16_t xfer_size) {
sbdsp.autoinit = autoinit;
sbdsp.adpcm.format = 0; // ensure PCM mode
bool dma_stereo = (mixer_state[0x0E] & 2);
if (dma_stereo && ((xfer_size + 1) == 1)) {
// HACK: many SBPro programs follow Creative's documentation and perform 1 byte single-cycle
// DMA transfer to seemingly reset the mixer stereo state, then waiting for IRQ to come.
// since the sbdsp.dma_xfer_count calculation divides by 2 for stereo case, it results in
// xfer_count==0, so the transfer never actually occurs.
// for those transfers, force mono mode.
dma_stereo = false;
}
sbdsp.dma_16bit = false; // always 8 bit
sbdsp.dma_signed = false; // always unsigned
sbdsp.dma_stereo = dma_stereo;
sbdsp.dma_stereo_sbpro = dma_stereo;
sbdsp.dma_bytes_per_frame = sbdsp.dma_stereo ? 2 : 1;
sbdsp.dma_xfer_count = (xfer_size + 1) / sbdsp.dma_bytes_per_frame;
sbdsp.dma_xfer_count_left = sbdsp.dma_xfer_count;
sbdsp_set_dma_interval();
sbdsp.dma_done = false;
sbdsp_dma_enable();
}
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_PAUSE_DMA_8:
case DSP_PAUSE_DMA_16:
sbdsp.current_command = 0;
sbdsp_dma_disable(true);
break;
case DSP_CONTINUE_DMA_8:
case DSP_CONTINUE_DMA_16:
sbdsp.current_command = 0;
sbdsp.dma_done = false;
sbdsp_dma_enable();
break;
case DSP_CONTINUE_AUTOINIT_8:
case DSP_CONTINUE_AUTOINIT_16:
sbdsp.autoinit = 1;
sbdsp.current_command = 0;
break;
case DSP_EXIT_AUTOINIT_8:
case DSP_EXIT_AUTOINIT_16:
sbdsp.autoinit = 0;
sbdsp.current_command = 0;
break;
case DSP_DMA_AUTO:
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_pcm_dma(1, sbdsp.dma_block_size);
break;
case DSP_DMA_HS_AUTO:
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_pcm_dma(1, sbdsp.dma_block_size);
break;
case DSP_SET_TIME_CONSTANT:
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sbdsp_handle_time_constant(sbdsp.inbox);
sbdsp_set_dma_interval();
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_SET_SAMPLING_RATE: // set playback sample rate
case DSP_SET_SAMPLING_RATE+1: // set recording sample rate - misused by FT2
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) { // wSamplingRate.HighByte
sbdsp.sample_rate = sbdsp.inbox << 8;
sbdsp.dav_dsp = 0;
} else if (sbdsp.current_command_index == 2) { // wSamplingRate.LowByte
sbdsp.sample_rate |= sbdsp.inbox;
sbdsp.time_constant = 0;
sbdsp_set_dma_interval();
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;
}
sbdsp.current_command_index++;
}
break;
case DSP_DMA_HS_SINGLE:
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_pcm_dma(0, sbdsp.dma_block_size);
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) {
sbdsp.dma_sample_count += (sbdsp.inbox << 8);
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_pcm_dma(0, sbdsp.dma_sample_count);
}
sbdsp.current_command_index++;
}
break;
// Single-cycle ADPCM (2 param bytes: length-1 low, length-1 high)
case 0x74: case 0x75: // 4-bit ADPCM / with ref
case 0x76: case 0x77: // 2.6-bit ADPCM / with ref
case 0x16: case 0x17: // 2-bit ADPCM / with ref
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) {
sbdsp.dma_sample_count += (sbdsp.inbox << 8);
uint8_t cmd = sbdsp.current_command;
uint8_t fmt = (cmd >= 0x74) ? ((cmd <= 0x75) ? 4 : 3) : 2;
bool ref = cmd & 1;
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_adpcm_dma(0, sbdsp.dma_sample_count, fmt, ref);
}
sbdsp.current_command_index++;
}
break;
// Auto-init ADPCM (no params, use dma_block_size)
case 0x7D: // 4-bit ADPCM auto-init
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_adpcm_dma(1, sbdsp.dma_block_size, 4, true);
break;
case 0x7F: // 2.6-bit ADPCM auto-init
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_adpcm_dma(1, sbdsp.dma_block_size, 3, true);
break;
case 0x1F: // 2-bit ADPCM auto-init
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_start_adpcm_dma(1, sbdsp.dma_block_size, 2, true);
break;
case DSP_IRQ_8:
sbdsp.current_command = 0;
sbdsp.irq_8_pending = true;
PIC_ActivateIRQ();
break;
case DSP_IRQ_16:
sbdsp.current_command = 0;
sbdsp.irq_16_pending = true;
PIC_ActivateIRQ();
break;
case DSP_VERSION:
if (sbdsp.current_command_index == 0) {
sbdsp.current_command_index = 1;
sbdsp_output(sbdsp.dsp_version.major);
} else {
if (!sbdsp.dav_pc) {
sbdsp.current_command = 0;
sbdsp_output(sbdsp.dsp_version.minor);
}
}
break;
case DSP_IDENT:
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_output(~sbdsp.inbox);
}
sbdsp.current_command_index++;
}
break;
case DSP_IDENT_E2: // yet another "protection"... used by CT-VOICE.DRV
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp.ident_e2[0] += sbdsp.inbox ^ sbdsp.ident_e2[1];
sbdsp.ident_e2[1] = (sbdsp.ident_e2[1] >> 2) | (sbdsp.ident_e2[1] << 6);
// ideally it should output this byte via DMA, but we don't support DMA IORs yet
// so stuff to outbox for now
sbdsp_output(sbdsp.ident_e2[0]);
}
sbdsp.current_command_index++;
}
break;
case DSP_COPYRIGHT:
if (sbdsp.current_command_index == 0) {
sbdsp.current_command_index = 1;
sbdsp_output(copyright_string[0]);
} else {
if (!sbdsp.dav_pc) {
if (copyright_string[sbdsp.current_command_index] == '\0') {
sbdsp_output('\0');
sbdsp.current_command = 0;
} else {
sbdsp_output(copyright_string[sbdsp.current_command_index]);
}
sbdsp.current_command_index++;
}
}
break;
case DSP_ENABLE_SPEAKER:
if (sbdsp.dsp_version.major < 4) sbdsp.speaker_on = true;
sbdsp.current_command = 0;
break;
case DSP_DISABLE_SPEAKER:
if (sbdsp.dsp_version.major < 4) 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) {
int16_t s = ((int16_t)(int8_t)(sbdsp.inbox ^ 0x80)) << 8;
sbdsp.cur_sample = (uint32_t)(uint16_t)s | ((uint32_t)(uint16_t)s << 16);
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_DIRECT_ADC:
if (sbdsp.current_command_index == 0) {
sbdsp.current_command = 0;
sbdsp_output(0x80); // fake silent input
}
break;
case DSP_WRITETEST:
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
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;
// length of pause is samples - 1, so add 1
PIC_AddEvent(&DSP_DAC_Resume_event, sbdsp.dma_interval * (sbdsp.dac_pause_duration + 1), 0);
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_8051_READ:
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp_output(sb_8051_ram[sbdsp.inbox]);
}
sbdsp.current_command_index++;
}
break;
case DSP_8051_WRITE:
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sb_8051_ram[0xff] = sbdsp.inbox; // stash address in unused location
sbdsp.dav_dsp = 0;
} else if (sbdsp.current_command_index == 2) {
sb_8051_ram[sb_8051_ram[0xff]] = sbdsp.inbox;
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_DMA_IO_START ... DSP_DMA_IO_END:
// SB16 DMA transfer
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 0) { // bCommand + bMode
sbdsp.adpcm.format = 0; // SB16 commands are always PCM
sbdsp.dma_16bit = (sbdsp.current_command & 0xf0) == 0xb0;
sbdsp.autoinit = sbdsp.current_command & 0x4;
sbdsp.dma_signed = sbdsp.inbox & 0x10;
sbdsp.dma_stereo = sbdsp.inbox & 0x20;
sbdsp.dma_stereo_sbpro = 0;
sbdsp.dav_dsp = 0;
} else if (sbdsp.current_command_index == 1) { // wLength.LowByte
sbdsp.dma_sample_count = sbdsp.inbox;
sbdsp.dav_dsp = 0;
} else if (sbdsp.current_command_index == 2) { // wLength.HighByte
sbdsp.dma_sample_count |= (sbdsp.inbox << 8);
// HACK: force mono when stereo halving would round the
// count to 0 (e.g. count=0 -> 1 sample -> 0 frames).
// Miles AIL detection triggers this: 16-bit signed
// stereo single-cycle DMA with a 1-sample transfer.
// Same logic as the SBPro hack in sbdsp_start_pcm_dma()
if (sbdsp.dma_stereo && (sbdsp.dma_sample_count + 1) == 1) {
sbdsp.dma_stereo = false;
}
sbdsp.dma_bytes_per_frame = 1;
if (sbdsp.dma_16bit) {
sbdsp.dma_bytes_per_frame <<= 1;
}
if (sbdsp.dma_stereo) {
sbdsp.dma_bytes_per_frame <<= 1;
}
// SB16: sample_count counts individual DMA transfers minus 1.
// Each DMA event transfers one complete frame, so for stereo
// we halve the count (2 DMA words per stereo frame).
sbdsp.dav_dsp = 0;
sbdsp_set_dma_interval();
if (sbdsp.current_command & 0x8) {
// ADC (recording) - schedule fake IRQ, no real DMA
sbdsp.adc_active = true;
PIC_AddEvent(&DSP_ADC_event, sbdsp.dma_interval * (sbdsp.dma_sample_count + 1), 0);
} else {
// DAC (playback)
sbdsp.dma_xfer_count = sbdsp.dma_stereo
? (sbdsp.dma_sample_count + 1) >> 1
: sbdsp.dma_sample_count + 1;
sbdsp.dma_xfer_count_left = sbdsp.dma_xfer_count;
sbdsp.speaker_on = true;
sbdsp.dma_done = false;
sbdsp_dma_enable();
}
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_ASP_SET_CODEC_PARAM:
case DSP_ASP_SET_REGISTER:
// 2 data bytes, accept and ignore (no ASP on ViBRA)
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sbdsp.dav_dsp = 0;
} else if (sbdsp.current_command_index == 2) {
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_ASP_GET_REGISTER:
// 1 data byte. Returns chip identification values for specific registers.
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
uint8_t val;
switch (sbdsp.inbox) {
case 0x05: val = 0x01; break;
case 0x09: val = 0xF8; break;
case 0x83: val = 0xFF; break;
default: val = 0x00; break;
}
sbdsp_output(val);
}
sbdsp.current_command_index++;
}
break;
// SB MIDI commands
case DSP_MIDI_WRITE_POLL: // 0x38: write MIDI byte
if (sbdsp.dav_dsp) {
if (sbdsp.current_command_index == 1) {
#ifdef SOUND_MPU
MIDI_RawOutByte(sbdsp.inbox);
#endif
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
}
sbdsp.current_command_index++;
}
break;
case DSP_MIDI_READ_POLL: // 0x30: read MIDI byte
if (sbdsp.current_command_index == 0) {
sbdsp.current_command = 0;
sbdsp_output(0x00); // no MIDI input
}
break;
case 0x31: // MIDI Read Interrupt - accept and ignore
case 0xA0: // Disable Stereo Input Mode
case 0xA8: // Enable Stereo Input Mode
sbdsp.current_command = 0;
break;
case 0x34: // MIDI UART read+write poll
case 0x35: // MIDI UART read interrupt+Write poll
sbdsp.midi_uart_mode = true;
sbdsp.current_command = 0;
break;
// recording commands - fake ADC
case 0x24: // DMA ADC, 8-bit single-cycle (2 param bytes: length-1)
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) {
sbdsp.dma_sample_count += (sbdsp.inbox << 8);
sbdsp.dav_dsp = 0;
sbdsp.current_command = 0;
sbdsp.autoinit = 0;
sbdsp.dma_16bit = false;
sbdsp.adc_active = true;
sbdsp_set_dma_interval();
PIC_AddEvent(&DSP_ADC_event, sbdsp.dma_interval * (sbdsp.dma_sample_count + 1), 0);
}
sbdsp.current_command_index++;
}
break;
case 0x2C: // autoinit DMA ADC, 8-bit
sbdsp.current_command = 0;
sbdsp.autoinit = 1;
sbdsp.dma_16bit = false;
sbdsp.adc_active = true;
sbdsp_set_dma_interval();
PIC_AddEvent(&DSP_ADC_event, sbdsp.dma_interval * (sbdsp.dma_block_size + 1), 0);
break;
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.current_command = 0;
sbdsp.current_command_index = 0;
sbdsp.dma_block_size = 0x7FF; //default per 2.01
sbdsp.dma_xfer_count = 0;
sbdsp.dma_xfer_count_left = 0;
sbdsp.dma_stereo = false;
sbdsp.dma_stereo_sbpro = false;
sbdsp.dma_signed = false;
sbdsp.speaker_on = (sbdsp.dsp_version.major >= 4); // SB16 speaker always on
sbdsp.dma_done = false;
sbdsp.dac_resume_pending = false;
memset(&sbdsp.adpcm, 0, sizeof(sbdsp.adpcm));
sbdsp.rs.ring_head = sbdsp.rs.ring_tail = 0;
sbdsp.outbox = 0xAA;
sbdsp.dav_pc = 1;
sbdsp.reset_state = 0;
return 0;
}
static PIC_TimerEvent DSP_Reset_Event = {
.handler = DSP_Reset_EventHandler,
};
static __force_inline void sbdsp_reset(uint8_t value) {
value &= 1; // Some games may write unknown data for bits other than the LSB.
switch (value) {
case 1:
PIC_RemoveEvent(&DSP_Reset_Event);
while (sbdsp.dsp_busy) tight_loop_contents(); // TODO timeout!!!!!!
sbdsp.reset_state = 1;
sbdsp.autoinit = 0;
sbdsp.midi_uart_mode = false;
sbdsp_dma_disable(false);
break;
case 0:
if (sbdsp.reset_state == 1) {
sbdsp.reset_state = 2;
sbdsp.dav_pc = 0;
PIC_RemoveEvent(&DSP_Reset_Event);
PIC_AddEvent(&DSP_Reset_Event, 100, 0);
}
break;
default:
break;
}
}
// --------------
// SB Mixer functions
#if 1
// volume table, 0.12 fixedpoint, -1.5dB per step
static const int32_t sbmixer_voltab[32] = {
0, 9, 15, 21, 29, 39, 51, 65, // vol 0..7
77, 92, 109, 130, 154, 183, 217, 258, // vol 8..15
307, 365, 434, 516, 613, 728, 866, 1029, // vol 16..23
1223, 1453, 1727, 2053, 2440, 2900, 3446, 4096 // vol 24..31
};
#else
// volume table, 0.12 fixedpoint, -2dB per step (matches SB16 mixer scale?)
static const int32_t sbmixer_voltab[32] = {
0, 2, 4, 6, 8, 10, 13, 16, // vol 0..7
21, 26, 33, 41, 52, 65, 82, 103, // vol 8..15
130, 163, 205, 258, 325, 410, 516, 649, // vol 16..23
817, 1029, 1295, 1631, 2053, 2584, 3254, 4096 // vol 24..31
};
#endif
// set SB16 output switches
static void sbmixer_out_switch(uint8_t value) {
mixer_state[MIXER_OUT_SWITCH] = value;
volume.cd_audio[1] = (value & (1 << 1)) ? sbmixer_voltab[mixer_state[MIXER_VOL_CD_R] >> 3] : 0;
volume.cd_audio[0] = (value & (1 << 2)) ? sbmixer_voltab[mixer_state[MIXER_VOL_CD_L] >> 3] : 0;
// ignore Mic/Line at this moment
}
// set volume using SBPro mixer registers
static void sbmixer_pro_set(uint8_t sbpro_reg, uint8_t value) {
mixer_state[sbpro_reg] = value;
// update SB16 and actual mixer registers
int vol_r = ((value >> 0) & 0xF), vol_l = (value >> 4) & 0xF;
vol_l = (vol_l << 1) | (vol_l >> 3);
vol_r = (vol_r << 1) | (vol_r >> 3);
int sb16_reg = 0;
int32_t *volreg = NULL;
switch (sbpro_reg) {
case MIXER_VOL_MASTER: sb16_reg = MIXER_VOL_MASTER_L; volreg = &volume.sb_master[0]; break;
case MIXER_VOL_VOICE: sb16_reg = MIXER_VOL_VOICE_L; volreg = &volume.sb_pcm[0]; break;
case MIXER_VOL_MIDI: sb16_reg = MIXER_VOL_MIDI_L; volreg = &volume.opl[0]; break;
case MIXER_VOL_CD: sb16_reg = MIXER_VOL_CD_L; volreg = (mixer_state[MIXER_OUT_SWITCH] & (3 << 1)) ? &volume.cd_audio[0] : NULL; break;
case MIXER_VOL_LINE: sb16_reg = MIXER_VOL_LINE_L; volreg = NULL; break;
default: break;
}
if (sb16_reg) {
mixer_state[sb16_reg + 0] = vol_l << 3;
mixer_state[sb16_reg + 1] = vol_r << 3;
}
if (volreg) {
volreg[0] = sbmixer_voltab[vol_l];
volreg[1] = sbmixer_voltab[vol_r];
}
if (sbpro_reg == MIXER_VOL_LINE && wtvol_cb) {
uint8_t vol5 = (vol_l > vol_r) ? vol_l : vol_r;
wtvol_cb(vol5 * 100 / 31);
}
}
// set volume using SB16 mixer registers
static void sbmixer_16_set(uint8_t sb16_reg, uint8_t value) {
mixer_state[sb16_reg] = value & 0xF8;
int sbpro_reg = 0;
int32_t *volreg = NULL;
switch (sb16_reg) {
case MIXER_VOL_MASTER_L: sbpro_reg = MIXER_VOL_MASTER; volreg = &volume.sb_master[0]; break;
case MIXER_VOL_MASTER_R: sbpro_reg = MIXER_VOL_MASTER; volreg = &volume.sb_master[1]; break;
case MIXER_VOL_VOICE_L: sbpro_reg = MIXER_VOL_VOICE; volreg = &volume.sb_pcm[0]; break;
case MIXER_VOL_VOICE_R: sbpro_reg = MIXER_VOL_VOICE; volreg = &volume.sb_pcm[1]; break;
case MIXER_VOL_MIDI_L: sbpro_reg = MIXER_VOL_MIDI; volreg = &volume.opl[0]; break;
case MIXER_VOL_MIDI_R: sbpro_reg = MIXER_VOL_MIDI; volreg = &volume.opl[1]; break;
case MIXER_VOL_CD_L: sbpro_reg = MIXER_VOL_CD; volreg = (mixer_state[MIXER_OUT_SWITCH] & (1 << 2)) ? &volume.cd_audio[0] : NULL; break;
case MIXER_VOL_CD_R: sbpro_reg = MIXER_VOL_CD; volreg = (mixer_state[MIXER_OUT_SWITCH] & (1 << 1)) ? &volume.cd_audio[1] : NULL; break;
case MIXER_VOL_LINE_L: sbpro_reg = MIXER_VOL_LINE; volreg = NULL; break;
case MIXER_VOL_LINE_R: sbpro_reg = MIXER_VOL_LINE; volreg = NULL; break;
default: break;
}
if (sbpro_reg) {
int regmask = (sb16_reg & 1) ? 0x0F : 0xF0;
int proval = (value & 0xF0) >> ((sb16_reg & 1) ? 0 : 4);
mixer_state[sbpro_reg] = (mixer_state[sbpro_reg] & regmask) | proval;
}
if (volreg) {
*volreg = sbmixer_voltab[value >> 3];
}
if ((sb16_reg == MIXER_VOL_LINE_L || sb16_reg == MIXER_VOL_LINE_R) && wtvol_cb) {
wtvol_cb((value >> 3) * 100 / 31);
}
}
static void sbmixer_reset(void) {
// preserve IRQ/DMA select registers across mixer reset
uint8_t saved_irq = mixer_state[MIXER_INTERRUPT];
uint8_t saved_dma = mixer_state[MIXER_DMA];
memset(mixer_state, 0, sizeof(mixer_state));
mixer_state[MIXER_INTERRUPT] = saved_irq;
mixer_state[MIXER_DMA] = saved_dma;
// initialize default values
sbmixer_pro_set(MIXER_VOL_MASTER, 0xFF);
sbmixer_pro_set(MIXER_VOL_VOICE, 0xEE);
sbmixer_pro_set(MIXER_VOL_MIDI, 0xEE);
sbmixer_pro_set(MIXER_VOL_CD, 0xEE);
sbmixer_pro_set(MIXER_VOL_LINE, 0x00);
sbmixer_out_switch(0x6); // CD L/R enabled, Line/Mic disabled
// preinit (non-functional) bass/treble settings
mixer_state[0x44] = mixer_state[0x45] = mixer_state[0x46] = mixer_state[0x47] = 0x80;
}
static __force_inline uint8_t sbmixer_read(void) {
switch (sbdsp.mixer_command) {
case MIXER_IRQ_STATUS:
// Bits 0-2: IRQ pending flags. Upper bits: ViBRA type identifier.
return (sbdsp.irq_8_pending ? 0x01 : 0) | (sbdsp.irq_16_pending ? 0x02 : 0) | 0x70;
case MIXER_VOL_LINE_L:
case MIXER_VOL_LINE_R:
if (wt_volume_ptr) {
return (uint8_t)((*wt_volume_ptr * 31 / 100) << 3);
}
return mixer_state[sbdsp.mixer_command];
case MIXER_VOL_LINE:
if (wt_volume_ptr) {
uint8_t nibble = *wt_volume_ptr * 15 / 100;
return (nibble << 4) | nibble;
}
return mixer_state[sbdsp.mixer_command];
case 0x8E:
return mixer_state[0x8E] | 0xC0;
case 0xFF:
return 0xFF;
default:
return mixer_state[sbdsp.mixer_command];
}
}
static __force_inline void sbmixer_write(uint8_t value) {
// mixer locking
switch(sbdsp.options.lockMixer) {
case MIXER_LOCK_ALL_BUT_SBP_VOICE:
if (sbdsp.mixer_command != MIXER_VOL_VOICE)
// fallthrough
case MIXER_LOCK_ALL:
return;
default:
break;
}
switch (sbdsp.mixer_command) {
case 0x00: // Mixer reset
sbmixer_reset();
break;
case 0x02: // SBPro Master Volume alias (chains to 0x22)
mixer_state[0x02] = value;
sbmixer_pro_set(MIXER_VOL_MASTER, value);
break;
case MIXER_VOL_MASTER:
case MIXER_VOL_MIDI:
case MIXER_VOL_CD:
case MIXER_VOL_LINE:
case MIXER_VOL_VOICE:
sbmixer_pro_set(sbdsp.mixer_command, value);
break;
case MIXER_VOL_MASTER_L ... MIXER_VOL_LINE_R:
sbmixer_16_set(sbdsp.mixer_command, value);
break;
case MIXER_OUT_SWITCH:
sbmixer_out_switch(value);
break;
case MIXER_INTERRUPT:
// sbdsp.interrupt = value;
break;
case MIXER_DMA:
// sbdsp.dma = value;
break;
case 0xFF:
break;
case MIXER_STEREO:
//sbdsp.dma_stereo = value & 2;
// no break
default:
mixer_state[sbdsp.mixer_command] = value;
break;
}
}
uint8_t sbdsp_read(uint8_t address) {
uint8_t out = 0xFF;
// emulate address aliasing seen on pre-SB16
switch (address) {
case DSP_READ: // +0xA
case DSP_READ+1: // +0xB
out = sbdsp.outbox;
sbdsp.dav_pc = 0;
return out;
case DSP_READ_STATUS: // +0xE
if (sbdsp.irq_8_pending) {
sbdsp.irq_8_pending = false;
PIC_DeActivateIRQ();
}
return (sbdsp.dav_pc << 7) | DSP_UNUSED_STATUS_BITS_PULLED_HIGH;
case DSP_IRQ_16_STATUS: // +0xF
if (sbdsp.irq_16_pending) {
sbdsp.irq_16_pending = false;
PIC_DeActivateIRQ();
}
return 0xff;
case DSP_WRITE_STATUS: // +0xC
case DSP_WRITE_STATUS+1:// +0xD
if (sbdsp.reset_state)
return 0xFF; // busy during reset
return ((sbdsp.dav_dsp | sbdsp.dsp_busy | sbdsp.dac_resume_pending) << 7) | DSP_UNUSED_STATUS_BITS_PULLED_HIGH;
case MIXER_DATA:
return sbmixer_read();
default:
return 0xFF;
}
}
void sbdsp_write(uint8_t address, uint8_t value) {
switch (address) {
case DSP_WRITE: // 0xC
case DSP_WRITE+1: // 0xD
// in MIDI UART mode, all writes to DSP data port are MIDI output
if (sbdsp.midi_uart_mode) {
#ifdef SOUND_MPU
MIDI_RawOutByte(value);
#endif
break;
}
if (sbdsp.dav_dsp) DBG_PRINTF("WARN - DAV_DSP OVERWRITE\n");
sbdsp.inbox = value;
sbdsp.dav_dsp = 1;
break;
case DSP_RESET: // 0x6
case DSP_RESET+1: // 0x7
sbdsp_reset(value);
break;
case MIXER_COMMAND:
sbdsp.mixer_command = value;
break;
case MIXER_DATA:
sbmixer_write(value);
break;
default:
break;
}
}