use crate::{AudexError, Result};
use std::fmt;
fn validate_finite(value: f32, label: &str) -> std::result::Result<(), AudexError> {
if value.is_finite() {
Ok(())
} else {
Err(AudexError::InvalidData(format!(
"{} must be finite, got: {}",
label, value
)))
}
}
pub const REPLAYGAIN_REFERENCE_LEVEL: f32 = 89.0;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(into = "ReplayGainInfoRaw"))]
pub struct ReplayGainInfo {
track_gain: Option<f32>,
track_peak: Option<f32>,
album_gain: Option<f32>,
album_peak: Option<f32>,
reference_level: f32,
pub warnings: Vec<String>,
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize, serde::Serialize)]
struct ReplayGainInfoRaw {
track_gain: Option<f32>,
track_peak: Option<f32>,
album_gain: Option<f32>,
album_peak: Option<f32>,
reference_level: f32,
}
#[cfg(feature = "serde")]
impl From<ReplayGainInfo> for ReplayGainInfoRaw {
fn from(info: ReplayGainInfo) -> Self {
Self {
track_gain: info.track_gain,
track_peak: info.track_peak,
album_gain: info.album_gain,
album_peak: info.album_peak,
reference_level: info.reference_level,
}
}
}
#[cfg(feature = "serde")]
impl TryFrom<ReplayGainInfoRaw> for ReplayGainInfo {
type Error = String;
fn try_from(raw: ReplayGainInfoRaw) -> std::result::Result<Self, Self::Error> {
if let Some(v) = raw.track_gain {
if !v.is_finite() {
return Err(format!("track_gain must be finite, got: {}", v));
}
}
if let Some(v) = raw.track_peak {
if !v.is_finite() {
return Err(format!("track_peak must be finite, got: {}", v));
}
}
if let Some(v) = raw.album_gain {
if !v.is_finite() {
return Err(format!("album_gain must be finite, got: {}", v));
}
}
if let Some(v) = raw.album_peak {
if !v.is_finite() {
return Err(format!("album_peak must be finite, got: {}", v));
}
}
if !raw.reference_level.is_finite() {
return Err(format!(
"reference_level must be finite, got: {}",
raw.reference_level
));
}
Ok(ReplayGainInfo {
track_gain: raw.track_gain,
track_peak: raw.track_peak,
album_gain: raw.album_gain,
album_peak: raw.album_peak,
reference_level: raw.reference_level,
warnings: Vec::new(),
})
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ReplayGainInfo {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = ReplayGainInfoRaw::deserialize(deserializer)?;
ReplayGainInfo::try_from(raw).map_err(serde::de::Error::custom)
}
}
impl Default for ReplayGainInfo {
fn default() -> Self {
Self {
track_gain: None,
track_peak: None,
album_gain: None,
album_peak: None,
reference_level: REPLAYGAIN_REFERENCE_LEVEL,
warnings: Vec::new(),
}
}
}
impl ReplayGainInfo {
pub fn new() -> Self {
Self::default()
}
pub fn track_gain(&self) -> Option<f32> {
self.track_gain
}
pub fn track_peak(&self) -> Option<f32> {
self.track_peak
}
pub fn album_gain(&self) -> Option<f32> {
self.album_gain
}
pub fn album_peak(&self) -> Option<f32> {
self.album_peak
}
pub fn reference_level(&self) -> f32 {
self.reference_level
}
pub fn with_track(track_gain: f32, track_peak: f32) -> Result<Self> {
validate_finite(track_gain, "track gain")?;
validate_finite(track_peak, "track peak")?;
Ok(Self {
track_gain: Some(track_gain),
track_peak: Some(track_peak),
..Default::default()
})
}
pub fn with_album(album_gain: f32, album_peak: f32) -> Result<Self> {
validate_finite(album_gain, "album gain")?;
validate_finite(album_peak, "album peak")?;
Ok(Self {
album_gain: Some(album_gain),
album_peak: Some(album_peak),
..Default::default()
})
}
pub fn with_both(
track_gain: f32,
track_peak: f32,
album_gain: f32,
album_peak: f32,
) -> Result<Self> {
validate_finite(track_gain, "track gain")?;
validate_finite(track_peak, "track peak")?;
validate_finite(album_gain, "album gain")?;
validate_finite(album_peak, "album peak")?;
Ok(Self {
track_gain: Some(track_gain),
track_peak: Some(track_peak),
album_gain: Some(album_gain),
album_peak: Some(album_peak),
reference_level: REPLAYGAIN_REFERENCE_LEVEL,
warnings: Vec::new(),
})
}
pub fn has_info(&self) -> bool {
self.track_gain.is_some()
|| self.track_peak.is_some()
|| self.album_gain.is_some()
|| self.album_peak.is_some()
}
pub fn has_track_info(&self) -> bool {
self.track_gain.is_some() && self.track_peak.is_some()
}
pub fn has_album_info(&self) -> bool {
self.album_gain.is_some() && self.album_peak.is_some()
}
pub fn track_adjustment_factor(&self) -> Option<f32> {
self.track_gain
.filter(|g| g.is_finite())
.map(|gain| 10.0_f32.powf(gain / 20.0))
}
pub fn album_adjustment_factor(&self) -> Option<f32> {
self.album_gain
.filter(|g| g.is_finite())
.map(|gain| 10.0_f32.powf(gain / 20.0))
}
pub fn set_track_gain(&mut self, gain: Option<f32>) -> Result<()> {
if let Some(g) = gain {
validate_finite(g, "track gain")?;
}
self.track_gain = gain;
Ok(())
}
pub fn set_track_peak(&mut self, peak: Option<f32>) -> Result<()> {
if let Some(p) = peak {
validate_finite(p, "track peak")?;
}
self.track_peak = peak;
Ok(())
}
pub fn set_album_gain(&mut self, gain: Option<f32>) -> Result<()> {
if let Some(g) = gain {
validate_finite(g, "album gain")?;
}
self.album_gain = gain;
Ok(())
}
pub fn set_album_peak(&mut self, peak: Option<f32>) -> Result<()> {
if let Some(p) = peak {
validate_finite(p, "album peak")?;
}
self.album_peak = peak;
Ok(())
}
pub fn set_reference_level(&mut self, level: f32) -> Result<()> {
validate_finite(level, "reference level")?;
self.reference_level = level;
Ok(())
}
pub fn clear(&mut self) {
self.track_gain = None;
self.track_peak = None;
self.album_gain = None;
self.album_peak = None;
self.reference_level = REPLAYGAIN_REFERENCE_LEVEL;
}
}
impl fmt::Display for ReplayGainInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ReplayGain [")?;
if let Some(gain) = self.track_gain {
write!(f, "track: {:.2} dB", gain)?;
if let Some(peak) = self.track_peak {
write!(f, " (peak: {:.6})", peak)?;
}
}
if self.track_gain.is_some() && self.album_gain.is_some() {
write!(f, ", ")?;
}
if let Some(gain) = self.album_gain {
write!(f, "album: {:.2} dB", gain)?;
if let Some(peak) = self.album_peak {
write!(f, " (peak: {:.6})", peak)?;
}
}
write!(f, "]")
}
}
pub fn parse_gain(s: &str) -> Result<f32> {
let s = s.trim();
let s = if let Some(stripped) = s.strip_suffix(" dB") {
stripped
} else if let Some(stripped) = s.strip_suffix("dB") {
stripped
} else {
s
};
let value = s
.trim()
.parse::<f32>()
.map_err(|_| AudexError::InvalidData(format!("Invalid gain value: {}", s)))?;
if !value.is_finite() {
return Err(AudexError::InvalidData(format!(
"Gain value must be finite, got: {}",
value
)));
}
Ok(value)
}
pub fn parse_peak(s: &str) -> Result<f32> {
let s = s.trim();
let value = s
.parse::<f32>()
.map_err(|_| AudexError::InvalidData(format!("Invalid peak value: {}", s)))?;
if !value.is_finite() {
return Err(AudexError::InvalidData(format!(
"Peak value must be finite, got: {}",
value
)));
}
Ok(value)
}
pub fn format_gain(gain: f32) -> Result<String> {
if !gain.is_finite() {
return Err(AudexError::InvalidData(format!(
"Gain value must be finite, got: {}",
gain
)));
}
Ok(format!("{:+.2} dB", gain))
}
pub fn format_peak(peak: f32) -> Result<String> {
if !peak.is_finite() {
return Err(AudexError::InvalidData(format!(
"Peak value must be finite, got: {}",
peak
)));
}
Ok(format!("{:.6}", peak))
}
pub mod vorbis_keys {
pub const TRACK_GAIN: &str = "REPLAYGAIN_TRACK_GAIN";
pub const TRACK_PEAK: &str = "REPLAYGAIN_TRACK_PEAK";
pub const ALBUM_GAIN: &str = "REPLAYGAIN_ALBUM_GAIN";
pub const ALBUM_PEAK: &str = "REPLAYGAIN_ALBUM_PEAK";
pub const REFERENCE_LOUDNESS: &str = "REPLAYGAIN_REFERENCE_LOUDNESS";
}
pub fn from_vorbis_comments(
comments: &std::collections::HashMap<String, Vec<String>>,
) -> ReplayGainInfo {
let mut info = ReplayGainInfo::default();
let get_value = |key: &str| -> Option<&String> {
comments
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.and_then(|(_, v)| v.first())
};
if let Some(gain_str) = get_value(vorbis_keys::TRACK_GAIN) {
match parse_gain(gain_str) {
Ok(gain) => {
if let Err(_e) = info.set_track_gain(Some(gain)) {
let msg = format!("{}: setter rejected value: {}", vorbis_keys::TRACK_GAIN, _e);
warn_event!(key = vorbis_keys::TRACK_GAIN, error = %_e, "ReplayGain setter rejected parsed value");
info.warnings.push(msg);
}
}
Err(_) => {
let msg = format!(
"{}: unparseable value: {}",
vorbis_keys::TRACK_GAIN,
gain_str
);
warn_event!(key = vorbis_keys::TRACK_GAIN, value = %gain_str, "unparseable ReplayGain field");
info.warnings.push(msg);
}
}
}
if let Some(peak_str) = get_value(vorbis_keys::TRACK_PEAK) {
match parse_peak(peak_str) {
Ok(peak) => {
if let Err(_e) = info.set_track_peak(Some(peak)) {
let msg = format!("{}: setter rejected value: {}", vorbis_keys::TRACK_PEAK, _e);
warn_event!(key = vorbis_keys::TRACK_PEAK, error = %_e, "ReplayGain setter rejected parsed value");
info.warnings.push(msg);
}
}
Err(_) => {
let msg = format!(
"{}: unparseable value: {}",
vorbis_keys::TRACK_PEAK,
peak_str
);
warn_event!(key = vorbis_keys::TRACK_PEAK, value = %peak_str, "unparseable ReplayGain field");
info.warnings.push(msg);
}
}
}
if let Some(gain_str) = get_value(vorbis_keys::ALBUM_GAIN) {
match parse_gain(gain_str) {
Ok(gain) => {
if let Err(_e) = info.set_album_gain(Some(gain)) {
let msg = format!("{}: setter rejected value: {}", vorbis_keys::ALBUM_GAIN, _e);
warn_event!(key = vorbis_keys::ALBUM_GAIN, error = %_e, "ReplayGain setter rejected parsed value");
info.warnings.push(msg);
}
}
Err(_) => {
let msg = format!(
"{}: unparseable value: {}",
vorbis_keys::ALBUM_GAIN,
gain_str
);
warn_event!(key = vorbis_keys::ALBUM_GAIN, value = %gain_str, "unparseable ReplayGain field");
info.warnings.push(msg);
}
}
}
if let Some(peak_str) = get_value(vorbis_keys::ALBUM_PEAK) {
match parse_peak(peak_str) {
Ok(peak) => {
if let Err(_e) = info.set_album_peak(Some(peak)) {
let msg = format!("{}: setter rejected value: {}", vorbis_keys::ALBUM_PEAK, _e);
warn_event!(key = vorbis_keys::ALBUM_PEAK, error = %_e, "ReplayGain setter rejected parsed value");
info.warnings.push(msg);
}
}
Err(_) => {
let msg = format!(
"{}: unparseable value: {}",
vorbis_keys::ALBUM_PEAK,
peak_str
);
warn_event!(key = vorbis_keys::ALBUM_PEAK, value = %peak_str, "unparseable ReplayGain field");
info.warnings.push(msg);
}
}
}
if let Some(ref_str) = get_value(vorbis_keys::REFERENCE_LOUDNESS) {
match parse_gain(ref_str) {
Ok(reference) => {
if let Err(_e) = info.set_reference_level(reference) {
let msg = format!(
"{}: setter rejected value: {}",
vorbis_keys::REFERENCE_LOUDNESS,
_e
);
warn_event!(key = vorbis_keys::REFERENCE_LOUDNESS, error = %_e, "ReplayGain setter rejected parsed value");
info.warnings.push(msg);
}
}
Err(_) => {
let msg = format!(
"{}: unparseable value: {}",
vorbis_keys::REFERENCE_LOUDNESS,
ref_str
);
warn_event!(key = vorbis_keys::REFERENCE_LOUDNESS, value = %ref_str, "unparseable ReplayGain field");
info.warnings.push(msg);
}
}
}
info
}
pub fn to_vorbis_comments(
info: &ReplayGainInfo,
comments: &mut std::collections::HashMap<String, Vec<String>>,
) -> Result<()> {
let set_value =
|key: &str, value: String, map: &mut std::collections::HashMap<String, Vec<String>>| {
map.insert(key.to_uppercase(), vec![value]);
};
if let Some(gain) = info.track_gain {
set_value(vorbis_keys::TRACK_GAIN, format_gain(gain)?, comments);
}
if let Some(peak) = info.track_peak {
set_value(vorbis_keys::TRACK_PEAK, format_peak(peak)?, comments);
}
if let Some(gain) = info.album_gain {
set_value(vorbis_keys::ALBUM_GAIN, format_gain(gain)?, comments);
}
if let Some(peak) = info.album_peak {
set_value(vorbis_keys::ALBUM_PEAK, format_peak(peak)?, comments);
}
if !info.reference_level.is_finite() {
return Err(AudexError::InvalidData(format!(
"Reference level must be finite, got: {}",
info.reference_level
)));
}
if (info.reference_level - REPLAYGAIN_REFERENCE_LEVEL).abs() > 0.01 {
set_value(
vorbis_keys::REFERENCE_LOUDNESS,
format_gain(info.reference_level)?,
comments,
);
}
Ok(())
}
pub fn clear_vorbis_comments(comments: &mut std::collections::HashMap<String, Vec<String>>) {
let keys_to_remove: Vec<String> = comments
.keys()
.filter(|k| {
k.eq_ignore_ascii_case(vorbis_keys::TRACK_GAIN)
|| k.eq_ignore_ascii_case(vorbis_keys::TRACK_PEAK)
|| k.eq_ignore_ascii_case(vorbis_keys::ALBUM_GAIN)
|| k.eq_ignore_ascii_case(vorbis_keys::ALBUM_PEAK)
|| k.eq_ignore_ascii_case(vorbis_keys::REFERENCE_LOUDNESS)
})
.cloned()
.collect();
for key in keys_to_remove {
comments.remove(&key);
}
}