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::{
41 pool::{Handle, Pool},
42 reflect::prelude::*,
43 uuid_provider,
44 visitor::prelude::*,
45 SafeLock,
46};
47use std::{
48 sync::{Arc, Mutex, MutexGuard},
49 time::Duration,
50};
51use strum_macros::{AsRefStr, EnumString, VariantNames};
52
53/// Sample rate for output device.
54/// TODO: Make this configurable, for now its set to most commonly used sample rate of 44100 Hz.
55pub const SAMPLE_RATE: u32 = 44100;
56
57/// Distance model defines how volume of sound will decay when distance to listener changes.
58#[derive(Copy, Clone, Debug, Eq, PartialEq, Reflect, Visit, AsRefStr, EnumString, VariantNames)]
59#[repr(u32)]
60pub enum DistanceModel {
61 /// No distance attenuation at all.
62 None = 0,
63
64 /// Distance will decay using following formula:
65 ///
66 /// `clamped_distance = min(max(distance, radius), max_distance)`
67 /// `attenuation = radius / (radius + rolloff_factor * (clamped_distance - radius))`
68 ///
69 /// where - `radius` - of source at which it has maximum volume,
70 /// `max_distance` - distance at which decay will stop,
71 /// `rolloff_factor` - coefficient that defines how fast volume will decay
72 ///
73 /// # Notes
74 ///
75 /// This is default distance model of context.
76 InverseDistance = 1,
77
78 /// Distance will decay using following formula:
79 ///
80 /// `clamped_distance = min(max(distance, radius), max_distance)`
81 /// `attenuation = 1.0 - radius * (clamped_distance - radius) / (max_distance - radius)`
82 ///
83 /// where - `radius` - of source at which it has maximum volume,
84 /// `max_distance` - distance at which decay will stop
85 ///
86 /// # Notes
87 ///
88 /// As you can see `rolloff_factor` is ignored here because of linear law.
89 LinearDistance = 2,
90
91 /// Distance will decay using following formula:
92 ///
93 /// `clamped_distance = min(max(distance, radius), max_distance)`
94 /// `(clamped_distance / radius) ^ (-rolloff_factor)`
95 ///
96 /// where - `radius` - of source at which it has maximum volume,
97 /// `max_distance` - distance at which decay will stop,
98 /// `rolloff_factor` - coefficient that defines how fast volume will decay
99 ExponentDistance = 3,
100}
101
102uuid_provider!(DistanceModel = "957f3b00-3f89-438c-b1b7-e841e8d75ba9");
103
104impl Default for DistanceModel {
105 fn default() -> Self {
106 Self::InverseDistance
107 }
108}
109
110/// See module docs.
111#[derive(Clone, Default, Debug, Visit)]
112pub struct SoundContext {
113 pub(crate) state: Option<Arc<Mutex<State>>>,
114}
115
116impl PartialEq for SoundContext {
117 fn eq(&self, other: &Self) -> bool {
118 Arc::ptr_eq(self.state.as_ref().unwrap(), other.state.as_ref().unwrap())
119 }
120}
121
122/// A set of flags, that can be used to define what should be skipped during the
123/// serialization of a sound context.
124#[derive(Default, Debug, Clone)]
125pub struct SerializationOptions {
126 /// All sources won't be serialized, if set.
127 pub skip_sources: bool,
128 /// Bus graph won't be serialized, if set.
129 pub skip_bus_graph: bool,
130}
131
132/// Internal state of context.
133#[derive(Default, Debug, Clone, Reflect)]
134pub struct State {
135 sources: Pool<SoundSource>,
136 listener: Listener,
137 render_duration: Duration,
138 renderer: Renderer,
139 bus_graph: AudioBusGraph,
140 distance_model: DistanceModel,
141 paused: bool,
142 /// A set of flags, that can be used to define what should be skipped during the
143 /// serialization of a sound context.
144 #[reflect(hidden)]
145 pub serialization_options: SerializationOptions,
146}
147
148impl State {
149 /// Extracts a source from the context and reserves its handle. It is used to temporarily take
150 /// ownership over source, and then put node back using given ticket.
151 pub fn take_reserve(
152 &mut self,
153 handle: Handle<SoundSource>,
154 ) -> (Ticket<SoundSource>, SoundSource) {
155 self.sources.take_reserve(handle)
156 }
157
158 /// Puts source back by given ticket.
159 pub fn put_back(
160 &mut self,
161 ticket: Ticket<SoundSource>,
162 node: SoundSource,
163 ) -> Handle<SoundSource> {
164 self.sources.put_back(ticket, node)
165 }
166
167 /// Makes source handle vacant again.
168 pub fn forget_ticket(&mut self, ticket: Ticket<SoundSource>) {
169 self.sources.forget_ticket(ticket)
170 }
171
172 /// Pause/unpause the sound context. Paused context won't play any sounds.
173 pub fn pause(&mut self, pause: bool) {
174 self.paused = pause;
175 }
176
177 /// Returns true if the sound context is paused, false - otherwise.
178 pub fn is_paused(&self) -> bool {
179 self.paused
180 }
181
182 /// Sets new distance model.
183 pub fn set_distance_model(&mut self, distance_model: DistanceModel) {
184 self.distance_model = distance_model;
185 }
186
187 /// Returns current distance model.
188 pub fn distance_model(&self) -> DistanceModel {
189 self.distance_model
190 }
191
192 /// Normalizes given frequency using context's sampling rate. Normalized frequency then can be used
193 /// to create filters.
194 pub fn normalize_frequency(&self, f: f32) -> f32 {
195 f / SAMPLE_RATE as f32
196 }
197
198 /// Returns amount of time context spent on rendering all sound sources.
199 pub fn full_render_duration(&self) -> Duration {
200 self.render_duration
201 }
202
203 /// Sets new renderer.
204 pub fn set_renderer(&mut self, renderer: Renderer) -> Renderer {
205 std::mem::replace(&mut self.renderer, renderer)
206 }
207
208 /// Returns shared reference to current renderer.
209 pub fn renderer(&self) -> &Renderer {
210 &self.renderer
211 }
212
213 /// Returns mutable reference to current renderer.
214 pub fn renderer_mut(&mut self) -> &mut Renderer {
215 &mut self.renderer
216 }
217
218 /// Adds new sound source and returns handle of it by which it can be accessed later on.
219 pub fn add_source(&mut self, source: SoundSource) -> Handle<SoundSource> {
220 self.sources.spawn(source)
221 }
222
223 /// Removes sound source from the context.
224 pub fn remove_source(&mut self, source: Handle<SoundSource>) {
225 self.sources.free(source);
226 }
227
228 /// Returns shared reference to a pool with all sound sources.
229 pub fn sources(&self) -> &Pool<SoundSource> {
230 &self.sources
231 }
232
233 /// Returns mutable reference to a pool with all sound sources.
234 pub fn sources_mut(&mut self) -> &mut Pool<SoundSource> {
235 &mut self.sources
236 }
237
238 /// Returns shared reference to sound source at given handle. If handle is invalid, this method will panic.
239 pub fn source(&self, handle: Handle<SoundSource>) -> &SoundSource {
240 self.sources.borrow(handle)
241 }
242
243 /// Checks whether a handle to a sound source is valid or not.
244 pub fn is_valid_handle(&self, handle: Handle<SoundSource>) -> bool {
245 self.sources.is_valid_handle(handle)
246 }
247
248 /// Returns mutable reference to sound source at given handle. If handle is invalid, this method will panic.
249 pub fn source_mut(&mut self, handle: Handle<SoundSource>) -> &mut SoundSource {
250 self.sources.borrow_mut(handle)
251 }
252
253 /// Returns mutable reference to sound source at given handle. If handle is invalid, this method will panic.
254 pub fn try_get_source_mut(&mut self, handle: Handle<SoundSource>) -> Option<&mut SoundSource> {
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}