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);