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::{f32, time::Duration};
20use strum::EnumIter;
21use tracing::warn;
22use vek::*;
23
24use crate::audio::{self, sfx::SfxTag};
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            true
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    tag: Option<SfxTag>,
335    distance_sq: f32,
336    // Increments every time we play a distinct sound through this channel
337    pub play_counter: usize,
338}
339
340impl SfxChannel {
341    pub fn new() -> Self {
342        Self {
343            track: None,
344            source: None,
345            source_initial_volume: 0.0,
346            pos: Vec3::zero(),
347            tag: None,
348            distance_sq: f32::MAX,
349            play_counter: 0,
350        }
351    }
352
353    pub fn drop_track(&mut self) { self.track = None; }
354
355    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
356        self.source = source_handle;
357    }
358
359    /// Sets the volume of the source, not the track. This is only used to
360    /// change the volume of a sound based on the player's distance from its
361    /// emitter.
362    pub fn set_source_volume(&mut self, volume: f32) {
363        let tween = Tween {
364            duration: Duration::from_secs_f32(0.0),
365            ..Default::default()
366        };
367        if let Some(source) = self.source.as_mut() {
368            source.set_volume(audio::to_decibels(volume), tween);
369        }
370    }
371
372    pub fn play(
373        &mut self,
374        source: AnySoundData,
375        volume: f32,
376        mut track: SpatialTrackHandle,
377        tag: SfxTag,
378        distance_sq: f32,
379    ) -> usize {
380        match track.play(source) {
381            Ok(handle) => {
382                self.source = Some(handle);
383                self.source_initial_volume = volume
384            },
385            Err(e) => {
386                warn!(?e, "Cannot play sfx")
387            },
388        }
389        self.track = Some(track);
390        self.play_counter += 1;
391        self.tag = Some(tag);
392        self.distance_sq = distance_sq;
393        self.play_counter
394    }
395
396    pub fn stop(&mut self) {
397        if let Some(source) = self.source.as_mut() {
398            source.stop(Tween::default())
399        }
400    }
401
402    /// Sets volume of the track.
403    pub fn set_volume(&mut self, volume: f32, duration: Option<f32>) {
404        if let Some(track) = self.track.as_mut() {
405            let tween = Tween {
406                duration: Duration::from_secs_f32(duration.unwrap_or(0.0)),
407                ..Default::default()
408            };
409            track.set_volume(audio::to_decibels(volume), tween)
410        }
411    }
412
413    pub fn set_pos(&mut self, pos: Vec3<f32>) { self.pos = pos; }
414
415    pub fn tag_priority(&self) -> usize {
416        if let Some(tag) = self.tag {
417            tag as usize
418        } else {
419            usize::MAX
420        }
421    }
422
423    pub fn distance_sq(&self) -> f32 { self.distance_sq }
424
425    pub fn is_done(&self) -> bool {
426        self.source
427            .as_ref()
428            .is_none_or(|source| source.state() == PlaybackState::Stopped)
429    }
430
431    pub fn is_lower_or_equal_priority_than(&self, tag: SfxTag) -> bool {
432        if let Some(channel_tag) = self.tag {
433            (tag as usize) <= (channel_tag as usize)
434        } else {
435            true
436        }
437    }
438
439    /// Update volume of sounds based on position of player
440    pub fn update(&mut self, player_pos: Vec3<f32>) {
441        if let Some(track) = self.track.as_mut() {
442            let tween = Tween {
443                duration: Duration::from_secs_f32(0.0),
444                ..Default::default()
445            };
446            track.set_position(self.pos, tween);
447        }
448
449        // A multiplier between 0.0 and 1.0, with 0.0 being the furthest away from and
450        // 1.0 being closest to the player.
451        let ratio = calculate_player_attenuation(player_pos, self.pos);
452        self.set_source_volume(self.source_initial_volume * 5.0 * ratio);
453    }
454}
455
456impl Default for SfxChannel {
457    fn default() -> Self { Self::new() }
458}
459
460#[derive(Eq, PartialEq, Copy, Clone, Debug)]
461pub enum UiChannelTag {
462    LevelUp,
463}
464
465/// An UiChannel uses a non-spatial audio sink, and is designed for short-lived
466/// audio which is not spatially controlled, but does not need control over
467/// playback or fading/transitions
468pub struct UiChannel {
469    track: TrackHandle,
470    source: Option<AnySoundHandle>,
471    pub tag: Option<UiChannelTag>,
472}
473
474impl UiChannel {
475    pub fn new(route_to: &mut TrackHandle) -> Result<Self, kira::ResourceLimitReached> {
476        let track = route_to.add_sub_track(TrackBuilder::default())?;
477        Ok(Self {
478            track,
479            source: None,
480            tag: None,
481        })
482    }
483
484    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
485        self.source = source_handle;
486    }
487
488    pub fn play(&mut self, source: AnySoundData, tag: Option<UiChannelTag>) {
489        match self.track.play(source) {
490            Ok(handle) => {
491                self.source = Some(handle);
492                self.tag = tag;
493            },
494            Err(e) => {
495                warn!(?e, "Cannot play ui sfx")
496            },
497        }
498    }
499
500    pub fn stop(&mut self) {
501        if let Some(source) = self.source.as_mut() {
502            source.stop(Tween::default())
503        }
504    }
505
506    pub fn set_volume(&mut self, volume: f32) {
507        self.track
508            .set_volume(audio::to_decibels(volume), Tween::default())
509    }
510
511    pub fn is_done(&self) -> bool {
512        self.source
513            .as_ref()
514            .is_none_or(|source| source.state() == PlaybackState::Stopped)
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use crate::audio::channel::{SFX_DIST_LIMIT, SFX_DIST_LIMIT_SQR};
521
522    #[test]
523    // Small optimization so sqrt() isn't called at runtime
524    fn test_sfx_dist_limit_eq_sfx_dist_limit_sqr() {
525        assert!(SFX_DIST_LIMIT.powf(2.0) == SFX_DIST_LIMIT_SQR)
526    }
527}