use bevy_ecs::prelude::*;
use bevy_math::ops;
use bevy_reflect::prelude::*;
#[derive(Resource, Debug, Default, Clone, Copy, Reflect)]
#[reflect(Resource, Debug, Default, Clone)]
pub struct GlobalVolume {
pub volume: Volume,
}
impl From<Volume> for GlobalVolume {
fn from(volume: Volume) -> Self {
Self { volume }
}
}
impl GlobalVolume {
pub fn new(volume: Volume) -> Self {
Self { volume }
}
}
#[derive(Clone, Copy, Debug, Reflect)]
#[reflect(Clone, Debug, PartialEq)]
pub enum Volume {
Linear(f32),
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(),
}
}
}
impl PartialOrd for Volume {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
use Volume::{Decibels, Linear};
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 {
pub fn to_linear(&self) -> f32 {
match self {
Self::Linear(v) => v.abs(),
Self::Decibels(v) => decibels_to_linear(*v),
}
}
pub fn to_decibels(&self) -> f32 {
match self {
Self::Linear(v) => linear_to_decibels(*v),
Self::Decibels(v) => *v,
}
}
pub const SILENT: Self = Volume::Linear(0.0);
pub fn increase_by_percentage(&self, percentage: f32) -> Self {
let factor = 1.0 + (percentage / 100.0);
Volume::Linear(self.to_linear() * factor)
}
pub fn decrease_by_percentage(&self, percentage: f32) -> Self {
let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0);
Volume::Linear(self.to_linear() * factor)
}
pub fn scale_to_factor(&self, factor: f32) -> Self {
Volume::Linear(self.to_linear() * factor)
}
pub fn fade_towards(&self, target: Volume, factor: f32) -> Self {
let current_linear = self.to_linear();
let target_linear = target.to_linear();
let factor_clamped = factor.clamp(0.0, 1.0);
let interpolated = current_linear + (target_linear - current_linear) * factor_clamped;
Volume::Linear(interpolated)
}
}
impl core::ops::Mul<Self> 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(db)) => self * Linear(decibels_to_linear(db)),
(Decibels(..), Linear(l)) => self * Decibels(linear_to_decibels(l)),
}
}
}
impl core::ops::MulAssign<Self> for Volume {
fn mul_assign(&mut self, rhs: Self) {
*self = *self * rhs;
}
}
impl core::ops::Div<Self> 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(db)) => self / Linear(decibels_to_linear(db)),
(Decibels(..), Linear(l)) => self / Decibels(linear_to_decibels(l)),
}
}
}
impl core::ops::DivAssign<Self> for Volume {
fn div_assign(&mut self, rhs: Self) {
*self = *self / rhs;
}
}
#[cfg(test)]
mod tests {
use super::Volume::{self, Decibels, Linear};
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}dB, got {db_test}dB (delta {db_delta})",
);
assert!(
linear_relative_delta.abs() < 1e-3,
"Expected ~{linear}, got {linear_test} (relative delta {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_eq!(
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"
);
}
#[test]
fn test_increase_by_percentage() {
let volume = Linear(1.0);
let increased = volume.increase_by_percentage(100.0);
assert_eq!(increased.to_linear(), 2.0);
let increased = volume.increase_by_percentage(50.0);
assert_eq!(increased.to_linear(), 1.5);
}
#[test]
fn test_decrease_by_percentage() {
let volume = Linear(1.0);
let decreased = volume.decrease_by_percentage(50.0);
assert_eq!(decreased.to_linear(), 0.5);
let decreased = volume.decrease_by_percentage(25.0);
assert_eq!(decreased.to_linear(), 0.75);
let decreased = volume.decrease_by_percentage(100.0);
assert_eq!(decreased.to_linear(), 0.0);
}
#[test]
fn test_scale_to_factor() {
let volume = Linear(0.8);
let scaled = volume.scale_to_factor(1.25);
assert_eq!(scaled.to_linear(), 1.0);
}
#[test]
fn test_fade_towards() {
let current = Linear(1.0);
let target = Linear(0.0);
let faded = current.fade_towards(target, 0.5);
assert_eq!(faded.to_linear(), 0.5);
let faded = current.fade_towards(target, 0.0);
assert_eq!(faded.to_linear(), 1.0);
let faded = current.fade_towards(target, 1.0);
assert_eq!(faded.to_linear(), 0.0);
}
#[test]
fn test_decibel_math_properties() {
let volume = Linear(1.0);
let adjusted = volume * Decibels(20.0);
assert_approx_eq(adjusted, Linear(10.0));
let adjusted = volume / Decibels(20.0);
assert_approx_eq(adjusted, Linear(0.1));
}
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 {a:?} to be approximately equal to {b:?}",
),
(a, b) => assert!(
(a.to_decibels() - b.to_decibels()).abs() < EPSILON,
"Expected {a:?} to be approximately equal to {b:?}",
),
}
}
#[test]
fn volume_ops_mul() {
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));
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));
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() {
let mut volume = Linear(0.5);
volume *= Linear(0.5);
assert_approx_eq(volume, Linear(0.25));
let mut volume = Decibels(6.0);
volume *= Decibels(6.0);
assert_approx_eq(volume, Decibels(12.0));
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() {
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));
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));
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() {
let mut volume = Linear(0.5);
volume /= Linear(0.5);
assert_approx_eq(volume, Linear(1.0));
let mut volume = Decibels(6.0);
volume /= Decibels(6.0);
assert_approx_eq(volume, Decibels(0.0));
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));
}
}