fyrox_sound/engine.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//! Sound engine module
22//!
23//! ## Overview
24//!
25//! Sound engine manages contexts, feeds output device with data.
26
27use crate::context::{SoundContext, SAMPLE_RATE};
28use fyrox_core::visitor::{Visit, VisitResult, Visitor};
29use std::error::Error;
30use std::sync::{Arc, Mutex, MutexGuard};
31
32/// Sound engine manages contexts, feeds output device with data. Sound engine instance can be cloned,
33/// however this is always a "shallow" clone, because actual sound engine data is wrapped in Arc.
34#[derive(Clone)]
35pub struct SoundEngine(Arc<Mutex<State>>);
36
37impl Default for SoundEngine {
38 fn default() -> Self {
39 Self::without_device()
40 }
41}
42
43/// Internal state of the sound engine.
44pub struct State {
45 contexts: Vec<SoundContext>,
46 output_device: Option<tinyaudio::OutputDevice>,
47}
48
49impl SoundEngine {
50 /// Creates new instance of the sound engine. It is possible to have multiple engines running at
51 /// the same time, but you shouldn't do this because you can create multiple contexts which
52 /// should cover 99% of use cases.
53 pub fn new() -> Result<Self, Box<dyn Error>> {
54 let engine = Self::without_device();
55 engine.initialize_audio_output_device()?;
56 Ok(engine)
57 }
58
59 /// Creates new instance of a sound engine without OS audio output device (so called headless mode).
60 /// The user should periodically run [`State::render`] if they want to implement their own sample sending
61 /// method to an output device (or a file, etc.).
62 pub fn without_device() -> Self {
63 Self(Arc::new(Mutex::new(State {
64 contexts: Default::default(),
65 output_device: None,
66 })))
67 }
68
69 /// Tries to initialize default audio output device.
70 pub fn initialize_audio_output_device(&self) -> Result<(), Box<dyn Error>> {
71 let state = self.clone();
72
73 let device = tinyaudio::run_output_device(
74 tinyaudio::OutputDeviceParameters {
75 sample_rate: SAMPLE_RATE as usize,
76 channels_count: 2,
77 channel_sample_count: SoundContext::SAMPLES_PER_CHANNEL,
78 },
79 {
80 move |buf| {
81 // SAFETY: This is safe as long as channels count above is 2.
82 let data = unsafe {
83 std::slice::from_raw_parts_mut(
84 buf.as_mut_ptr() as *mut (f32, f32),
85 buf.len() / 2,
86 )
87 };
88
89 state.state().render(data);
90 }
91 },
92 )?;
93
94 self.state().output_device = Some(device);
95
96 Ok(())
97 }
98
99 /// Destroys current audio output device (if any).
100 pub fn destroy_audio_output_device(&self) {
101 self.state().output_device = None;
102 }
103
104 /// Provides direct access to actual engine data.
105 pub fn state(&self) -> MutexGuard<State> {
106 self.0.lock().unwrap()
107 }
108}
109
110impl State {
111 /// Adds new context to the engine. Each context must be added to the engine to emit
112 /// sounds.
113 pub fn add_context(&mut self, context: SoundContext) {
114 self.contexts.push(context);
115 }
116
117 /// Removes a context from the engine. Removed context will no longer produce any sound.
118 pub fn remove_context(&mut self, context: SoundContext) {
119 if let Some(position) = self.contexts.iter().position(|c| c == &context) {
120 self.contexts.remove(position);
121 }
122 }
123
124 /// Removes all contexts from the engine.
125 pub fn remove_all_contexts(&mut self) {
126 self.contexts.clear()
127 }
128
129 /// Checks if a context is registered in the engine.
130 pub fn has_context(&self, context: &SoundContext) -> bool {
131 self.contexts
132 .iter()
133 .any(|c| Arc::ptr_eq(c.state.as_ref().unwrap(), context.state.as_ref().unwrap()))
134 }
135
136 /// Returns a reference to context container.
137 pub fn contexts(&self) -> &[SoundContext] {
138 &self.contexts
139 }
140
141 /// Returns the length of buf to be passed to [`Self::render()`].
142 pub fn render_buffer_len() -> usize {
143 SoundContext::SAMPLES_PER_CHANNEL
144 }
145
146 /// Renders the sound into buf. The buf must have at least [`Self::render_buffer_len()`]
147 /// elements. This method must be used if and only if the engine was created via
148 /// [`SoundEngine::without_device`].
149 ///
150 /// ## Deadlocks
151 ///
152 /// This method internally locks added sound contexts so it must be called when all the contexts
153 /// are unlocked or you'll get a deadlock.
154 pub fn render(&mut self, buf: &mut [(f32, f32)]) {
155 buf.fill((0.0, 0.0));
156 self.render_inner(buf);
157 }
158
159 fn render_inner(&mut self, buf: &mut [(f32, f32)]) {
160 for context in self.contexts.iter_mut() {
161 context.state().render(buf);
162 }
163 }
164}
165
166impl Visit for State {
167 fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
168 if visitor.is_reading() {
169 self.contexts.clear();
170 }
171
172 let mut region = visitor.enter_region(name)?;
173
174 self.contexts.visit("Contexts", &mut region)?;
175
176 Ok(())
177 }
178}