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}