audionimbus 0.13.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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! Global context management and configuration.

use crate::error::{to_option_error, SteamAudioError};
use crate::version::SteamAudioVersion;

/// A context object, which controls low-level operations of Steam Audio.
///
/// Typically, a context is specified once during the execution of the client program, before calling any other API functions.
///
/// # Examples
///
/// ```
/// use audionimbus::{Context, ContextSettings, SimdLevel};
///
/// // Create with default settings.
/// let context = Context::default();
///
/// // Or with custom settings.
/// let settings = ContextSettings::new().with_simd_level(SimdLevel::AVX2);
/// let context = Context::try_new(&settings)?;
/// # Ok::<(), audionimbus::SteamAudioError>(())
/// ```
#[derive(Debug)]
pub struct Context(pub(crate) audionimbus_sys::IPLContext);

impl Context {
    /// Creates a new context with the given [`ContextSettings`].
    ///
    /// # Errors
    ///
    /// Returns [`SteamAudioError`] if context creation fails, typically due to:
    /// - Incompatible API version
    /// - Memory allocation failure
    /// - External dependency initialization failure
    pub fn try_new(settings: &ContextSettings) -> Result<Self, SteamAudioError> {
        let mut context = Self(std::ptr::null_mut());

        let status = unsafe {
            audionimbus_sys::iplContextCreate(
                &mut audionimbus_sys::IPLContextSettings::from(settings),
                context.raw_ptr_mut(),
            )
        };

        if let Some(error) = to_option_error(status) {
            return Err(error);
        }

        Ok(context)
    }

    /// Returns the raw FFI pointer to the underlying Steam Audio context.
    ///
    /// # Safety
    ///
    /// This is intended for internal use and advanced scenarios. The returned pointer
    /// must not outlive the `Context` object.
    pub const fn raw_ptr(&self) -> audionimbus_sys::IPLContext {
        self.0
    }

    /// Returns a mutable reference to the raw FFI pointer.
    ///
    /// # Safety
    ///
    /// This is intended for internal use and advanced scenarios. The returned pointer
    /// must not outlive the `Context` object.
    pub const fn raw_ptr_mut(&mut self) -> &mut audionimbus_sys::IPLContext {
        &mut self.0
    }
}

impl Default for Context {
    fn default() -> Self {
        let settings = ContextSettings::default();
        Self::try_new(&settings).expect("failed to create default context")
    }
}

impl Drop for Context {
    fn drop(&mut self) {
        unsafe { audionimbus_sys::iplContextRelease(&raw mut self.0) }
    }
}

unsafe impl Send for Context {}
unsafe impl Sync for Context {}

impl Clone for Context {
    /// Retains an additional reference to the context.
    ///
    /// The returned [`Context`] shares the same underlying Steam Audio object.
    /// The context will not be destroyed until all clones are dropped.
    fn clone(&self) -> Self {
        // SAFETY: iplContextRetain increments the reference count of the underlying
        // Steam Audio context and returns a new handle to it. The context will not
        // be destroyed until all references are released.
        Self(unsafe { audionimbus_sys::iplContextRetain(self.0) })
    }
}

/// Settings used to create a [`Context`].
pub struct ContextSettings {
    /// The API version.
    ///
    /// Context creation will fail if `phonon.dll` does not implement a compatible version of the API.
    /// Typically, this should be set to [`SteamAudioVersion::default()`].
    version: SteamAudioVersion,

    /// If `Some`, Steam Audio will call this function to record log messages generated by certain operations.
    ///
    /// See macro [`log_callback`](crate::log_callback) to easily define a log callback using
    /// Rust-friendly types. For instance:
    ///
    /// ```
    /// # use audionimbus::{log_callback, Context, ContextSettings};
    /// let settings = ContextSettings::new().with_log_callback(log_callback!(|level, message| {
    ///     println!("{level:?}: {message}");
    /// }));
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    log_callback: Option<
        unsafe extern "C" fn(
            level: audionimbus_sys::IPLLogLevel,
            message: *const std::os::raw::c_char,
        ),
    >,

    /// If `Some`, Steam Audio will call this function whenever it needs to allocate memory.
    ///
    /// See macro [`allocate_callback`](crate::allocate_callback) to easily define a memory allocation callback.
    /// For instance:
    ///
    /// ```
    /// # use audionimbus::{allocate_callback, free_callback, Context, ContextSettings};
    /// # free_callback!(my_free, |ptr| {
    /// #   // ...
    /// # });
    /// use std::alloc::{alloc, Layout};
    /// use std::ffi::c_void;
    ///
    /// let settings = ContextSettings::new()
    ///     .with_allocate_callback(allocate_callback!(|size, alignment| {
    ///         unsafe {
    ///             let layout = Layout::from_size_align_unchecked(size, alignment);
    ///             alloc(layout) as *mut c_void
    ///         }
    ///     }))
    ///     .with_free_callback(my_free);
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    allocate_callback:
        Option<unsafe extern "C" fn(size: usize, alignment: usize) -> *mut std::ffi::c_void>,

    /// If `Some`, Steam Audio will call this function whenever it needs to free memory.
    ///
    /// See macro [`free_callback`](crate::free_callback) to easily define a callback function to free memory.
    /// For instance:
    ///
    /// ```
    /// # use audionimbus::{free_callback, allocate_callback, Context, ContextSettings};
    /// # use std::alloc::{alloc, Layout};
    /// # use std::ffi::c_void;
    /// # allocate_callback!(my_allocator, |size, alignment| {
    /// #     unsafe {
    /// #         let layout = Layout::from_size_align_unchecked(size, alignment);
    /// #         alloc(layout) as *mut c_void
    /// #     }
    /// # });
    /// let settings = ContextSettings::new()
    ///     .with_allocate_callback(my_allocator)
    ///     .with_free_callback(free_callback!(|ptr| {
    ///         // ...
    ///     }));
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    free_callback: Option<unsafe extern "C" fn(memory_block: *mut std::ffi::c_void)>,

    /// The maximum SIMD instruction set level that Steam Audio should use.
    ///
    /// Steam Audio automatically chooses the best instruction set to use based on the user’s CPU, but you can prevent it from using certain newer instruction sets using this parameter.
    /// For example, with some workloads, AVX512 instructions consume enough power that the CPU clock speed will be throttled, resulting in lower performance than expected.
    /// If you observe this in your application, set this parameter to [`SimdLevel::AVX2`] or lower.
    simd_level: SimdLevel,

    /// Additional flags for modifying the behavior of the created context.
    flags: ContextFlags,
}

impl ContextSettings {
    /// Creates new [`ContextSettings`] with default values.
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the API version for this context.
    ///
    /// Context creation will fail if `phonon.dll` does not implement a compatible version of the API.
    /// Typically, this should be set to [`SteamAudioVersion::default()`].
    ///
    /// # Examples
    ///
    /// ```
    /// use audionimbus::{Context, ContextSettings, SteamAudioVersion};
    ///
    /// let settings = ContextSettings::new().with_version(SteamAudioVersion::default());
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    pub fn with_version(mut self, version: SteamAudioVersion) -> Self {
        self.version = version;
        self
    }

    /// Sets a callback function for logging Steam Audio messages.
    ///
    /// Use the [`log_callback!`](crate::log_callback) macro to easily define a log callback
    /// with Rust-friendly types instead of manually creating an `extern "C"` function.
    ///
    /// # Examples
    ///
    /// ```
    /// use audionimbus::{log_callback, Context, ContextSettings};
    ///
    /// let settings = ContextSettings::new().with_log_callback(log_callback!(|level, message| {
    ///     println!("{level:?}: {message}");
    /// }));
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    pub fn with_log_callback(
        mut self,
        log_callback: unsafe extern "C" fn(
            level: audionimbus_sys::IPLLogLevel,
            message: *const std::os::raw::c_char,
        ),
    ) -> Self {
        self.log_callback = Some(log_callback);
        self
    }

    /// Sets a callback function for custom memory allocation.
    ///
    /// Use the [`allocate_callback!`](crate::allocate_callback) macro to easily define
    /// an allocation callback with Rust-friendly types instead of manually creating an
    /// `extern "C"` function.
    ///
    /// # Examples
    ///
    /// ```
    /// use audionimbus::{allocate_callback, free_callback, Context, ContextSettings};
    /// use std::alloc::{alloc, Layout};
    /// use std::ffi::c_void;
    ///
    /// let settings = ContextSettings::new()
    ///     .with_allocate_callback(allocate_callback!(|size, alignment| {
    ///         unsafe {
    ///             let layout = Layout::from_size_align_unchecked(size, alignment);
    ///             alloc(layout) as *mut c_void
    ///         }
    ///     }))
    ///     .with_free_callback(free_callback!(|ptr| {
    ///         // Free the memory...
    ///     }));
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    pub fn with_allocate_callback(
        mut self,
        allocate_callback: unsafe extern "C" fn(
            size: usize,
            alignment: usize,
        ) -> *mut std::ffi::c_void,
    ) -> Self {
        self.allocate_callback = Some(allocate_callback);
        self
    }

    /// Sets a callback function for custom memory deallocation.
    ///
    /// Use the [`free_callback!`](crate::free_callback) macro to easily define a free
    /// callback with Rust-friendly types instead of manually creating an `extern "C"` function.
    ///
    /// # Examples
    ///
    /// ```
    /// use audionimbus::{allocate_callback, free_callback, Context, ContextSettings};
    /// use std::alloc::{alloc, Layout};
    /// use std::ffi::c_void;
    ///
    /// let settings = ContextSettings::new()
    ///     .with_allocate_callback(allocate_callback!(|size, alignment| {
    ///         unsafe {
    ///             let layout = Layout::from_size_align_unchecked(size, alignment);
    ///             alloc(layout) as *mut c_void
    ///         }
    ///     }))
    ///     .with_free_callback(free_callback!(|ptr| {
    ///         // Free the memory...
    ///     }));
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    pub fn with_free_callback(
        mut self,
        free_callback: unsafe extern "C" fn(memory_block: *mut std::ffi::c_void),
    ) -> Self {
        self.free_callback = Some(free_callback);
        self
    }

    /// Sets the maximum SIMD instruction set level to use.
    ///
    /// Steam Audio automatically chooses the best instruction set based on the CPU, but you
    /// can limit it to prevent issues. For example, AVX512 instructions may cause CPU
    /// throttling with some workloads, reducing performance. If you observe this, set the
    /// level to [`SimdLevel::AVX2`] or lower.
    ///
    /// # Examples
    ///
    /// ```
    /// use audionimbus::{Context, ContextSettings, SimdLevel};
    ///
    /// let settings = ContextSettings::new().with_simd_level(SimdLevel::AVX2);
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    pub fn with_simd_level(mut self, simd_level: SimdLevel) -> Self {
        self.simd_level = simd_level;
        self
    }

    /// Sets additional flags for modifying the context's behavior.
    ///
    /// # Arguments
    ///
    /// - `flags`: Additional flags for modifying the behavior of the created context.
    ///
    /// # Examples
    ///
    /// ```
    /// use audionimbus::{Context, ContextFlags, ContextSettings};
    ///
    /// // Enable validation for debugging (significant performance penalty).
    /// let settings = ContextSettings::new().with_flags(ContextFlags::VALIDATION);
    /// let context = Context::try_new(&settings)?;
    /// # Ok::<(), audionimbus::SteamAudioError>(())
    /// ```
    pub fn with_flags(mut self, flags: ContextFlags) -> Self {
        self.flags = flags;
        self
    }
}

impl Default for ContextSettings {
    fn default() -> Self {
        Self {
            version: SteamAudioVersion::default(),
            log_callback: None,
            allocate_callback: None,
            free_callback: None,
            simd_level: SimdLevel::default(),
            flags: ContextFlags::empty(),
        }
    }
}

impl From<&ContextSettings> for audionimbus_sys::IPLContextSettings {
    fn from(settings: &ContextSettings) -> Self {
        Self {
            version: settings.version.into(),
            logCallback: settings.log_callback,
            allocateCallback: settings.allocate_callback,
            freeCallback: settings.free_callback,
            simdLevel: settings.simd_level.into(),
            flags: settings.flags.into(),
        }
    }
}

/// SIMD instruction sets that Steam Audio can attempt to use.
#[derive(Debug, Copy, Clone, Default)]
pub enum SimdLevel {
    /// Intel Streaming SIMD Extensions 2.
    /// Up to 4 simultaneous floating-point operations.
    SSE2 = 0,

    /// Intel Streaming SIMD Extensions 4.2 or older.
    /// Up to 4 simultaneous floating-point operations.
    SSE4 = 1,

    /// Intel Advanced Vector Extensions or older.
    /// Up to 8 simultaneous floating-point operations.
    AVX = 2,

    /// Intel Advanced Vector Extensions 2 or older.
    /// Up to 8 simultaneous floating-point operations.
    AVX2 = 3,

    /// Intel Advanced Vector Extensions 512 or older.
    /// Up to 16 simultaneous floating-point operations.
    #[default]
    AVX512 = 4,
}

impl From<SimdLevel> for audionimbus_sys::IPLSIMDLevel {
    fn from(simd_level: SimdLevel) -> Self {
        match simd_level {
            SimdLevel::SSE2 => Self::IPL_SIMDLEVEL_SSE2,
            SimdLevel::SSE4 => Self::IPL_SIMDLEVEL_SSE4,
            SimdLevel::AVX => Self::IPL_SIMDLEVEL_AVX,
            SimdLevel::AVX2 => Self::IPL_SIMDLEVEL_AVX2,
            SimdLevel::AVX512 => Self::IPL_SIMDLEVEL_AVX512,
        }
    }
}

bitflags::bitflags! {
    /// Additional flags for modifying the behavior of a Steam Audio context.
    #[derive(Debug, Copy, Clone)]
    pub struct ContextFlags: u32 {
        /// All API functions perform extra validation checks.
        /// NOTE: This imposes a significant performance penalty.
        const VALIDATION = 1 << 0;

        /// Force this enum to be 32 bits in size.
        const FORCE_32BIT = 1 << 1;
    }
}

impl From<ContextFlags> for audionimbus_sys::IPLContextFlags {
    fn from(context_flags: ContextFlags) -> Self {
        Self(context_flags.bits() as _)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_context_clone() {
        let context = Context::default();
        let clone = context.clone();
        assert_eq!(context.raw_ptr(), clone.raw_ptr());
        drop(context);
        assert!(!clone.raw_ptr().is_null());
    }

    #[test]
    fn test_context_settings_simd_levels() {
        let levels = [
            SimdLevel::SSE2,
            SimdLevel::SSE4,
            SimdLevel::AVX,
            SimdLevel::AVX2,
            SimdLevel::AVX512,
        ];

        for level in levels {
            let settings = ContextSettings::new().with_simd_level(level);
            let result = Context::try_new(&settings);
            assert!(result.is_ok());
        }
    }
}