#![forbid(unsafe_code)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_lossless)]
const MAX_FLOOR1_CLASSES: usize = 16;
const MAX_FLOOR1_VALUES: usize = 256;
#[derive(Clone, Copy, Debug, Default)]
pub struct Floor1Class {
pub dimensions: u8,
pub sub_books: u8,
pub master_book: u8,
}
#[derive(Clone, Debug)]
pub struct Floor1Config {
pub multiplier: u8,
pub partitions: u8,
pub partition_class_list: Vec<u8>,
pub classes: Vec<Floor1Class>,
pub x_list: Vec<u16>,
}
impl Default for Floor1Config {
fn default() -> Self {
Self {
multiplier: 1,
partitions: 0,
partition_class_list: Vec::new(),
classes: Vec::new(),
x_list: vec![0, 255],
}
}
}
#[derive(Clone, Debug)]
pub struct Floor1Curve {
pub amplitudes: Vec<i16>,
pub x_list: Vec<u16>,
pub unused: bool,
}
impl Floor1Curve {
#[must_use]
pub fn interpolate_at(&self, x: f64, n: usize) -> f64 {
if self.unused || self.x_list.is_empty() {
return 0.0;
}
let xs = &self.x_list;
let ys = &self.amplitudes;
let m = self.x_list.len();
let scale = n as f64;
let mut lo_idx = 0;
let mut hi_idx = m.saturating_sub(1);
for i in 1..m {
if f64::from(xs[i]) / scale <= x / scale {
lo_idx = i;
}
}
if lo_idx + 1 < m {
hi_idx = lo_idx + 1;
}
let x0 = f64::from(xs[lo_idx]);
let x1 = f64::from(xs[hi_idx]);
let y0 = f64::from(ys[lo_idx]);
let y1 = f64::from(ys[hi_idx]);
if (x1 - x0).abs() < 1e-10 {
return y0;
}
y0 + (y1 - y0) * (x - x0) / (x1 - x0)
}
#[must_use]
pub fn to_linear(&self, multiplier: u8) -> Vec<f64> {
let scale = f64::from(multiplier) * std::f64::consts::LN_10 / 20.0;
self.amplitudes
.iter()
.map(|&a| (f64::from(a) * scale).exp())
.collect()
}
}
pub fn encode_floor1(log_spectrum: &[f64], x_list: &[u16], multiplier: u8) -> Vec<i16> {
let m = x_list.len();
let n = log_spectrum.len();
let inv_scale = 20.0 / (f64::from(multiplier) * std::f64::consts::LN_10);
(0..m)
.map(|i| {
let x = x_list[i] as usize;
let x_clamped = x.min(n.saturating_sub(1));
let db_val = log_spectrum[x_clamped] * inv_scale;
db_val.round().clamp(0.0, 255.0) as i16
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_floor1_config_default() {
let cfg = Floor1Config::default();
assert_eq!(cfg.x_list, vec![0, 255]);
assert_eq!(cfg.multiplier, 1);
}
#[test]
fn test_floor1_curve_unused() {
let curve = Floor1Curve {
amplitudes: vec![0; 2],
x_list: vec![0, 255],
unused: true,
};
let v = curve.interpolate_at(128.0, 256);
assert_eq!(v, 0.0);
}
#[test]
fn test_floor1_curve_interpolate_midpoint() {
let curve = Floor1Curve {
amplitudes: vec![0, 100],
x_list: vec![0, 256],
unused: false,
};
let v = curve.interpolate_at(128.0, 256);
assert!((v - 50.0).abs() < 1.0, "Expected ~50, got {v}");
}
#[test]
fn test_floor1_to_linear_amplitude_zero() {
let curve = Floor1Curve {
amplitudes: vec![0],
x_list: vec![128],
unused: false,
};
let lin = curve.to_linear(1);
assert!((lin[0] - 1.0).abs() < 1e-9, "exp(0) should be 1.0");
}
#[test]
fn test_encode_floor1_flat_spectrum() {
let n = 256;
let log_spec = vec![1.0f64; n]; let x_list: Vec<u16> = vec![0, 128, 255];
let amps = encode_floor1(&log_spec, &x_list, 1);
assert_eq!(amps.len(), 3);
assert_eq!(amps[0], amps[1]);
assert_eq!(amps[1], amps[2]);
}
#[test]
fn test_encode_floor1_clamped_to_u8_range() {
let log_spec = vec![100.0f64]; let amps = encode_floor1(&log_spec, &[0], 1);
assert_eq!(amps[0], 255);
}
}