use crate::fixed::fixed::{Q15, Q16_16, Q24_8, Q7_25, Q8_8};
use crate::fixed::units::{
Amp, Amplification, ChannelVolume, EnvValue, Finetune, Frequency, GlobalVolume, Panning,
Period, Pitch, PitchDelta, RetrigMul, SampleStep, Volume,
};
#[inline]
fn f32_to_i32_sat(x: f32, lo: i32, hi: i32) -> i32 {
if x.is_nan() {
return 0;
}
let r = libm_round(x);
if r <= lo as f32 {
lo
} else if r >= hi as f32 {
hi
} else {
r as i32
}
}
#[inline]
fn f32_to_u32_sat(x: f32, hi: u32) -> u32 {
if x.is_nan() || x <= 0.0 {
return 0;
}
let r = libm_round(x);
if r >= hi as f32 {
hi
} else {
r as u32
}
}
#[inline]
fn libm_round(x: f32) -> f32 {
if x >= 0.0 {
let truncated = x as i64 as f32;
let frac = x - truncated;
if frac >= 0.5 {
truncated + 1.0
} else {
truncated
}
} else if x < 0.0 {
let truncated = x as i64 as f32;
let frac = x - truncated; if frac <= -0.5 {
truncated - 1.0
} else {
truncated
}
} else {
0.0
}
}
#[inline]
fn clamp_f32(x: f32, lo: f32, hi: f32) -> f32 {
if x.is_nan() || x < lo {
lo
} else if x > hi {
hi
} else {
x
}
}
impl Q15 {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw() as f32 / 32768.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 32768.0;
Self::from_raw(f32_to_i32_sat(scaled, -32768, 32767) as i16)
}
}
impl Q8_8 {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw() as f32 / 256.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 256.0;
Self::from_raw(f32_to_i32_sat(scaled, -32768, 32767) as i16)
}
}
impl Q24_8 {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw() as f32 / 256.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 256.0;
let r = if x.is_nan() {
0
} else if scaled <= i32::MIN as f32 {
i32::MIN
} else if scaled >= i32::MAX as f32 {
i32::MAX
} else {
libm_round(scaled) as i32
};
Q24_8::from_raw(r)
}
}
impl Q16_16 {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw() as f32 / 65536.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 65536.0;
Self::from_raw(f32_to_u32_sat(scaled, u32::MAX))
}
}
impl Q7_25 {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw() as f32 / 33_554_432.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 33_554_432.0;
Self::from_raw(f32_to_u32_sat(scaled, u32::MAX))
}
}
impl Amp {
#[inline]
pub fn to_f32(self) -> f32 {
self.into_q15().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
Amp::from_q15(Q15::from_f32(x))
}
}
impl Volume {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
Volume::from_q15(Q15::from_f32(clamp_f32(x, 0.0, 1.0)))
}
}
impl ChannelVolume {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let clamped = clamp_f32(x, 0.0, 1.0);
ChannelVolume::from_q15(Q15::from_f32(clamped))
}
}
impl EnvValue {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw_q15().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
EnvValue::from_q15(Q15::from_f32(x))
}
}
impl GlobalVolume {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw_q15().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let clamped = clamp_f32(x, 0.0, 1.0);
GlobalVolume::from_q15(Q15::from_f32(clamped))
}
}
impl Amplification {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw_q4_12() as f32 / 4096.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 4096.0;
Amplification::from_raw_q4_12(f32_to_i32_sat(scaled, i16::MIN as i32, i16::MAX as i32) as i16)
}
}
impl RetrigMul {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw_q3_13() as f32 / 8192.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let scaled = x * 8192.0;
RetrigMul::from_raw_q3_13(f32_to_i32_sat(scaled, i16::MIN as i32, i16::MAX as i32) as i16)
}
}
impl Panning {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let clamped = clamp_f32(x, 0.0, 1.0);
Panning::from_q15(Q15::from_f32(clamped))
}
}
impl Pitch {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
let clamped = clamp_f32(x, 0.0, 119.0);
Pitch::from_q8_8(Q8_8::from_f32(clamped))
}
}
impl PitchDelta {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
PitchDelta::from_q8_8(Q8_8::from_f32(x))
}
}
impl Finetune {
#[inline]
pub fn to_f32(self) -> f32 {
self.into_q15().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
Finetune::from_q15(Q15::from_f32(x))
}
}
impl Period {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw() as f32
}
#[inline]
pub fn from_f32(x: f32) -> Self {
Period::from_raw(f32_to_u32_sat(x, u16::MAX as u32) as u16)
}
}
impl Frequency {
#[inline]
pub fn to_f32(self) -> f32 {
self.raw_q24_8() as f32 / 256.0
}
#[inline]
pub fn from_f32(x: f32) -> Self {
if x <= 0.0 || x.is_nan() {
return Frequency::ZERO;
}
let scaled = x * 256.0;
if scaled >= u32::MAX as f32 {
Frequency::from_raw_q24_8(u32::MAX)
} else {
Frequency::from_raw_q24_8(scaled as u32)
}
}
}
impl SampleStep {
#[inline]
pub fn to_f32(self) -> f32 {
self.into_q7_25().to_f32()
}
#[inline]
pub fn from_f32(x: f32) -> Self {
SampleStep::from_raw_q7_25(Q7_25::from_f32(x))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn q15_endpoints() {
assert_eq!(Q15::NEG_ONE.to_f32(), -1.0);
assert_eq!(Q15::ZERO.to_f32(), 0.0);
assert_eq!(Q15::HALF.to_f32(), 0.5);
assert!((Q15::ONE.to_f32() - 1.0).abs() < 1e-4);
}
#[test]
fn q15_round_trip_lossy_within_one_lsb() {
for raw in [-32768, -16384, -1, 0, 1, 16384, 32767] {
let q = Q15::from_raw(raw);
let q2 = Q15::from_f32(q.to_f32());
assert!((q2.raw() - q.raw()).abs() <= 1, "raw {} drifted to {}", raw, q2.raw());
}
}
#[test]
fn q15_from_f32_saturation() {
assert_eq!(Q15::from_f32(2.0), Q15::ONE);
assert_eq!(Q15::from_f32(-2.0), Q15::NEG_ONE);
assert_eq!(Q15::from_f32(f32::NAN), Q15::ZERO);
assert_eq!(Q15::from_f32(f32::INFINITY), Q15::ONE);
assert_eq!(Q15::from_f32(f32::NEG_INFINITY), Q15::NEG_ONE);
}
#[test]
fn q8_8_round_trip() {
for v in [-128.0_f32, -1.5, 0.0, 0.5, 48.0, 119.0, 127.0] {
let q = Q8_8::from_f32(v);
let back = q.to_f32();
assert!((back - v).abs() < 0.005, "{} → {}", v, back);
}
}
#[test]
fn q16_16_round_trip() {
for hz in [0.0_f32, 1.0, 440.0, 8372.0, 22050.0, 65535.0] {
let q = Q16_16::from_f32(hz);
let back = q.to_f32();
assert!((back - hz).abs() < 1e-4, "{} → {}", hz, back);
}
}
#[test]
fn q7_25_round_trip() {
for step in [0.01_f32, 0.17, 0.5, 1.0, 2.0, 16.0] {
let q = Q7_25::from_f32(step);
let back = q.to_f32();
assert!((back - step).abs() < 1e-6, "{} → {}", step, back);
}
}
#[test]
fn volume_clamps_above_one() {
assert_eq!(Volume::from_f32(2.0), Volume::FULL);
assert_eq!(Volume::from_f32(1.0), Volume::FULL);
assert!((Volume::FULL.to_f32() - 1.0).abs() < 1e-4);
}
#[test]
fn volume_clamps_below_zero() {
assert_eq!(Volume::from_f32(-0.5), Volume::SILENT);
assert_eq!(Volume::SILENT.to_f32(), 0.0);
}
#[test]
fn panning_round_trip_centre() {
let p = Panning::from_f32(0.5);
assert!((p.to_f32() - 0.5).abs() < 1e-4);
assert_eq!(p, Panning::CENTER);
}
#[test]
fn panning_endpoints() {
assert_eq!(Panning::from_f32(0.0), Panning::LEFT);
assert_eq!(Panning::from_f32(1.0), Panning::RIGHT);
}
#[test]
fn pitch_clamps_to_valid_range() {
assert_eq!(Pitch::from_f32(-10.0), Pitch::C0);
assert_eq!(Pitch::from_f32(200.0), Pitch::B9);
let p = Pitch::from_f32(48.0);
assert!((p.to_f32() - 48.0).abs() < 0.005);
}
#[test]
fn finetune_round_trip() {
for v in [-1.0_f32, -0.5, 0.0, 0.25, 0.5, 0.75] {
let q = Finetune::from_f32(v);
let back = q.to_f32();
assert!((back - v).abs() < 1e-4, "{} → {}", v, back);
}
}
#[test]
fn frequency_a4_round_trip() {
let f = Frequency::from_f32(440.0);
let back = f.to_f32();
assert!((back - 440.0).abs() < 0.01, "got {}", back);
}
#[test]
fn amplification_unity_is_one() {
assert!((Amplification::UNITY.to_f32() - 1.0).abs() < 1e-4);
let amp = Amplification::from_f32(2.5);
assert!((amp.to_f32() - 2.5).abs() < 1e-3);
}
#[test]
fn retrig_mul_known_ratios() {
let m = RetrigMul::from_f32(0.6875);
assert!((m.to_f32() - 0.6875).abs() < 1e-3);
let m = RetrigMul::from_f32(2.0);
assert!((m.to_f32() - 2.0).abs() < 1e-3);
}
#[test]
fn round_trip_drift_is_bounded() {
let mut q = Q15::from_raw(12345);
for _ in 0..1 {
q = Q15::from_f32(q.to_f32());
}
assert!((q.raw() - 12345).abs() <= 1);
}
}