const ATARI_ST_CLOCK: f32 = 2_000_000.0;
#[derive(Debug, Clone, Copy, Default)]
pub struct ChannelState {
pub tone_period: u16,
pub frequency_hz: Option<f32>,
pub note_name: Option<&'static str>,
pub midi_note: Option<u8>,
pub amplitude: u8,
pub amplitude_normalized: f32,
pub tone_enabled: bool,
pub noise_enabled: bool,
pub envelope_enabled: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EnvelopeState {
pub period: u16,
pub shape: u8,
pub shape_name: &'static str,
pub is_sustaining: bool,
pub frequency_hz: Option<f32>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoiseState {
pub period: u8,
pub any_channel_enabled: bool,
}
#[derive(Debug, Clone, Default)]
pub struct ChannelStates {
pub channels: [ChannelState; 3],
pub envelope: EnvelopeState,
pub noise: NoiseState,
pub mixer_raw: u8,
}
impl ChannelStates {
pub fn from_registers(regs: &[u8; 16]) -> Self {
Self::from_registers_with_clock(regs, ATARI_ST_CLOCK)
}
pub fn from_registers_with_clock(regs: &[u8; 16], master_clock: f32) -> Self {
let mixer = regs[7];
let channels = [
Self::extract_channel(regs, 0, mixer, master_clock),
Self::extract_channel(regs, 1, mixer, master_clock),
Self::extract_channel(regs, 2, mixer, master_clock),
];
let env_period = (regs[11] as u16) | ((regs[12] as u16) << 8);
let env_shape = regs[13] & 0x0F;
let envelope = EnvelopeState {
period: env_period,
shape: env_shape,
shape_name: envelope_shape_name(env_shape),
is_sustaining: env_shape >= 8,
frequency_hz: if env_period > 0 {
Some(master_clock / (256.0 * env_period as f32))
} else {
None
},
};
let noise_period = regs[6] & 0x1F;
let noise = NoiseState {
period: noise_period,
any_channel_enabled: (mixer & 0x38) != 0x38, };
ChannelStates {
channels,
envelope,
noise,
mixer_raw: mixer,
}
}
fn extract_channel(
regs: &[u8; 16],
channel: usize,
mixer: u8,
master_clock: f32,
) -> ChannelState {
let period_lo_reg = channel * 2;
let period_hi_reg = channel * 2 + 1;
let amp_reg = 8 + channel;
let period_lo = regs[period_lo_reg] as u16;
let period_hi = (regs[period_hi_reg] & 0x0F) as u16;
let tone_period = period_lo | (period_hi << 8);
let amp_raw = regs[amp_reg];
let amplitude = amp_raw & 0x0F;
let envelope_enabled = (amp_raw & 0x10) != 0;
let tone_bit = 1 << channel;
let noise_bit = 8 << channel;
let tone_enabled = (mixer & tone_bit) == 0;
let noise_enabled = (mixer & noise_bit) == 0;
let frequency_hz = if tone_period > 0 {
Some(master_clock / (16.0 * tone_period as f32))
} else {
None
};
let (note_name, midi_note) = frequency_hz.map(frequency_to_note).unwrap_or((None, None));
let amplitude_normalized = if envelope_enabled {
1.0
} else {
amplitude as f32 / 15.0
};
ChannelState {
tone_period,
frequency_hz,
note_name,
midi_note,
amplitude,
amplitude_normalized,
tone_enabled,
noise_enabled,
envelope_enabled,
}
}
pub fn max_amplitude(&self) -> f32 {
self.channels
.iter()
.map(|ch| ch.amplitude_normalized)
.fold(0.0, f32::max)
}
pub fn any_envelope_enabled(&self) -> bool {
self.channels.iter().any(|ch| ch.envelope_enabled)
}
pub fn active_channels(&self) -> impl Iterator<Item = (usize, &ChannelState)> {
self.channels.iter().enumerate().filter(|(_, ch)| {
ch.amplitude > 0 && (ch.tone_enabled || ch.noise_enabled || ch.envelope_enabled)
})
}
}
fn envelope_shape_name(shape: u8) -> &'static str {
match shape & 0x0F {
0x00..=0x03 => "\\___", 0x04..=0x07 => "/___", 0x08 => "\\\\\\\\", 0x09 => "\\___", 0x0A => "\\/\\/", 0x0B => "\\¯¯¯", 0x0C => "////", 0x0D => "/¯¯¯", 0x0E => "/\\/\\", 0x0F => "/___", _ => "????",
}
}
fn frequency_to_note(freq: f32) -> (Option<&'static str>, Option<u8>) {
if !(20.0..=20000.0).contains(&freq) {
return (None, None);
}
let midi_float = (freq / 440.0).log2().mul_add(12.0, 69.0);
let midi = midi_float.round() as i32;
if !(0..=127).contains(&midi) {
return (None, None);
}
let midi_u8 = midi as u8;
static NOTE_NAMES: [&str; 128] = [
"C-1", "C#-1", "D-1", "D#-1", "E-1", "F-1", "F#-1", "G-1", "G#-1", "A-1", "A#-1", "B-1",
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", "C1", "C#1",
"D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", "C2", "C#2", "D2", "D#2",
"E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", "C3", "C#3", "D3", "D#3", "E3", "F3",
"F#3", "G3", "G#3", "A3", "A#3", "B3", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4",
"G#4", "A4", "A#4", "B4", "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5",
"A#5", "B5", "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", "C8", "C#8",
"D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", "C9", "C#9", "D9", "D#9",
"E9", "F9", "F#9", "G9",
];
let note_name = NOTE_NAMES.get(midi_u8 as usize).copied();
(note_name, Some(midi_u8))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_channel_a() {
let mut regs = [0u8; 16];
regs[0] = 0x1C; regs[1] = 0x01; regs[7] = 0x3E; regs[8] = 0x0F;
let states = ChannelStates::from_registers(®s);
assert_eq!(states.channels[0].tone_period, 284);
assert_eq!(states.channels[0].amplitude, 15);
assert!(states.channels[0].tone_enabled);
assert!(!states.channels[0].noise_enabled);
let freq = states.channels[0].frequency_hz.unwrap();
assert!((freq - 440.0).abs() < 5.0, "Expected ~440Hz, got {freq}");
}
#[test]
fn test_envelope_mode() {
let mut regs = [0u8; 16];
regs[8] = 0x1F; regs[11] = 0x00; regs[12] = 0x10; regs[13] = 0x0E;
let states = ChannelStates::from_registers(®s);
assert!(states.channels[0].envelope_enabled);
assert_eq!(states.envelope.period, 4096);
assert_eq!(states.envelope.shape, 0x0E);
assert!(states.envelope.is_sustaining);
}
#[test]
fn test_frequency_to_note_a4() {
let (name, midi) = frequency_to_note(440.0);
assert_eq!(name, Some("A4"));
assert_eq!(midi, Some(69));
}
#[test]
fn test_frequency_to_note_c4() {
let (name, midi) = frequency_to_note(261.63);
assert_eq!(name, Some("C4"));
assert_eq!(midi, Some(60));
}
}