Skip to content

Commit 2660ddc

Browse files
Support decibels in bevy_audio::Volume (#17605)
# 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 #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 <[email protected]>
1 parent eee7fd5 commit 2660ddc

File tree

7 files changed

+529
-82
lines changed

7 files changed

+529
-82
lines changed

crates/bevy_audio/src/audio.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl PlaybackSettings {
7474
/// added again.
7575
pub const ONCE: PlaybackSettings = PlaybackSettings {
7676
mode: PlaybackMode::Once,
77-
volume: Volume(1.0),
77+
volume: Volume::Linear(1.0),
7878
speed: 1.0,
7979
paused: false,
8080
muted: false,

crates/bevy_audio/src/audio_output.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
170170
}
171171

172172
sink.set_speed(settings.speed);
173-
sink.set_volume(settings.volume.0 * global_volume.volume.0);
173+
sink.set_volume(settings.volume * global_volume.volume);
174174

175175
if settings.paused {
176176
sink.pause();
@@ -210,7 +210,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
210210
}
211211

212212
sink.set_speed(settings.speed);
213-
sink.set_volume(settings.volume.0 * global_volume.volume.0);
213+
sink.set_volume(settings.volume * global_volume.volume);
214214

215215
if settings.paused {
216216
sink.pause();

crates/bevy_audio/src/sinks.rs

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,26 @@ use bevy_math::Vec3;
33
use bevy_transform::prelude::Transform;
44
use rodio::{Sink, SpatialSink};
55

6+
use crate::Volume;
7+
68
/// Common interactions with an audio sink.
79
pub trait AudioSinkPlayback {
8-
/// Gets the volume of the sound.
9-
///
10-
/// The value `1.0` is the "normal" volume (unfiltered input). Any value
11-
/// other than `1.0` will multiply each sample by this value.
10+
/// Gets the volume of the sound as a [`Volume`].
1211
///
1312
/// If the sink is muted, this returns the managed volume rather than the
14-
/// sink's actual volume. This allows you to use the volume as if the sink
15-
/// were not muted, because a muted sink has a volume of 0.
16-
fn volume(&self) -> f32;
13+
/// sink's actual volume. This allows you to use the returned volume as if
14+
/// the sink were not muted, because a muted sink has a physical volume of
15+
/// 0.
16+
fn volume(&self) -> Volume;
1717

18-
/// Changes the volume of the sound.
19-
///
20-
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0`
21-
/// will multiply each sample by this value.
18+
/// Changes the volume of the sound to the given [`Volume`].
2219
///
2320
/// If the sink is muted, changing the volume won't unmute it, i.e. the
24-
/// sink's volume will remain at `0.0`. However, the sink will remember the
25-
/// volume change and it will be used when [`unmute`](Self::unmute) is
26-
/// called. This allows you to control the volume even when the sink is
27-
/// muted.
28-
///
29-
/// # Note on Audio Volume
30-
///
31-
/// An increase of 10 decibels (dB) roughly corresponds to the perceived volume doubling in intensity.
32-
/// As this function scales not the volume but the amplitude, a conversion might be necessary.
33-
/// For example, to halve the perceived volume you need to decrease the volume by 10 dB.
34-
/// This corresponds to 20log(x) = -10dB, solving x = 10^(-10/20) = 0.316.
35-
/// Multiply the current volume by 0.316 to halve the perceived volume.
36-
fn set_volume(&mut self, volume: f32);
21+
/// sink's volume will remain "off" / "muted". However, the sink will
22+
/// remember the volume change and it will be used when
23+
/// [`unmute`](Self::unmute) is called. This allows you to control the
24+
/// volume even when the sink is muted.
25+
fn set_volume(&mut self, volume: Volume);
3726

3827
/// Gets the speed of the sound.
3928
///
@@ -132,7 +121,7 @@ pub struct AudioSink {
132121
/// If the sink is muted, this is `Some(volume)` where `volume` is the
133122
/// user's intended volume setting, even if the underlying sink's volume is
134123
/// 0.
135-
pub(crate) managed_volume: Option<f32>,
124+
pub(crate) managed_volume: Option<Volume>,
136125
}
137126

138127
impl AudioSink {
@@ -146,15 +135,16 @@ impl AudioSink {
146135
}
147136

148137
impl AudioSinkPlayback for AudioSink {
149-
fn volume(&self) -> f32 {
150-
self.managed_volume.unwrap_or_else(|| self.sink.volume())
138+
fn volume(&self) -> Volume {
139+
self.managed_volume
140+
.unwrap_or_else(|| Volume::Linear(self.sink.volume()))
151141
}
152142

153-
fn set_volume(&mut self, volume: f32) {
143+
fn set_volume(&mut self, volume: Volume) {
154144
if self.is_muted() {
155145
self.managed_volume = Some(volume);
156146
} else {
157-
self.sink.set_volume(volume);
147+
self.sink.set_volume(volume.to_linear());
158148
}
159149
}
160150

@@ -197,7 +187,7 @@ impl AudioSinkPlayback for AudioSink {
197187

198188
fn unmute(&mut self) {
199189
if let Some(volume) = self.managed_volume.take() {
200-
self.sink.set_volume(volume);
190+
self.sink.set_volume(volume.to_linear());
201191
}
202192
}
203193
}
@@ -227,7 +217,7 @@ pub struct SpatialAudioSink {
227217
/// If the sink is muted, this is `Some(volume)` where `volume` is the
228218
/// user's intended volume setting, even if the underlying sink's volume is
229219
/// 0.
230-
pub(crate) managed_volume: Option<f32>,
220+
pub(crate) managed_volume: Option<Volume>,
231221
}
232222

233223
impl SpatialAudioSink {
@@ -241,15 +231,16 @@ impl SpatialAudioSink {
241231
}
242232

243233
impl AudioSinkPlayback for SpatialAudioSink {
244-
fn volume(&self) -> f32 {
245-
self.managed_volume.unwrap_or_else(|| self.sink.volume())
234+
fn volume(&self) -> Volume {
235+
self.managed_volume
236+
.unwrap_or_else(|| Volume::Linear(self.sink.volume()))
246237
}
247238

248-
fn set_volume(&mut self, volume: f32) {
239+
fn set_volume(&mut self, volume: Volume) {
249240
if self.is_muted() {
250241
self.managed_volume = Some(volume);
251242
} else {
252-
self.sink.set_volume(volume);
243+
self.sink.set_volume(volume.to_linear());
253244
}
254245
}
255246

@@ -292,7 +283,7 @@ impl AudioSinkPlayback for SpatialAudioSink {
292283

293284
fn unmute(&mut self) {
294285
if let Some(volume) = self.managed_volume.take() {
295-
self.sink.set_volume(volume);
286+
self.sink.set_volume(volume.to_linear());
296287
}
297288
}
298289
}
@@ -326,11 +317,11 @@ mod tests {
326317

327318
fn test_audio_sink_playback<T: AudioSinkPlayback>(mut audio_sink: T) {
328319
// Test volume
329-
assert_eq!(audio_sink.volume(), 1.0); // default volume
330-
audio_sink.set_volume(0.5);
331-
assert_eq!(audio_sink.volume(), 0.5);
332-
audio_sink.set_volume(1.0);
333-
assert_eq!(audio_sink.volume(), 1.0);
320+
assert_eq!(audio_sink.volume(), Volume::Linear(1.0)); // default volume
321+
audio_sink.set_volume(Volume::Linear(0.5));
322+
assert_eq!(audio_sink.volume(), Volume::Linear(0.5));
323+
audio_sink.set_volume(Volume::Linear(1.0));
324+
assert_eq!(audio_sink.volume(), Volume::Linear(1.0));
334325

335326
// Test speed
336327
assert_eq!(audio_sink.speed(), 1.0); // default speed
@@ -361,11 +352,11 @@ mod tests {
361352
assert!(!audio_sink.is_muted());
362353

363354
// Test volume with mute
364-
audio_sink.set_volume(0.5);
355+
audio_sink.set_volume(Volume::Linear(0.5));
365356
audio_sink.mute();
366-
assert_eq!(audio_sink.volume(), 0.5); // returns managed volume even though sink volume is 0
357+
assert_eq!(audio_sink.volume(), Volume::Linear(0.5)); // returns managed volume even though sink volume is 0
367358
audio_sink.unmute();
368-
assert_eq!(audio_sink.volume(), 0.5); // managed volume is restored
359+
assert_eq!(audio_sink.volume(), Volume::Linear(0.5)); // managed volume is restored
369360

370361
// Test toggle mute
371362
audio_sink.toggle_mute();

0 commit comments

Comments
 (0)