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