Skip to main content

rg3d_sound_sdl/
lib.rs

1//! Use rg3d-sound with SDL's audio subsystem
2//!
3//! This crate allows you to use [SDL2's][sdl2] audio backend to output rendered audio data from
4//! [`rg3d_sound`]. This provides maximum portability between operating systems as SDL audio works
5//! almost everywhere SDL does, and it will also take advantage of newer audio interfaces like
6//! Pulseaudio and PipeWire on Linux.
7//! # Example
8//! ```no_run
9//! use std::{fs::File, io::BufReader, thread, time::Duration};
10//! # use std::error::Error;
11//!
12//! use rg3d_sound::{
13//! buffer::{DataSource, SoundBufferResource},
14//! context::SoundContext,
15//! source::{generic::GenericSourceBuilder, Status},
16//! };
17//!
18//!# fn main() -> Result<(), Box<dyn Error>> {
19//! let sdl = sdl2::init()?;
20//! let audio = sdl.audio()?;
21//! let (engine, device) = rg3d_sound_sdl::open(&audio, None)?;
22//! device.resume();
23//!
24//! let ctx = SoundContext::new();
25//! engine.lock().unwrap().add_context(ctx.clone());
26//!
27//! let sound_buffer = SoundBufferResource::new_generic(DataSource::File {
28//! path: "ding.wav".into(),
29//! data: BufReader::new(File::open("ding.wav")?),
30//! })
31//! .expect("Failed to create data source");
32//!
33//! let source = GenericSourceBuilder::new()
34//! .with_buffer(sound_buffer)
35//! .with_status(Status::Playing)
36//! .build_source()?;
37//!
38//! ctx.state().add_source(source);
39//!
40//! thread::sleep(Duration::from_millis(1090));
41//! # Ok(())
42//! # }
43//! ```
44
45use std::sync::{Arc, Mutex};
46
47use rg3d_sound::engine::SoundEngine;
48use sdl2::audio::{AudioCallback, AudioDevice, AudioFormat, AudioSpecDesired};
49
50/// Opens a new audio device.
51///
52/// On success, returns both the SDL [`AudioDevice`], and a handle to a
53/// [`SoundEngine`] which will drive the device. On error, returns the SDL error.
54/// # Example
55/// ```no_run
56/// let sdl = sdl2::init().unwrap();
57/// let audio = sdl.audio().unwrap();
58/// let (engine, device) = rg3d_sound_sdl::open(&audio, None).unwrap();
59/// device.resume();
60/// ```
61pub fn open<'a>(
62    subsystem: &sdl2::AudioSubsystem,
63    device: impl Into<Option<&'a str>>,
64) -> Result<(Arc<Mutex<SoundEngine>>, AudioDevice<Callback>), String> {
65    let desired = desired_spec();
66    let engine = SoundEngine::without_device();
67    let callback_engine = Arc::clone(&engine);
68
69    subsystem
70        .open_playback(device, &desired, |obtained| {
71            assert_eq!(
72                obtained.freq as u32,
73                rg3d_sound::context::SAMPLE_RATE,
74                "Invalid sample rate"
75            );
76            assert_eq!(obtained.channels, 2, "Invalid number of channels");
77            assert_eq!(
78                obtained.format,
79                AudioFormat::f32_sys(),
80                "Invalid sample format"
81            );
82            assert_eq!(
83                obtained.samples as usize,
84                SoundEngine::render_buffer_len(),
85                "Invalid buffer size"
86            );
87            Callback::new(callback_engine)
88        })
89        .map(|dev| (engine, dev))
90}
91
92/// Obtain the desired SDL audio parameters for use with `rg3d_sound`. This is used internally by
93/// [`open`] to configure the playback device.
94/// # Panics
95/// This function will panic if the returned buffer size from [`SoundEngine::render_buffer_len`] is
96/// too large for SDL (I.E. buffer_size > u16::MAX).
97///
98/// This crate also staticly asserts that [`SAMPLE_RATE`][rg3d_sound::context::SAMPLE_RATE] <=
99/// `i32::MAX`.
100/// # Example
101/// ```
102/// let desired = rg3d_sound_sdl::desired_spec();
103/// assert_eq!(desired.freq, Some(44_100));
104/// assert_eq!(desired.channels, Some(2));
105/// ```
106pub fn desired_spec() -> AudioSpecDesired {
107    let samples = SoundEngine::render_buffer_len()
108        .try_into()
109        .expect("Audio buffer too large");
110    AudioSpecDesired {
111        freq: Some(rg3d_sound::context::SAMPLE_RATE as _),
112        channels: Some(2),
113        samples: Some(samples),
114    }
115}
116
117/// An [`AudioCallback`] used to feed the SDL audio device with rendered audio from a
118/// [`SoundEngine`]
119pub struct Callback {
120    engine: Arc<Mutex<SoundEngine>>,
121}
122
123impl Callback {
124    /// Create a new `Callback` from an existing [`SoundEngine`]. The engine must be opened with
125    /// [`SoundEngine::without_device`] so that the manual rendering functions can be used.
126    pub fn new(engine: Arc<Mutex<SoundEngine>>) -> Self {
127        Self { engine }
128    }
129}
130
131impl AudioCallback for Callback {
132    type Channel = f32;
133
134    fn callback(&mut self, buf: &mut [Self::Channel]) {
135        let buf = to_tuple_slice(buf);
136        let mut engine = self.engine.lock().unwrap();
137        engine.render(buf);
138    }
139}
140
141/// Converts a slice of [`f32`] values, of even length, to a slice of `(f32, f32)` tuples. The
142/// returned slice will be half the length of the input slice.
143/// # Panics
144/// This function will panic if the input slice has an odd number of elements.
145///
146/// This crate also staticly asserts that the alignment and size of `(f32, f32)` and `[f32; 2]` are
147/// identical.
148pub fn to_tuple_slice(slice: &mut [f32]) -> &mut [(f32, f32)] {
149    let ptr = slice.as_mut_ptr();
150    let len = slice.len();
151    debug_assert!(len % 2 == 0);
152    unsafe { std::slice::from_raw_parts_mut(ptr.cast(), len / 2) }
153}
154
155static_assertions::assert_eq_align!((f32, f32), [f32; 2]);
156static_assertions::assert_eq_size!((f32, f32), [f32; 2]);
157
158static_assertions::const_assert!(rg3d_sound::context::SAMPLE_RATE <= i32::MAX as u32);