Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support decibels in bevy_audio::Volume #17605

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

mgi388
Copy link
Contributor

@mgi388 mgi388 commented Jan 29, 2025

Objective

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. Instead of enums, it newtypes Volume, with constructors (and no new()) as requested and blessed on Discord.
  3. It removes any handling or consideration of "phase inversion" by making Volume::from_linear clamp negative values to 0 instead of allowing them. Experts to prove me wrong, but I don't think Bevy needs to try and handle that, at least not now, and so the simplest solution seems to me to just clamp negative values to 0. I kept the debug_assert! though, and made it more helpful to users by adding an error message to it.

Testing

  • Change soundtrack.rs to have a negative volume.
  • Ran cargo run --example soundtrack and prove it panics.
  • Ran cargo run --example soundtrack --release and prove it does not panic.
  • 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::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 f32s. AudioSinkPlayback::volume() now returns a Volume rather than an f32. AudioSinkPlayback::set_volume now receives an impl Into<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 and the Volume:: from_linear and Volume:: to_linear functions should be used to migrate between f32s and Volume.
  • The GlobalVolume::new function now receives a Volume instead of an f32.

@alice-i-cecile alice-i-cecile added A-Audio Sounds playback and modification C-Usability A targeted quality-of-life change that makes Bevy easier to use X-Contentious There are nontrivial implications that should be thought through S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jan 30, 2025
@mgi388 mgi388 changed the title WIP: Support decibels in bevy_audio::Volume Support decibels in bevy_audio::Volume Jan 31, 2025
@mgi388 mgi388 marked this pull request as ready for review January 31, 2025 03:00
/// called. This allows you to control the volume even when the sink is
/// muted.
///
/// # Note on Audio Volume
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this part of the docs because the function now receives a Volume not an f32 and I feel like users can go to the Volume docs to read about decibels, but on top of that I feel like they can just read about decibels externally if they want details on the math.

Copy link
Contributor

@SolarLiner SolarLiner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I went into the territory of bikeshedding, but I have some comments on this, which maybe warrants a new pass at the PR.

crates/bevy_audio/src/sinks.rs Outdated Show resolved Hide resolved
crates/bevy_audio/src/volume.rs Outdated Show resolved Hide resolved
@@ -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.to_linear() + 0.1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This thing of having to "unwrap" volumes just to manipulate them is a bit of a let down, to be honest. The solution with two types would allow us to implement numerical operations on them, making them more straightforward to use, and remove the need to "leak the abstraction" so to say.

I'm not sure if this is bothersome enough to warrant a change though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be fine implementing core::ops::Add and core::ops::Sub for Volume. As above:

I think your argument makes sense: If we're trying to encapsulate the underlying storage of Volume from the user, why is linear favored over decibes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding Add/Sub on Volume has the same caveats of favoring one unit over the other ; if you implement them in terms of linear volume, then people using decibels will end up with the wrong values (e.g. -6 dB + -6 dB = -12 dB; -6 dB = 0.5; 0.5 + 0.5 = 1 = 0 dB) as adding decibels means multiplying the linear values (because the log transforms multiplications into additions). This is why I said that if implementing those is valuable, then it might be better to separate the Volume enum into separate types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a change that adds Add/Sub. I'm not really confident in the math, I need to do more research to see if what I'm doing is correct. I used some calculators to get these test values and formulas, but again, not really confident. If you have any really good references, or you want to review the math directly, help is welcome.

I also need to handle the mixed Decibels / Linear case. I don't think it's defined how to add/sub these, so I'm guessing we just need to pick a kind and convert the other to that kind, then do the op?

Copy link
Contributor Author

@mgi388 mgi388 Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alice-i-cecile alice-i-cecile added D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jan 31, 2025
@BenjaminBrienen
Copy link
Contributor

I think the points in the previous review should be addressed. I agree with the problems they pointed out.

@mgi388 mgi388 added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 3, 2025
(Decibels(a), Decibels(b)) => Decibels(
10.0 * ops::log10(ops::powf(10.0f32, a / 10.0) + ops::powf(10.0f32, b / 10.0)),
),
(a, b) => panic!("not implemented: {:?} + {:?}", a, b),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I was saying separate types are better with operations. Since it doesn't make sense to work with mixed linear/decibel values, it becomes a chore to make sure (or check at runtime) that all values are of the right in order to prevent a panic. Not that volume math is going to be happening all over the place, but this is a paper cut and seems preventable at the type system level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could let the left-hand side dictate the unit of the result, and convert the right hand side to this unit. This would make the binary operation work more similarly to the add/sub-assign operator (which would be nice to implement if you fancy doing it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I didn't intend to leave the panic.

let the left-hand side dictate the unit of the result

Sounds reasonable to me.

I can do the -assign op as well.


Re:

This is why I was saying separate types are better with operations.

Did I overlook what you were saying (again), and you were not proposing to go back to enums, but you were proposing to somehow to have completely separate types pub struct LinearVolume and pub struct DecibelVolume (or whatever they are named), and somehow Bevy supports both types (e.g. using traits where required I guess)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry this is from a previous comment saying that it would be nicer if we want to be able to add/sub volume between each other, that separate types would make it nicer to force the user to not mix units, because you otherwise end up with having to implement adding a linear volume and a decibel volume, and it's unclear what the result should be.

That being said, the solution of tying the resulting unit to that of the left-hand side seems a good compromise to continue using enums.

match (self, rhs) {
(Linear(a), Linear(b)) => Linear(a + b),
(Decibels(a), Decibels(b)) => Decibels(
10.0 * ops::log10(ops::powf(10.0f32, a / 10.0) + ops::powf(10.0f32, b / 10.0)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, seeing this makes me realize that this is indeed the correct implementation for +, but it's not the most useful, because in most cases with volumes you want to multiply them together, so I would suggest additionally implementing ops::Mul with multiplication of linear values, and straight addition of decibel values.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the above implementation is "wrong" in the sense that we are generally talking about power waves in audio (both when measuring sound pressure, and when working with voltage level), which means the correct coefficient is 20 instead of 10 in front of the log and in the division. This is equivalent (and most probably will compile the same) to Linear(a.to_linear() + b.to_linear()).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yeah this is what I wasn't too sure about the coefficient. I can adjust.


Re:

so I would suggest additionally implementing ops::Mul with multiplication of linear values, and straight addition of decibel values.

I can implement Mul. I noticed that for getting the volume of a single source where we need to multiply by the global volume it would be useful to not have to do the linear dance it's doing.

Can you confirm:

LinearVol(a) x LinearVol(b) = a x b (e.g. LinearVol(0.5) x LinearVol(0.5) = 0.5 x 0.5) = 0.25 = LinearVol(0.25)). And this is extremely intuitive for me: If my single source is playing at "half" but I've said that my global volume is also "half", I thoroughly expect it to play at half that: 0.25.

And just checking that my interpretation of the decibels version is correct: DecibelsVol(a) x DecibelsVol(b) = a + b (e.g. DecibelsVol(0.5) x DecibelsVol(0.5) = 0.5 + 0.5 = 1.0 = DecibelsVol(1.0)).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Audio Sounds playback and modification C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Contentious There are nontrivial implications that should be thought through
Projects
Status: Needs Update
Development

Successfully merging this pull request may close these issues.

Expose both linear and logarithmic volume settings
4 participants