rodio/spatial_sink.rs
1use std::f32;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use cpal::FromSample;
6
7use crate::source::{SeekError, Spatial};
8use crate::stream::{OutputStreamHandle, PlayError};
9use crate::{Sample, Sink, Source};
10
11/// A sink that allows changing the position of the source and the listeners
12/// ears while playing. The sources played are then transformed to give a simple
13/// spatial effect. See [`Spatial`] for details.
14pub struct SpatialSink {
15 sink: Sink,
16 positions: Arc<Mutex<SoundPositions>>,
17}
18
19struct SoundPositions {
20 emitter_position: [f32; 3],
21 left_ear: [f32; 3],
22 right_ear: [f32; 3],
23}
24
25impl SpatialSink {
26 /// Builds a new `SpatialSink`.
27 pub fn try_new(
28 stream: &OutputStreamHandle,
29 emitter_position: [f32; 3],
30 left_ear: [f32; 3],
31 right_ear: [f32; 3],
32 ) -> Result<SpatialSink, PlayError> {
33 Ok(SpatialSink {
34 sink: Sink::try_new(stream)?,
35 positions: Arc::new(Mutex::new(SoundPositions {
36 emitter_position,
37 left_ear,
38 right_ear,
39 })),
40 })
41 }
42
43 /// Sets the position of the sound emitter in 3 dimensional space.
44 pub fn set_emitter_position(&self, pos: [f32; 3]) {
45 self.positions.lock().unwrap().emitter_position = pos;
46 }
47
48 /// Sets the position of the left ear in 3 dimensional space.
49 pub fn set_left_ear_position(&self, pos: [f32; 3]) {
50 self.positions.lock().unwrap().left_ear = pos;
51 }
52
53 /// Sets the position of the right ear in 3 dimensional space.
54 pub fn set_right_ear_position(&self, pos: [f32; 3]) {
55 self.positions.lock().unwrap().right_ear = pos;
56 }
57
58 /// Appends a sound to the queue of sounds to play.
59 #[inline]
60 pub fn append<S>(&self, source: S)
61 where
62 S: Source + Send + 'static,
63 f32: FromSample<S::Item>,
64 S::Item: Sample + Send,
65 {
66 let positions = self.positions.clone();
67 let pos_lock = self.positions.lock().unwrap();
68 let source = Spatial::new(
69 source,
70 pos_lock.emitter_position,
71 pos_lock.left_ear,
72 pos_lock.right_ear,
73 )
74 .periodic_access(Duration::from_millis(10), move |i| {
75 let pos = positions.lock().unwrap();
76 i.set_positions(pos.emitter_position, pos.left_ear, pos.right_ear);
77 });
78 self.sink.append(source);
79 }
80
81 // Gets the volume of the sound.
82 ///
83 /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will
84 /// multiply each sample by this value.
85 #[inline]
86 pub fn volume(&self) -> f32 {
87 self.sink.volume()
88 }
89
90 /// Changes the volume of the sound.
91 ///
92 /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will
93 /// multiply each sample by this value.
94 #[inline]
95 pub fn set_volume(&self, value: f32) {
96 self.sink.set_volume(value);
97 }
98
99 /// Changes the play speed of the sound. Does not adjust the samples, only the playback speed.
100 ///
101 /// # Note:
102 /// 1. **Increasing the speed will increase the pitch by the same factor**
103 /// - If you set the speed to 0.5 this will halve the frequency of the sound
104 /// lowering its pitch.
105 /// - If you set the speed to 2 the frequency will double raising the
106 /// pitch of the sound.
107 /// 2. **Change in the speed affect the total duration inversely**
108 /// - If you set the speed to 0.5, the total duration will be twice as long.
109 /// - If you set the speed to 2 the total duration will be halve of what it
110 /// was.
111 ///
112 /// See [`Speed`] for details
113 #[inline]
114 pub fn speed(&self) -> f32 {
115 self.sink.speed()
116 }
117
118 /// Changes the speed of the sound.
119 ///
120 /// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0` will
121 /// change the play speed of the sound.
122 #[inline]
123 pub fn set_speed(&self, value: f32) {
124 self.sink.set_speed(value)
125 }
126
127 /// Resumes playback of a paused sound.
128 ///
129 /// No effect if not paused.
130 #[inline]
131 pub fn play(&self) {
132 self.sink.play();
133 }
134
135 /// Pauses playback of this sink.
136 ///
137 /// No effect if already paused.
138 ///
139 /// A paused sound can be resumed with `play()`.
140 pub fn pause(&self) {
141 self.sink.pause();
142 }
143
144 /// Gets if a sound is paused
145 ///
146 /// Sounds can be paused and resumed using pause() and play(). This gets if a sound is paused.
147 pub fn is_paused(&self) -> bool {
148 self.sink.is_paused()
149 }
150
151 /// Removes all currently loaded `Source`s from the `SpatialSink` and pauses it.
152 ///
153 /// See `pause()` for information about pausing a `Sink`.
154 #[inline]
155 pub fn clear(&self) {
156 self.sink.clear();
157 }
158
159 /// Stops the sink by emptying the queue.
160 #[inline]
161 pub fn stop(&self) {
162 self.sink.stop()
163 }
164
165 /// Destroys the sink without stopping the sounds that are still playing.
166 #[inline]
167 pub fn detach(self) {
168 self.sink.detach();
169 }
170
171 /// Sleeps the current thread until the sound ends.
172 #[inline]
173 pub fn sleep_until_end(&self) {
174 self.sink.sleep_until_end();
175 }
176
177 /// Returns true if this sink has no more sounds to play.
178 #[inline]
179 pub fn empty(&self) -> bool {
180 self.sink.empty()
181 }
182
183 /// Returns the number of sounds currently in the queue.
184 #[allow(clippy::len_without_is_empty)]
185 #[inline]
186 pub fn len(&self) -> usize {
187 self.sink.len()
188 }
189
190 /// Attempts to seek to a given position in the current source.
191 ///
192 /// This blocks between 0 and ~5 milliseconds.
193 ///
194 /// As long as the duration of the source is known seek is guaranteed to saturate
195 /// at the end of the source. For example given a source that reports a total duration
196 /// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to
197 /// 42 seconds.
198 ///
199 /// # Errors
200 /// This function will return [`SeekError::NotSupported`] if one of the underlying
201 /// sources does not support seeking.
202 ///
203 /// It will return an error if an implementation ran
204 /// into one during the seek.
205 ///
206 /// When seeking beyond the end of a source this
207 /// function might return an error if the duration of the source is not known.
208 pub fn try_seek(&self, pos: Duration) -> Result<(), SeekError> {
209 self.sink.try_seek(pos)
210 }
211
212 /// Returns the position of the sound that's being played.
213 ///
214 /// This takes into account any speedup or delay applied.
215 ///
216 /// Example: if you apply a speedup of *2* to an mp3 decoder source and
217 /// [`get_pos()`](Sink::get_pos) returns *5s* then the position in the mp3
218 /// recording is *10s* from its start.
219 #[inline]
220 pub fn get_pos(&self) -> Duration {
221 self.sink.get_pos()
222 }
223}