1const ATARI_ST_CLOCK: f32 = 2_000_000.0;
22
23#[derive(Debug, Clone, Copy, Default)]
25pub struct ChannelState {
26 pub tone_period: u16,
28 pub frequency_hz: Option<f32>,
30 pub note_name: Option<&'static str>,
32 pub midi_note: Option<u8>,
34 pub amplitude: u8,
36 pub amplitude_normalized: f32,
38 pub tone_enabled: bool,
40 pub noise_enabled: bool,
42 pub envelope_enabled: bool,
44}
45
46#[derive(Debug, Clone, Copy, Default)]
48pub struct EnvelopeState {
49 pub period: u16,
51 pub shape: u8,
53 pub shape_name: &'static str,
55 pub is_sustaining: bool,
57 pub frequency_hz: Option<f32>,
59}
60
61#[derive(Debug, Clone, Copy, Default)]
63pub struct NoiseState {
64 pub period: u8,
66 pub any_channel_enabled: bool,
68}
69
70#[derive(Debug, Clone, Default)]
72pub struct ChannelStates {
73 pub channels: [ChannelState; 3],
75 pub envelope: EnvelopeState,
77 pub noise: NoiseState,
79 pub mixer_raw: u8,
81}
82
83impl ChannelStates {
84 pub fn from_registers(regs: &[u8; 16]) -> Self {
97 Self::from_registers_with_clock(regs, ATARI_ST_CLOCK)
98 }
99
100 pub fn from_registers_with_clock(regs: &[u8; 16], master_clock: f32) -> Self {
109 let mixer = regs[7];
110
111 let channels = [
113 Self::extract_channel(regs, 0, mixer, master_clock),
114 Self::extract_channel(regs, 1, mixer, master_clock),
115 Self::extract_channel(regs, 2, mixer, master_clock),
116 ];
117
118 let env_period = (regs[11] as u16) | ((regs[12] as u16) << 8);
120 let env_shape = regs[13] & 0x0F;
121 let envelope = EnvelopeState {
122 period: env_period,
123 shape: env_shape,
124 shape_name: envelope_shape_name(env_shape),
125 is_sustaining: env_shape >= 8,
126 frequency_hz: if env_period > 0 {
127 Some(master_clock / (256.0 * env_period as f32))
129 } else {
130 None
131 },
132 };
133
134 let noise_period = regs[6] & 0x1F;
136 let noise = NoiseState {
137 period: noise_period,
138 any_channel_enabled: (mixer & 0x38) != 0x38, };
140
141 ChannelStates {
142 channels,
143 envelope,
144 noise,
145 mixer_raw: mixer,
146 }
147 }
148
149 fn extract_channel(
150 regs: &[u8; 16],
151 channel: usize,
152 mixer: u8,
153 master_clock: f32,
154 ) -> ChannelState {
155 let period_lo_reg = channel * 2;
157 let period_hi_reg = channel * 2 + 1;
158 let amp_reg = 8 + channel;
159
160 let period_lo = regs[period_lo_reg] as u16;
162 let period_hi = (regs[period_hi_reg] & 0x0F) as u16;
163 let tone_period = period_lo | (period_hi << 8);
164
165 let amp_raw = regs[amp_reg];
167 let amplitude = amp_raw & 0x0F;
168 let envelope_enabled = (amp_raw & 0x10) != 0;
169
170 let tone_bit = 1 << channel;
172 let noise_bit = 8 << channel;
173 let tone_enabled = (mixer & tone_bit) == 0;
174 let noise_enabled = (mixer & noise_bit) == 0;
175
176 let frequency_hz = if tone_period > 0 {
178 Some(master_clock / (16.0 * tone_period as f32))
180 } else {
181 None
182 };
183
184 let (note_name, midi_note) = frequency_hz.map(frequency_to_note).unwrap_or((None, None));
186
187 ChannelState {
188 tone_period,
189 frequency_hz,
190 note_name,
191 midi_note,
192 amplitude,
193 amplitude_normalized: amplitude as f32 / 15.0,
194 tone_enabled,
195 noise_enabled,
196 envelope_enabled,
197 }
198 }
199
200 pub fn max_amplitude(&self) -> f32 {
202 self.channels
203 .iter()
204 .map(|ch| ch.amplitude_normalized)
205 .fold(0.0, f32::max)
206 }
207
208 pub fn any_envelope_enabled(&self) -> bool {
210 self.channels.iter().any(|ch| ch.envelope_enabled)
211 }
212
213 pub fn active_channels(&self) -> impl Iterator<Item = (usize, &ChannelState)> {
217 self.channels.iter().enumerate().filter(|(_, ch)| {
218 ch.amplitude > 0 && (ch.tone_enabled || ch.noise_enabled || ch.envelope_enabled)
219 })
220 }
221}
222
223fn envelope_shape_name(shape: u8) -> &'static str {
225 match shape & 0x0F {
226 0x00..=0x03 => "\\___", 0x04..=0x07 => "/___", 0x08 => "\\\\\\\\", 0x09 => "\\___", 0x0A => "\\/\\/", 0x0B => "\\¯¯¯", 0x0C => "////", 0x0D => "/¯¯¯", 0x0E => "/\\/\\", 0x0F => "/___", _ => "????",
237 }
238}
239
240fn frequency_to_note(freq: f32) -> (Option<&'static str>, Option<u8>) {
244 if !(20.0..=20000.0).contains(&freq) {
245 return (None, None);
246 }
247
248 let midi_float = 12.0 * (freq / 440.0).log2() + 69.0;
251 let midi = midi_float.round() as i32;
252
253 if !(0..=127).contains(&midi) {
254 return (None, None);
255 }
256
257 let midi_u8 = midi as u8;
258
259 static NOTE_NAMES: [&str; 128] = [
261 "C-1", "C#-1", "D-1", "D#-1", "E-1", "F-1", "F#-1", "G-1", "G#-1", "A-1", "A#-1", "B-1",
262 "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", "C1", "C#1",
263 "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", "C2", "C#2", "D2", "D#2",
264 "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", "C3", "C#3", "D3", "D#3", "E3", "F3",
265 "F#3", "G3", "G#3", "A3", "A#3", "B3", "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4",
266 "G#4", "A4", "A#4", "B4", "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5",
267 "A#5", "B5", "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
268 "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", "C8", "C#8",
269 "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", "C9", "C#9", "D9", "D#9",
270 "E9", "F9", "F#9", "G9",
271 ];
272
273 let note_name = NOTE_NAMES.get(midi_u8 as usize).copied();
274 (note_name, Some(midi_u8))
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_extract_channel_a() {
283 let mut regs = [0u8; 16];
285 regs[0] = 0x1C; regs[1] = 0x01; regs[7] = 0x3E; regs[8] = 0x0F; let states = ChannelStates::from_registers(®s);
291
292 assert_eq!(states.channels[0].tone_period, 284);
293 assert_eq!(states.channels[0].amplitude, 15);
294 assert!(states.channels[0].tone_enabled);
295 assert!(!states.channels[0].noise_enabled);
296
297 let freq = states.channels[0].frequency_hz.unwrap();
299 assert!((freq - 440.0).abs() < 5.0, "Expected ~440Hz, got {}", freq);
300 }
301
302 #[test]
303 fn test_envelope_mode() {
304 let mut regs = [0u8; 16];
305 regs[8] = 0x1F; regs[11] = 0x00; regs[12] = 0x10; regs[13] = 0x0E; let states = ChannelStates::from_registers(®s);
311
312 assert!(states.channels[0].envelope_enabled);
313 assert_eq!(states.envelope.period, 4096);
314 assert_eq!(states.envelope.shape, 0x0E);
315 assert!(states.envelope.is_sustaining);
316 }
317
318 #[test]
319 fn test_frequency_to_note_a4() {
320 let (name, midi) = frequency_to_note(440.0);
321 assert_eq!(name, Some("A4"));
322 assert_eq!(midi, Some(69));
323 }
324
325 #[test]
326 fn test_frequency_to_note_c4() {
327 let (name, midi) = frequency_to_note(261.63);
328 assert_eq!(name, Some("C4"));
329 assert_eq!(midi, Some(60));
330 }
331}