From 2660ddc4c5e04098d9df64afcb1ed99a7877dc74 Mon Sep 17 00:00:00 2001 From: mgi388 <135186256+mgi388@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:26:43 +0100 Subject: [PATCH] Support decibels in bevy_audio::Volume (#17605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Allow users to configure volume using decibels by changing the `Volume` type from newtyping an `f32` to an enum with `Linear` and `Decibels` variants. - Fixes #9507. - Alternative reworked version of closed #9582. ## Solution Compared to https://github.com/bevyengine/bevy/pull/9582, this PR has the following main differences: 1. It uses the term "linear scale" instead of "amplitude" per https://github.com/bevyengine/bevy/pull/9582/files#r1513529491. 2. Supports `ops` for doing `Volume` arithmetic. Can add two volumes, e.g. to increase/decrease the current volume. Can multiply two volumes, e.g. to get the “effective” volume of an audio source considering global volume. [requested and blessed on Discord]: https://discord.com/channels/691052431525675048/749430447326625812/1318272597003341867 ## Testing - Ran `cargo run --example soundtrack`. - Ran `cargo run --example audio_control`. - Ran `cargo run --example spatial_audio_2d`. - Ran `cargo run --example spatial_audio_3d`. - Ran `cargo run --example pitch`. - Ran `cargo run --example decodable`. - Ran `cargo run --example audio`. --- ## Migration Guide Audio volume can now be configured using decibel values, as well as using linear scale values. To enable this, some types and functions in `bevy_audio` have changed. - `Volume` is now an enum with `Linear` and `Decibels` variants. Before: ```rust let v = Volume(1.0); ``` After: ```rust let volume = Volume::Linear(1.0); let volume = Volume::Decibels(0.0); // or now you can deal with decibels if you prefer ``` - `Volume::ZERO` has been renamed to the more semantically correct `Volume::SILENT` because `Volume` now supports decibels and "zero volume" in decibels actually means "normal volume". - The `AudioSinkPlayback` trait's volume-related methods now deal with `Volume` types rather than `f32`s. `AudioSinkPlayback::volume()` now returns a `Volume` rather than an `f32`. `AudioSinkPlayback::set_volume` now receives a `Volume` rather than an `f32`. This affects the `AudioSink` and `SpatialAudioSink` implementations of the trait. The previous `f32` values are equivalent to the volume converted to linear scale so the `Volume:: Linear` variant should be used to migrate between `f32`s and `Volume`. - The `GlobalVolume::new` function now receives a `Volume` instead of an `f32`. --------- Co-authored-by: Zachary Harrold --- crates/bevy_audio/src/audio.rs | 2 +- crates/bevy_audio/src/audio_output.rs | 4 +- crates/bevy_audio/src/sinks.rs | 79 ++-- crates/bevy_audio/src/volume.rs | 504 ++++++++++++++++++++++++-- examples/audio/audio_control.rs | 6 +- examples/audio/decodable.rs | 4 +- examples/audio/soundtrack.rs | 16 +- 7 files changed, 531 insertions(+), 84 deletions(-) diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 1d0149381b..93a0a7505a 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -74,7 +74,7 @@ impl PlaybackSettings { /// added again. pub const ONCE: PlaybackSettings = PlaybackSettings { mode: PlaybackMode::Once, - volume: Volume(1.0), + volume: Volume::Linear(1.0), speed: 1.0, paused: false, muted: false, diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index c098ac8382..1869fb4755 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -170,7 +170,7 @@ pub(crate) fn play_queued_audio_system( } sink.set_speed(settings.speed); - sink.set_volume(settings.volume.0 * global_volume.volume.0); + sink.set_volume(settings.volume * global_volume.volume); if settings.paused { sink.pause(); @@ -210,7 +210,7 @@ pub(crate) fn play_queued_audio_system( } sink.set_speed(settings.speed); - sink.set_volume(settings.volume.0 * global_volume.volume.0); + sink.set_volume(settings.volume * global_volume.volume); if settings.paused { sink.pause(); diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index d4be43261f..b0c77456e1 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -3,37 +3,26 @@ use bevy_math::Vec3; use bevy_transform::prelude::Transform; use rodio::{Sink, SpatialSink}; +use crate::Volume; + /// Common interactions with an audio sink. pub trait AudioSinkPlayback { - /// Gets the volume of the sound. - /// - /// The value `1.0` is the "normal" volume (unfiltered input). Any value - /// other than `1.0` will multiply each sample by this value. + /// Gets the volume of the sound as a [`Volume`]. /// /// If the sink is muted, this returns the managed volume rather than the - /// sink's actual volume. This allows you to use the volume as if the sink - /// were not muted, because a muted sink has a volume of 0. - fn volume(&self) -> f32; + /// sink's actual volume. This allows you to use the returned volume as if + /// the sink were not muted, because a muted sink has a physical volume of + /// 0. + fn volume(&self) -> Volume; - /// Changes the volume of the sound. - /// - /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` - /// will multiply each sample by this value. + /// Changes the volume of the sound to the given [`Volume`]. /// /// If the sink is muted, changing the volume won't unmute it, i.e. the - /// sink's volume will remain at `0.0`. However, the sink will remember the - /// volume change and it will be used when [`unmute`](Self::unmute) is - /// called. This allows you to control the volume even when the sink is - /// muted. - /// - /// # Note on Audio Volume - /// - /// An increase of 10 decibels (dB) roughly corresponds to the perceived volume doubling in intensity. - /// As this function scales not the volume but the amplitude, a conversion might be necessary. - /// For example, to halve the perceived volume you need to decrease the volume by 10 dB. - /// This corresponds to 20log(x) = -10dB, solving x = 10^(-10/20) = 0.316. - /// Multiply the current volume by 0.316 to halve the perceived volume. - fn set_volume(&mut self, volume: f32); + /// sink's volume will remain "off" / "muted". However, the sink will + /// remember the volume change and it will be used when + /// [`unmute`](Self::unmute) is called. This allows you to control the + /// volume even when the sink is muted. + fn set_volume(&mut self, volume: Volume); /// Gets the speed of the sound. /// @@ -132,7 +121,7 @@ pub struct AudioSink { /// If the sink is muted, this is `Some(volume)` where `volume` is the /// user's intended volume setting, even if the underlying sink's volume is /// 0. - pub(crate) managed_volume: Option, + pub(crate) managed_volume: Option, } impl AudioSink { @@ -146,15 +135,16 @@ impl AudioSink { } impl AudioSinkPlayback for AudioSink { - fn volume(&self) -> f32 { - self.managed_volume.unwrap_or_else(|| self.sink.volume()) + fn volume(&self) -> Volume { + self.managed_volume + .unwrap_or_else(|| Volume::Linear(self.sink.volume())) } - fn set_volume(&mut self, volume: f32) { + fn set_volume(&mut self, volume: Volume) { if self.is_muted() { self.managed_volume = Some(volume); } else { - self.sink.set_volume(volume); + self.sink.set_volume(volume.to_linear()); } } @@ -197,7 +187,7 @@ impl AudioSinkPlayback for AudioSink { fn unmute(&mut self) { if let Some(volume) = self.managed_volume.take() { - self.sink.set_volume(volume); + self.sink.set_volume(volume.to_linear()); } } } @@ -227,7 +217,7 @@ pub struct SpatialAudioSink { /// If the sink is muted, this is `Some(volume)` where `volume` is the /// user's intended volume setting, even if the underlying sink's volume is /// 0. - pub(crate) managed_volume: Option, + pub(crate) managed_volume: Option, } impl SpatialAudioSink { @@ -241,15 +231,16 @@ impl SpatialAudioSink { } impl AudioSinkPlayback for SpatialAudioSink { - fn volume(&self) -> f32 { - self.managed_volume.unwrap_or_else(|| self.sink.volume()) + fn volume(&self) -> Volume { + self.managed_volume + .unwrap_or_else(|| Volume::Linear(self.sink.volume())) } - fn set_volume(&mut self, volume: f32) { + fn set_volume(&mut self, volume: Volume) { if self.is_muted() { self.managed_volume = Some(volume); } else { - self.sink.set_volume(volume); + self.sink.set_volume(volume.to_linear()); } } @@ -292,7 +283,7 @@ impl AudioSinkPlayback for SpatialAudioSink { fn unmute(&mut self) { if let Some(volume) = self.managed_volume.take() { - self.sink.set_volume(volume); + self.sink.set_volume(volume.to_linear()); } } } @@ -326,11 +317,11 @@ mod tests { fn test_audio_sink_playback(mut audio_sink: T) { // Test volume - assert_eq!(audio_sink.volume(), 1.0); // default volume - audio_sink.set_volume(0.5); - assert_eq!(audio_sink.volume(), 0.5); - audio_sink.set_volume(1.0); - assert_eq!(audio_sink.volume(), 1.0); + assert_eq!(audio_sink.volume(), Volume::Linear(1.0)); // default volume + audio_sink.set_volume(Volume::Linear(0.5)); + assert_eq!(audio_sink.volume(), Volume::Linear(0.5)); + audio_sink.set_volume(Volume::Linear(1.0)); + assert_eq!(audio_sink.volume(), Volume::Linear(1.0)); // Test speed assert_eq!(audio_sink.speed(), 1.0); // default speed @@ -361,11 +352,11 @@ mod tests { assert!(!audio_sink.is_muted()); // Test volume with mute - audio_sink.set_volume(0.5); + audio_sink.set_volume(Volume::Linear(0.5)); audio_sink.mute(); - assert_eq!(audio_sink.volume(), 0.5); // returns managed volume even though sink volume is 0 + assert_eq!(audio_sink.volume(), Volume::Linear(0.5)); // returns managed volume even though sink volume is 0 audio_sink.unmute(); - assert_eq!(audio_sink.volume(), 0.5); // managed volume is restored + assert_eq!(audio_sink.volume(), Volume::Linear(0.5)); // managed volume is restored // Test toggle mute audio_sink.toggle_mute(); diff --git a/crates/bevy_audio/src/volume.rs b/crates/bevy_audio/src/volume.rs index f12fe0497f..0fb43f15ef 100644 --- a/crates/bevy_audio/src/volume.rs +++ b/crates/bevy_audio/src/volume.rs @@ -1,48 +1,504 @@ -use bevy_derive::Deref; use bevy_ecs::prelude::*; +use bevy_math::ops; use bevy_reflect::prelude::*; /// Use this [`Resource`] to control the global volume of all audio. /// -/// Note: changing this value will not affect already playing audio. -#[derive(Resource, Default, Clone, Copy, Reflect)] -#[reflect(Resource, Default)] +/// Note: Changing [`GlobalVolume`] does not affect already playing audio. +#[derive(Resource, Debug, Default, Clone, Copy, Reflect)] +#[reflect(Resource, Debug, Default)] pub struct GlobalVolume { /// The global volume of all audio. pub volume: Volume, } +impl From for GlobalVolume { + fn from(volume: Volume) -> Self { + Self { volume } + } +} + impl GlobalVolume { /// Create a new [`GlobalVolume`] with the given volume. - pub fn new(volume: f32) -> Self { - Self { - volume: Volume::new(volume), + pub fn new(volume: Volume) -> Self { + Self { volume } + } +} + +/// A [`Volume`] represents an audio source's volume level. +/// +/// To create a new [`Volume`] from a linear scale value, use +/// [`Volume::Linear`]. +/// +/// To create a new [`Volume`] from decibels, use [`Volume::Decibels`]. +#[derive(Clone, Copy, Debug, Reflect)] +#[reflect(Debug, PartialEq)] +pub enum Volume { + /// Create a new [`Volume`] from the given volume in linear scale. + /// + /// In a linear scale, the value `1.0` represents the "normal" volume, + /// meaning the audio is played at its original level. Values greater than + /// `1.0` increase the volume, while values between `0.0` and `1.0` decrease + /// the volume. A value of `0.0` effectively mutes the audio. + /// + /// # Examples + /// + /// ``` + /// # use bevy_audio::Volume; + /// # use bevy_math::ops; + /// # + /// # const EPSILON: f32 = 0.01; + /// + /// let volume = Volume::Linear(0.5); + /// assert_eq!(volume.to_linear(), 0.5); + /// assert!(ops::abs(volume.to_decibels() - -6.0206) < EPSILON); + /// + /// let volume = Volume::Linear(0.0); + /// assert_eq!(volume.to_linear(), 0.0); + /// assert_eq!(volume.to_decibels(), f32::NEG_INFINITY); + /// + /// let volume = Volume::Linear(1.0); + /// assert_eq!(volume.to_linear(), 1.0); + /// assert!(ops::abs(volume.to_decibels() - 0.0) < EPSILON); + /// ``` + Linear(f32), + /// Create a new [`Volume`] from the given volume in decibels. + /// + /// In a decibel scale, the value `0.0` represents the "normal" volume, + /// meaning the audio is played at its original level. Values greater than + /// `0.0` increase the volume, while values less than `0.0` decrease the + /// volume. A value of [`f32::NEG_INFINITY`] decibels effectively mutes the + /// audio. + /// + /// # Examples + /// + /// ``` + /// # use bevy_audio::Volume; + /// # use bevy_math::ops; + /// # + /// # const EPSILON: f32 = 0.01; + /// + /// let volume = Volume::Decibels(-5.998); + /// assert!(ops::abs(volume.to_linear() - 0.5) < EPSILON); + /// + /// let volume = Volume::Decibels(f32::NEG_INFINITY); + /// assert_eq!(volume.to_linear(), 0.0); + /// + /// let volume = Volume::Decibels(0.0); + /// assert_eq!(volume.to_linear(), 1.0); + /// + /// let volume = Volume::Decibels(20.0); + /// assert_eq!(volume.to_linear(), 10.0); + /// ``` + Decibels(f32), +} + +impl Default for Volume { + fn default() -> Self { + Self::Linear(1.0) + } +} + +impl PartialEq for Volume { + fn eq(&self, other: &Self) -> bool { + use Volume::{Decibels, Linear}; + + match (self, other) { + (Linear(a), Linear(b)) => a.abs() == b.abs(), + (Decibels(a), Decibels(b)) => a == b, + (a, b) => a.to_decibels() == b.to_decibels(), } } } -/// A volume level equivalent to a non-negative float. -#[derive(Clone, Copy, Deref, Debug, Reflect)] -#[reflect(Debug)] -pub struct Volume(pub(crate) f32); +impl PartialOrd for Volume { + fn partial_cmp(&self, other: &Self) -> Option { + use Volume::{Decibels, Linear}; -impl Default for Volume { - fn default() -> Self { - Self(1.0) + Some(match (self, other) { + (Linear(a), Linear(b)) => a.abs().total_cmp(&b.abs()), + (Decibels(a), Decibels(b)) => a.total_cmp(b), + (a, b) => a.to_decibels().total_cmp(&b.to_decibels()), + }) } } +#[inline] +fn decibels_to_linear(decibels: f32) -> f32 { + ops::powf(10.0f32, decibels / 20.0) +} + +#[inline] +fn linear_to_decibels(linear: f32) -> f32 { + 20.0 * ops::log10(linear.abs()) +} + impl Volume { - /// Create a new volume level. - pub fn new(volume: f32) -> Self { - debug_assert!(volume >= 0.0); - Self(f32::max(volume, 0.)) - } - /// Get the value of the volume level. - pub fn get(&self) -> f32 { - self.0 + /// Returns the volume in linear scale as a float. + pub fn to_linear(&self) -> f32 { + match self { + Self::Linear(v) => v.abs(), + Self::Decibels(v) => decibels_to_linear(*v), + } } - /// Zero (silent) volume level - pub const ZERO: Self = Volume(0.0); + /// Returns the volume in decibels as a float. + /// + /// If the volume is silent / off / muted, i.e. it's underlying linear scale + /// is `0.0`, this method returns negative infinity. + pub fn to_decibels(&self) -> f32 { + match self { + Self::Linear(v) => linear_to_decibels(*v), + Self::Decibels(v) => *v, + } + } + + /// The silent volume. Also known as "off" or "muted". + pub const SILENT: Self = Volume::Linear(0.0); +} + +impl core::ops::Add for Volume { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + use Volume::{Decibels, Linear}; + + match (self, rhs) { + (Linear(a), Linear(b)) => Linear(a + b), + (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( + decibels_to_linear(a) + decibels_to_linear(b), + )), + // {Linear, Decibels} favors the left hand side of the operation by + // first converting the right hand side to the same type as the left + // hand side and then performing the operation. + (Linear(..), Decibels(db)) => self + Linear(decibels_to_linear(db)), + (Decibels(..), Linear(l)) => self + Decibels(linear_to_decibels(l)), + } + } +} + +impl core::ops::AddAssign for Volume { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl core::ops::Sub for Volume { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + use Volume::{Decibels, Linear}; + + match (self, rhs) { + (Linear(a), Linear(b)) => Linear(a - b), + (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( + decibels_to_linear(a) - decibels_to_linear(b), + )), + // {Linear, Decibels} favors the left hand side of the operation by + // first converting the right hand side to the same type as the left + // hand side and then performing the operation. + (Linear(..), Decibels(db)) => self - Linear(decibels_to_linear(db)), + (Decibels(..), Linear(l)) => self - Decibels(linear_to_decibels(l)), + } + } +} + +impl core::ops::SubAssign for Volume { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl core::ops::Mul for Volume { + type Output = Self; + + fn mul(self, rhs: Self) -> Self { + use Volume::{Decibels, Linear}; + + match (self, rhs) { + (Linear(a), Linear(b)) => Linear(a * b), + (Decibels(a), Decibels(b)) => Decibels(a + b), + // {Linear, Decibels} favors the left hand side of the operation by + // first converting the right hand side to the same type as the left + // hand side and then performing the operation. + (Linear(..), Decibels(db)) => self * Linear(decibels_to_linear(db)), + (Decibels(..), Linear(l)) => self * Decibels(linear_to_decibels(l)), + } + } +} + +impl core::ops::MulAssign for Volume { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl core::ops::Div for Volume { + type Output = Self; + + fn div(self, rhs: Self) -> Self { + use Volume::{Decibels, Linear}; + + match (self, rhs) { + (Linear(a), Linear(b)) => Linear(a / b), + (Decibels(a), Decibels(b)) => Decibels(a - b), + // {Linear, Decibels} favors the left hand side of the operation by + // first converting the right hand side to the same type as the left + // hand side and then performing the operation. + (Linear(..), Decibels(db)) => self / Linear(decibels_to_linear(db)), + (Decibels(..), Linear(l)) => self / Decibels(linear_to_decibels(l)), + } + } +} + +impl core::ops::DivAssign for Volume { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +#[cfg(test)] +mod tests { + use super::Volume::{self, Decibels, Linear}; + + /// Based on [Wikipedia's Decibel article]. + /// + /// [Wikipedia's Decibel article]: https://web.archive.org/web/20230810185300/https://en.wikipedia.org/wiki/Decibel + const DECIBELS_LINEAR_TABLE: [(f32, f32); 27] = [ + (100., 100000.), + (90., 31623.), + (80., 10000.), + (70., 3162.), + (60., 1000.), + (50., 316.2), + (40., 100.), + (30., 31.62), + (20., 10.), + (10., 3.162), + (5.998, 1.995), + (3.003, 1.413), + (1.002, 1.122), + (0., 1.), + (-1.002, 0.891), + (-3.003, 0.708), + (-5.998, 0.501), + (-10., 0.3162), + (-20., 0.1), + (-30., 0.03162), + (-40., 0.01), + (-50., 0.003162), + (-60., 0.001), + (-70., 0.0003162), + (-80., 0.0001), + (-90., 0.00003162), + (-100., 0.00001), + ]; + + #[test] + fn volume_conversion() { + for (db, linear) in DECIBELS_LINEAR_TABLE { + for volume in [Linear(linear), Decibels(db), Linear(-linear)] { + let db_test = volume.to_decibels(); + let linear_test = volume.to_linear(); + + let db_delta = db_test - db; + let linear_relative_delta = (linear_test - linear) / linear; + + assert!( + db_delta.abs() < 1e-2, + "Expected ~{}dB, got {}dB (delta {})", + db, + db_test, + db_delta + ); + assert!( + linear_relative_delta.abs() < 1e-3, + "Expected ~{}, got {} (relative delta {})", + linear, + linear_test, + linear_relative_delta + ); + } + } + } + + #[test] + fn volume_conversion_special() { + assert!( + Decibels(f32::INFINITY).to_linear().is_infinite(), + "Infinite decibels is equivalent to infinite linear scale" + ); + assert!( + Linear(f32::INFINITY).to_decibels().is_infinite(), + "Infinite linear scale is equivalent to infinite decibels" + ); + + assert!( + Linear(f32::NEG_INFINITY).to_decibels().is_infinite(), + "Negative infinite linear scale is equivalent to infinite decibels" + ); + assert!( + Decibels(f32::NEG_INFINITY).to_linear().abs() == 0.0, + "Negative infinity decibels is equivalent to zero linear scale" + ); + + assert!( + Linear(0.0).to_decibels().is_infinite(), + "Zero linear scale is equivalent to negative infinity decibels" + ); + assert!( + Linear(-0.0).to_decibels().is_infinite(), + "Negative zero linear scale is equivalent to negative infinity decibels" + ); + + assert!( + Decibels(f32::NAN).to_linear().is_nan(), + "NaN decibels is equivalent to NaN linear scale" + ); + assert!( + Linear(f32::NAN).to_decibels().is_nan(), + "NaN linear scale is equivalent to NaN decibels" + ); + } + + fn assert_approx_eq(a: Volume, b: Volume) { + const EPSILON: f32 = 0.0001; + + match (a, b) { + (Decibels(a), Decibels(b)) | (Linear(a), Linear(b)) => assert!( + (a - b).abs() < EPSILON, + "Expected {:?} to be approximately equal to {:?}", + a, + b + ), + (a, b) => assert!( + (a.to_decibels() - b.to_decibels()).abs() < EPSILON, + "Expected {:?} to be approximately equal to {:?}", + a, + b + ), + } + } + + #[test] + fn volume_ops_add() { + // Linear to Linear. + assert_approx_eq(Linear(0.5) + Linear(0.5), Linear(1.0)); + assert_approx_eq(Linear(0.5) + Linear(0.1), Linear(0.6)); + assert_approx_eq(Linear(0.5) + Linear(-0.5), Linear(0.0)); + + // Decibels to Decibels. + assert_approx_eq(Decibels(0.0) + Decibels(0.0), Decibels(6.0206003)); + assert_approx_eq(Decibels(6.0) + Decibels(6.0), Decibels(12.020599)); + assert_approx_eq(Decibels(-6.0) + Decibels(-6.0), Decibels(0.020599423)); + + // {Linear, Decibels} favors the left hand side of the operation. + assert_approx_eq(Linear(0.5) + Decibels(0.0), Linear(1.5)); + assert_approx_eq(Decibels(0.0) + Linear(0.5), Decibels(3.521825)); + } + + #[test] + fn volume_ops_add_assign() { + // Linear to Linear. + let mut volume = Linear(0.5); + volume += Linear(0.5); + assert_approx_eq(volume, Linear(1.0)); + } + + #[test] + fn volume_ops_sub() { + // Linear to Linear. + assert_approx_eq(Linear(0.5) - Linear(0.5), Linear(0.0)); + assert_approx_eq(Linear(0.5) - Linear(0.1), Linear(0.4)); + assert_approx_eq(Linear(0.5) - Linear(-0.5), Linear(1.0)); + + // Decibels to Decibels. + assert_eq!(Decibels(0.0) - Decibels(0.0), Decibels(f32::NEG_INFINITY)); + assert_approx_eq(Decibels(6.0) - Decibels(4.0), Decibels(-7.736506)); + assert_eq!(Decibels(-6.0) - Decibels(-6.0), Decibels(f32::NEG_INFINITY)); + } + + #[test] + fn volume_ops_sub_assign() { + // Linear to Linear. + let mut volume = Linear(0.5); + volume -= Linear(0.5); + assert_approx_eq(volume, Linear(0.0)); + } + + #[test] + fn volume_ops_mul() { + // Linear to Linear. + assert_approx_eq(Linear(0.5) * Linear(0.5), Linear(0.25)); + assert_approx_eq(Linear(0.5) * Linear(0.1), Linear(0.05)); + assert_approx_eq(Linear(0.5) * Linear(-0.5), Linear(-0.25)); + + // Decibels to Decibels. + assert_approx_eq(Decibels(0.0) * Decibels(0.0), Decibels(0.0)); + assert_approx_eq(Decibels(6.0) * Decibels(6.0), Decibels(12.0)); + assert_approx_eq(Decibels(-6.0) * Decibels(-6.0), Decibels(-12.0)); + + // {Linear, Decibels} favors the left hand side of the operation. + assert_approx_eq(Linear(0.5) * Decibels(0.0), Linear(0.5)); + assert_approx_eq(Decibels(0.0) * Linear(0.501), Decibels(-6.003246)); + } + + #[test] + fn volume_ops_mul_assign() { + // Linear to Linear. + let mut volume = Linear(0.5); + volume *= Linear(0.5); + assert_approx_eq(volume, Linear(0.25)); + + // Decibels to Decibels. + let mut volume = Decibels(6.0); + volume *= Decibels(6.0); + assert_approx_eq(volume, Decibels(12.0)); + + // {Linear, Decibels} favors the left hand side of the operation. + let mut volume = Linear(0.5); + volume *= Decibels(0.0); + assert_approx_eq(volume, Linear(0.5)); + let mut volume = Decibels(0.0); + volume *= Linear(0.501); + assert_approx_eq(volume, Decibels(-6.003246)); + } + + #[test] + fn volume_ops_div() { + // Linear to Linear. + assert_approx_eq(Linear(0.5) / Linear(0.5), Linear(1.0)); + assert_approx_eq(Linear(0.5) / Linear(0.1), Linear(5.0)); + assert_approx_eq(Linear(0.5) / Linear(-0.5), Linear(-1.0)); + + // Decibels to Decibels. + assert_approx_eq(Decibels(0.0) / Decibels(0.0), Decibels(0.0)); + assert_approx_eq(Decibels(6.0) / Decibels(6.0), Decibels(0.0)); + assert_approx_eq(Decibels(-6.0) / Decibels(-6.0), Decibels(0.0)); + + // {Linear, Decibels} favors the left hand side of the operation. + assert_approx_eq(Linear(0.5) / Decibels(0.0), Linear(0.5)); + assert_approx_eq(Decibels(0.0) / Linear(0.501), Decibels(6.003246)); + } + + #[test] + fn volume_ops_div_assign() { + // Linear to Linear. + let mut volume = Linear(0.5); + volume /= Linear(0.5); + assert_approx_eq(volume, Linear(1.0)); + + // Decibels to Decibels. + let mut volume = Decibels(6.0); + volume /= Decibels(6.0); + assert_approx_eq(volume, Decibels(0.0)); + + // {Linear, Decibels} favors the left hand side of the operation. + let mut volume = Linear(0.5); + volume /= Decibels(0.0); + assert_approx_eq(volume, Linear(0.5)); + let mut volume = Decibels(0.0); + volume /= Linear(0.501); + assert_approx_eq(volume, Decibels(6.003246)); + } } diff --git a/examples/audio/audio_control.rs b/examples/audio/audio_control.rs index 89600c69de..51f90133f5 100644 --- a/examples/audio/audio_control.rs +++ b/examples/audio/audio_control.rs @@ -1,6 +1,6 @@ //! This example illustrates how to load and play an audio file, and control how it's played. -use bevy::{math::ops, prelude::*}; +use bevy::{audio::Volume, math::ops, prelude::*}; fn main() { App::new() @@ -78,9 +78,9 @@ fn volume( if keyboard_input.just_pressed(KeyCode::Equal) { let current_volume = sink.volume(); - sink.set_volume(current_volume + 0.1); + sink.set_volume(current_volume + Volume::Linear(0.1)); } else if keyboard_input.just_pressed(KeyCode::Minus) { let current_volume = sink.volume(); - sink.set_volume(current_volume - 0.1); + sink.set_volume(current_volume - Volume::Linear(0.1)); } } diff --git a/examples/audio/decodable.rs b/examples/audio/decodable.rs index 8eb267e019..c738668219 100644 --- a/examples/audio/decodable.rs +++ b/examples/audio/decodable.rs @@ -1,7 +1,7 @@ //! Shows how to create a custom [`Decodable`] type by implementing a Sine wave. use bevy::{ - audio::{AddAudioSource, AudioPlugin, Source}, + audio::{AddAudioSource, AudioPlugin, Source, Volume}, math::ops, prelude::*, reflect::TypePath, @@ -86,7 +86,7 @@ fn main() { let mut app = App::new(); // register the audio source so that it can be used app.add_plugins(DefaultPlugins.set(AudioPlugin { - global_volume: GlobalVolume::new(0.2), + global_volume: Volume::Linear(0.2).into(), ..default() })) .add_audio_source::() diff --git a/examples/audio/soundtrack.rs b/examples/audio/soundtrack.rs index 13a8f5425c..8a6a0dfb9a 100644 --- a/examples/audio/soundtrack.rs +++ b/examples/audio/soundtrack.rs @@ -1,7 +1,7 @@ //! This example illustrates how to load and play different soundtracks, //! transitioning between them as the game state changes. -use bevy::prelude::*; +use bevy::{audio::Volume, prelude::*}; fn main() { App::new() @@ -82,7 +82,7 @@ fn change_track( AudioPlayer(soundtrack_player.track_list.first().unwrap().clone()), PlaybackSettings { mode: bevy::audio::PlaybackMode::Loop, - volume: bevy::audio::Volume::ZERO, + volume: Volume::SILENT, ..default() }, FadeIn, @@ -93,7 +93,7 @@ fn change_track( AudioPlayer(soundtrack_player.track_list.get(1).unwrap().clone()), PlaybackSettings { mode: bevy::audio::PlaybackMode::Loop, - volume: bevy::audio::Volume::ZERO, + volume: Volume::SILENT, ..default() }, FadeIn, @@ -115,9 +115,9 @@ fn fade_in( ) { for (mut audio, entity) in audio_sink.iter_mut() { let current_volume = audio.volume(); - audio.set_volume(current_volume + time.delta_secs() / FADE_TIME); - if audio.volume() >= 1.0 { - audio.set_volume(1.0); + audio.set_volume(current_volume + Volume::Linear(time.delta_secs() / FADE_TIME)); + if audio.volume().to_linear() >= 1.0 { + audio.set_volume(Volume::Linear(1.0)); commands.entity(entity).remove::(); } } @@ -132,8 +132,8 @@ fn fade_out( ) { for (mut audio, entity) in audio_sink.iter_mut() { let current_volume = audio.volume(); - audio.set_volume(current_volume - time.delta_secs() / FADE_TIME); - if audio.volume() <= 0.0 { + audio.set_volume(current_volume - Volume::Linear(time.delta_secs() / FADE_TIME)); + if audio.volume().to_linear() <= 0.0 { commands.entity(entity).despawn(); } }