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 = 100.0;
35pub const SFX_DIST_LIMIT_SQR: f32 = 10000.0;
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    pub pos: Vec3<f32>,
327}
328
329impl SfxChannel {
330    pub fn new(
331        route_to: &mut TrackHandle,
332        listener: ListenerId,
333    ) -> Result<Self, kira::ResourceLimitReached> {
334        let sfx_track_builder = SpatialTrackBuilder::new()
335            .distances((1.0, SFX_DIST_LIMIT))
336            .attenuation_function(Some(Easing::OutPowf(0.66)));
337        let track = route_to.add_spatial_sub_track(listener, Vec3::zero(), sfx_track_builder)?;
338        Ok(Self {
339            track,
340            source: None,
341            pos: Vec3::zero(),
342        })
343    }
344
345    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
346        self.source = source_handle;
347    }
348
349    pub fn play(&mut self, source: AnySoundData) {
350        match self.track.play(source) {
351            Ok(handle) => self.source = Some(handle),
352            Err(e) => {
353                warn!(?e, "Cannot play sfx")
354            },
355        }
356    }
357
358    pub fn stop(&mut self) {
359        if let Some(source) = self.source.as_mut() {
360            source.stop(Tween::default())
361        }
362    }
363
364    /// Sets volume of the track, not the source. This is to be used only for
365    /// multiplying the volume post distance calculation.
366    pub fn set_volume(&mut self, volume: f32) {
367        let tween = Tween {
368            duration: Duration::from_secs_f32(0.0),
369            ..Default::default()
370        };
371        self.track.set_volume(audio::to_decibels(volume), tween)
372    }
373
374    pub fn is_done(&self) -> bool {
375        self.source
376            .as_ref()
377            .is_none_or(|source| source.state() == PlaybackState::Stopped)
378    }
379
380    /// Update volume of sounds based on position of player
381    pub fn update(&mut self, emitter_pos: Vec3<f32>, player_pos: Vec3<f32>) {
382        let tween = Tween {
383            duration: Duration::from_secs_f32(0.0),
384            ..Default::default()
385        };
386        self.track.set_position(emitter_pos, tween);
387        self.pos = emitter_pos;
388
389        let player_distance_to_source_sqr = player_pos
390            .distance_squared(self.pos)
391            .min(SFX_DIST_LIMIT_SQR);
392        // A multiplier between 0.0 and 1.0, with 0.0 being the furthest away from and
393        // 1.0 being closest to the player.
394        let ratio = (-(player_distance_to_source_sqr - SFX_DIST_LIMIT_SQR) / SFX_DIST_LIMIT_SQR)
395            .powf(5.0)
396            .clamp(0.0, 1.0);
397        self.set_volume(ratio);
398    }
399}
400
401#[derive(Eq, PartialEq, Copy, Clone, Debug)]
402pub enum UiChannelTag {
403    LevelUp,
404}
405
406/// An UiChannel uses a non-spatial audio sink, and is designed for short-lived
407/// audio which is not spatially controlled, but does not need control over
408/// playback or fading/transitions
409pub struct UiChannel {
410    track: TrackHandle,
411    source: Option<AnySoundHandle>,
412    pub tag: Option<UiChannelTag>,
413}
414
415impl UiChannel {
416    pub fn new(route_to: &mut TrackHandle) -> Result<Self, kira::ResourceLimitReached> {
417        let track = route_to.add_sub_track(TrackBuilder::default())?;
418        Ok(Self {
419            track,
420            source: None,
421            tag: None,
422        })
423    }
424
425    pub fn set_source(&mut self, source_handle: Option<AnySoundHandle>) {
426        self.source = source_handle;
427    }
428
429    pub fn play(&mut self, source: AnySoundData, tag: Option<UiChannelTag>) {
430        match self.track.play(source) {
431            Ok(handle) => {
432                self.source = Some(handle);
433                self.tag = tag;
434            },
435            Err(e) => {
436                warn!(?e, "Cannot play ui sfx")
437            },
438        }
439    }
440
441    pub fn stop(&mut self) {
442        if let Some(source) = self.source.as_mut() {
443            source.stop(Tween::default())
444        }
445    }
446
447    pub fn set_volume(&mut self, volume: f32) {
448        self.track
449            .set_volume(audio::to_decibels(volume), Tween::default())
450    }
451
452    pub fn is_done(&self) -> bool {
453        self.source
454            .as_ref()
455            .is_none_or(|source| source.state() == PlaybackState::Stopped)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use crate::audio::channel::{SFX_DIST_LIMIT, SFX_DIST_LIMIT_SQR};
462
463    #[test]
464    // Small optimization so sqrt() isn't called at runtime
465    fn test_sfx_dist_limit_eq_sfx_dist_limit_sqr() {
466        assert!(SFX_DIST_LIMIT.powf(2.0) == SFX_DIST_LIMIT_SQR)
467    }
468}