Skip to main content

fyrox_sound/
context.rs

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