rg3d_sound/
context.rs

1//! Context module.
2//!
3//! # Overview
4//!
5//! Context is a sort of "sound scene" - an isolated storage for a set of sound sources, effects, filters, etc.
6//! rg3d-sound can manage multiple contexts at the same time. Main usage for multiple contexts is a typical
7//! situation in games where you have multiple scenes: a scene for main menu, a scene for game level, a scene
8//! for inventory and so on. With this approach of multiple contexts it is very easy to manage such scenes:
9//! for example your main menu have a complex scene with some sounds and you decide to load a game level -
10//! once the level is loaded you just set master gain of main menu context and it will no longer produce any
11//! sounds, only your level will do.
12
13use crate::pool::Ticket;
14use crate::{
15    effects::{Effect, EffectRenderTrait},
16    listener::Listener,
17    renderer::{render_source_default, Renderer},
18    source::{SoundSource, Status},
19};
20use rg3d_core::visitor::VisitError;
21use rg3d_core::{
22    pool::{Handle, Pool},
23    visitor::{Visit, VisitResult, Visitor},
24};
25use std::sync::MutexGuard;
26use std::{
27    sync::{Arc, Mutex},
28    time::Duration,
29};
30
31/// Sample rate for output device.
32/// TODO: Make this configurable, for now its set to most commonly used sample rate of 44100 Hz.
33pub const SAMPLE_RATE: u32 = 44100;
34
35/// Distance model defines how volume of sound will decay when distance to listener changes.
36#[derive(Copy, Clone, Debug, Eq, PartialEq)]
37#[repr(u32)]
38pub enum DistanceModel {
39    /// No distance attenuation at all.
40    None = 0,
41
42    /// Distance will decay using following formula:
43    ///
44    /// `clamped_distance = min(max(distance, radius), max_distance)`
45    /// `attenuation = radius / (radius + rolloff_factor * (clamped_distance - radius))`
46    ///
47    /// where - `radius` - of source at which it has maximum volume,
48    ///         `max_distance` - distance at which decay will stop,
49    ///         `rolloff_factor` - coefficient that defines how fast volume will decay
50    ///
51    /// # Notes
52    ///
53    /// This is default distance model of context.
54    InverseDistance = 1,
55
56    /// Distance will decay using following formula:
57    ///
58    /// `clamped_distance = min(max(distance, radius), max_distance)`
59    /// `attenuation = 1.0 - radius * (clamped_distance - radius) / (max_distance - radius)`
60    ///
61    /// where - `radius` - of source at which it has maximum volume,
62    ///         `max_distance` - distance at which decay will stop
63    ///
64    /// # Notes
65    ///
66    /// As you can see `rolloff_factor` is ignored here because of linear law.
67    LinearDistance = 2,
68
69    /// Distance will decay using following formula:
70    ///
71    /// `clamped_distance = min(max(distance, radius), max_distance)`
72    /// `(clamped_distance / radius) ^ (-rolloff_factor)`
73    ///
74    /// where - `radius` - of source at which it has maximum volume,
75    ///         `max_distance` - distance at which decay will stop,
76    ///         `rolloff_factor` - coefficient that defines how fast volume will decay
77    ExponentDistance = 3,
78}
79
80impl Default for DistanceModel {
81    fn default() -> Self {
82        Self::InverseDistance
83    }
84}
85
86/// See module docs.
87#[derive(Clone, Default, Debug)]
88pub struct SoundContext {
89    pub(in crate) state: Option<Arc<Mutex<State>>>,
90}
91
92impl PartialEq for SoundContext {
93    fn eq(&self, other: &Self) -> bool {
94        Arc::ptr_eq(self.state.as_ref().unwrap(), other.state.as_ref().unwrap())
95    }
96}
97
98/// Internal state of context.
99#[derive(Default, Debug, Clone)]
100pub struct State {
101    sources: Pool<SoundSource>,
102    listener: Listener,
103    master_gain: f32,
104    render_duration: Duration,
105    renderer: Renderer,
106    effects: Pool<Effect>,
107    distance_model: DistanceModel,
108    paused: bool,
109}
110
111impl State {
112    /// Extracts a source from the context and reserves its handle. It is used to temporarily take
113    /// ownership over source, and then put node back using given ticket.
114    pub fn take_reserve(
115        &mut self,
116        handle: Handle<SoundSource>,
117    ) -> (Ticket<SoundSource>, SoundSource) {
118        self.sources.take_reserve(handle)
119    }
120
121    /// Puts source back by given ticket.
122    pub fn put_back(
123        &mut self,
124        ticket: Ticket<SoundSource>,
125        node: SoundSource,
126    ) -> Handle<SoundSource> {
127        self.sources.put_back(ticket, node)
128    }
129
130    /// Makes source handle vacant again.
131    pub fn forget_ticket(&mut self, ticket: Ticket<SoundSource>) {
132        self.sources.forget_ticket(ticket)
133    }
134
135    /// Pause/unpause the sound context. Paused context won't play any sounds.
136    pub fn pause(&mut self, pause: bool) {
137        self.paused = pause;
138    }
139
140    /// Returns true if the sound context is paused, false - otherwise.
141    pub fn is_paused(&self) -> bool {
142        self.paused
143    }
144
145    /// Sets new distance model.
146    pub fn set_distance_model(&mut self, distance_model: DistanceModel) {
147        self.distance_model = distance_model;
148    }
149
150    /// Returns current distance model.
151    pub fn distance_model(&self) -> DistanceModel {
152        self.distance_model
153    }
154
155    /// Adds new effect to effects chain. Each sample from
156    pub fn add_effect(&mut self, effect: Effect) -> Handle<Effect> {
157        self.effects.spawn(effect)
158    }
159
160    /// Removes effect by given handle.
161    pub fn remove_effect(&mut self, effect: Handle<Effect>) {
162        self.effects.free(effect);
163    }
164
165    /// Normalizes given frequency using context's sampling rate. Normalized frequency then can be used
166    /// to create filters.
167    pub fn normalize_frequency(&self, f: f32) -> f32 {
168        f / SAMPLE_RATE as f32
169    }
170
171    /// Returns amount of time context spent on rendering all sound sources.
172    pub fn full_render_duration(&self) -> Duration {
173        self.render_duration
174    }
175
176    /// Sets new renderer.
177    pub fn set_renderer(&mut self, renderer: Renderer) -> Renderer {
178        std::mem::replace(&mut self.renderer, renderer)
179    }
180
181    /// Returns shared reference to current renderer.
182    pub fn renderer(&self) -> &Renderer {
183        &self.renderer
184    }
185
186    /// Returns mutable reference to current renderer.
187    pub fn renderer_mut(&mut self) -> &mut Renderer {
188        &mut self.renderer
189    }
190
191    /// Sets new master gain. Master gain is used to control total sound volume that will be passed to output
192    /// device.
193    pub fn set_master_gain(&mut self, gain: f32) {
194        self.master_gain = gain;
195    }
196
197    /// Returns master gain.
198    pub fn master_gain(&self) -> f32 {
199        self.master_gain
200    }
201
202    /// Adds new sound source and returns handle of it by which it can be accessed later on.
203    pub fn add_source(&mut self, source: SoundSource) -> Handle<SoundSource> {
204        self.sources.spawn(source)
205    }
206
207    /// Returns shared reference to a pool with all sound sources.
208    pub fn sources(&self) -> &Pool<SoundSource> {
209        &self.sources
210    }
211
212    /// Returns mutable reference to a pool with all sound sources.
213    pub fn sources_mut(&mut self) -> &mut Pool<SoundSource> {
214        &mut self.sources
215    }
216
217    /// Returns shared reference to sound source at given handle. If handle is invalid, this method will panic.
218    pub fn source(&self, handle: Handle<SoundSource>) -> &SoundSource {
219        self.sources.borrow(handle)
220    }
221
222    /// Checks whether a handle to a sound source is valid or not.
223    pub fn is_valid_handle(&self, handle: Handle<SoundSource>) -> bool {
224        self.sources.is_valid_handle(handle)
225    }
226
227    /// Returns mutable reference to sound source at given handle. If handle is invalid, this method will panic.
228    pub fn source_mut(&mut self, handle: Handle<SoundSource>) -> &mut SoundSource {
229        self.sources.borrow_mut(handle)
230    }
231
232    /// Returns shared reference to listener. Engine has only one listener.
233    pub fn listener(&self) -> &Listener {
234        &self.listener
235    }
236
237    /// Returns mutable reference to listener. Engine has only one listener.
238    pub fn listener_mut(&mut self) -> &mut Listener {
239        &mut self.listener
240    }
241
242    /// Returns shared reference to effect at given handle. If handle is invalid, this method will panic.
243    pub fn effect(&self, handle: Handle<Effect>) -> &Effect {
244        self.effects.borrow(handle)
245    }
246
247    /// Returns mutable reference to effect at given handle. If handle is invalid, this method will panic.
248    pub fn effect_mut(&mut self, handle: Handle<Effect>) -> &mut Effect {
249        self.effects.borrow_mut(handle)
250    }
251
252    pub(crate) fn render(&mut self, master_gain: f32, buf: &mut [(f32, f32)]) {
253        let last_time = rg3d_core::instant::Instant::now();
254
255        if !self.paused {
256            self.sources.retain(|source| {
257                let done = source.is_play_once() && source.status() == Status::Stopped;
258                !done
259            });
260
261            for source in self
262                .sources
263                .iter_mut()
264                .filter(|s| s.status() == Status::Playing)
265            {
266                source.render(buf.len());
267
268                match self.renderer {
269                    Renderer::Default => {
270                        // Simple rendering path. Much faster (4-5 times) than HRTF path.
271                        render_source_default(source, &self.listener, self.distance_model, buf);
272                    }
273                    Renderer::HrtfRenderer(ref mut hrtf_renderer) => {
274                        hrtf_renderer.render_source(
275                            source,
276                            &self.listener,
277                            self.distance_model,
278                            buf,
279                        );
280                    }
281                }
282            }
283
284            for effect in self.effects.iter_mut() {
285                effect.render(&self.sources, &self.listener, self.distance_model, buf);
286            }
287
288            let global_gain = self.master_gain * master_gain;
289
290            // Apply master gain to be able to control total sound volume.
291            for (left, right) in buf {
292                *left *= global_gain;
293                *right *= global_gain;
294            }
295        }
296
297        self.render_duration = rg3d_core::instant::Instant::now() - last_time;
298    }
299}
300
301impl SoundContext {
302    /// TODO: This is magic constant that gives 1024 + 1 number when summed with
303    ///       HRTF length for faster FFT calculations. Find a better way of selecting this.
304    pub const HRTF_BLOCK_LEN: usize = 513;
305
306    pub(in crate) const HRTF_INTERPOLATION_STEPS: usize = 8;
307
308    pub(in crate) const SAMPLES_PER_CHANNEL: usize =
309        Self::HRTF_BLOCK_LEN * Self::HRTF_INTERPOLATION_STEPS;
310
311    /// Creates new instance of context. Internally context starts new thread which will call render all
312    /// sound source and send samples to default output device. This method returns Arc<Mutex<Context>>
313    /// because separate thread also uses context.
314    pub fn new() -> Self {
315        Self {
316            state: Some(Arc::new(Mutex::new(State {
317                sources: Pool::new(),
318                listener: Listener::new(),
319                master_gain: 1.0,
320                render_duration: Default::default(),
321                renderer: Renderer::Default,
322                effects: Pool::new(),
323                distance_model: DistanceModel::InverseDistance,
324                paused: false,
325            }))),
326        }
327    }
328
329    /// Returns internal state of the context.
330    ///
331    /// ## Deadlocks
332    ///
333    /// This method internally locks a mutex, so if you'll try to do something like this:
334    ///
335    /// ```no_run
336    /// # use rg3d_sound::context::SoundContext;
337    /// # let ctx = SoundContext::new();
338    /// let state = ctx.state();
339    /// // Do something
340    /// // ...
341    /// ctx.state(); // This will cause a deadlock.
342    /// ```
343    ///
344    /// You'll get a deadlock, so general rule here is to not store result of this method
345    /// anywhere.
346    pub fn state(&self) -> MutexGuard<'_, State> {
347        self.state.as_ref().unwrap().lock().unwrap()
348    }
349
350    /// Creates deep copy instead of shallow which is done by clone().
351    pub fn deep_clone(&self) -> SoundContext {
352        SoundContext {
353            state: Some(Arc::new(Mutex::new(self.state().clone()))),
354        }
355    }
356
357    /// Returns true if context is corrupted.
358    pub fn is_invalid(&self) -> bool {
359        self.state.is_none()
360    }
361}
362
363impl Visit for SoundContext {
364    fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
365        visitor.enter_region(name)?;
366
367        self.state.visit("State", visitor)?;
368
369        visitor.leave_region()
370    }
371}
372
373impl Visit for State {
374    fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
375        visitor.enter_region(name)?;
376
377        if visitor.is_reading() {
378            self.sources.clear();
379            self.effects.clear();
380            self.renderer = Renderer::Default;
381        }
382
383        self.master_gain.visit("MasterGain", visitor)?;
384        self.listener.visit("Listener", visitor)?;
385        self.sources.visit("Sources", visitor)?;
386        self.effects.visit("Effects", visitor)?;
387        self.renderer.visit("Renderer", visitor)?;
388        let _ = self.paused.visit("Paused", visitor);
389
390        let mut distance_model = self.distance_model as u32;
391        distance_model.visit("DistanceModel", visitor)?;
392        if visitor.is_reading() {
393            self.distance_model = match distance_model {
394                0 => DistanceModel::None,
395                1 => DistanceModel::InverseDistance,
396                2 => DistanceModel::LinearDistance,
397                3 => DistanceModel::ExponentDistance,
398                _ => {
399                    return VisitResult::Err(VisitError::User(format!(
400                        "Invalid distance model id {}!",
401                        distance_model
402                    )))
403                }
404            }
405        }
406
407        visitor.leave_region()
408    }
409}