Skip to main content

veloren_voxygen/audio/
channel.rs

1//! Distinct audio playback channels for music and sound effects
2//!
3//! Voxygen's audio system uses a limited number of channels to play multiple
4//! sounds simultaneously. Each additional channel used decreases performance
5//! in-game, so the amount of channels utilized should be kept to a minimum.
6//!
7//! When constructing a new [`AudioFrontend`](../struct.AudioFrontend.html), the
8//! number of sfx channels are determined by the `num_sfx_channels` value
9//! defined in the client
10//! [`AudioSettings`](../../settings/struct.AudioSettings.html)
11
12use kira::{
13    Easing, StartTime, Tween,
14    clock::ClockTime,
15    sound::PlaybackState,
16    track::{SpatialTrackHandle, TrackBuilder, TrackHandle},
17};
18use serde::Deserialize;
19use std::time::Duration;
20use strum::EnumIter;
21use tracing::warn;
22use vek::*;
23
24use crate::audio;
25
26use super::soundcache::{AnySoundData, AnySoundHandle};
27
28/// We watch the states of nearby entities in order to emit SFX at their
29/// position based on their state. This constant limits the radius that we
30/// observe to prevent tracking distant entities. It approximates the distance
31/// at which the volume of the sfx emitted is too quiet to be meaningful for the
32/// player.
33pub const SFX_DIST_LIMIT: f32 = 256.0;
34pub const SFX_DIST_LIMIT_SQR: f32 = SFX_DIST_LIMIT * SFX_DIST_LIMIT;
35
36pub fn calculate_player_attenuation(player_pos: Vec3<f32>, emitter_pos: Vec3<f32>) -> f32 {
37    1.0 - (player_pos.distance(emitter_pos) * (1.0 / SFX_DIST_LIMIT))
38        .clamp(0.0, 1.0)
39        .powf(1.0 / 2.0)
40}
41
42/// Each `MusicChannel` has a `MusicChannelTag` which help us determine when we
43/// should transition between two types of in-game music. For example, we
44/// transition between `TitleMusic` and `Exploration` when a player enters the
45/// world by crossfading over a slow duration. In the future, transitions in the
46/// world such as `Exploration` -> `BossBattle` would transition more rapidly.
47#[derive(PartialEq, Clone, Copy, Hash, Eq, Deserialize, Debug)]
48pub enum MusicChannelTag {
49    TitleMusic,
50    Exploration,
51    Combat,
52}
53
54/// A MusicChannel is designed to play music which
55/// is always heard at the player's position.
56pub struct MusicChannel {
57    tag: MusicChannelTag,
58    track: TrackHandle,
59    source: Option<AnySoundHandle>,
60    length: f32,
61    loop_data: (bool, LoopPoint, LoopPoint), // Loops?, Start, End
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub enum LoopPoint {
66    Start,
67    End,
68    Point(f64),
69}
70
71impl MusicChannel {
72    pub fn new(route_to: &mut TrackHandle) -> Result<Self, kira::ResourceLimitReached> {
73        let track = route_to.add_sub_track(TrackBuilder::new().volume(audio::to_decibels(0.0)))?;
74        Ok(Self {
75            tag: MusicChannelTag::TitleMusic,
76            track,
77            source: None,
78            length: 0.0,
79            loop_data: (false, LoopPoint::Start, LoopPoint::End),
80        })
81    }
82
83    pub fn set_tag(&mut self, tag: MusicChannelTag) { self.tag = tag; }
84
85    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
86        self.source = source_handle;
87    }
88
89    pub fn set_length(&mut self, length: f32) { self.length = length; }
90
91    // Gets the currently set loop data
92    pub fn get_loop_data(&self) -> (bool, LoopPoint, LoopPoint) { self.loop_data }
93
94    /// Sets whether the sound loops, and the start and end points of the loop
95    pub fn set_loop_data(&mut self, loops: bool, start: LoopPoint, end: LoopPoint) {
96        if let Some(source) = self.source.as_mut() {
97            self.loop_data = (loops, start, end);
98            if loops {
99                match (start, end) {
100                    (LoopPoint::Start, LoopPoint::End) => {
101                        source.set_loop_region(0.0..);
102                    },
103                    (LoopPoint::Start, LoopPoint::Point(end)) => {
104                        source.set_loop_region(..end);
105                    },
106                    (LoopPoint::Point(start), LoopPoint::End) => {
107                        source.set_loop_region(start..);
108                    },
109                    (LoopPoint::Point(start), LoopPoint::Point(end)) => {
110                        source.set_loop_region(start..end);
111                    },
112                    _ => {
113                        warn!("Invalid loop points given")
114                    },
115                }
116            } else {
117                source.set_loop_region(None);
118            }
119        }
120    }
121
122    pub fn play(
123        &mut self,
124        mut source: AnySoundData,
125        now: ClockTime,
126        fade_in: Option<f32>,
127        delay: Option<f32>,
128    ) {
129        if let Some(fade_in) = fade_in {
130            let fade_in_tween = Tween {
131                duration: Duration::from_secs_f32(fade_in),
132                ..Default::default()
133            };
134            source = source.fade_in_tween(fade_in_tween);
135        }
136
137        if let Some(delay) = delay {
138            source = source.start_time(now + delay as f64);
139        }
140
141        match self.track.play(source) {
142            Ok(handle) => self.source = Some(handle),
143            Err(e) => {
144                warn!(?e, "Cannot play music")
145            },
146        }
147    }
148
149    /// Stop whatever is playing on this channel with an optional fadeout and
150    /// delay
151    pub fn stop(&mut self, duration: Option<f32>, delay: Option<f32>) {
152        if let Some(source) = self.source.as_mut() {
153            let tween = Tween {
154                duration: Duration::from_secs_f32(duration.unwrap_or(0.1)),
155                start_time: StartTime::Delayed(Duration::from_secs_f32(delay.unwrap_or(0.0))),
156                ..Default::default()
157            };
158            source.stop(tween)
159        };
160    }
161
162    /// Set the volume of the current channel.
163    pub fn set_volume(&mut self, volume: f32) {
164        self.track
165            .set_volume(audio::to_decibels(volume), Tween::default());
166    }
167
168    /// Fade to a given amplitude over a given duration, optionally after a
169    /// delay
170    pub fn fade_to(&mut self, volume: f32, duration: f32, delay: Option<f32>) {
171        let mut start_time = StartTime::Immediate;
172        if let Some(delay) = delay {
173            start_time = StartTime::Delayed(Duration::from_secs_f32(delay))
174        }
175        let tween = Tween {
176            start_time,
177            duration: Duration::from_secs_f32(duration),
178            easing: Easing::Linear,
179        };
180        self.track.set_volume(audio::to_decibels(volume), tween);
181    }
182
183    /// Fade to silence over a given duration and stop, optionally after a delay
184    /// Use fade_to() if this fade is temporary
185    pub fn fade_out(&mut self, duration: f32, delay: Option<f32>) {
186        self.stop(Some(duration), delay);
187    }
188
189    /// Returns true if the sound has stopped playing (whether by fading out or
190    /// by finishing)
191    pub fn is_done(&self) -> bool {
192        self.source
193            .as_ref()
194            .is_none_or(|source| source.state() == PlaybackState::Stopped)
195    }
196
197    pub fn get_tag(&self) -> MusicChannelTag { self.tag }
198
199    /// Get a mutable reference to the channel's track
200    pub fn get_track(&mut self) -> &mut TrackHandle { &mut self.track }
201
202    pub fn get_source(&mut self) -> Option<&mut AnySoundHandle> { self.source.as_mut() }
203
204    pub fn get_length(&self) -> f32 { self.length }
205}
206
207/// AmbienceChannelTags are used for non-positional sfx.
208#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, EnumIter)]
209pub enum AmbienceChannelTag {
210    Wind,
211    Rain,
212    ThunderRumbling,
213    Leaves,
214    Cave,
215    Thunder,
216    RiverLoud,
217    RiverQuiet,
218}
219
220/// An AmbienceChannel uses a non-positional audio sink designed to play sounds
221/// which are always heard at the camera's position.
222#[derive(Debug)]
223pub struct AmbienceChannel {
224    tag: AmbienceChannelTag,
225    target_volume: f32,
226    track: TrackHandle,
227    source: Option<AnySoundHandle>,
228    pub looping: bool,
229}
230
231impl AmbienceChannel {
232    pub fn new(
233        tag: AmbienceChannelTag,
234        init_volume: f32,
235        route_to: &mut TrackHandle,
236        looping: bool,
237    ) -> Result<Self, kira::ResourceLimitReached> {
238        let ambience_track_builder = TrackBuilder::new();
239        let track =
240            route_to.add_sub_track(ambience_track_builder.volume(audio::to_decibels(0.0)))?;
241
242        Ok(Self {
243            tag,
244            target_volume: init_volume,
245            track,
246            source: None,
247            looping,
248        })
249    }
250
251    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
252        self.source = source_handle;
253    }
254
255    pub fn play(&mut self, mut source: AnySoundData, fade_in: Option<f32>, delay: Option<f32>) {
256        let mut tween = Tween::default();
257        if let Some(fade_in) = fade_in {
258            tween.duration = Duration::from_secs_f32(fade_in);
259        }
260        if let Some(delay) = delay {
261            tween.start_time = StartTime::Delayed(Duration::from_secs_f32(delay));
262        }
263        source = source.fade_in_tween(tween);
264        match self.track.play(source) {
265            Ok(handle) => self.source = Some(handle),
266            Err(e) => {
267                warn!(?e, "Cannot play ambience")
268            },
269        }
270    }
271
272    /// Stop whatever is playing on this channel with an optional fadeout and
273    /// delay
274    pub fn stop(&mut self, duration: Option<f32>, delay: Option<f32>) {
275        if let Some(source) = self.source.as_mut() {
276            let tween = Tween {
277                duration: Duration::from_secs_f32(duration.unwrap_or(0.1)),
278                start_time: StartTime::Delayed(Duration::from_secs_f32(delay.unwrap_or(0.0))),
279                ..Default::default()
280            };
281            source.stop(tween)
282        }
283    }
284
285    /// Set the channel to a volume, fading over a given duration
286    pub fn fade_to(&mut self, volume: f32, duration: f32) {
287        self.track.set_volume(audio::to_decibels(volume), Tween {
288            start_time: StartTime::Immediate,
289            duration: Duration::from_secs_f32(duration),
290            easing: Easing::Linear,
291        });
292        self.target_volume = volume;
293    }
294
295    pub fn get_source(&mut self) -> Option<&mut AnySoundHandle> { self.source.as_mut() }
296
297    /// Get an immutable reference to the channel's track for purposes of
298    /// setting the output destination of a sound
299    pub fn get_track(&self) -> &TrackHandle { &self.track }
300
301    /// Get a mutable reference to the channel's track
302    pub fn get_track_mut(&mut self) -> &mut TrackHandle { &mut self.track }
303
304    /// Get the volume of this channel. The volume may be in the process of
305    /// being faded to.
306    pub fn get_target_volume(&self) -> f32 { self.target_volume }
307
308    pub fn get_tag(&self) -> AmbienceChannelTag { self.tag }
309
310    pub fn set_tag(&mut self, tag: AmbienceChannelTag) { self.tag = tag }
311
312    pub fn is_active(&self) -> bool { self.get_target_volume() == 0.0 }
313
314    pub fn is_stopped(&self) -> bool {
315        if let Some(source) = self.source.as_ref() {
316            source.state() == PlaybackState::Stopped
317        } else {
318            false
319        }
320    }
321}
322
323/// An SfxChannel uses a positional audio sink, and is designed for short-lived
324/// audio which can be spatially controlled, but does not need control over
325/// playback or fading/transitions
326///
327/// Note: currently, emitters are static once spawned
328#[derive(Debug)]
329pub struct SfxChannel {
330    track: Option<SpatialTrackHandle>,
331    source: Option<AnySoundHandle>,
332    source_initial_volume: f32,
333    pos: Vec3<f32>,
334    // Increments every time we play a distinct sound through this channel
335    pub play_counter: usize,
336}
337
338impl SfxChannel {
339    pub fn new() -> Self {
340        Self {
341            track: None,
342            source: None,
343            source_initial_volume: 0.0,
344            pos: Vec3::zero(),
345            play_counter: 0,
346        }
347    }
348
349    pub fn drop_track(&mut self) { self.track = None; }
350
351    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
352        self.source = source_handle;
353    }
354
355    /// Sets the volume of the source, not the track. This is only used to
356    /// change the volume of a sound based on the player's distance from its
357    /// emitter.
358    pub fn set_source_volume(&mut self, volume: f32) {
359        let tween = Tween {
360            duration: Duration::from_secs_f32(0.0),
361            ..Default::default()
362        };
363        if let Some(source) = self.source.as_mut() {
364            source.set_volume(audio::to_decibels(volume), tween);
365        }
366    }
367
368    pub fn play(
369        &mut self,
370        source: AnySoundData,
371        volume: f32,
372        mut track: SpatialTrackHandle,
373    ) -> usize {
374        match track.play(source) {
375            Ok(handle) => {
376                self.source = Some(handle);
377                self.source_initial_volume = volume
378            },
379            Err(e) => {
380                warn!(?e, "Cannot play sfx")
381            },
382        }
383        self.track = Some(track);
384        self.play_counter += 1;
385        self.play_counter
386    }
387
388    pub fn stop(&mut self) {
389        if let Some(source) = self.source.as_mut() {
390            source.stop(Tween::default())
391        }
392    }
393
394    /// Sets volume of the track.
395    pub fn set_volume(&mut self, volume: f32, duration: Option<f32>) {
396        if let Some(track) = self.track.as_mut() {
397            let tween = Tween {
398                duration: Duration::from_secs_f32(duration.unwrap_or(0.0)),
399                ..Default::default()
400            };
401            track.set_volume(audio::to_decibels(volume), tween)
402        }
403    }
404
405    pub fn set_pos(&mut self, pos: Vec3<f32>) { self.pos = pos; }
406
407    pub fn is_done(&self) -> bool {
408        self.source
409            .as_ref()
410            .is_none_or(|source| source.state() == PlaybackState::Stopped)
411    }
412
413    /// Update volume of sounds based on position of player
414    pub fn update(&mut self, player_pos: Vec3<f32>) {
415        if let Some(track) = self.track.as_mut() {
416            let tween = Tween {
417                duration: Duration::from_secs_f32(0.0),
418                ..Default::default()
419            };
420            track.set_position(self.pos, tween);
421        }
422
423        // A multiplier between 0.0 and 1.0, with 0.0 being the furthest away from and
424        // 1.0 being closest to the player.
425        let ratio = calculate_player_attenuation(player_pos, self.pos);
426        self.set_source_volume(self.source_initial_volume * 5.0 * ratio);
427    }
428}
429
430impl Default for SfxChannel {
431    fn default() -> Self { Self::new() }
432}
433
434#[derive(Eq, PartialEq, Copy, Clone, Debug)]
435pub enum UiChannelTag {
436    LevelUp,
437}
438
439/// An UiChannel uses a non-spatial audio sink, and is designed for short-lived
440/// audio which is not spatially controlled, but does not need control over
441/// playback or fading/transitions
442pub struct UiChannel {
443    track: TrackHandle,
444    source: Option<AnySoundHandle>,
445    pub tag: Option<UiChannelTag>,
446}
447
448impl UiChannel {
449    pub fn new(route_to: &mut TrackHandle) -> Result<Self, kira::ResourceLimitReached> {
450        let track = route_to.add_sub_track(TrackBuilder::default())?;
451        Ok(Self {
452            track,
453            source: None,
454            tag: None,
455        })
456    }
457
458    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
459        self.source = source_handle;
460    }
461
462    pub fn play(&mut self, source: AnySoundData, tag: Option<UiChannelTag>) {
463        match self.track.play(source) {
464            Ok(handle) => {
465                self.source = Some(handle);
466                self.tag = tag;
467            },
468            Err(e) => {
469                warn!(?e, "Cannot play ui sfx")
470            },
471        }
472    }
473
474    pub fn stop(&mut self) {
475        if let Some(source) = self.source.as_mut() {
476            source.stop(Tween::default())
477        }
478    }
479
480    pub fn set_volume(&mut self, volume: f32) {
481        self.track
482            .set_volume(audio::to_decibels(volume), Tween::default())
483    }
484
485    pub fn is_done(&self) -> bool {
486        self.source
487            .as_ref()
488            .is_none_or(|source| source.state() == PlaybackState::Stopped)
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use crate::audio::channel::{SFX_DIST_LIMIT, SFX_DIST_LIMIT_SQR};
495
496    #[test]
497    // Small optimization so sqrt() isn't called at runtime
498    fn test_sfx_dist_limit_eq_sfx_dist_limit_sqr() {
499        assert!(SFX_DIST_LIMIT.powf(2.0) == SFX_DIST_LIMIT_SQR)
500    }
501}