use tracing::warn;
use super::error::Error;
#[derive(Debug, Clone, PartialEq)]
pub struct Volume {
volumes: Vec<f64>,
}
impl Volume {
pub fn new(volumes: Vec<f64>) -> Self {
let volumes = volumes.into_iter().map(|v| {
let clamped = v.clamp(0.0, 4.0);
if v > 2.0 && v <= 4.0 {
warn!("Volume {v} exceeds safe limit (2.0). Audio damage possible at high amplification.");
} else if v > 4.0 {
warn!("Volume {v} clamped to maximum (4.0). Use values ≤2.0 for safe operation.");
} else if v < 0.0 {
warn!("Negative volume {v} clamped to 0.0.");
}
clamped
}).collect();
Self { volumes }
}
pub fn with_amplification(volumes: Vec<f64>) -> Result<Self, Error> {
for (channel, &volume) in volumes.iter().enumerate() {
if !(0.0..=4.0).contains(&volume) {
return Err(Error::InvalidVolume { channel, volume });
}
}
Ok(Self { volumes })
}
pub fn mono(volume: f64) -> Self {
Self::new(vec![volume])
}
pub fn stereo(left: f64, right: f64) -> Self {
Self::new(vec![left, right])
}
pub fn channel(&self, channel: usize) -> Option<f64> {
self.volumes.get(channel).copied()
}
#[allow(clippy::cognitive_complexity)]
pub fn set_channel(&mut self, channel: usize, volume: f64) -> Result<(), Error> {
if let Some(vol) = self.volumes.get_mut(channel) {
let clamped = volume.clamp(0.0, 4.0);
if volume > 2.0 && volume <= 4.0 {
warn!(
"Volume {volume} exceeds safe limit (2.0). Audio damage possible at high amplification."
);
} else if volume > 4.0 {
warn!(
"Volume {volume} clamped to maximum (4.0). Use values ≤2.0 for safe operation."
);
} else if volume < 0.0 {
warn!("Negative volume {volume} clamped to 0.0.");
}
*vol = clamped;
Ok(())
} else {
Err(Error::InvalidChannel { channel })
}
}
pub fn average(&self) -> f64 {
if self.volumes.is_empty() {
0.0
} else {
self.volumes.iter().sum::<f64>() / self.volumes.len() as f64
}
}
pub fn channels(&self) -> usize {
self.volumes.len()
}
pub fn as_slice(&self) -> &[f64] {
&self.volumes
}
pub fn muted(channels: usize) -> Self {
Self::new(vec![0.0; channels])
}
pub fn normal(channels: usize) -> Self {
Self::new(vec![1.0; channels])
}
pub fn from_percentage(percentage: f64, channels: usize) -> Self {
let volume = percentage / 100.0;
Self::new(vec![volume; channels])
}
pub fn to_percentage(&self) -> Vec<f64> {
self.volumes.iter().map(|&v| v * 100.0).collect()
}
pub fn average_percentage(&self) -> f64 {
self.average() * 100.0
}
pub fn is_muted(&self) -> bool {
self.volumes.iter().all(|&v| v == 0.0)
}
pub fn is_normal(&self) -> bool {
self.volumes.iter().all(|&v| v == 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_clamps_negative_volumes_to_zero() {
let volume = Volume::new(vec![-1.0, -0.5]);
assert_eq!(volume.as_slice(), &[0.0, 0.0]);
}
#[test]
fn new_clamps_volumes_above_four_to_four() {
let volume = Volume::new(vec![5.0, 10.0]);
assert_eq!(volume.as_slice(), &[4.0, 4.0]);
}
#[test]
fn new_preserves_volumes_in_valid_range() {
let volume = Volume::new(vec![0.5, 1.0, 2.0, 3.5]);
assert_eq!(volume.as_slice(), &[0.5, 1.0, 2.0, 3.5]);
}
#[test]
fn mono_creates_single_channel_volume() {
let volume = Volume::mono(1.5);
assert_eq!(volume.channels(), 1);
assert_eq!(volume.channel(0), Some(1.5));
}
#[test]
fn stereo_creates_two_channel_volume() {
let volume = Volume::stereo(0.8, 1.2);
assert_eq!(volume.channels(), 2);
assert_eq!(volume.channel(0), Some(0.8));
assert_eq!(volume.channel(1), Some(1.2));
}
#[test]
fn with_amplification_accepts_valid_range() {
let result = Volume::with_amplification(vec![0.0, 2.0, 4.0]);
assert!(result.is_ok());
let volume = result.unwrap();
assert_eq!(volume.as_slice(), &[0.0, 2.0, 4.0]);
}
#[test]
fn with_amplification_rejects_negative_volume() {
let result = Volume::with_amplification(vec![-0.1]);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::InvalidVolume { channel: 0, .. }
));
}
#[test]
fn with_amplification_rejects_volume_above_four() {
let result = Volume::with_amplification(vec![4.1]);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::InvalidVolume { channel: 0, .. }
));
}
#[test]
fn set_channel_updates_valid_channel() {
let mut volume = Volume::stereo(1.0, 1.0);
let result = volume.set_channel(0, 0.5);
assert!(result.is_ok());
assert_eq!(volume.channel(0), Some(0.5));
assert_eq!(volume.channel(1), Some(1.0));
}
#[test]
fn set_channel_clamps_out_of_range_values() {
let mut volume = Volume::mono(1.0);
volume.set_channel(0, -1.0).ok();
assert_eq!(volume.channel(0), Some(0.0));
volume.set_channel(0, 5.0).ok();
assert_eq!(volume.channel(0), Some(4.0));
}
#[test]
fn set_channel_returns_error_for_invalid_channel() {
let mut volume = Volume::mono(1.0);
let result = volume.set_channel(5, 1.0);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::InvalidChannel { channel: 5 }
));
}
#[test]
fn channel_returns_volume_for_valid_index() {
let volume = Volume::new(vec![0.5, 1.0, 1.5]);
assert_eq!(volume.channel(0), Some(0.5));
assert_eq!(volume.channel(1), Some(1.0));
assert_eq!(volume.channel(2), Some(1.5));
}
#[test]
fn channel_returns_none_for_invalid_index() {
let volume = Volume::mono(1.0);
assert_eq!(volume.channel(1), None);
assert_eq!(volume.channel(100), None);
}
#[test]
fn average_returns_zero_for_empty_channels() {
let volume = Volume::new(vec![]);
assert_eq!(volume.average(), 0.0);
}
#[test]
fn average_calculates_correct_value_for_multiple_channels() {
let volume = Volume::new(vec![0.5, 1.0, 1.5, 2.0]);
let avg = volume.average();
assert!((avg - 1.25).abs() < 0.01);
}
#[test]
fn is_muted_returns_true_when_all_channels_zero() {
let volume = Volume::new(vec![0.0, 0.0, 0.0]);
assert!(volume.is_muted());
}
#[test]
fn is_muted_returns_false_when_any_channel_nonzero() {
let volume = Volume::new(vec![0.0, 0.1, 0.0]);
assert!(!volume.is_muted());
}
#[test]
fn is_normal_returns_true_when_all_channels_one() {
let volume = Volume::new(vec![1.0, 1.0, 1.0]);
assert!(volume.is_normal());
}
#[test]
fn is_normal_returns_false_when_any_channel_not_one() {
let volume = Volume::new(vec![1.0, 0.9, 1.0]);
assert!(!volume.is_normal());
}
#[test]
fn from_percentage_converts_correctly() {
let volume = Volume::from_percentage(50.0, 2);
assert_eq!(volume.channels(), 2);
assert_eq!(volume.channel(0), Some(0.5));
assert_eq!(volume.channel(1), Some(0.5));
}
#[test]
fn to_percentage_converts_correctly() {
let volume = Volume::new(vec![0.5, 1.0, 1.5]);
let percentages = volume.to_percentage();
assert_eq!(percentages, vec![50.0, 100.0, 150.0]);
}
#[test]
fn average_percentage_returns_average_as_percentage() {
let volume = Volume::stereo(0.5, 1.0);
let avg_pct = volume.average_percentage();
assert!((avg_pct - 75.0).abs() < 0.01);
}
}