Skip to content

Commit a35eed0

Browse files
foxzoolalice-i-cecilejanhohenheim
authored
fix: Ensure linear volume subtraction does not go below zero (#19423)
fix: [Ensure linear volume subtraction does not go below zero ](#19417) ## Solution - Clamp the result of linear volume subtraction to a minimum of 0.0 - Add a new test case to verify behavior when subtracting beyond zero --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Jan Hohenheim <[email protected]>
1 parent d0f1b3e commit a35eed0

File tree

4 files changed

+189
-99
lines changed

4 files changed

+189
-99
lines changed

crates/bevy_audio/src/volume.rs

Lines changed: 155 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ impl GlobalVolume {
3434
#[derive(Clone, Copy, Debug, Reflect)]
3535
#[reflect(Clone, Debug, PartialEq)]
3636
pub enum Volume {
37-
/// Create a new [`Volume`] from the given volume in linear scale.
37+
/// Create a new [`Volume`] from the given volume in the linear scale.
3838
///
3939
/// In a linear scale, the value `1.0` represents the "normal" volume,
4040
/// meaning the audio is played at its original level. Values greater than
@@ -144,7 +144,7 @@ impl Volume {
144144

145145
/// Returns the volume in decibels as a float.
146146
///
147-
/// If the volume is silent / off / muted, i.e. its underlying linear scale
147+
/// If the volume is silent / off / muted, i.e., its underlying linear scale
148148
/// is `0.0`, this method returns negative infinity.
149149
pub fn to_decibels(&self) -> f32 {
150150
match self {
@@ -155,57 +155,95 @@ impl Volume {
155155

156156
/// The silent volume. Also known as "off" or "muted".
157157
pub const SILENT: Self = Volume::Linear(0.0);
158-
}
159-
160-
impl core::ops::Add<Self> for Volume {
161-
type Output = Self;
162158

163-
fn add(self, rhs: Self) -> Self {
164-
use Volume::{Decibels, Linear};
165-
166-
match (self, rhs) {
167-
(Linear(a), Linear(b)) => Linear(a + b),
168-
(Decibels(a), Decibels(b)) => Decibels(linear_to_decibels(
169-
decibels_to_linear(a) + decibels_to_linear(b),
170-
)),
171-
// {Linear, Decibels} favors the left hand side of the operation by
172-
// first converting the right hand side to the same type as the left
173-
// hand side and then performing the operation.
174-
(Linear(..), Decibels(db)) => self + Linear(decibels_to_linear(db)),
175-
(Decibels(..), Linear(l)) => self + Decibels(linear_to_decibels(l)),
176-
}
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)
177178
}
178-
}
179179

180-
impl core::ops::AddAssign<Self> for Volume {
181-
fn add_assign(&mut self, rhs: Self) {
182-
*self = *self + rhs;
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)
183199
}
184-
}
185-
186-
impl core::ops::Sub<Self> for Volume {
187-
type Output = Self;
188200

189-
fn sub(self, rhs: Self) -> Self {
190-
use Volume::{Decibels, Linear};
191-
192-
match (self, rhs) {
193-
(Linear(a), Linear(b)) => Linear(a - b),
194-
(Decibels(a), Decibels(b)) => Decibels(linear_to_decibels(
195-
decibels_to_linear(a) - decibels_to_linear(b),
196-
)),
197-
// {Linear, Decibels} favors the left hand side of the operation by
198-
// first converting the right hand side to the same type as the left
199-
// hand side and then performing the operation.
200-
(Linear(..), Decibels(db)) => self - Linear(decibels_to_linear(db)),
201-
(Decibels(..), Linear(l)) => self - Decibels(linear_to_decibels(l)),
202-
}
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)
203220
}
204-
}
205221

206-
impl core::ops::SubAssign<Self> for Volume {
207-
fn sub_assign(&mut self, rhs: Self) {
208-
*self = *self - rhs;
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)
209247
}
210248
}
211249

@@ -337,8 +375,9 @@ mod tests {
337375
Linear(f32::NEG_INFINITY).to_decibels().is_infinite(),
338376
"Negative infinite linear scale is equivalent to infinite decibels"
339377
);
340-
assert!(
341-
Decibels(f32::NEG_INFINITY).to_linear().abs() == 0.0,
378+
assert_eq!(
379+
Decibels(f32::NEG_INFINITY).to_linear().abs(),
380+
0.0,
342381
"Negative infinity decibels is equivalent to zero linear scale"
343382
);
344383

@@ -361,6 +400,74 @@ mod tests {
361400
);
362401
}
363402

403+
#[test]
404+
fn test_increase_by_percentage() {
405+
let volume = Linear(1.0);
406+
407+
// 100% increase should double the volume
408+
let increased = volume.increase_by_percentage(100.0);
409+
assert_eq!(increased.to_linear(), 2.0);
410+
411+
// 50% increase
412+
let increased = volume.increase_by_percentage(50.0);
413+
assert_eq!(increased.to_linear(), 1.5);
414+
}
415+
416+
#[test]
417+
fn test_decrease_by_percentage() {
418+
let volume = Linear(1.0);
419+
420+
// 50% decrease should halve the volume
421+
let decreased = volume.decrease_by_percentage(50.0);
422+
assert_eq!(decreased.to_linear(), 0.5);
423+
424+
// 25% decrease
425+
let decreased = volume.decrease_by_percentage(25.0);
426+
assert_eq!(decreased.to_linear(), 0.75);
427+
428+
// 100% decrease should result in silence
429+
let decreased = volume.decrease_by_percentage(100.0);
430+
assert_eq!(decreased.to_linear(), 0.0);
431+
}
432+
433+
#[test]
434+
fn test_scale_to_factor() {
435+
let volume = Linear(0.8);
436+
let scaled = volume.scale_to_factor(1.25);
437+
assert_eq!(scaled.to_linear(), 1.0);
438+
}
439+
440+
#[test]
441+
fn test_fade_towards() {
442+
let current = Linear(1.0);
443+
let target = Linear(0.0);
444+
445+
// 50% fade should result in 0.5 linear volume
446+
let faded = current.fade_towards(target, 0.5);
447+
assert_eq!(faded.to_linear(), 0.5);
448+
449+
// 0% fade should keep current volume
450+
let faded = current.fade_towards(target, 0.0);
451+
assert_eq!(faded.to_linear(), 1.0);
452+
453+
// 100% fade should reach target volume
454+
let faded = current.fade_towards(target, 1.0);
455+
assert_eq!(faded.to_linear(), 0.0);
456+
}
457+
458+
#[test]
459+
fn test_decibel_math_properties() {
460+
let volume = Linear(1.0);
461+
462+
// Adding 20dB should multiply linear volume by 10
463+
let adjusted = volume * Decibels(20.0);
464+
assert_approx_eq(adjusted, Linear(10.0));
465+
466+
// Subtracting 20dB should divide linear volume by 10
467+
let adjusted = volume / Decibels(20.0);
468+
assert_approx_eq(adjusted, Linear(0.1));
469+
}
470+
364471
fn assert_approx_eq(a: Volume, b: Volume) {
365472
const EPSILON: f32 = 0.0001;
366473

@@ -380,52 +487,6 @@ mod tests {
380487
}
381488
}
382489

383-
#[test]
384-
fn volume_ops_add() {
385-
// Linear to Linear.
386-
assert_approx_eq(Linear(0.5) + Linear(0.5), Linear(1.0));
387-
assert_approx_eq(Linear(0.5) + Linear(0.1), Linear(0.6));
388-
assert_approx_eq(Linear(0.5) + Linear(-0.5), Linear(0.0));
389-
390-
// Decibels to Decibels.
391-
assert_approx_eq(Decibels(0.0) + Decibels(0.0), Decibels(6.0206003));
392-
assert_approx_eq(Decibels(6.0) + Decibels(6.0), Decibels(12.020599));
393-
assert_approx_eq(Decibels(-6.0) + Decibels(-6.0), Decibels(0.020599423));
394-
395-
// {Linear, Decibels} favors the left hand side of the operation.
396-
assert_approx_eq(Linear(0.5) + Decibels(0.0), Linear(1.5));
397-
assert_approx_eq(Decibels(0.0) + Linear(0.5), Decibels(3.521825));
398-
}
399-
400-
#[test]
401-
fn volume_ops_add_assign() {
402-
// Linear to Linear.
403-
let mut volume = Linear(0.5);
404-
volume += Linear(0.5);
405-
assert_approx_eq(volume, Linear(1.0));
406-
}
407-
408-
#[test]
409-
fn volume_ops_sub() {
410-
// Linear to Linear.
411-
assert_approx_eq(Linear(0.5) - Linear(0.5), Linear(0.0));
412-
assert_approx_eq(Linear(0.5) - Linear(0.1), Linear(0.4));
413-
assert_approx_eq(Linear(0.5) - Linear(-0.5), Linear(1.0));
414-
415-
// Decibels to Decibels.
416-
assert_eq!(Decibels(0.0) - Decibels(0.0), Decibels(f32::NEG_INFINITY));
417-
assert_approx_eq(Decibels(6.0) - Decibels(4.0), Decibels(-7.736506));
418-
assert_eq!(Decibels(-6.0) - Decibels(-6.0), Decibels(f32::NEG_INFINITY));
419-
}
420-
421-
#[test]
422-
fn volume_ops_sub_assign() {
423-
// Linear to Linear.
424-
let mut volume = Linear(0.5);
425-
volume -= Linear(0.5);
426-
assert_approx_eq(volume, Linear(0.0));
427-
}
428-
429490
#[test]
430491
fn volume_ops_mul() {
431492
// Linear to Linear.

examples/audio/audio_control.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! This example illustrates how to load and play an audio file, and control how it's played.
22
3-
use bevy::{audio::Volume, math::ops, prelude::*};
3+
use bevy::{math::ops, prelude::*};
44

55
fn main() {
66
App::new()
@@ -105,9 +105,9 @@ fn volume(
105105

106106
if keyboard_input.just_pressed(KeyCode::Equal) {
107107
let current_volume = sink.volume();
108-
sink.set_volume(current_volume + Volume::Linear(0.1));
108+
sink.set_volume(current_volume.increase_by_percentage(10.0));
109109
} else if keyboard_input.just_pressed(KeyCode::Minus) {
110110
let current_volume = sink.volume();
111-
sink.set_volume(current_volume - Volume::Linear(0.1));
111+
sink.set_volume(current_volume.increase_by_percentage(-10.0));
112112
}
113113
}

examples/audio/soundtrack.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ fn fade_in(
115115
) {
116116
for (mut audio, entity) in audio_sink.iter_mut() {
117117
let current_volume = audio.volume();
118-
audio.set_volume(current_volume + Volume::Linear(time.delta_secs() / FADE_TIME));
118+
audio.set_volume(
119+
current_volume.fade_towards(Volume::Linear(1.0), time.delta_secs() / FADE_TIME),
120+
);
119121
if audio.volume().to_linear() >= 1.0 {
120122
audio.set_volume(Volume::Linear(1.0));
121123
commands.entity(entity).remove::<FadeIn>();
@@ -132,7 +134,9 @@ fn fade_out(
132134
) {
133135
for (mut audio, entity) in audio_sink.iter_mut() {
134136
let current_volume = audio.volume();
135-
audio.set_volume(current_volume - Volume::Linear(time.delta_secs() / FADE_TIME));
137+
audio.set_volume(
138+
current_volume.fade_towards(Volume::Linear(0.0), time.delta_secs() / FADE_TIME),
139+
);
136140
if audio.volume().to_linear() <= 0.0 {
137141
commands.entity(entity).despawn();
138142
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: remove the Add/Sub impls on Volume
3+
pull_requests: [ 19423 ]
4+
---
5+
6+
Linear volumes are like percentages, and it does not make sense to add or subtract percentages.
7+
As such, use the new `increase_by_percentage` function instead of addition or subtraction.
8+
9+
```rust
10+
// 0.16
11+
fn audio_system() {
12+
let linear_a = Volume::Linear(0.5);
13+
let linear_b = Volume::Linear(0.1);
14+
let linear_c = linear_a + linear_b;
15+
let linear_d = linear_a - linear_b;
16+
}
17+
18+
// 0.17
19+
fn audio_system() {
20+
let linear_a = Volume::Linear(0.5);
21+
let linear_b = Volume::Linear(0.1);
22+
let linear_c = linear_a.increase_by_percentage(10.0);
23+
let linear_d = linear_a.increase_by_percentage(-10.0);
24+
}
25+
```

0 commit comments

Comments
 (0)