bevy_audio/
volume.rs

1use bevy_ecs::prelude::*;
2use bevy_math::ops;
3use bevy_reflect::prelude::*;
4
5/// Use this [`Resource`] to control the global volume of all audio.
6///
7/// Note: Changing [`GlobalVolume`] does not affect already playing audio.
8#[derive(Resource, Debug, Default, Clone, Copy, Reflect)]
9#[reflect(Resource, Debug, Default, Clone)]
10pub struct GlobalVolume {
11    /// The global volume of all audio.
12    pub volume: Volume,
13}
14
15impl From<Volume> for GlobalVolume {
16    fn from(volume: Volume) -> Self {
17        Self { volume }
18    }
19}
20
21impl GlobalVolume {
22    /// Create a new [`GlobalVolume`] with the given volume.
23    pub fn new(volume: Volume) -> Self {
24        Self { volume }
25    }
26}
27
28/// A [`Volume`] represents an audio source's volume level.
29///
30/// To create a new [`Volume`] from a linear scale value, use
31/// [`Volume::Linear`].
32///
33/// To create a new [`Volume`] from decibels, use [`Volume::Decibels`].
34#[derive(Clone, Copy, Debug, Reflect)]
35#[reflect(Clone, Debug, PartialEq)]
36pub enum Volume {
37    /// Create a new [`Volume`] from the given volume in the linear scale.
38    ///
39    /// In a linear scale, the value `1.0` represents the "normal" volume,
40    /// meaning the audio is played at its original level. Values greater than
41    /// `1.0` increase the volume, while values between `0.0` and `1.0` decrease
42    /// the volume. A value of `0.0` effectively mutes the audio.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// # use bevy_audio::Volume;
48    /// # use bevy_math::ops;
49    /// #
50    /// # const EPSILON: f32 = 0.01;
51    ///
52    /// let volume = Volume::Linear(0.5);
53    /// assert_eq!(volume.to_linear(), 0.5);
54    /// assert!(ops::abs(volume.to_decibels() - -6.0206) < EPSILON);
55    ///
56    /// let volume = Volume::Linear(0.0);
57    /// assert_eq!(volume.to_linear(), 0.0);
58    /// assert_eq!(volume.to_decibels(), f32::NEG_INFINITY);
59    ///
60    /// let volume = Volume::Linear(1.0);
61    /// assert_eq!(volume.to_linear(), 1.0);
62    /// assert!(ops::abs(volume.to_decibels() - 0.0) < EPSILON);
63    /// ```
64    Linear(f32),
65    /// Create a new [`Volume`] from the given volume in decibels.
66    ///
67    /// In a decibel scale, the value `0.0` represents the "normal" volume,
68    /// meaning the audio is played at its original level. Values greater than
69    /// `0.0` increase the volume, while values less than `0.0` decrease the
70    /// volume. A value of [`f32::NEG_INFINITY`] decibels effectively mutes the
71    /// audio.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// # use bevy_audio::Volume;
77    /// # use bevy_math::ops;
78    /// #
79    /// # const EPSILON: f32 = 0.01;
80    ///
81    /// let volume = Volume::Decibels(-5.998);
82    /// assert!(ops::abs(volume.to_linear() - 0.5) < EPSILON);
83    ///
84    /// let volume = Volume::Decibels(f32::NEG_INFINITY);
85    /// assert_eq!(volume.to_linear(), 0.0);
86    ///
87    /// let volume = Volume::Decibels(0.0);
88    /// assert_eq!(volume.to_linear(), 1.0);
89    ///
90    /// let volume = Volume::Decibels(20.0);
91    /// assert_eq!(volume.to_linear(), 10.0);
92    /// ```
93    Decibels(f32),
94}
95
96impl Default for Volume {
97    fn default() -> Self {
98        Self::Linear(1.0)
99    }
100}
101
102impl PartialEq for Volume {
103    fn eq(&self, other: &Self) -> bool {
104        use Volume::{Decibels, Linear};
105
106        match (self, other) {
107            (Linear(a), Linear(b)) => a.abs() == b.abs(),
108            (Decibels(a), Decibels(b)) => a == b,
109            (a, b) => a.to_decibels() == b.to_decibels(),
110        }
111    }
112}
113
114impl PartialOrd for Volume {
115    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
116        use Volume::{Decibels, Linear};
117
118        Some(match (self, other) {
119            (Linear(a), Linear(b)) => a.abs().total_cmp(&b.abs()),
120            (Decibels(a), Decibels(b)) => a.total_cmp(b),
121            (a, b) => a.to_decibels().total_cmp(&b.to_decibels()),
122        })
123    }
124}
125
126#[inline]
127fn decibels_to_linear(decibels: f32) -> f32 {
128    ops::powf(10.0f32, decibels / 20.0)
129}
130
131#[inline]
132fn linear_to_decibels(linear: f32) -> f32 {
133    20.0 * ops::log10(linear.abs())
134}
135
136impl Volume {
137    /// Returns the volume in linear scale as a float.
138    pub fn to_linear(&self) -> f32 {
139        match self {
140            Self::Linear(v) => v.abs(),
141            Self::Decibels(v) => decibels_to_linear(*v),
142        }
143    }
144
145    /// Returns the volume in decibels as a float.
146    ///
147    /// If the volume is silent / off / muted, i.e., its underlying linear scale
148    /// is `0.0`, this method returns negative infinity.
149    pub fn to_decibels(&self) -> f32 {
150        match self {
151            Self::Linear(v) => linear_to_decibels(*v),
152            Self::Decibels(v) => *v,
153        }
154    }
155
156    /// The silent volume. Also known as "off" or "muted".
157    pub const SILENT: Self = Volume::Linear(0.0);
158
159    /// Increases the volume by the specified percentage.
160    ///
161    /// This method works in the linear domain, where a 100% increase
162    /// means doubling the volume (equivalent to +6.02dB).
163    ///
164    /// # Arguments
165    /// * `percentage` - The percentage to increase (50.0 means 50% increase)
166    ///
167    /// # Examples
168    /// ```
169    /// use bevy_audio::Volume;
170    ///
171    /// let volume = Volume::Linear(1.0);
172    /// let increased = volume.increase_by_percentage(100.0);
173    /// assert_eq!(increased.to_linear(), 2.0);
174    /// ```
175    pub fn increase_by_percentage(&self, percentage: f32) -> Self {
176        let factor = 1.0 + (percentage / 100.0);
177        Volume::Linear(self.to_linear() * factor)
178    }
179
180    /// Decreases the volume by the specified percentage.
181    ///
182    /// This method works in the linear domain, where a 50% decrease
183    /// means halving the volume (equivalent to -6.02dB).
184    ///
185    /// # Arguments
186    /// * `percentage` - The percentage to decrease (50.0 means 50% decrease)
187    ///
188    /// # Examples
189    /// ```
190    /// use bevy_audio::Volume;
191    ///
192    /// let volume = Volume::Linear(1.0);
193    /// let decreased = volume.decrease_by_percentage(50.0);
194    /// assert_eq!(decreased.to_linear(), 0.5);
195    /// ```
196    pub fn decrease_by_percentage(&self, percentage: f32) -> Self {
197        let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0);
198        Volume::Linear(self.to_linear() * factor)
199    }
200
201    /// Scales the volume to a specific linear factor relative to the current volume.
202    ///
203    /// This is different from `adjust_by_linear` as it sets the volume to be
204    /// exactly the factor times the original volume, rather than applying
205    /// the factor to the current volume.
206    ///
207    /// # Arguments
208    /// * `factor` - The scaling factor (2.0 = twice as loud, 0.5 = half as loud)
209    ///
210    /// # Examples
211    /// ```
212    /// use bevy_audio::Volume;
213    ///
214    /// let volume = Volume::Linear(0.8);
215    /// let scaled = volume.scale_to_factor(1.25);
216    /// assert_eq!(scaled.to_linear(), 1.0);
217    /// ```
218    pub fn scale_to_factor(&self, factor: f32) -> Self {
219        Volume::Linear(self.to_linear() * factor)
220    }
221
222    /// Creates a fade effect by interpolating between current volume and target volume.
223    ///
224    /// This method performs linear interpolation in the linear domain, which
225    /// provides a more natural-sounding fade effect.
226    ///
227    /// # Arguments
228    /// * `target` - The target volume to fade towards
229    /// * `factor` - The interpolation factor (0.0 = current volume, 1.0 = target volume)
230    ///
231    /// # Examples
232    /// ```
233    /// use bevy_audio::Volume;
234    ///
235    /// let current = Volume::Linear(1.0);
236    /// let target = Volume::Linear(0.0);
237    /// let faded = current.fade_towards(target, 0.5);
238    /// assert_eq!(faded.to_linear(), 0.5);
239    /// ```
240    pub fn fade_towards(&self, target: Volume, factor: f32) -> Self {
241        let current_linear = self.to_linear();
242        let target_linear = target.to_linear();
243        let factor_clamped = factor.clamp(0.0, 1.0);
244
245        let interpolated = current_linear + (target_linear - current_linear) * factor_clamped;
246        Volume::Linear(interpolated)
247    }
248}
249
250impl core::ops::Mul<Self> for Volume {
251    type Output = Self;
252
253    fn mul(self, rhs: Self) -> Self {
254        use Volume::{Decibels, Linear};
255
256        match (self, rhs) {
257            (Linear(a), Linear(b)) => Linear(a * b),
258            (Decibels(a), Decibels(b)) => Decibels(a + b),
259            // {Linear, Decibels} favors the left hand side of the operation by
260            // first converting the right hand side to the same type as the left
261            // hand side and then performing the operation.
262            (Linear(..), Decibels(db)) => self * Linear(decibels_to_linear(db)),
263            (Decibels(..), Linear(l)) => self * Decibels(linear_to_decibels(l)),
264        }
265    }
266}
267
268impl core::ops::MulAssign<Self> for Volume {
269    fn mul_assign(&mut self, rhs: Self) {
270        *self = *self * rhs;
271    }
272}
273
274impl core::ops::Div<Self> for Volume {
275    type Output = Self;
276
277    fn div(self, rhs: Self) -> Self {
278        use Volume::{Decibels, Linear};
279
280        match (self, rhs) {
281            (Linear(a), Linear(b)) => Linear(a / b),
282            (Decibels(a), Decibels(b)) => Decibels(a - b),
283            // {Linear, Decibels} favors the left hand side of the operation by
284            // first converting the right hand side to the same type as the left
285            // hand side and then performing the operation.
286            (Linear(..), Decibels(db)) => self / Linear(decibels_to_linear(db)),
287            (Decibels(..), Linear(l)) => self / Decibels(linear_to_decibels(l)),
288        }
289    }
290}
291
292impl core::ops::DivAssign<Self> for Volume {
293    fn div_assign(&mut self, rhs: Self) {
294        *self = *self / rhs;
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::Volume::{self, Decibels, Linear};
301
302    /// Based on [Wikipedia's Decibel article].
303    ///
304    /// [Wikipedia's Decibel article]: https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel
305    const DECIBELS_LINEAR_TABLE: [(f32, f32); 27] = [
306        (100., 100000.),
307        (90., 31623.),
308        (80., 10000.),
309        (70., 3162.),
310        (60., 1000.),
311        (50., 316.2),
312        (40., 100.),
313        (30., 31.62),
314        (20., 10.),
315        (10., 3.162),
316        (5.998, 1.995),
317        (3.003, 1.413),
318        (1.002, 1.122),
319        (0., 1.),
320        (-1.002, 0.891),
321        (-3.003, 0.708),
322        (-5.998, 0.501),
323        (-10., 0.3162),
324        (-20., 0.1),
325        (-30., 0.03162),
326        (-40., 0.01),
327        (-50., 0.003162),
328        (-60., 0.001),
329        (-70., 0.0003162),
330        (-80., 0.0001),
331        (-90., 0.00003162),
332        (-100., 0.00001),
333    ];
334
335    #[test]
336    fn volume_conversion() {
337        for (db, linear) in DECIBELS_LINEAR_TABLE {
338            for volume in [Linear(linear), Decibels(db), Linear(-linear)] {
339                let db_test = volume.to_decibels();
340                let linear_test = volume.to_linear();
341
342                let db_delta = db_test - db;
343                let linear_relative_delta = (linear_test - linear) / linear;
344
345                assert!(
346                    db_delta.abs() < 1e-2,
347                    "Expected ~{db}dB, got {db_test}dB (delta {db_delta})",
348                );
349                assert!(
350                    linear_relative_delta.abs() < 1e-3,
351                    "Expected ~{linear}, got {linear_test} (relative delta {linear_relative_delta})",
352                );
353            }
354        }
355    }
356
357    #[test]
358    fn volume_conversion_special() {
359        assert!(
360            Decibels(f32::INFINITY).to_linear().is_infinite(),
361            "Infinite decibels is equivalent to infinite linear scale"
362        );
363        assert!(
364            Linear(f32::INFINITY).to_decibels().is_infinite(),
365            "Infinite linear scale is equivalent to infinite decibels"
366        );
367
368        assert!(
369            Linear(f32::NEG_INFINITY).to_decibels().is_infinite(),
370            "Negative infinite linear scale is equivalent to infinite decibels"
371        );
372        assert_eq!(
373            Decibels(f32::NEG_INFINITY).to_linear().abs(),
374            0.0,
375            "Negative infinity decibels is equivalent to zero linear scale"
376        );
377
378        assert!(
379            Linear(0.0).to_decibels().is_infinite(),
380            "Zero linear scale is equivalent to negative infinity decibels"
381        );
382        assert!(
383            Linear(-0.0).to_decibels().is_infinite(),
384            "Negative zero linear scale is equivalent to negative infinity decibels"
385        );
386
387        assert!(
388            Decibels(f32::NAN).to_linear().is_nan(),
389            "NaN decibels is equivalent to NaN linear scale"
390        );
391        assert!(
392            Linear(f32::NAN).to_decibels().is_nan(),
393            "NaN linear scale is equivalent to NaN decibels"
394        );
395    }
396
397    #[test]
398    fn test_increase_by_percentage() {
399        let volume = Linear(1.0);
400
401        // 100% increase should double the volume
402        let increased = volume.increase_by_percentage(100.0);
403        assert_eq!(increased.to_linear(), 2.0);
404
405        // 50% increase
406        let increased = volume.increase_by_percentage(50.0);
407        assert_eq!(increased.to_linear(), 1.5);
408    }
409
410    #[test]
411    fn test_decrease_by_percentage() {
412        let volume = Linear(1.0);
413
414        // 50% decrease should halve the volume
415        let decreased = volume.decrease_by_percentage(50.0);
416        assert_eq!(decreased.to_linear(), 0.5);
417
418        // 25% decrease
419        let decreased = volume.decrease_by_percentage(25.0);
420        assert_eq!(decreased.to_linear(), 0.75);
421
422        // 100% decrease should result in silence
423        let decreased = volume.decrease_by_percentage(100.0);
424        assert_eq!(decreased.to_linear(), 0.0);
425    }
426
427    #[test]
428    fn test_scale_to_factor() {
429        let volume = Linear(0.8);
430        let scaled = volume.scale_to_factor(1.25);
431        assert_eq!(scaled.to_linear(), 1.0);
432    }
433
434    #[test]
435    fn test_fade_towards() {
436        let current = Linear(1.0);
437        let target = Linear(0.0);
438
439        // 50% fade should result in 0.5 linear volume
440        let faded = current.fade_towards(target, 0.5);
441        assert_eq!(faded.to_linear(), 0.5);
442
443        // 0% fade should keep current volume
444        let faded = current.fade_towards(target, 0.0);
445        assert_eq!(faded.to_linear(), 1.0);
446
447        // 100% fade should reach target volume
448        let faded = current.fade_towards(target, 1.0);
449        assert_eq!(faded.to_linear(), 0.0);
450    }
451
452    #[test]
453    fn test_decibel_math_properties() {
454        let volume = Linear(1.0);
455
456        // Adding 20dB should multiply linear volume by 10
457        let adjusted = volume * Decibels(20.0);
458        assert_approx_eq(adjusted, Linear(10.0));
459
460        // Subtracting 20dB should divide linear volume by 10
461        let adjusted = volume / Decibels(20.0);
462        assert_approx_eq(adjusted, Linear(0.1));
463    }
464
465    fn assert_approx_eq(a: Volume, b: Volume) {
466        const EPSILON: f32 = 0.0001;
467
468        match (a, b) {
469            (Decibels(a), Decibels(b)) | (Linear(a), Linear(b)) => assert!(
470                (a - b).abs() < EPSILON,
471                "Expected {a:?} to be approximately equal to {b:?}",
472            ),
473            (a, b) => assert!(
474                (a.to_decibels() - b.to_decibels()).abs() < EPSILON,
475                "Expected {a:?} to be approximately equal to {b:?}",
476            ),
477        }
478    }
479
480    #[test]
481    fn volume_ops_mul() {
482        // Linear to Linear.
483        assert_approx_eq(Linear(0.5) * Linear(0.5), Linear(0.25));
484        assert_approx_eq(Linear(0.5) * Linear(0.1), Linear(0.05));
485        assert_approx_eq(Linear(0.5) * Linear(-0.5), Linear(-0.25));
486
487        // Decibels to Decibels.
488        assert_approx_eq(Decibels(0.0) * Decibels(0.0), Decibels(0.0));
489        assert_approx_eq(Decibels(6.0) * Decibels(6.0), Decibels(12.0));
490        assert_approx_eq(Decibels(-6.0) * Decibels(-6.0), Decibels(-12.0));
491
492        // {Linear, Decibels} favors the left hand side of the operation.
493        assert_approx_eq(Linear(0.5) * Decibels(0.0), Linear(0.5));
494        assert_approx_eq(Decibels(0.0) * Linear(0.501), Decibels(-6.003246));
495    }
496
497    #[test]
498    fn volume_ops_mul_assign() {
499        // Linear to Linear.
500        let mut volume = Linear(0.5);
501        volume *= Linear(0.5);
502        assert_approx_eq(volume, Linear(0.25));
503
504        // Decibels to Decibels.
505        let mut volume = Decibels(6.0);
506        volume *= Decibels(6.0);
507        assert_approx_eq(volume, Decibels(12.0));
508
509        // {Linear, Decibels} favors the left hand side of the operation.
510        let mut volume = Linear(0.5);
511        volume *= Decibels(0.0);
512        assert_approx_eq(volume, Linear(0.5));
513        let mut volume = Decibels(0.0);
514        volume *= Linear(0.501);
515        assert_approx_eq(volume, Decibels(-6.003246));
516    }
517
518    #[test]
519    fn volume_ops_div() {
520        // Linear to Linear.
521        assert_approx_eq(Linear(0.5) / Linear(0.5), Linear(1.0));
522        assert_approx_eq(Linear(0.5) / Linear(0.1), Linear(5.0));
523        assert_approx_eq(Linear(0.5) / Linear(-0.5), Linear(-1.0));
524
525        // Decibels to Decibels.
526        assert_approx_eq(Decibels(0.0) / Decibels(0.0), Decibels(0.0));
527        assert_approx_eq(Decibels(6.0) / Decibels(6.0), Decibels(0.0));
528        assert_approx_eq(Decibels(-6.0) / Decibels(-6.0), Decibels(0.0));
529
530        // {Linear, Decibels} favors the left hand side of the operation.
531        assert_approx_eq(Linear(0.5) / Decibels(0.0), Linear(0.5));
532        assert_approx_eq(Decibels(0.0) / Linear(0.501), Decibels(6.003246));
533    }
534
535    #[test]
536    fn volume_ops_div_assign() {
537        // Linear to Linear.
538        let mut volume = Linear(0.5);
539        volume /= Linear(0.5);
540        assert_approx_eq(volume, Linear(1.0));
541
542        // Decibels to Decibels.
543        let mut volume = Decibels(6.0);
544        volume /= Decibels(6.0);
545        assert_approx_eq(volume, Decibels(0.0));
546
547        // {Linear, Decibels} favors the left hand side of the operation.
548        let mut volume = Linear(0.5);
549        volume /= Decibels(0.0);
550        assert_approx_eq(volume, Linear(0.5));
551        let mut volume = Decibels(0.0);
552        volume /= Linear(0.501);
553        assert_approx_eq(volume, Decibels(6.003246));
554    }
555}