use serde::{Deserialize, Serialize};
use goonj::directivity::DirectivityPattern as GoonjPattern;
use hisab::Vec3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SourceDirectivity {
Omnidirectional,
Cardioid,
Subcardioid,
Supercardioid,
Figure8,
}
impl SourceDirectivity {
#[inline]
#[must_use]
pub fn gain(self, direction: Vec3, front: Vec3) -> f32 {
self.to_goonj().gain(direction, front)
}
#[inline]
#[must_use]
pub fn gain_polar(self, theta: f32) -> f32 {
let cos_theta = theta.cos().clamp(-1.0, 1.0);
match self {
Self::Omnidirectional => 1.0,
Self::Cardioid => (0.5 * (1.0 + cos_theta)).max(0.0),
Self::Subcardioid => 0.75 + 0.25 * cos_theta,
Self::Supercardioid => (0.37 + 0.63 * cos_theta).max(0.0),
Self::Figure8 => cos_theta.abs(),
}
}
fn to_goonj(self) -> GoonjPattern {
match self {
Self::Omnidirectional => GoonjPattern::Omnidirectional,
Self::Cardioid => GoonjPattern::Cardioid,
Self::Subcardioid => GoonjPattern::Subcardioid,
Self::Supercardioid => GoonjPattern::Supercardioid,
Self::Figure8 => GoonjPattern::Figure8,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_omnidirectional_is_unity_in_all_directions() {
let front = Vec3::new(1.0, 0.0, 0.0);
for dir in [
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
] {
let g = SourceDirectivity::Omnidirectional.gain(dir, front);
assert!((g - 1.0).abs() < 1e-5, "omni dir {dir:?} → gain {g}");
}
}
#[test]
fn test_cardioid_null_at_rear() {
assert!((SourceDirectivity::Cardioid.gain_polar(0.0) - 1.0).abs() < 1e-5);
assert!(SourceDirectivity::Cardioid.gain_polar(std::f32::consts::PI) < 1e-5);
}
#[test]
fn test_figure8_nulls_at_sides() {
assert!((SourceDirectivity::Figure8.gain_polar(0.0) - 1.0).abs() < 1e-5);
assert!((SourceDirectivity::Figure8.gain_polar(std::f32::consts::PI) - 1.0).abs() < 1e-5);
assert!(SourceDirectivity::Figure8.gain_polar(std::f32::consts::FRAC_PI_2) < 1e-5);
}
#[test]
fn test_supercardioid_has_small_rear_lobe() {
let g_rear = SourceDirectivity::Supercardioid.gain_polar(std::f32::consts::PI);
assert!(g_rear < 1e-5);
let g_oblique =
SourceDirectivity::Supercardioid.gain_polar(2.0 * std::f32::consts::PI / 3.0);
assert!(g_oblique > 0.0);
}
#[test]
fn test_polar_matches_3d_for_axisymmetric() {
let front = Vec3::new(1.0, 0.0, 0.0);
for theta_deg in [0, 45, 90, 135, 180] {
let theta = (theta_deg as f32).to_radians();
let dir = Vec3::new(theta.cos(), theta.sin(), 0.0);
for pat in [
SourceDirectivity::Cardioid,
SourceDirectivity::Subcardioid,
SourceDirectivity::Supercardioid,
SourceDirectivity::Figure8,
] {
let g_3d = pat.gain(dir, front);
let g_polar = pat.gain_polar(theta);
assert!(
(g_3d - g_polar).abs() < 1e-4,
"{pat:?} @ {theta_deg}°: 3D={g_3d} polar={g_polar}"
);
}
}
}
#[test]
fn test_serde_roundtrip() {
let pat = SourceDirectivity::Cardioid;
let json = serde_json::to_string(&pat).unwrap();
let back: SourceDirectivity = serde_json::from_str(&json).unwrap();
assert_eq!(pat, back);
}
}