-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: Zachary Harrold <[email protected]>
/// called. This allows you to control the volume even when the sink is | ||
/// muted. | ||
/// | ||
/// # Note on Audio Volume |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
examples/audio/audio_control.rs
Outdated
@@ -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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
- https://au.noisemeters.com/apps/db-calculator/
- https://www.muzique.com/schem/deci.htm
- https://noisetools.net/decibelcalculator
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test cases may be a good place to start to even eyeball: https://github.com/bevyengine/bevy/pull/17605/files#diff-13f694a1cbe0a27794a4621cb4231d0b7b868598d7a4824a1dadf677de07cd48R290-R320
I think the points in the previous review should be addressed. I agree with the problems they pointed out. |
Co-authored-by: Zachary Harrold <[email protected]>
(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), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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)), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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())
.
There was a problem hiding this comment.
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)
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is correct.
Objective
Volume
type.VolumeLevel
to use Decibel and Amplitude Ratio Units #9582.Solution
Compared to #9582, this PR has the following main differences:
Volume
, with constructors (and nonew()
) as requested and blessed on Discord.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 thedebug_assert!
though, and made it more helpful to users by adding an error message to it.Testing
soundtrack.rs
to have a negative volume.cargo run --example soundtrack
and prove it panics.cargo run --example soundtrack --release
and prove it does not panic.cargo run --example audio_control
.cargo run --example spatial_audio_2d
.cargo run --example spatial_audio_3d
.cargo run --example pitch
.cargo run --example decodable
.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 correctVolume::SILENT
becauseVolume
now supports decibels and "zero volume" in decibels actually means "normal volume".AudioSinkPlayback
trait's volume-related methods now deal withVolume
types rather thanf32
s.AudioSinkPlayback::volume()
now returns aVolume
rather than anf32
.AudioSinkPlayback::set_volume
now receives animpl Into<Volume>
rather than anf32
. This affects theAudioSink
andSpatialAudioSink
implementations of the trait. The previousf32
values are equivalent to theVolume
converted to linear scale and theVolume:: from_linear
andVolume:: to_linear
functions should be used to migrate betweenf32
s andVolume
.GlobalVolume::new
function now receives aVolume
instead of anf32
.