audionimbus 0.16.0

A safe wrapper around Steam Audio that provides spatial audio capabilities with realistic occlusion, reverb, and HRTF effects, accounting for physical attributes and scene geometry.
Documentation
//! Directivity patterns for modeling sound intensity as a function of the source's orientation.

use crate::callback::DirectivityCallback;
use crate::context::Context;
use crate::geometry;

/// A directivity pattern that can be used to model changes in sound intensity as a function of the source’s orientation.
/// Can be used with both direct and indirect sound propagation.
#[derive(Clone, Debug)]
pub enum Directivity {
    /// The default directivity model is a weighted dipole.
    /// This is a linear blend between an omnidirectional source (which emits sound with equal intensity in all directions), and a dipole oriented along the z-axis in the source’s coordinate system (which focuses sound along the +z and -z axes).
    WeightedDipole {
        /// How much of the dipole to blend into the directivity pattern.
        /// 0.0 = pure omnidirectional, 1.0 = pure dipole.
        /// 0.5 results in a cardioid directivity pattern.
        weight: f32,

        /// How “sharp” the dipole is.
        /// Higher values result in sound being focused within a narrower range of directions.
        power: f32,
    },

    /// A callback function to implement any other arbitrary directivity pattern.
    Callback(DirectivityCallback),
}

impl Default for Directivity {
    fn default() -> Self {
        Self::WeightedDipole {
            weight: 0.5,
            power: 0.5,
        }
    }
}

impl From<&Directivity> for audionimbus_sys::IPLDirectivity {
    fn from(directivity: &Directivity) -> Self {
        let (dipole_weight, dipole_power, callback, user_data) = match directivity {
            Directivity::WeightedDipole { weight, power } => {
                (*weight, *power, None, std::ptr::null_mut())
            }
            Directivity::Callback(callback) => {
                let (callback_fn, user_data) = callback.as_raw_parts();
                (f32::default(), f32::default(), Some(callback_fn), user_data)
            }
        };

        Self {
            dipoleWeight: dipole_weight,
            dipolePower: dipole_power,
            callback,
            userData: user_data,
        }
    }
}

/// Calculates the attenuation of a source due to its directivity pattern and orientation relative to a listener.
pub fn directivity_attenuation(
    context: &Context,
    source: geometry::CoordinateSystem,
    listener: geometry::Point,
    directivity: &Directivity,
) -> f32 {
    unsafe {
        audionimbus_sys::iplDirectivityCalculate(
            context.raw_ptr(),
            source.into(),
            listener.into(),
            &mut directivity.into(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{CoordinateSystem, Point, Vector3};
    use std::sync::{Arc, Mutex};

    #[test]
    fn test_default_model() {
        let context = Context::default();
        let source = CoordinateSystem::default();

        let listener = Point::new(0.0, 0.0, 1.0);
        let attenuation =
            directivity_attenuation(&context, source, listener, &Directivity::default());
        assert_eq!(attenuation, 1.0); // No attenuation when listener is in front

        let listener = Point::new(0.0, 1.0, 0.0);
        let attenuation =
            directivity_attenuation(&context, source, listener, &Directivity::default());
        assert_eq!(attenuation, 0.70710677);
    }

    #[test]
    fn test_weighted_dipole() {
        let context = Context::default();
        let source = CoordinateSystem::default();

        // Test listener in front (along +z).
        let listener_front = Point::new(0.0, 0.0, 1.0);
        // Test listener behind (along -z).
        let listener_behind = Point::new(0.0, 0.0, -1.0);
        // Test listener to the side (along +x).
        let listener_side = Point::new(1.0, 0.0, 0.0);

        let directivity = Directivity::WeightedDipole {
            weight: 0.5, // Cardioid pattern
            power: 1.0,
        };

        let attenuation_front =
            directivity_attenuation(&context, source, listener_front, &directivity);
        let attenuation_behind =
            directivity_attenuation(&context, source, listener_behind, &directivity);
        let attenuation_side =
            directivity_attenuation(&context, source, listener_side, &directivity);

        // Side should have less intensity.
        assert!(attenuation_side < attenuation_front);
        assert_eq!(attenuation_behind, 0.0);
    }

    #[test]
    fn test_omnidirectional() {
        let context = Context::default();
        let source = CoordinateSystem::default();

        // Test multiple directions.
        let directions = [
            Point::new(1.0, 0.0, 0.0),
            Point::new(0.0, 1.0, 0.0),
            Point::new(0.0, 0.0, 1.0),
            Point::new(-1.0, 0.0, 0.0),
        ];

        let directivity = Directivity::WeightedDipole {
            weight: 0.0, // Omnidirectional
            power: 1.0,
        };

        let mut attenuations = Vec::new();
        for &listener in &directions {
            let attn = directivity_attenuation(&context, source, listener, &directivity);
            attenuations.push(attn);
        }

        // All directions should have similar attenuation.
        let first = attenuations[0];
        for &attenuation in &attenuations {
            assert!((attenuation - first).abs() < 0.01);
        }
    }

    #[test]
    fn callback_receives_direction() {
        let context = Context::default();
        let source = CoordinateSystem::default();
        let listener = Point::new(1.0, 0.0, 0.0);
        let received = Arc::new(Mutex::new(None));
        let callback_received = Arc::clone(&received);
        let directivity = Directivity::Callback(DirectivityCallback::new(move |direction| {
            *callback_received.lock().unwrap() = Some(direction);
            0.5
        }));

        let attenuation = directivity_attenuation(&context, source, listener, &directivity);

        assert_eq!(attenuation, 0.5);
        assert_eq!(*received.lock().unwrap(), Some(Vector3::new(1.0, 0.0, 0.0)));
    }

    #[test]
    fn callbacks_keep_distinct_user_data() {
        let context = Context::default();
        let source = CoordinateSystem::default();
        let listener = Point::new(1.0, 0.0, 0.0);
        let first = Directivity::Callback(DirectivityCallback::new(|_| 0.25));
        let second = Directivity::Callback(DirectivityCallback::new(|_| 0.75));

        assert_eq!(
            directivity_attenuation(&context, source, listener, &first),
            0.25
        );
        assert_eq!(
            directivity_attenuation(&context, source, listener, &second),
            0.75
        );
    }
}