ym2149_common/
channel_state.rs

1//! Channel state extraction from YM2149 registers.
2//!
3//! This module provides a unified way to extract visualization-ready data
4//! from YM2149 register dumps. This works for all formats (YM, AKS, AY, SNDH)
5//! since they all ultimately write to the same YM2149 registers.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use ym2149_common::ChannelStates;
11//!
12//! let registers: [u8; 16] = [/* ... register dump ... */];
13//! let states = ChannelStates::from_registers(&registers);
14//!
15//! for (i, ch) in states.channels.iter().enumerate() {
16//!     println!("Channel {}: {:?}Hz, amp={}", i, ch.frequency_hz, ch.amplitude);
17//! }
18//! ```
19
20/// Standard Atari ST master clock for frequency calculations.
21const ATARI_ST_CLOCK: f32 = 2_000_000.0;
22
23/// State of a single YM2149 channel extracted from registers.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct ChannelState {
26    /// Tone period from registers (12-bit, 0-4095).
27    pub tone_period: u16,
28    /// Calculated frequency in Hz (None if period is 0).
29    pub frequency_hz: Option<f32>,
30    /// Musical note name (e.g., "A4", "C#5").
31    pub note_name: Option<&'static str>,
32    /// MIDI note number (21-108 for piano range, None if out of range).
33    pub midi_note: Option<u8>,
34    /// Raw amplitude value (0-15).
35    pub amplitude: u8,
36    /// Normalized amplitude (0.0-1.0) for visualization.
37    pub amplitude_normalized: f32,
38    /// Whether tone output is enabled for this channel.
39    pub tone_enabled: bool,
40    /// Whether noise output is enabled for this channel.
41    pub noise_enabled: bool,
42    /// Whether envelope mode is enabled (bit 4 of amplitude register).
43    pub envelope_enabled: bool,
44}
45
46/// Envelope generator state.
47#[derive(Debug, Clone, Copy, Default)]
48pub struct EnvelopeState {
49    /// Envelope period from registers (16-bit).
50    pub period: u16,
51    /// Envelope shape (0-15).
52    pub shape: u8,
53    /// Human-readable shape description.
54    pub shape_name: &'static str,
55    /// Whether envelope is in "sustain" mode (shapes 8-15).
56    pub is_sustaining: bool,
57    /// Envelope frequency in Hz (None if period is 0).
58    pub frequency_hz: Option<f32>,
59}
60
61/// Noise generator state.
62#[derive(Debug, Clone, Copy, Default)]
63pub struct NoiseState {
64    /// Noise period (5-bit, 0-31).
65    pub period: u8,
66    /// Whether noise is enabled on any channel.
67    pub any_channel_enabled: bool,
68}
69
70/// Complete state of all YM2149 channels and generators.
71#[derive(Debug, Clone, Default)]
72pub struct ChannelStates {
73    /// State of channels A, B, C.
74    pub channels: [ChannelState; 3],
75    /// Envelope generator state.
76    pub envelope: EnvelopeState,
77    /// Noise generator state.
78    pub noise: NoiseState,
79    /// Raw mixer register value (for debugging).
80    pub mixer_raw: u8,
81}
82
83impl ChannelStates {
84    /// Extract channel states from a YM2149 register dump.
85    ///
86    /// This is the main entry point for visualization. It works with any
87    /// register dump regardless of the source format (YM, AKS, AY, SNDH).
88    ///
89    /// # Arguments
90    ///
91    /// * `regs` - 16-byte register dump from `Ym2149Backend::dump_registers()`
92    ///
93    /// # Returns
94    ///
95    /// Complete state of all channels and generators.
96    pub fn from_registers(regs: &[u8; 16]) -> Self {
97        Self::from_registers_with_clock(regs, ATARI_ST_CLOCK)
98    }
99
100    /// Extract channel states with a custom master clock frequency.
101    ///
102    /// Use this when emulating non-Atari ST systems with different clock rates.
103    ///
104    /// # Arguments
105    ///
106    /// * `regs` - 16-byte register dump
107    /// * `master_clock` - Master clock frequency in Hz
108    pub fn from_registers_with_clock(regs: &[u8; 16], master_clock: f32) -> Self {
109        let mixer = regs[7];
110
111        // Extract channel states
112        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        // Extract envelope state
119        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                // Envelope frequency = master_clock / (256 * period)
128                Some(master_clock / (256.0 * env_period as f32))
129            } else {
130                None
131            },
132        };
133
134        // Extract noise state
135        let noise_period = regs[6] & 0x1F;
136        let noise = NoiseState {
137            period: noise_period,
138            any_channel_enabled: (mixer & 0x38) != 0x38, // Bits 3-5 inverted
139        };
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        // Register offsets per channel
156        let period_lo_reg = channel * 2;
157        let period_hi_reg = channel * 2 + 1;
158        let amp_reg = 8 + channel;
159
160        // Extract period (12-bit)
161        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        // Extract amplitude
166        let amp_raw = regs[amp_reg];
167        let amplitude = amp_raw & 0x0F;
168        let envelope_enabled = (amp_raw & 0x10) != 0;
169
170        // Mixer bits (active low)
171        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        // Calculate frequency
177        let frequency_hz = if tone_period > 0 {
178            // Frequency = master_clock / (16 * period)
179            Some(master_clock / (16.0 * tone_period as f32))
180        } else {
181            None
182        };
183
184        // Convert to musical note
185        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    /// Get the maximum amplitude across all channels (for VU meter).
201    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    /// Check if any channel has envelope mode enabled.
209    pub fn any_envelope_enabled(&self) -> bool {
210        self.channels.iter().any(|ch| ch.envelope_enabled)
211    }
212
213    /// Get channels that are actively producing sound.
214    ///
215    /// A channel is "active" if it has amplitude > 0 and either tone or noise enabled.
216    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
223/// Get human-readable name for envelope shape.
224fn envelope_shape_name(shape: u8) -> &'static str {
225    match shape & 0x0F {
226        0x00..=0x03 => "\\___", // Decay
227        0x04..=0x07 => "/___",  // Attack
228        0x08 => "\\\\\\\\",     // Sawtooth down
229        0x09 => "\\___",        // Decay (one-shot)
230        0x0A => "\\/\\/",       // Triangle
231        0x0B => "\\¯¯¯",        // Decay + hold high
232        0x0C => "////",         // Sawtooth up
233        0x0D => "/¯¯¯",         // Attack + hold high
234        0x0E => "/\\/\\",       // Triangle (inverted)
235        0x0F => "/___",         // Attack (one-shot)
236        _ => "????",
237    }
238}
239
240/// Convert frequency to musical note.
241///
242/// Returns (note_name, midi_note) or (None, None) if out of range.
243fn 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    // MIDI note number: 69 = A4 = 440Hz
249    // n = 12 * log2(f / 440) + 69
250    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    // Note names
260    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        // Set up registers for channel A: period=284 (A4), amplitude=15
284        let mut regs = [0u8; 16];
285        regs[0] = 0x1C; // Period low (284 & 0xFF = 28 = 0x1C)
286        regs[1] = 0x01; // Period high (284 >> 8 = 1)
287        regs[7] = 0x3E; // Mixer: tone A on (bit 0 = 0)
288        regs[8] = 0x0F; // Volume A = 15
289
290        let states = ChannelStates::from_registers(&regs);
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        // Frequency should be ~440Hz (A4)
298        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; // Volume A = envelope mode (bit 4 set)
306        regs[11] = 0x00; // Envelope period low
307        regs[12] = 0x10; // Envelope period high (4096)
308        regs[13] = 0x0E; // Envelope shape = triangle
309
310        let states = ChannelStates::from_registers(&regs);
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}