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 = 200.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        .sqrt()
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. Currently the only use
208/// is for wind.
209#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, EnumIter)]
210pub enum AmbienceChannelTag {
211    Wind,
212    Rain,
213    ThunderRumbling,
214    Leaves,
215    Cave,
216    Thunder,
217}
218
219/// An AmbienceChannel uses a non-positional audio sink designed to play sounds
220/// which are always heard at the camera's position.
221#[derive(Debug)]
222pub struct AmbienceChannel {
223    tag: AmbienceChannelTag,
224    target_volume: f32,
225    track: TrackHandle,
226    source: Option<AnySoundHandle>,
227    pub looping: bool,
228}
229
230impl AmbienceChannel {
231    pub fn new(
232        tag: AmbienceChannelTag,
233        init_volume: f32,
234        route_to: &mut TrackHandle,
235        looping: bool,
236    ) -> Result<Self, kira::ResourceLimitReached> {
237        let ambience_track_builder = TrackBuilder::new();
238        let track =
239            route_to.add_sub_track(ambience_track_builder.volume(audio::to_decibels(0.0)))?;
240
241        Ok(Self {
242            tag,
243            target_volume: init_volume,
244            track,
245            source: None,
246            looping,
247        })
248    }
249
250    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
251        self.source = source_handle;
252    }
253
254    pub fn play(&mut self, mut source: AnySoundData, fade_in: Option<f32>, delay: Option<f32>) {
255        let mut tween = Tween::default();
256        if let Some(fade_in) = fade_in {
257            tween.duration = Duration::from_secs_f32(fade_in);
258        }
259        if let Some(delay) = delay {
260            tween.start_time = StartTime::Delayed(Duration::from_secs_f32(delay));
261        }
262        source = source.fade_in_tween(tween);
263        match self.track.play(source) {
264            Ok(handle) => self.source = Some(handle),
265            Err(e) => {
266                warn!(?e, "Cannot play ambience")
267            },
268        }
269    }
270
271    /// Stop whatever is playing on this channel with an optional fadeout and
272    /// delay
273    pub fn stop(&mut self, duration: Option<f32>, delay: Option<f32>) {
274        if let Some(source) = self.source.as_mut() {
275            let tween = Tween {
276                duration: Duration::from_secs_f32(duration.unwrap_or(0.1)),
277                start_time: StartTime::Delayed(Duration::from_secs_f32(delay.unwrap_or(0.0))),
278                ..Default::default()
279            };
280            source.stop(tween)
281        }
282    }
283
284    /// Set the channel to a volume, fading over a given duration
285    pub fn fade_to(&mut self, volume: f32, duration: f32) {
286        self.track.set_volume(audio::to_decibels(volume), Tween {
287            start_time: StartTime::Immediate,
288            duration: Duration::from_secs_f32(duration),
289            easing: Easing::Linear,
290        });
291        self.target_volume = volume;
292    }
293
294    pub fn get_source(&mut self) -> Option<&mut AnySoundHandle> { self.source.as_mut() }
295
296    /// Get an immutable reference to the channel's track for purposes of
297    /// setting the output destination of a sound
298    pub fn get_track(&self) -> &TrackHandle { &self.track }
299
300    /// Get a mutable reference to the channel's track
301    pub fn get_track_mut(&mut self) -> &mut TrackHandle { &mut self.track }
302
303    /// Get the volume of this channel. The volume may be in the process of
304    /// being faded to.
305    pub fn get_target_volume(&self) -> f32 { self.target_volume }
306
307    pub fn get_tag(&self) -> AmbienceChannelTag { self.tag }
308
309    pub fn set_tag(&mut self, tag: AmbienceChannelTag) { self.tag = tag }
310
311    pub fn is_active(&self) -> bool { self.get_target_volume() == 0.0 }
312
313    pub fn is_stopped(&self) -> bool {
314        if let Some(source) = self.source.as_ref() {
315            source.state() == PlaybackState::Stopped
316        } else {
317            false
318        }
319    }
320}
321
322/// An SfxChannel uses a positional audio sink, and is designed for short-lived
323/// audio which can be spatially controlled, but does not need control over
324/// playback or fading/transitions
325///
326/// Note: currently, emitters are static once spawned
327#[derive(Debug)]
328pub struct SfxChannel {
329    track: Option<SpatialTrackHandle>,
330    source: Option<AnySoundHandle>,
331    source_initial_volume: f32,
332    pos: Vec3<f32>,
333    // Increments every time we play a distinct sound through this channel
334    pub play_counter: usize,
335}
336
337impl SfxChannel {
338    pub fn new() -> Self {
339        Self {
340            track: None,
341            source: None,
342            source_initial_volume: 0.0,
343            pos: Vec3::zero(),
344            play_counter: 0,
345        }
346    }
347
348    pub fn drop_track(&mut self) { self.track = None; }
349
350    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
351        self.source = source_handle;
352    }
353
354    /// Sets the volume of the source, not the track. This is only used to
355    /// change the volume of a sound based on the player's distance from its
356    /// emitter.
357    pub fn set_source_volume(&mut self, volume: f32) {
358        let tween = Tween {
359            duration: Duration::from_secs_f32(0.0),
360            ..Default::default()
361        };
362        if let Some(source) = self.source.as_mut() {
363            source.set_volume(audio::to_decibels(volume), tween);
364        }
365    }
366
367    pub fn play(
368        &mut self,
369        source: AnySoundData,
370        volume: f32,
371        mut track: SpatialTrackHandle,
372    ) -> usize {
373        match track.play(source) {
374            Ok(handle) => {
375                self.source = Some(handle);
376                self.source_initial_volume = volume
377            },
378            Err(e) => {
379                warn!(?e, "Cannot play sfx")
380            },
381        }
382        self.track = Some(track);
383        self.play_counter += 1;
384        self.play_counter
385    }
386
387    pub fn stop(&mut self) {
388        if let Some(source) = self.source.as_mut() {
389            source.stop(Tween::default())
390        }
391    }
392
393    /// Sets volume of the track.
394    pub fn set_volume(&mut self, volume: f32, duration: Option<f32>) {
395        if let Some(track) = self.track.as_mut() {
396            let tween = Tween {
397                duration: Duration::from_secs_f32(duration.unwrap_or(0.0)),
398                ..Default::default()
399            };
400            track.set_volume(audio::to_decibels(volume), tween)
401        }
402    }
403
404    pub fn set_pos(&mut self, pos: Vec3<f32>) { self.pos = pos; }
405
406    pub fn is_done(&self) -> bool {
407        self.source
408            .as_ref()
409            .is_none_or(|source| source.state() == PlaybackState::Stopped)
410    }
411
412    /// Update volume of sounds based on position of player
413    pub fn update(&mut self, player_pos: Vec3<f32>) {
414        if let Some(track) = self.track.as_mut() {
415            let tween = Tween {
416                duration: Duration::from_secs_f32(0.0),
417                ..Default::default()
418            };
419            track.set_position(self.pos, tween);
420        }
421
422        // A multiplier between 0.0 and 1.0, with 0.0 being the furthest away from and
423        // 1.0 being closest to the player.
424        let ratio = calculate_player_attenuation(player_pos, self.pos);
425        self.set_source_volume(self.source_initial_volume * 5.0 * ratio);
426    }
427}
428
429impl Default for SfxChannel {
430    fn default() -> Self { Self::new() }
431}
432
433#[derive(Eq, PartialEq, Copy, Clone, Debug)]
434pub enum UiChannelTag {
435    LevelUp,
436}
437
438/// An UiChannel uses a non-spatial audio sink, and is designed for short-lived
439/// audio which is not spatially controlled, but does not need control over
440/// playback or fading/transitions
441pub struct UiChannel {
442    track: TrackHandle,
443    source: Option<AnySoundHandle>,
444    pub tag: Option<UiChannelTag>,
445}
446
447impl UiChannel {
448    pub fn new(route_to: &mut TrackHandle) -> Result<Self, kira::ResourceLimitReached> {
449        let track = route_to.add_sub_track(TrackBuilder::default())?;
450        Ok(Self {
451            track,
452            source: None,
453            tag: None,
454        })
455    }
456
457    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
458        self.source = source_handle;
459    }
460
461    pub fn play(&mut self, source: AnySoundData, tag: Option<UiChannelTag>) {
462        match self.track.play(source) {
463            Ok(handle) => {
464                self.source = Some(handle);
465                self.tag = tag;
466            },
467            Err(e) => {
468                warn!(?e, "Cannot play ui sfx")
469            },
470        }
471    }
472
473    pub fn stop(&mut self) {
474        if let Some(source) = self.source.as_mut() {
475            source.stop(Tween::default())
476        }
477    }
478
479    pub fn set_volume(&mut self, volume: f32) {
480        self.track
481            .set_volume(audio::to_decibels(volume), Tween::default())
482    }
483
484    pub fn is_done(&self) -> bool {
485        self.source
486            .as_ref()
487            .is_none_or(|source| source.state() == PlaybackState::Stopped)
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use crate::audio::channel::{SFX_DIST_LIMIT, SFX_DIST_LIMIT_SQR};
494
495    #[test]
496    // Small optimization so sqrt() isn't called at runtime
497    fn test_sfx_dist_limit_eq_sfx_dist_limit_sqr() {
498        assert!(SFX_DIST_LIMIT.powf(2.0) == SFX_DIST_LIMIT_SQR)
499    }
500}