use std::collections::VecDeque;
pub const HISTORY_SIZE: usize = 9;
#[derive(Clone, Debug, Default)]
pub struct NoteEntry {
pub note: String,
pub freq: f32,
}
impl NoteEntry {
pub fn new(note: String, freq: f32) -> Self {
Self { note, freq }
}
pub fn silence() -> Self {
Self {
note: "---".to_string(),
freq: 0.0,
}
}
}
#[derive(Clone, Debug)]
pub struct ChannelHistory {
notes: VecDeque<NoteEntry>,
current_idx: usize,
last_freq: f32,
last_envelope_shape: Option<String>,
}
impl Default for ChannelHistory {
fn default() -> Self {
Self::new()
}
}
impl ChannelHistory {
pub fn new() -> Self {
let mut notes = VecDeque::with_capacity(HISTORY_SIZE * 2);
for _ in 0..HISTORY_SIZE {
notes.push_back(NoteEntry::silence());
}
Self {
notes,
current_idx: HISTORY_SIZE / 2, last_freq: 0.0,
last_envelope_shape: None,
}
}
pub fn update(
&mut self,
note: &str,
freq: f32,
has_output: bool,
envelope_shape: Option<&str>,
) {
if let Some(shape) = envelope_shape {
self.last_envelope_shape = Some(shape.to_string());
}
let freq_changed = if self.last_freq > 0.0 && freq > 0.0 {
((freq - self.last_freq) / self.last_freq).abs() > 0.01
} else {
freq != self.last_freq
};
let is_note_on = has_output && freq > 0.0;
if freq_changed && is_note_on {
self.notes.push_back(NoteEntry::new(note.to_string(), freq));
while self.notes.len() > HISTORY_SIZE * 2 {
self.notes.pop_front();
}
self.current_idx = self.notes.len().saturating_sub(1);
}
self.last_freq = if is_note_on { freq } else { 0.0 };
}
pub fn last_envelope_shape(&self) -> Option<&str> {
self.last_envelope_shape.as_deref()
}
pub fn visible_notes(&self) -> (Vec<&NoteEntry>, usize) {
let total = self.notes.len();
if total == 0 {
return (vec![], 0);
}
let half = HISTORY_SIZE / 2;
let start = self.current_idx.saturating_sub(half);
let end = (start + HISTORY_SIZE).min(total);
let actual_start = if end - start < HISTORY_SIZE && end == total {
total.saturating_sub(HISTORY_SIZE)
} else {
start
};
let visible: Vec<&NoteEntry> = self.notes.range(actual_start..end).collect();
let current_pos = self.current_idx.saturating_sub(actual_start);
let clamped_pos = current_pos.min(visible.len().saturating_sub(1));
(visible, clamped_pos)
}
}
#[derive(Clone, Debug)]
pub struct NoteHistory {
channels: [ChannelHistory; 12],
}
impl Default for NoteHistory {
fn default() -> Self {
Self::new()
}
}
impl NoteHistory {
pub fn new() -> Self {
Self {
channels: std::array::from_fn(|_| ChannelHistory::new()),
}
}
pub fn update_channel(
&mut self,
channel: usize,
note: &str,
freq: f32,
has_output: bool,
envelope_shape: Option<&str>,
) {
if channel < 12 {
self.channels[channel].update(note, freq, has_output, envelope_shape);
}
}
pub fn channel(&self, idx: usize) -> &ChannelHistory {
&self.channels[idx.min(11)]
}
}