use crate::channel_state::ChannelStates;
use std::collections::VecDeque;
pub const MAX_PSG_COUNT: usize = 4;
pub const MAX_CHANNEL_COUNT: usize = MAX_PSG_COUNT * 3;
pub const WAVEFORM_SIZE: usize = 256;
pub const VISUAL_SAMPLE_RATE: f32 = 44100.0;
pub const SAMPLES_PER_UPDATE: usize = 64;
pub const SPECTRUM_OCTAVES: usize = 8;
pub const BINS_PER_OCTAVE: usize = 4;
pub const SPECTRUM_BINS: usize = SPECTRUM_OCTAVES * BINS_PER_OCTAVE;
pub const SPECTRUM_DECAY: f32 = 0.85;
pub const SPECTRUM_BASE_FREQ: f32 = 32.703;
#[derive(Clone)]
pub struct WaveformSynthesizer {
waveform: [VecDeque<f32>; MAX_CHANNEL_COUNT],
phase: [f32; MAX_CHANNEL_COUNT],
psg_count: usize,
}
impl Default for WaveformSynthesizer {
fn default() -> Self {
Self::new()
}
}
impl WaveformSynthesizer {
#[must_use]
pub fn new() -> Self {
Self {
waveform: std::array::from_fn(|_| VecDeque::with_capacity(WAVEFORM_SIZE)),
phase: [0.0; MAX_CHANNEL_COUNT],
psg_count: 1,
}
}
pub fn set_psg_count(&mut self, count: usize) {
self.psg_count = count.clamp(1, MAX_PSG_COUNT);
}
#[must_use]
pub fn psg_count(&self) -> usize {
self.psg_count
}
#[must_use]
pub fn channel_count(&self) -> usize {
self.psg_count * 3
}
pub fn update(&mut self, channel_states: &ChannelStates) {
self.update_psg(0, channel_states);
}
pub fn update_psg(&mut self, psg_index: usize, channel_states: &ChannelStates) {
if psg_index >= MAX_PSG_COUNT {
return;
}
let base_channel = psg_index * 3;
for (local_ch, ch_state) in channel_states.channels.iter().enumerate() {
let global_ch = base_channel + local_ch;
if global_ch >= MAX_CHANNEL_COUNT {
break;
}
let freq = ch_state.frequency_hz.unwrap_or(0.0);
let has_output =
ch_state.tone_enabled || ch_state.noise_enabled || ch_state.envelope_enabled;
let has_amplitude = ch_state.amplitude > 0 || ch_state.envelope_enabled;
let amplitude = if has_output && has_amplitude {
if ch_state.envelope_enabled {
1.0
} else {
ch_state.amplitude_normalized
}
} else {
0.0
};
let phase_increment = if freq > 0.0 {
freq / VISUAL_SAMPLE_RATE
} else {
0.0
};
let envelope_shape = channel_states.envelope.shape;
for _ in 0..SAMPLES_PER_UPDATE {
let sample =
self.synthesize_sample(ch_state, global_ch, amplitude, envelope_shape, freq);
if self.waveform[global_ch].len() >= WAVEFORM_SIZE {
self.waveform[global_ch].pop_front();
}
self.waveform[global_ch].push_back(sample);
self.phase[global_ch] = (self.phase[global_ch] + phase_increment).fract();
}
}
}
pub fn update_multi_psg(&mut self, register_banks: &[[u8; 16]], psg_count: usize) {
self.set_psg_count(psg_count);
for (psg_idx, registers) in register_banks.iter().enumerate().take(psg_count) {
let channel_states = ChannelStates::from_registers(registers);
self.update_psg(psg_idx, &channel_states);
}
}
#[inline]
fn synthesize_sample(
&self,
ch_state: &crate::channel_state::ChannelState,
ch: usize,
amplitude: f32,
envelope_shape: u8,
freq: f32,
) -> f32 {
let phase = self.phase[ch];
if ch_state.envelope_enabled && freq > 0.0 {
self.synthesize_envelope_sample(envelope_shape, phase, amplitude)
} else if ch_state.tone_enabled && freq > 0.0 {
if phase < 0.5 { amplitude } else { -amplitude }
} else if ch_state.noise_enabled {
let noise = (phase * 12345.0).sin() * 2.0 - 1.0;
noise * amplitude * 0.7
} else {
0.0
}
}
#[inline]
fn synthesize_envelope_sample(&self, shape: u8, phase: f32, amplitude: f32) -> f32 {
let sample = match shape & 0x0F {
0x00..=0x03 | 0x09 => {
phase.mul_add(-2.0, 1.0)
}
0x04..=0x07 | 0x0F => {
phase.mul_add(2.0, -1.0)
}
0x08 => {
phase.mul_add(-2.0, 1.0)
}
0x0A => {
if phase < 0.5 {
phase.mul_add(4.0, -1.0) } else {
phase.mul_add(-4.0, 3.0) }
}
0x0B => {
if phase < 0.5 { 1.0 - phase * 4.0 } else { 1.0 }
}
0x0C => {
phase.mul_add(2.0, -1.0)
}
0x0D => {
if phase < 0.5 { phase * 4.0 - 1.0 } else { 1.0 }
}
0x0E => {
if phase < 0.5 {
1.0 - phase * 4.0 } else {
phase * 4.0 - 3.0 }
}
_ => 0.0,
};
sample * amplitude
}
#[must_use]
pub fn get_samples(&self) -> Vec<[f32; 3]> {
let len = self.waveform[0]
.len()
.min(self.waveform[1].len())
.min(self.waveform[2].len());
(0..len)
.map(|i| {
[
self.waveform[0].get(i).copied().unwrap_or(0.0),
self.waveform[1].get(i).copied().unwrap_or(0.0),
self.waveform[2].get(i).copied().unwrap_or(0.0),
]
})
.collect()
}
#[must_use]
pub fn channel_waveform(&self, channel: usize) -> &VecDeque<f32> {
&self.waveform[channel.min(MAX_CHANNEL_COUNT - 1)]
}
}
#[inline]
#[must_use]
pub fn freq_to_bin(freq: f32) -> usize {
if freq <= 0.0 {
return 0;
}
let octaves_above_c1 = (freq / SPECTRUM_BASE_FREQ).log2();
let bin = (octaves_above_c1 * BINS_PER_OCTAVE as f32).round() as i32;
bin.clamp(0, (SPECTRUM_BINS - 1) as i32) as usize
}
#[derive(Clone)]
pub struct SpectrumAnalyzer {
spectrum: [[f32; SPECTRUM_BINS]; MAX_CHANNEL_COUNT],
combined: [f32; SPECTRUM_BINS],
psg_count: usize,
}
impl Default for SpectrumAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl SpectrumAnalyzer {
#[must_use]
pub fn new() -> Self {
Self {
spectrum: [[0.0; SPECTRUM_BINS]; MAX_CHANNEL_COUNT],
combined: [0.0; SPECTRUM_BINS],
psg_count: 1,
}
}
pub fn set_psg_count(&mut self, count: usize) {
self.psg_count = count.clamp(1, MAX_PSG_COUNT);
}
#[must_use]
pub fn psg_count(&self) -> usize {
self.psg_count
}
#[must_use]
pub fn channel_count(&self) -> usize {
self.psg_count * 3
}
pub fn update(&mut self, channel_states: &ChannelStates) {
self.update_psg(0, channel_states);
self.update_combined();
}
pub fn update_psg(&mut self, psg_index: usize, channel_states: &ChannelStates) {
if psg_index >= MAX_PSG_COUNT {
return;
}
let base_channel = psg_index * 3;
for (local_ch, ch_state) in channel_states.channels.iter().enumerate() {
let global_ch = base_channel + local_ch;
if global_ch >= MAX_CHANNEL_COUNT {
break;
}
let prev = self.spectrum[global_ch];
self.spectrum[global_ch] = [0.0; SPECTRUM_BINS];
let has_output =
ch_state.tone_enabled || ch_state.noise_enabled || ch_state.envelope_enabled;
let has_amplitude = ch_state.amplitude > 0 || ch_state.envelope_enabled;
let is_active = has_amplitude && has_output;
if is_active {
let magnitude = if ch_state.envelope_enabled {
1.0
} else {
ch_state.amplitude_normalized
};
if ch_state.tone_enabled
&& let Some(freq) = ch_state.frequency_hz
&& freq > 0.0
{
let bin = freq_to_bin(freq);
self.spectrum[global_ch][bin] = magnitude;
}
if ch_state.noise_enabled {
self.add_noise_to_spectrum(global_ch, channel_states.noise.period, magnitude);
}
if ch_state.envelope_enabled {
self.add_envelope_to_spectrum(global_ch, ch_state, channel_states, magnitude);
}
}
for (bin, &prev_val) in prev.iter().enumerate() {
if self.spectrum[global_ch][bin] < prev_val {
self.spectrum[global_ch][bin] = prev_val * SPECTRUM_DECAY;
}
}
}
}
pub fn update_multi_psg(&mut self, register_banks: &[[u8; 16]], psg_count: usize) {
self.set_psg_count(psg_count);
for (psg_idx, registers) in register_banks.iter().enumerate().take(psg_count) {
let channel_states = ChannelStates::from_registers(registers);
self.update_psg(psg_idx, &channel_states);
}
self.update_combined();
}
fn update_combined(&mut self) {
let channel_count = self.channel_count();
for (bin, combined) in self.combined.iter_mut().enumerate() {
*combined = (0..channel_count)
.map(|ch| self.spectrum[ch][bin])
.fold(0.0, f32::max);
}
}
fn add_noise_to_spectrum(&mut self, ch: usize, noise_period: u8, magnitude: f32) {
let noise_center = if noise_period == 0 {
SPECTRUM_BINS - 2 } else {
let ratio = 1.0 - (noise_period as f32 / 31.0);
((ratio * 0.6 + 0.3) * (SPECTRUM_BINS - 1) as f32) as usize
};
let noise_mag = magnitude * 0.7;
for offset in 0..=2 {
let bin = (noise_center + offset).min(SPECTRUM_BINS - 1);
self.spectrum[ch][bin] =
self.spectrum[ch][bin].max(noise_mag * (1.0 - offset as f32 * 0.25));
}
}
fn add_envelope_to_spectrum(
&mut self,
ch: usize,
ch_state: &crate::channel_state::ChannelState,
channel_states: &ChannelStates,
magnitude: f32,
) {
let buzz_freq = if ch_state.frequency_hz.is_some() && ch_state.tone_period > 0 {
ch_state.frequency_hz
} else {
channel_states.envelope.frequency_hz
};
if let Some(freq) = buzz_freq
&& freq > 0.0
{
let bin = freq_to_bin(freq);
self.spectrum[ch][bin] = self.spectrum[ch][bin].max(magnitude);
}
}
#[must_use]
pub fn get_bins(&self) -> &[f32; SPECTRUM_BINS] {
&self.combined
}
#[must_use]
pub fn channel_spectrum(&self, channel: usize) -> &[f32; SPECTRUM_BINS] {
&self.spectrum[channel.min(MAX_CHANNEL_COUNT - 1)]
}
#[must_use]
pub fn all_channel_spectrums(&self) -> &[[f32; SPECTRUM_BINS]; MAX_CHANNEL_COUNT] {
&self.spectrum
}
#[must_use]
pub fn high_freq_ratio(&self, channel: usize) -> f32 {
let ch = channel.min(2);
let total_energy: f32 = self.spectrum[ch].iter().sum();
let high_energy: f32 = self.spectrum[ch][8..].iter().sum();
if total_energy > 0.01 {
(high_energy / total_energy).clamp(0.0, 1.0)
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_freq_to_bin_c1() {
assert_eq!(freq_to_bin(32.703), 0);
}
#[test]
fn test_freq_to_bin_a4() {
let bin = freq_to_bin(440.0);
assert!(
(14..=16).contains(&bin),
"A4 should be around bin 15, got {bin}"
);
}
#[test]
fn test_freq_to_bin_bounds() {
assert_eq!(freq_to_bin(0.0), 0);
assert_eq!(freq_to_bin(-100.0), 0);
assert_eq!(freq_to_bin(20000.0), SPECTRUM_BINS - 1);
}
#[test]
fn test_waveform_phase_wrapping() {
let mut synth = WaveformSynthesizer::new();
let mut regs = [0u8; 16];
regs[0] = 1; regs[7] = 0x3E; regs[8] = 0x0F;
let states = ChannelStates::from_registers(®s);
synth.update(&states);
assert!(synth.phase[0] >= 0.0 && synth.phase[0] < 1.0);
}
#[test]
fn test_spectrum_decay() {
let mut analyzer = SpectrumAnalyzer::new();
let mut regs = [0u8; 16];
regs[0] = 0x1C;
regs[1] = 0x01; regs[7] = 0x3E;
regs[8] = 0x0F;
let states = ChannelStates::from_registers(®s);
analyzer.update(&states);
let initial_bin = freq_to_bin(440.0);
let initial_value = analyzer.spectrum[0][initial_bin];
assert!(initial_value > 0.0);
let silent_regs = [0u8; 16];
let silent_states = ChannelStates::from_registers(&silent_regs);
analyzer.update(&silent_states);
let decayed_value = analyzer.spectrum[0][initial_bin];
assert!(decayed_value > 0.0);
assert!(decayed_value < initial_value);
assert!((decayed_value - initial_value * SPECTRUM_DECAY).abs() < 0.01);
}
}