use serde::{Deserialize, Serialize};
use crate::fixed::tables::{
amiga_frequency_to_period, amiga_period_from_pitch, amiga_period_to_frequency,
amiga_period_to_pitch, linear_frequency_to_period, linear_period_to_frequency,
linear_period_to_pitch, linear_pitch_to_period,
};
use crate::fixed::units::{Frequency, Period, Pitch, PitchDelta};
#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub enum FrequencyType {
AmigaFrequencies,
#[default]
LinearFrequencies,
}
const FT2_ARPEGGIO_NOTE_MAX: i16 = 95;
const NOTE_C4: i16 = 48;
const NOTE_B9: i16 = 119;
#[derive(Clone)]
pub struct PeriodHelper {
pub freq_type: FrequencyType,
legacy_arpeggio_clamp: bool,
cache_key: Option<(Period, PitchDelta, PitchDelta, bool)>,
cache_value: Frequency,
}
impl Default for PeriodHelper {
fn default() -> Self {
Self::new(FrequencyType::default(), false)
}
}
impl PeriodHelper {
pub fn new(freq_type: FrequencyType, legacy_arpeggio_clamp: bool) -> Self {
Self {
freq_type,
legacy_arpeggio_clamp,
cache_key: None,
cache_value: Frequency::ZERO,
}
}
#[inline]
pub fn legacy_arpeggio_clamp_enabled(&self) -> bool {
self.legacy_arpeggio_clamp
}
}
impl PeriodHelper {
pub fn note_to_period(&self, pitch: Pitch) -> Period {
match self.freq_type {
FrequencyType::LinearFrequencies => linear_pitch_to_period(pitch),
FrequencyType::AmigaFrequencies => amiga_period_from_pitch(pitch),
}
}
pub fn period_to_pitch(&self, period: Period) -> Pitch {
match self.freq_type {
FrequencyType::LinearFrequencies => linear_period_to_pitch(period),
FrequencyType::AmigaFrequencies => amiga_period_to_pitch(period),
}
}
pub fn period_to_frequency(&self, period: Period) -> Frequency {
match self.freq_type {
FrequencyType::LinearFrequencies => linear_period_to_frequency(period),
FrequencyType::AmigaFrequencies => amiga_period_to_frequency(period),
}
}
pub fn frequency_to_period(&self, freq: Frequency) -> Period {
match self.freq_type {
FrequencyType::LinearFrequencies => linear_frequency_to_period(freq),
FrequencyType::AmigaFrequencies => amiga_frequency_to_period(freq),
}
}
}
impl PeriodHelper {
pub fn relative_pitch_to_c4freq(
&self,
relative_pitch: i8,
finetune: PitchDelta,
) -> Option<Frequency> {
let note_int = NOTE_C4 + relative_pitch as i16;
if !(0..=NOTE_B9).contains(¬e_int) {
return None;
}
let pitch = Pitch::from_semitone(note_int).shift(finetune);
let period = self.note_to_period(pitch);
Some(self.period_to_frequency(period))
}
pub fn c4freq_to_relative_pitch(&self, freq: Frequency) -> (i8, PitchDelta) {
let period = self.frequency_to_period(freq);
let pitch = self.period_to_pitch(period);
let nearest_int = pitch.semitone_round().clamp(0, NOTE_B9);
let relative_pitch = (nearest_int - NOTE_C4) as i8;
let ft_raw = pitch.as_q8_8_i32() - ((nearest_int as i32) << 8);
let finetune = PitchDelta::from_q8_8_i16(ft_raw as i16);
(relative_pitch, finetune)
}
}
impl PeriodHelper {
pub fn adjust_period(
&self,
period: Period,
arp_pitch: PitchDelta,
finetune: PitchDelta,
semitone: bool,
) -> Period {
let raw_pitch = self.period_to_pitch(period);
let mut pitch = if semitone {
raw_pitch.quantized()
} else {
raw_pitch
};
if self.legacy_arpeggio_clamp && arp_pitch.as_q8_8_i16() != 0 {
if pitch.semitone_ceil() >= FT2_ARPEGGIO_NOTE_MAX {
pitch = Pitch::from_semitone(FT2_ARPEGGIO_NOTE_MAX);
}
}
let pitch = pitch.shift(arp_pitch).shift(finetune);
self.note_to_period(pitch)
}
pub fn all_to_frequency(
&self,
period: Period,
arp_pitch: PitchDelta,
finetune: PitchDelta,
semitone: bool,
) -> Frequency {
let adjusted = self.adjust_period(period, arp_pitch, finetune, semitone);
self.period_to_frequency(adjusted)
}
pub fn all_to_frequency_cached(
&mut self,
period: Period,
arp_pitch: PitchDelta,
finetune: PitchDelta,
semitone: bool,
) -> Frequency {
let key = (period, arp_pitch, finetune, semitone);
if self.cache_key == Some(key) {
return self.cache_value;
}
let value = self.all_to_frequency(period, arp_pitch, finetune, semitone);
self.cache_key = Some(key);
self.cache_value = value;
value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn helper_linear() -> PeriodHelper {
PeriodHelper::new(FrequencyType::LinearFrequencies, false)
}
fn helper_amiga() -> PeriodHelper {
PeriodHelper::new(FrequencyType::AmigaFrequencies, false)
}
#[test]
fn linear_c4_period_round_trips() {
let ph = helper_linear();
let p_c4 = ph.note_to_period(Pitch::C4);
assert_eq!(p_c4.raw(), 4608);
let back = ph.period_to_pitch(p_c4);
assert_eq!(back, Pitch::C4);
}
#[test]
fn amiga_a4_round_trips() {
let ph = helper_amiga();
let p_a4 = ph.note_to_period(Pitch::A4);
let back = ph.period_to_pitch(p_a4);
let diff = (back.as_q8_8_i32() - Pitch::A4.as_q8_8_i32()).abs();
assert!(diff <= 32, "diff = {} Q8.8 units", diff);
}
#[test]
fn c4freq_round_trip_at_reference() {
let ph = helper_linear();
let f_c4 = ph.period_to_frequency(ph.note_to_period(Pitch::C4));
let (rp, ft) = ph.c4freq_to_relative_pitch(f_c4);
assert_eq!(rp, 0);
assert!(ft.as_q8_8_i16().abs() <= 1);
}
}