rg3d_sound/
engine.rs

1//! Sound engine module
2//!
3//! ## Overview
4//!
5//! Sound engine manages contexts, feeds output device with data.
6
7use crate::{context::SoundContext, device};
8use rg3d_core::visitor::{Visit, VisitResult, Visitor};
9use std::sync::{Arc, Mutex};
10
11/// Internal state of sound engine.
12#[derive(Default)]
13pub struct SoundEngine {
14    contexts: Vec<SoundContext>,
15    master_gain: f32,
16}
17
18impl SoundEngine {
19    /// Creates new instance of a sound engine. It is possible to have multiple engine running at
20    /// the same time, but you shouldn't do this because you can create multiple contexts which
21    /// should cover 99% of use cases.
22    pub fn new() -> Arc<Mutex<Self>> {
23        let engine = Arc::new(Mutex::new(Self {
24            contexts: Default::default(),
25            master_gain: 1.0,
26        }));
27
28        // Run the default output device. Internally it creates separate thread, so we have
29        // to share sound engine instance with it, this is the only reason why it is wrapped
30        // in Arc<Mutex<>>
31        device::run_device(4 * SoundContext::SAMPLES_PER_CHANNEL as u32, {
32            let state = engine.clone();
33            move |buf| {
34                if let Ok(mut state) = state.lock() {
35                    state.render_inner(buf);
36                }
37            }
38        });
39
40        engine
41    }
42
43    /// Creates new instance of a sound engine without running a device thread. The user must
44    /// periodically run [`Self::render`].
45    pub fn without_device() -> Arc<Mutex<Self>> {
46        Arc::new(Mutex::new(Self {
47            contexts: Default::default(),
48            master_gain: 1.0,
49        }))
50    }
51
52    /// Adds new context to the engine. Each context must be added to the engine to emit
53    /// sounds.
54    pub fn add_context(&mut self, context: SoundContext) {
55        self.contexts.push(context);
56    }
57
58    /// Removes a context from the engine. Removed context will no longer produce any sound.
59    pub fn remove_context(&mut self, context: SoundContext) {
60        if let Some(position) = self.contexts.iter().position(|c| c == &context) {
61            self.contexts.remove(position);
62        }
63    }
64
65    /// Checks if a context is registered in the engine.
66    pub fn has_context(&self, context: &SoundContext) -> bool {
67        self.contexts
68            .iter()
69            .any(|c| Arc::ptr_eq(c.state.as_ref().unwrap(), context.state.as_ref().unwrap()))
70    }
71
72    /// Returns a reference to context container.
73    pub fn contexts(&self) -> &[SoundContext] {
74        &self.contexts
75    }
76
77    /// Set global sound volume in [0; 1] range.
78    pub fn set_master_gain(&mut self, master_gain: f32) {
79        self.master_gain = master_gain;
80    }
81
82    /// Returns global sound volume in [0; 1] range.
83    pub fn master_gain(&self) -> f32 {
84        self.master_gain
85    }
86
87    /// Returns the length of buf to be passed to [`Self::render()`].
88    pub fn render_buffer_len() -> usize {
89        SoundContext::SAMPLES_PER_CHANNEL
90    }
91
92    /// Renders the sound into buf. The buf must have at least [`Self::render_buffer_len()`]
93    /// elements. This method must be used if and only if the engine was created via
94    /// [`Self::without_device`].
95    ///
96    /// ## Deadlocks
97    ///
98    /// This method internally locks added sound contexts so it must be called when all the contexts
99    /// are unlocked or you'll get a deadlock.
100    pub fn render(&mut self, buf: &mut [(f32, f32)]) {
101        buf.fill((0.0, 0.0));
102        self.render_inner(buf);
103    }
104
105    fn render_inner(&mut self, buf: &mut [(f32, f32)]) {
106        let master_gain = self.master_gain;
107        for context in self.contexts.iter_mut() {
108            context.state().render(master_gain, buf);
109        }
110    }
111}
112
113impl Visit for SoundEngine {
114    fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
115        visitor.enter_region(name)?;
116
117        if visitor.is_reading() {
118            self.contexts.clear();
119        }
120
121        self.master_gain.visit("MasterGain", visitor)?;
122        self.contexts.visit("Contexts", visitor)?;
123
124        visitor.leave_region()
125    }
126}