pub mod coupled;
pub mod driver;
pub(crate) mod fx_engine;
use sidera::registers::{
CTRL_GATE, CTRL_RING, CTRL_SYNC, CTRL_TEST, MODE_VOL, V1_ATTACK_DECAY, V1_CONTROL, V1_FREQ_HI,
V1_FREQ_LO, V1_PULSE_HI, V1_PULSE_LO, V1_SUSTAIN_RELEASE,
};
use sidera::{ChipModel, Sid};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SidModel {
Mos6581,
Mos8580,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SidRegion {
Pal,
Ntsc,
}
impl SidRegion {
#[inline]
fn clock_hz(self) -> u32 {
match self {
SidRegion::Pal => sidera::PAL_CLOCK,
SidRegion::Ntsc => sidera::NTSC_CLOCK,
}
}
}
#[derive(Clone, Copy, Default)]
pub struct SidVoicePatch {
pub waveform: u8,
pub pulse_width: u16,
pub attack_decay: u8,
pub sustain_release: u8,
pub ring: bool,
pub sync: bool,
pub test: bool,
pub gate: bool,
}
pub(super) const RELEASE_MS: [u16; 16] = [
6, 24, 48, 72, 114, 168, 204, 240, 300, 750, 1500, 2400, 3000, 9000, 15000, 24000,
];
pub(crate) const SEMI_NUM: i64 = 119;
pub(crate) const SEMI_DEN: i64 = 2000;
const SEMI_UP_Q16: u64 = 69433;
pub(crate) fn shift_semitones_up(base: u32, semis: u8) -> u32 {
let mut r = base as u64;
for _ in 0..semis {
r = (r * SEMI_UP_Q16) >> 16;
if r > u32::MAX as u64 {
r = u32::MAX as u64;
}
}
r as u32
}
const SEMI_DOWN_Q16: u64 = 61858;
pub(crate) fn shift_semitones_down(base: u32, semis: u8) -> u32 {
let mut r = base as u64;
for _ in 0..semis {
r = (r * SEMI_DOWN_Q16) >> 16;
}
r as u32
}
pub(crate) const C4_MILLI_HZ: u32 = 261_625;
pub(crate) const C4_NOTE: u8 = 48;
pub(crate) fn note_milli_hz(note: u8) -> u32 {
if note >= C4_NOTE {
shift_semitones_up(C4_MILLI_HZ, note - C4_NOTE)
} else {
shift_semitones_down(C4_MILLI_HZ, C4_NOTE - note)
}
}
pub(crate) const VIB_GATE_FRAMES: u32 = 4;
#[derive(Clone, Copy, Default)]
pub struct SidFx {
pub pw_sweep: bool,
pub pw_speed: i8,
pub pw_delay: u16,
pub pw_low_byte_inc: bool,
pub pw_hi_bound: u8,
pub pw_lo_bound: u8,
pub pw_bounds_set: bool,
pub pw_reseed_on_note: bool,
pub vib_len_gate: bool,
pub vibrato: bool,
pub vib_depth: u8,
pub vib_div: u8,
pub vib_tempvdif_reg: u16,
pub arp: bool,
pub interp_vib: bool,
pub interp_half_depth: u8,
pub interp_shift: u8,
pub wave_attack_ctrl: u8,
pub wave_attack_frames: u8,
pub wave_attack_note: u8,
pub wave_attack_note_set: bool,
pub wave_alt_ctrl: u8,
pub arp2_reg: u16,
pub arp3: bool,
pub arp3_off_a: i8,
pub arp3_off_b: i8,
pub skydive_climb: bool,
pub skydive_add: i16,
pub skydive_when: u8,
pub drum: bool,
pub filter: bool,
pub filter_resfilt: u8,
pub filter_step: i8,
pub filter_seed: u8,
pub filter_reseed_each_note: bool,
pub alt_arp_semitones: u8,
pub drum_no_freq_slide: bool,
pub zero_adsr_on_note_off: bool,
}
#[derive(Clone, Copy, Default)]
struct VoiceSlot {
owner: Option<usize>,
gated: bool,
release_nibble: u8,
release_tail: u32,
base_milli_hz: u32,
note_frame: u32,
skydive_semis: u8,
drum_hi: u8,
gate_off_ctrl: u8,
}
pub struct SidSynth {
chip: Sid,
chip_clock_hz: u32,
output_rate: u32,
slots: [VoiceSlot; 3],
fx: SidFx,
pw_cur: i32,
pulse_dir: u8,
pulse_delay_ctr: i32,
pw_init: bool,
reg: [u8; 0x20],
}
impl SidSynth {
pub fn new(output_rate: u32, model: SidModel, region: SidRegion) -> Self {
let output_rate = output_rate.max(1);
let mut chip = Sid::new(output_rate);
chip.set_model(match model {
SidModel::Mos6581 => ChipModel::Mos6581,
SidModel::Mos8580 => ChipModel::Mos8580,
});
let chip_clock_hz = region.clock_hz();
chip.set_chip_clock(chip_clock_hz);
chip.write_register(MODE_VOL, 0x0F);
Self {
chip,
chip_clock_hz,
output_rate,
slots: [VoiceSlot::default(); 3],
fx: SidFx::default(),
pw_cur: 0,
pulse_dir: 0,
pulse_delay_ctr: 0,
pw_init: false,
reg: [0u8; 0x20],
}
}
#[inline]
fn wr(&mut self, reg: u8, val: u8) {
if (reg as usize) < self.reg.len() {
self.reg[reg as usize] = val;
}
self.chip.write_register(reg, val);
}
pub fn owner_voice_regs(&self, owner: usize) -> Option<[u8; 7]> {
let slot = self.slot_of(owner)?;
let b = slot * 7;
let mut out = [0u8; 7];
out.copy_from_slice(&self.reg[b..b + 7]);
Some(out)
}
#[cfg(feature = "std")]
pub fn owner_state(&self, owner: usize) -> Option<(u32, bool)> {
let slot = self.slot_of(owner)?;
let s = &self.slots[slot];
Some((s.base_milli_hz, s.gated))
}
#[inline]
fn base(slot: usize) -> u8 {
slot as u8 * 7
}
fn assign_slot(&mut self, owner: usize) -> usize {
if let Some(i) = self.slots.iter().position(|s| s.owner == Some(owner)) {
return i;
}
if let Some(i) = self
.slots
.iter()
.position(|s| !s.gated && s.release_tail == 0)
{
return i;
}
0
}
fn slot_of(&self, owner: usize) -> Option<usize> {
self.slots.iter().position(|s| s.owner == Some(owner))
}
#[inline]
fn freq_reg(&self, milli_hz: u32) -> u16 {
fx_engine::freq_reg(milli_hz, self.chip_clock_hz)
}
fn write_freq(&mut self, slot: usize, milli_hz: u32) {
let freq_reg = self.freq_reg(milli_hz);
let b = Self::base(slot);
self.wr(b + V1_FREQ_LO, (freq_reg & 0xFF) as u8);
self.wr(b + V1_FREQ_HI, (freq_reg >> 8) as u8);
}
fn write_pw(&mut self, slot: usize, pw: u16) {
let b = Self::base(slot);
self.wr(b + V1_PULSE_LO, (pw & 0xFF) as u8);
self.wr(b + V1_PULSE_HI, ((pw >> 8) & 0x0F) as u8);
}
fn program_voice(&mut self, slot: usize, p: &SidVoicePatch) -> u8 {
let b = Self::base(slot);
self.wr(b + V1_ATTACK_DECAY, p.attack_decay);
self.wr(b + V1_SUSTAIN_RELEASE, p.sustain_release);
let ctrl = ((p.waveform & 0x0F) << 4)
| (CTRL_SYNC * p.sync as u8)
| (CTRL_RING * p.ring as u8)
| (CTRL_TEST * p.test as u8);
self.wr(b + V1_CONTROL, ctrl);
ctrl
}
pub fn note_on(
&mut self,
owner: usize,
patch: &SidVoicePatch,
fx: &SidFx,
milli_hz: u32,
) -> usize {
let slot = self.assign_slot(owner);
self.fx = *fx;
if fx.pw_reseed_on_note || !self.pw_init {
self.pw_cur = patch.pulse_width as i32;
}
if !self.pw_init {
self.pulse_dir = 0;
self.pulse_delay_ctr = fx.pw_delay as i32;
self.pw_init = true;
}
let gate_off_ctrl = self.program_voice(slot, patch);
let pw = self.pw_cur as u16;
self.write_pw(slot, pw);
self.write_freq(slot, milli_hz);
let b = Self::base(slot);
let has_waveform = (patch.waveform & 0x0F) != 0;
let ctrl = if has_waveform {
self.chip.control(slot as u8) | CTRL_GATE
} else {
self.chip.control(slot as u8) & !CTRL_GATE
};
self.wr(b + V1_CONTROL, ctrl);
self.slots[slot] = VoiceSlot {
owner: Some(owner),
gated: has_waveform,
release_nibble: patch.sustain_release & 0x0F,
release_tail: 0,
base_milli_hz: milli_hz,
note_frame: 0,
skydive_semis: 0,
drum_hi: (self.freq_reg(milli_hz) >> 8) as u8,
gate_off_ctrl,
};
slot
}
pub fn advance_pw(&mut self) {
if !self.fx.pw_sweep || (self.fx.pw_delay == 0 && self.fx.pw_speed == 0) {
return;
}
if !self.any_active() {
return; }
if self.fx.pw_low_byte_inc {
let step = (self.fx.pw_speed as u8) as i32;
let pw_lo = ((self.pw_cur & 0xFF) + step) & 0xFF;
self.pw_cur = (self.pw_cur & 0xF00) | pw_lo;
let pw = self.pw_cur as u16;
for slot in 0..self.slots.len() {
if self.slots[slot].owner.is_some() {
self.write_pw(slot, pw);
}
}
return;
}
self.pulse_delay_ctr -= 1;
if self.pulse_delay_ctr >= 0 {
return;
}
self.pulse_delay_ctr = self.fx.pw_delay as i32;
let step = (self.fx.pw_speed as u8) as i32; let (hi, lo) = if self.fx.pw_bounds_set {
(self.fx.pw_hi_bound as i32, self.fx.pw_lo_bound as i32)
} else {
(0x0E, 0x08)
};
if self.pulse_dir == 0 {
self.pw_cur += step;
if (self.pw_cur >> 8) >= hi {
self.pulse_dir = 1;
}
} else {
self.pw_cur -= step;
if (self.pw_cur >> 8) <= lo {
self.pulse_dir = 0;
}
}
self.pw_cur = self.pw_cur.clamp(0, 0xFFF);
let pw = self.pw_cur as u16;
for slot in 0..self.slots.len() {
if self.slots[slot].owner.is_some() {
self.write_pw(slot, pw);
}
}
}
pub fn advance_fx(&mut self, owner: usize, frame: u32, speed: u8) {
let Some(slot) = self.slot_of(owner) else {
return;
};
let fx = self.fx;
let (base, gated, note_frame, drum_hi0, gate_off_ctrl, skydive_semis) = {
let s = &mut self.slots[slot];
s.note_frame = s.note_frame.saturating_add(1);
if fx.skydive_climb && (s.gated || s.release_tail > 0) && frame & 3 == 0 {
s.skydive_semis = s.skydive_semis.saturating_add(1);
}
(
s.base_milli_hz,
s.gated,
s.note_frame,
s.drum_hi,
s.gate_off_ctrl,
s.skydive_semis,
)
};
if base == 0 {
return;
}
let mut st = fx_engine::FxState {
base_milli_hz: base,
gated,
note_frame,
drum_hi: drum_hi0,
gate_off_ctrl,
skydive_semis,
interp_ctr: 0,
interp_up: false,
};
let (reg, ctrl_override) =
fx_engine::modulate(&mut st, &fx, frame, speed, self.chip_clock_hz);
self.slots[slot].drum_hi = st.drum_hi;
let b = Self::base(slot);
self.wr(b + V1_FREQ_LO, (reg & 0xFF) as u8);
self.wr(b + V1_FREQ_HI, (reg >> 8) as u8);
if let Some(c) = ctrl_override {
self.wr(b + V1_CONTROL, c);
}
}
pub fn set_frequency(&mut self, owner: usize, milli_hz: u32) {
if let Some(slot) = self.slot_of(owner) {
self.slots[slot].base_milli_hz = milli_hz;
self.write_freq(slot, milli_hz);
}
}
pub fn note_off(&mut self, owner: usize, is_fetch: bool) {
if let Some(slot) = self.slot_of(owner) {
let b = Self::base(slot);
let ctrl = self.slots[slot].gate_off_ctrl;
self.wr(b + V1_CONTROL, ctrl);
self.slots[slot].gated = false;
if !is_fetch && self.fx.zero_adsr_on_note_off {
self.wr(b + V1_ATTACK_DECAY, 0);
self.wr(b + V1_SUSTAIN_RELEASE, 0);
}
let nibble = self.slots[slot].release_nibble;
self.slots[slot].release_tail = self.release_frames(nibble);
}
}
pub fn note_cut(&mut self, owner: usize) {
if let Some(slot) = self.slot_of(owner) {
let b = Self::base(slot);
let ctrl = self.chip.control(slot as u8) & !CTRL_GATE;
self.wr(b + V1_CONTROL, ctrl);
self.slots[slot] = VoiceSlot::default();
}
}
fn release_frames(&self, nibble: u8) -> u32 {
(RELEASE_MS[(nibble & 0x0F) as usize] as u32 * self.output_rate) / 1000
}
pub fn clock(&mut self) -> (i16, [i32; 3]) {
let out = self.chip.clock_with_voices();
for s in &mut self.slots {
if !s.gated && s.release_tail > 0 {
s.release_tail -= 1;
}
}
out
}
pub fn voice_owner(&self, i: usize) -> Option<usize> {
self.slots.get(i).and_then(|s| s.owner)
}
pub fn voice_active(&self, i: usize) -> bool {
self.slots
.get(i)
.map(|s| s.gated || s.release_tail > 0)
.unwrap_or(false)
}
pub fn any_active(&self) -> bool {
self.slots.iter().any(|s| s.gated || s.release_tail > 0)
}
pub fn active_voice_count(&self) -> usize {
self.slots
.iter()
.filter(|s| s.gated || s.release_tail > 0)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn saw() -> SidVoicePatch {
SidVoicePatch {
waveform: 0b0010, sustain_release: 0xF0, ..Default::default()
}
}
#[test]
fn synth_plays_then_releases_to_idle() {
let mut s = SidSynth::new(44100, SidModel::Mos8580, SidRegion::Pal);
let slot = s.note_on(0, &saw(), &SidFx::default(), 440_000);
assert_eq!(s.voice_owner(slot), Some(0));
let mut peak = 0i32;
for _ in 0..4410 {
let (_mix, v) = s.clock();
peak = peak.max(v[slot].abs());
}
assert!(peak > 0, "a gated voice should sound (peak={peak})");
assert!(s.any_active());
s.note_off(0, false);
let mut frames = 0;
while s.any_active() && frames < 44100 {
s.clock();
frames += 1;
}
assert!(
!s.any_active(),
"voice should fall idle after its release tail"
);
}
#[test]
fn two_notes_take_distinct_voices_and_route_independently() {
let mut s = SidSynth::new(44100, SidModel::Mos8580, SidRegion::Pal);
let a = s.note_on(0, &saw(), &SidFx::default(), 220_000);
let b = s.note_on(1, &saw(), &SidFx::default(), 440_000);
assert_ne!(a, b, "distinct owners get distinct hardware voices");
assert_eq!(s.voice_owner(a), Some(0));
assert_eq!(s.voice_owner(b), Some(1));
let mut pk = [0i32; 3];
for _ in 0..4410 {
let (_mix, v) = s.clock();
for i in 0..3 {
pk[i] = pk[i].max(v[i].abs());
}
}
assert!(
pk[a] > 0 && pk[b] > 0,
"both notes sound on their own voice"
);
let unused = (0..3).find(|i| *i != a && *i != b).unwrap();
assert_eq!(pk[unused], 0, "an unallocated voice stays silent on 8580");
}
#[test]
fn drum_plays_noise_burst_then_slides_freq_high() {
const SPEED: u8 = 3;
let mut s = SidSynth::new(44100, SidModel::Mos8580, SidRegion::Pal);
let patch = SidVoicePatch {
waveform: 0b0001,
attack_decay: 0x06,
sustain_release: 0x40,
..Default::default()
};
let fx = SidFx {
drum: true,
..Default::default()
};
let slot = s.note_on(0, &patch, &fx, 2_000_000);
let hi0 = s.owner_voice_regs(0).unwrap()[V1_FREQ_HI as usize];
assert!(
hi0 >= 4,
"test note should have a non-trivial freq-high byte"
);
let gate_off = (0b0001u8) << 4;
for _ in 1..SPEED {
s.advance_fx(0, 0, SPEED);
let r = s.owner_voice_regs(0).unwrap();
assert_eq!(r[V1_CONTROL as usize], 0x80, "first row plays noise");
assert_eq!(
r[V1_FREQ_HI as usize], hi0,
"freq-high held during noise burst"
);
}
let mut expect_hi = hi0;
for _ in 0..3 {
s.advance_fx(0, 0, SPEED);
let r = s.owner_voice_regs(0).unwrap();
assert_eq!(
r[V1_CONTROL as usize], gate_off,
"slide uses instr waveform, gate off"
);
assert_eq!(
r[V1_FREQ_HI as usize], expect_hi,
"freq-high emits then decrements"
);
expect_hi -= 1;
}
let _ = slot;
}
#[test]
fn freed_voice_is_reused() {
let mut s = SidSynth::new(44100, SidModel::Mos8580, SidRegion::Pal);
let a = s.note_on(0, &saw(), &SidFx::default(), 220_000);
s.note_cut(0); assert!(!s.any_active());
let b = s.note_on(1, &saw(), &SidFx::default(), 440_000);
assert_eq!(a, b, "a hard-cut voice is reused by the next note");
assert_eq!(s.voice_owner(b), Some(1));
}
}