use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct BFormatSample {
pub w: f32,
pub x: f32,
pub y: f32,
pub z: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AmbisonicsEncoder {
azimuth: f32,
elevation: f32,
#[serde(skip)]
cos_el: f32,
#[serde(skip)]
sin_el: f32,
#[serde(skip)]
cos_az: f32,
#[serde(skip)]
sin_az: f32,
}
impl AmbisonicsEncoder {
#[must_use]
pub fn new(azimuth: f32, elevation: f32) -> Self {
let mut enc = Self {
azimuth,
elevation,
cos_el: 0.0,
sin_el: 0.0,
cos_az: 0.0,
sin_az: 0.0,
};
enc.update_cache();
enc
}
pub fn set_position(&mut self, azimuth: f32, elevation: f32) {
self.azimuth = azimuth;
self.elevation = elevation;
self.update_cache();
}
#[inline]
#[must_use]
pub fn encode_sample(&self, input: f32) -> BFormatSample {
let (cos_el, sin_el, cos_az, sin_az) = if self.cos_el == 0.0
&& self.sin_el == 0.0
&& self.cos_az == 0.0
&& self.sin_az == 0.0
&& (self.azimuth != 0.0 || self.elevation != 0.0)
{
(
self.elevation.cos(),
self.elevation.sin(),
self.azimuth.cos(),
self.azimuth.sin(),
)
} else if self.cos_el == 0.0
&& self.sin_el == 0.0
&& self.cos_az == 0.0
&& self.sin_az == 0.0
{
(1.0, 0.0, 1.0, 0.0)
} else {
(self.cos_el, self.sin_el, self.cos_az, self.sin_az)
};
BFormatSample {
w: input,
x: input * cos_az * cos_el,
y: input * sin_az * cos_el,
z: input * sin_el,
}
}
#[must_use]
pub fn azimuth(&self) -> f32 {
self.azimuth
}
#[must_use]
pub fn elevation(&self) -> f32 {
self.elevation
}
fn update_cache(&mut self) {
self.cos_el = self.elevation.cos();
self.sin_el = self.elevation.sin();
self.cos_az = self.azimuth.cos();
self.sin_az = self.azimuth.sin();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_center_position_w_equals_input() {
let enc = AmbisonicsEncoder::new(0.0, 0.0);
let sample = enc.encode_sample(1.0);
assert!(
(sample.w - 1.0).abs() < f32::EPSILON,
"W should equal input at center"
);
assert!(
(sample.x - 1.0).abs() < 0.01,
"X should be ~1.0 at front center, got {}",
sample.x
);
assert!(
sample.y.abs() < 0.01,
"Y should be ~0 at center, got {}",
sample.y
);
assert!(
sample.z.abs() < 0.01,
"Z should be ~0 at center, got {}",
sample.z
);
}
#[test]
fn test_90_degrees_produces_expected_xy() {
let enc = AmbisonicsEncoder::new(std::f32::consts::FRAC_PI_2, 0.0);
let sample = enc.encode_sample(1.0);
assert!(
sample.x.abs() < 0.01,
"X should be ~0 at 90deg, got {}",
sample.x
);
assert!(
(sample.y - 1.0).abs() < 0.01,
"Y should be ~1 at 90deg, got {}",
sample.y
);
}
#[test]
fn test_elevation_produces_z() {
let enc = AmbisonicsEncoder::new(0.0, std::f32::consts::FRAC_PI_2);
let sample = enc.encode_sample(1.0);
assert!(
(sample.z - 1.0).abs() < 0.01,
"Z should be ~1 at 90deg elevation, got {}",
sample.z
);
}
#[test]
fn test_set_position() {
let mut enc = AmbisonicsEncoder::new(0.0, 0.0);
enc.set_position(std::f32::consts::FRAC_PI_2, 0.0);
let sample = enc.encode_sample(1.0);
assert!(
(sample.y - 1.0).abs() < 0.01,
"Y should be ~1 after set_position to 90deg"
);
}
#[test]
fn test_zero_input() {
let enc = AmbisonicsEncoder::new(0.5, 0.3);
let sample = enc.encode_sample(0.0);
assert_eq!(sample.w, 0.0);
assert_eq!(sample.x, 0.0);
assert_eq!(sample.y, 0.0);
assert_eq!(sample.z, 0.0);
}
#[test]
fn test_serde_roundtrip() {
let enc = AmbisonicsEncoder::new(0.7, 0.3);
let json = serde_json::to_string(&enc).unwrap();
let back: AmbisonicsEncoder = serde_json::from_str(&json).unwrap();
assert!((enc.azimuth - back.azimuth).abs() < f32::EPSILON);
assert!((enc.elevation - back.elevation).abs() < f32::EPSILON);
let sample = back.encode_sample(1.0);
assert!(sample.w.is_finite());
assert!(sample.x.is_finite());
}
#[test]
fn test_serde_roundtrip_bformat_sample() {
let sample = BFormatSample {
w: 1.0,
x: 0.5,
y: -0.3,
z: 0.1,
};
let json = serde_json::to_string(&sample).unwrap();
let back: BFormatSample = serde_json::from_str(&json).unwrap();
assert_eq!(sample, back);
}
}