use crate::fixed::fixed::{Q15, Q16_16};
use crate::fixed::units::{Frequency, Period};
const fn sine_q15_step(x: u32) -> Q15 {
let x = x & 0xFF;
let (q, t) = match x / 64 {
0 => (1, x), 1 => (1, 128 - x), 2 => (-1, x - 128), _ => (-1, 256 - x), };
let u = t as i64;
let prod = u * (128 - u); let num = 16 * prod;
let den = 81920 - 4 * prod;
let result_q15 = (num * 0x7FFF) / den;
let signed = (q as i64) * result_q15;
Q15::from_raw(signed as i16)
}
pub const SINE_Q15: [Q15; 256] = {
let mut t = [Q15::ZERO; 256];
let mut i = 0;
while i < 256 {
t[i] = sine_q15_step(i as u32);
i += 1;
}
t
};
#[inline]
pub fn sine(phase: crate::fixed::units::Phase) -> Q15 {
let phase_q16 = phase.raw();
let idx = (phase_q16 >> 8) as usize;
let frac = (phase_q16 & 0xFF) as i32;
let a = SINE_Q15[idx].raw() as i32;
let b = SINE_Q15[(idx + 1) & 0xFF].raw() as i32;
let r = a + (((b - a) * frac + 128) >> 8);
Q15::from_raw(r as i16)
}
const fn isqrt_q30_to_q15(x: u64) -> i64 {
if x == 0 {
return 0;
}
let mut r: u64 = 1u64 << 15; let mut prev: u64 = 0;
let mut iter = 0;
while iter < 32 {
let new = (r + x / r) >> 1;
if new == prev {
break;
}
prev = r;
r = new;
iter += 1;
}
r as i64
}
const fn pan_sqrt_step(i: u32) -> Q15 {
let p_q15 = (i * 0x7FFF) / 255;
let prod = (p_q15 as u64) * 0x7FFFu64;
let r = isqrt_q30_to_q15(prod);
Q15::from_raw(r as i16)
}
pub const SQRT_PAN_Q15: [Q15; 256] = {
let mut t = [Q15::ZERO; 256];
let mut i = 0;
while i < 256 {
t[i] = pan_sqrt_step(i as u32);
i += 1;
}
t
};
#[inline]
pub fn pan_sqrt(p: Q15) -> Q15 {
let raw = p.raw().clamp(0, 0x7FFF) as u32;
let idx_x256 = raw << 1; let idx = (idx_x256 >> 8) as usize; let frac = (idx_x256 & 0xFF) as i32;
let a = SQRT_PAN_Q15[idx].raw() as i32;
let b = SQRT_PAN_Q15[(idx + 1).min(255)].raw() as i32;
let r = a + (((b - a) * frac + 128) >> 8);
Q15::from_raw(r as i16)
}
pub const fn pow2_frac_q16_16(x_num: u32) -> u32 {
const LN2_Q64: u128 = 12_786_308_645_202_655_660u128;
let y: u128 = ((x_num as u128) << 64) / 768;
let z: u128 = (y * LN2_Q64) >> 64;
let mut term: u128 = 1u128 << 64; let mut sum: u128 = 0;
let mut n: u32 = 0;
while n < 16 {
sum += term;
term = (term * z) >> 64;
term /= (n as u128) + 1;
n += 1;
}
(sum >> 48) as u32
}
pub const SEMITONE_FROM_C_Q16_16: [Q16_16; 12] = {
let mut t = [Q16_16::ZERO; 12];
let mut i = 0u32;
while i < 12 {
t[i as usize] = Q16_16::from_raw(pow2_frac_q16_16(i * 64));
i += 1;
}
t
};
pub const SUBSEMITONE_Q16_16: [Q16_16; 64] = {
let mut t = [Q16_16::ZERO; 64];
let mut i = 0u32;
while i < 64 {
t[i as usize] = Q16_16::from_raw(pow2_frac_q16_16(i));
i += 1;
}
t
};
pub const C4_FREQ_HZ: u32 = 8372;
pub const C4_FREQ_HZ_LEGACY: u32 = 8363;
pub fn linear_period_to_frequency(p: Period) -> Frequency {
let period = p.raw() as i32;
let exp_768 = 4608 - period;
let octave = exp_768.div_euclid(768) as i8;
let in_oct = exp_768.rem_euclid(768) as usize; let semi = in_oct >> 6; let sub = in_oct & 63;
let mul = SEMITONE_FROM_C_Q16_16[semi].mul_q16_16(SUBSEMITONE_Q16_16[sub]);
let scaled_q16_16 = (mul.raw() as u64) * (C4_FREQ_HZ as u64);
let shifted_q16_16 = if octave >= 0 {
scaled_q16_16 << octave
} else {
scaled_q16_16 >> (-octave)
};
let q24_8 = shifted_q16_16 >> 8;
let raw = if q24_8 > u32::MAX as u64 {
u32::MAX
} else {
q24_8 as u32
};
Frequency::from_raw_q24_8(raw)
}
pub const fn linear_period_to_pitch(p: Period) -> crate::fixed::units::Pitch {
let period = p.raw() as i32;
let pitch_q8_8 = (7680 - period) << 2;
let pitch_q8_8 = if pitch_q8_8 < 0 {
0
} else if pitch_q8_8 > 119 << 8 {
119 << 8
} else {
pitch_q8_8
};
crate::fixed::units::Pitch::from_q8_8(crate::fixed::fixed::Q8_8::from_raw(pitch_q8_8 as i16))
}
pub const fn linear_pitch_to_period(p: crate::fixed::units::Pitch) -> Period {
let pitch_q8_8 = p.as_q8_8_i32();
let period = 7680 - (pitch_q8_8 >> 2);
let period = if period < 0 {
0
} else if period > 0xFFFF {
0xFFFF
} else {
period
};
Period::from_raw(period as u16)
}
pub const AMIGA_C0_PERIOD: u32 = 6779;
pub const AMIGA_C0_PERIOD_LEGACY: u32 = 6848;
pub fn amiga_period(note_semitone: i16, finetune_idx: u8) -> Period {
let n = note_semitone.clamp(0, 119) as i32;
let ft = (finetune_idx & 0x0F) as i32;
let p = n * 8 + ft - 8;
let octave = p.div_euclid(96);
let in_oct = p.rem_euclid(96) as usize; let semi = in_oct / 8; let sub = (in_oct % 8) * 8;
let mul = SEMITONE_FROM_C_Q16_16[semi].mul_q16_16(SUBSEMITONE_Q16_16[sub]);
let numerator = (AMIGA_C0_PERIOD as u64) << 32;
let period_q16_16 = numerator / (mul.raw() as u64);
let shifted = if octave >= 0 {
period_q16_16 >> (octave as u32).min(32)
} else {
let l = ((-octave) as u32).min(16);
period_q16_16 << l
};
let period_int = shifted.saturating_add(0x8000) >> 16;
Period::from_raw(period_int.min(0xFFFF) as u16)
}
pub fn amiga_period_from_pitch(pitch: crate::fixed::units::Pitch) -> Period {
let pitch_q8_8 = pitch.as_q8_8_i32().clamp(0, 119 << 8);
let p = pitch_q8_8 >> 2;
let octave = p / 768;
let in_oct = (p % 768) as usize;
let semi = in_oct >> 6; let sub = in_oct & 0x3F;
let mul = SEMITONE_FROM_C_Q16_16[semi].mul_q16_16(SUBSEMITONE_Q16_16[sub]);
let numerator = (AMIGA_C0_PERIOD as u64) << 32;
let period_q16_16 = numerator / (mul.raw() as u64);
let shifted = if octave >= 0 {
period_q16_16 >> (octave as u32).min(32)
} else {
let l = ((-octave) as u32).min(16);
period_q16_16 << l
};
let period_int = shifted.saturating_add(0x8000) >> 16;
Period::from_raw(period_int.min(0xFFFF) as u16)
}
pub fn amiga_period_to_frequency(p: Period) -> Frequency {
let raw = p.raw();
if raw == 0 {
return Frequency::ZERO;
}
let num: u64 = (3_546_894u64) << 8;
let q = num / (raw as u64);
let raw = if q > u32::MAX as u64 {
u32::MAX
} else {
q as u32
};
Frequency::from_raw_q24_8(raw)
}
pub fn amiga_frequency_to_period(f: Frequency) -> Period {
let raw = f.raw_q24_8();
if raw == 0 {
return Period::ZERO;
}
let num: u64 = (3_546_894u64) << 8;
let q = num / raw as u64;
Period::from_raw(q.min(0xFFFF) as u16)
}
pub fn linear_frequency_to_period(f: Frequency) -> Period {
let target = f.raw_q24_8();
if target == 0 {
return Period::from_raw(7680);
}
let mut lo: u32 = 0;
let mut hi: u32 = 7680;
while lo < hi {
let mid = (lo + hi) / 2;
let f_mid = linear_period_to_frequency(Period::from_raw(mid as u16));
if f_mid.raw_q24_8() > target {
lo = mid + 1;
} else {
hi = mid;
}
}
Period::from_raw(lo as u16)
}
pub fn amiga_period_to_pitch(p: Period) -> crate::fixed::units::Pitch {
use crate::fixed::units::Pitch;
let target = p.raw();
if target == 0 {
return Pitch::C0;
}
const MAX_UNIT: u32 = 119 * 64; let mut lo: u32 = 0;
let mut hi: u32 = MAX_UNIT;
while lo < hi {
let mid = (lo + hi + 1) / 2;
let mid_pitch = Pitch::from_q8_8_i16((mid * 4) as i16);
let p_mid = amiga_period_from_pitch(mid_pitch).raw();
if p_mid >= target {
lo = mid;
} else {
hi = mid - 1;
}
}
Pitch::from_q8_8_i16((lo * 4) as i16)
}
#[derive(Copy, Clone, Debug, Default)]
pub struct PeriodCache {
last_period: u16,
last_freq_q24_8: u32,
valid: bool,
}
impl PeriodCache {
#[inline]
pub const fn new() -> Self {
Self {
last_period: 0,
last_freq_q24_8: 0,
valid: false,
}
}
#[inline]
pub fn lookup(&mut self, p: Period) -> Frequency {
let raw = p.raw();
if self.valid && self.last_period == raw {
return Frequency::from_raw_q24_8(self.last_freq_q24_8);
}
let f = linear_period_to_frequency(p);
self.last_period = raw;
self.last_freq_q24_8 = f.raw_q24_8();
self.valid = true;
f
}
#[inline]
pub fn invalidate(&mut self) {
self.valid = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sine_table_endpoints() {
assert_eq!(SINE_Q15[0], Q15::ZERO);
let q = SINE_Q15[64].raw();
assert!(q > 0x7F00, "sin(π/2) should be ≈ ONE, got {:#x}", q);
let q = SINE_Q15[128].raw();
assert!(q.abs() < 0x100, "sin(π) should be ≈ 0, got {:#x}", q);
let q = SINE_Q15[192].raw();
assert!(q < -0x7F00, "sin(3π/2) should be ≈ -ONE, got {:#x}", q);
}
#[test]
fn sine_interp_smooth() {
for i in 0..256 {
let q = sine(crate::fixed::units::Phase::from_raw((i << 8) as u16));
let expected = SINE_Q15[i as usize];
assert!(
(q.raw() - expected.raw()).abs() <= 1,
"i={}: got {}, expected {}",
i,
q.raw(),
expected.raw()
);
}
}
#[test]
fn pan_sqrt_endpoints() {
assert_eq!(SQRT_PAN_Q15[0], Q15::ZERO);
let q = SQRT_PAN_Q15[255].raw();
assert!(q > 0x7F00);
}
#[test]
fn pan_sqrt_centre() {
let q = pan_sqrt(Q15::HALF).raw();
assert!((q - 0x5A82).abs() < 0x100, "got {:#x}", q);
}
#[test]
fn linear_period_c4_round_trip() {
let f = linear_period_to_frequency(Period::from_raw(4608));
let expected = Frequency::from_hz(C4_FREQ_HZ);
let got = f.raw_q24_8() as i64;
let exp = expected.raw_q24_8() as i64;
let diff = (got - exp).abs();
assert!(diff <= 1, "got raw {}, expected {}", got, exp);
}
#[test]
fn linear_period_one_octave_doubles() {
let f4 = linear_period_to_frequency(Period::from_raw(4608));
let f5 = linear_period_to_frequency(Period::from_raw(3840));
let r4 = f4.raw_q24_8() as u64;
let r5 = f5.raw_q24_8() as u64;
let ratio = (r5 * 1000) / r4;
assert!((ratio as i64 - 2000).abs() < 5, "ratio*1000 = {}", ratio);
}
#[test]
fn linear_period_a4_is_known_freq() {
let f = linear_period_to_frequency(Period::from_raw(4032));
let hz = (f.raw_q24_8() as f64) / 256.0;
assert!(
(hz - 14080.0).abs() < 1.0,
"got {} Hz, expected ~14080 (= 440 × 32)",
hz
);
}
#[test]
fn linear_period_to_pitch_round_trip() {
for note in 0..=119_i16 {
let p = linear_pitch_to_period(crate::fixed::units::Pitch::from_semitone(note));
let pitch_back = linear_period_to_pitch(p);
assert_eq!(pitch_back.raw().round(), note, "note {} round-trip", note);
}
}
#[test]
fn linear_pitch_to_period_known_values() {
let p = linear_pitch_to_period(crate::fixed::units::Pitch::C0);
assert_eq!(p, Period::from_raw(7680));
let p = linear_pitch_to_period(crate::fixed::units::Pitch::C4);
assert_eq!(p, Period::from_raw(4608));
let p = linear_pitch_to_period(crate::fixed::units::Pitch::B9);
assert_eq!(p, Period::from_raw(64));
}
#[test]
fn semitone_table_endpoints() {
assert_eq!(SEMITONE_FROM_C_Q16_16[0].raw(), 0x10000);
let raw = SEMITONE_FROM_C_Q16_16[7].raw();
assert!(
(raw as i64 - 0x17F91).abs() < 4,
"got {:#x}, expected ≈ 0x17F91",
raw
);
}
#[test]
fn subsemitone_table_endpoints() {
assert_eq!(SUBSEMITONE_Q16_16[0].raw(), 0x10000);
let raw = SUBSEMITONE_Q16_16[63].raw();
assert!(
(raw as i64 - 0x10EFA).abs() < 4,
"got {:#x}, expected ≈ 0x10EFA",
raw
);
}
#[test]
fn linear_period_full_range_smooth() {
let mut last = u64::MAX;
for period in (1..=7680).step_by(64) {
let f = linear_period_to_frequency(Period::from_raw(period));
let raw = f.raw_q24_8() as u64;
assert!(raw < last, "non-monotonic at period {}", period);
last = raw;
}
}
#[test]
fn period_cache_hit_returns_same_value() {
let mut cache = PeriodCache::new();
let p = Period::from_raw(4608);
let f1 = cache.lookup(p);
let f2 = cache.lookup(p);
let f3 = cache.lookup(p);
assert_eq!(f1, f2);
assert_eq!(f2, f3);
}
#[test]
fn period_cache_miss_recomputes() {
let mut cache = PeriodCache::new();
let f1 = cache.lookup(Period::from_raw(4608));
let f2 = cache.lookup(Period::from_raw(3840)); assert_ne!(f1, f2);
let r1 = f1.raw_q24_8() as u64;
let r2 = f2.raw_q24_8() as u64;
assert!((r2 * 1000 / r1) as i64 - 2000 < 5);
}
#[test]
fn period_cache_matches_uncached() {
let mut cache = PeriodCache::new();
for period in [4608, 3840, 7680, 64, 4032] {
let p = Period::from_raw(period);
let cached = cache.lookup(p);
let uncached = linear_period_to_frequency(p);
assert_eq!(cached, uncached, "mismatch at period {}", period);
}
}
#[test]
fn amiga_period_c0_centred_is_reference() {
let p = amiga_period(0, 8);
assert_eq!(p, Period::from_raw(AMIGA_C0_PERIOD as u16));
}
#[test]
fn amiga_period_a4_known_value() {
let p = amiga_period(57, 8);
assert!(
(p.raw() as i32 - 252).abs() <= 2,
"got {:?}, expected 252 ± 2",
p
);
}
#[test]
fn amiga_period_octaves_halve() {
let p0 = amiga_period(0, 8).raw();
let p3 = amiga_period(36, 8).raw();
let p4 = amiga_period(48, 8).raw();
let p7 = amiga_period(84, 8).raw();
assert!((p0 as i32 - 6779).abs() <= 1);
assert!((p3 as i32 - 847).abs() <= 1);
assert!((p4 as i32 - 424).abs() <= 1);
assert!((p7 as i32 - 53).abs() <= 2);
}
#[test]
fn amiga_period_matches_midi_aligned_table() {
let cases = [
(0, 6779), (12, 3390), (24, 1695), (36, 847), (37, 800), (45, 503), (48, 424), (57, 252), (60, 212), (72, 106), (83, 56), ];
for (note, expected) in cases {
let p = amiga_period(note, 8);
assert!(
(p.raw() as i32 - expected as i32).abs() <= 2,
"amiga_period({}, 8) = {:?}, expected {} ± 2",
note,
p,
expected
);
}
}
#[test]
fn amiga_period_a4_drives_paula_at_concert_pitch() {
let p = amiga_period(57, 8);
let f = amiga_period_to_frequency(p);
let hz = (f.raw_q24_8() as f64) / 256.0;
assert!(
(hz - 14080.0).abs() < 20.0,
"A-4 should drive Paula at ≈ 14080 Hz (= 440 × 32), got {:.2}",
hz
);
}
#[test]
fn amiga_period_finetune_within_one_unit() {
let cases = [
(36, 0, 898), (36, 8, 847), (36, 15, 806), (48, 8, 424), ];
for (note, ft, expected) in cases {
let p = amiga_period(note, ft);
assert!(
(p.raw() as i32 - expected as i32).abs() <= 2,
"amiga_period({}, {}) = {:?}, expected {} ± 2",
note,
ft,
p,
expected
);
}
}
#[test]
fn amiga_legacy_constant_still_available() {
assert_eq!(AMIGA_C0_PERIOD_LEGACY, 6848);
assert_eq!(AMIGA_C0_PERIOD, 6779);
assert!(AMIGA_C0_PERIOD < AMIGA_C0_PERIOD_LEGACY);
}
#[test]
fn amiga_to_freq_a4_is_about_440hz_paula() {
let f = amiga_period_to_frequency(Period::from_raw(508));
let hz_int = (f.raw_q24_8() >> 8) as i64;
assert!((hz_int - 6982).abs() < 2, "got {}", hz_int);
}
}