use crate::analysis::{analyze_data, ChannelMode};
use crate::ape::{
parse_undo_values, parse_undo_wrap, read_ape_tag, replace_ape_tag, ApeReplayGain,
TAG_MP3GAIN_ALBUM_MINMAX, TAG_MP3GAIN_MINMAX, TAG_MP3GAIN_UNDO,
};
use crate::error::{Error, Result};
use crate::frame::{apply_gain_to_data, scan_gain_range, GainMode, SaturationStats};
use std::fs;
use std::path::Path;
pub const GAIN_STEP_DB: f64 = 1.505_149_978_319_906;
pub const MAX_GAIN: u8 = 255;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Channel {
Left,
Right,
}
impl Channel {
pub fn index(&self) -> usize {
match self {
Channel::Left => 0,
Channel::Right => 1,
}
}
pub fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(Channel::Left),
1 => Some(Channel::Right),
_ => None,
}
}
pub fn name(&self) -> &'static str {
match self {
Channel::Left => "left",
Channel::Right => "right",
}
}
}
impl std::fmt::Display for Channel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Channel::Left => f.write_str("Left"),
Channel::Right => f.write_str("Right"),
}
}
}
#[derive(Debug, Clone)]
pub struct GainOptions {
steps: i32,
wrap: bool,
undo: bool,
channel: Option<Channel>,
replaygain: Option<ApeReplayGain>,
}
impl GainOptions {
pub fn new(steps: i32) -> Self {
Self {
steps,
wrap: false,
undo: false,
channel: None,
replaygain: None,
}
}
pub fn from_db(db: f64) -> Self {
Self::new(db_to_steps(db))
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
pub fn undo(mut self, undo: bool) -> Self {
self.undo = undo;
self
}
pub fn channel(mut self, channel: Channel) -> Self {
self.channel = Some(channel);
self
}
pub(crate) fn replaygain(mut self, rg: ApeReplayGain) -> Self {
self.replaygain = Some(rg);
self
}
pub fn apply(&self, file_path: &Path) -> Result<usize> {
self.apply_to_path(file_path, file_path)
}
pub fn apply_to_path(&self, read_from: &Path, write_to: &Path) -> Result<usize> {
Ok(self.apply_to_path_with_stats(read_from, write_to)?.frames)
}
pub(crate) fn apply_to_path_with_stats(
&self,
read_from: &Path,
write_to: &Path,
) -> Result<SaturationStats> {
self.apply_to_path_with_stats_preread(read_from, write_to, None)
}
pub(crate) fn apply_to_path_with_stats_preread(
&self,
read_from: &Path,
write_to: &Path,
preread: Option<Vec<u8>>,
) -> Result<SaturationStats> {
let same_path = read_from == write_to;
let read = |preread: Option<Vec<u8>>| match preread {
Some(data) => Ok(data),
None => fs::read(read_from).map_err(|e| Error::io_read(read_from, e)),
};
if self.steps == 0 {
if let Some(rg) = &self.replaygain {
let data = read(preread)?;
let mut tag = read_ape_tag(&data).unwrap_or_default();
tag.set_replaygain(rg);
let new_data = replace_ape_tag(&data, &tag);
fs::write(write_to, &new_data).map_err(|e| Error::io_write(write_to, e))?;
} else if !same_path {
match preread {
Some(data) => {
fs::write(write_to, &data).map_err(|e| Error::io_write(write_to, e))?;
}
None => {
fs::copy(read_from, write_to).map_err(|e| Error::io_write(write_to, e))?;
}
}
}
return Ok(SaturationStats::default());
}
let data = read(preread)?;
if let Some(channel) = self.channel {
if self.undo {
apply_gain_channel_with_undo(data, write_to, channel, self.steps)
} else {
apply_gain_channel_impl(data, write_to, channel, self.steps)
}
} else {
let mode = if self.wrap {
GainMode::Wrapping
} else {
GainMode::Saturating
};
if self.undo {
apply_gain_with_undo_impl_to_path(
data,
write_to,
self.steps,
mode,
self.replaygain.as_ref(),
)
} else {
apply_gain_simple_to_path(
data,
write_to,
self.steps,
mode,
self.replaygain.as_ref(),
)
}
}
}
}
pub fn apply_gain(file_path: &Path, gain_steps: i32) -> Result<usize> {
GainOptions::new(gain_steps).apply(file_path)
}
pub fn apply_gain_db(file_path: &Path, gain_db: f64) -> Result<usize> {
GainOptions::from_db(gain_db).apply(file_path)
}
pub(crate) fn apply_undo_to_data(data: &mut [u8], left: i32, right: i32, wrap: bool) -> usize {
if left == right {
let mode = if wrap {
GainMode::Wrapping
} else {
GainMode::Saturating
};
apply_gain_to_data(data, left, mode, None).frames
} else {
let left_frames = apply_gain_to_data(data, left, GainMode::Saturating, Some(0)).frames;
let right_frames = apply_gain_to_data(data, right, GainMode::Saturating, Some(1)).frames;
left_frames.max(right_frames)
}
}
pub fn undo_gain(file_path: &Path) -> Result<usize> {
let mut data = fs::read(file_path).map_err(|e| Error::io_read(file_path, e))?;
let mut tag = read_ape_tag(&data).ok_or(Error::NoApeTag)?;
let undo_value = tag.get(TAG_MP3GAIN_UNDO).ok_or(Error::NoUndoTag)?;
let (left, right) = parse_undo_values(Some(undo_value));
let wrap = parse_undo_wrap(Some(undo_value));
if left == 0 && right == 0 {
return Ok(0);
}
let frames = apply_undo_to_data(&mut data, left, right, wrap);
tag.remove(TAG_MP3GAIN_UNDO);
tag.remove(TAG_MP3GAIN_MINMAX);
tag.remove(TAG_MP3GAIN_ALBUM_MINMAX);
tag.remove_replaygain();
let new_data = replace_ape_tag(&data, &tag);
crate::apply::atomic_write(file_path, &new_data)?;
Ok(frames)
}
pub fn db_to_steps(db: f64) -> i32 {
(db / GAIN_STEP_DB).round() as i32
}
pub fn steps_to_db(steps: i32) -> f64 {
steps as f64 * GAIN_STEP_DB
}
pub fn peak_to_pcm_sample(peak: f64) -> f64 {
peak * 32768.0
}
pub fn peak_to_headroom_db(peak: f64) -> Option<f64> {
if peak > 0.0 {
Some(-20.0 * peak.log10())
} else {
None
}
}
pub fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 20.0)
}
pub fn apply_gain_to_peak(peak: f64, gain_db: f64) -> f64 {
peak * db_to_linear(gain_db)
}
pub fn would_clip(peak: f64, gain_db: f64) -> bool {
apply_gain_to_peak(peak, gain_db) > 1.0
}
fn apply_gain_simple_to_path(
mut data: Vec<u8>,
write_to: &Path,
gain_steps: i32,
mode: GainMode,
replaygain: Option<&ApeReplayGain>,
) -> Result<SaturationStats> {
let stats = apply_gain_to_data(&mut data, gain_steps, mode, None);
if let Some(rg) = replaygain {
let mut tag = read_ape_tag(&data).unwrap_or_default();
tag.set_replaygain(rg);
let new_data = replace_ape_tag(&data, &tag);
fs::write(write_to, &new_data).map_err(|e| Error::io_write(write_to, e))?;
} else {
fs::write(write_to, &data).map_err(|e| Error::io_write(write_to, e))?;
}
Ok(stats)
}
fn apply_gain_with_undo_impl_to_path(
mut data: Vec<u8>,
write_to: &Path,
gain_steps: i32,
mode: GainMode,
replaygain: Option<&ApeReplayGain>,
) -> Result<SaturationStats> {
let mut tag = read_ape_tag(&data).unwrap_or_default();
let (existing_left, existing_right) = parse_undo_values(tag.get(TAG_MP3GAIN_UNDO));
let wrap = mode == GainMode::Wrapping;
tag.set_undo_gain(
existing_left.saturating_sub(gain_steps),
existing_right.saturating_sub(gain_steps),
wrap,
);
let stats = apply_gain_to_data(&mut data, gain_steps, mode, None);
if stats.frames == 0 {
return Err(Error::NoMp3Frames);
}
tag.set_minmax(stats.min_gain, stats.max_gain);
if let Some(rg) = replaygain {
tag.set_replaygain(rg);
}
let new_data = replace_ape_tag(&data, &tag);
fs::write(write_to, &new_data).map_err(|e| Error::io_write(write_to, e))?;
Ok(stats)
}
fn apply_gain_channel_impl(
mut data: Vec<u8>,
write_to: &Path,
channel: Channel,
gain_steps: i32,
) -> Result<SaturationStats> {
let analysis = analyze_data(&data)?;
if analysis.channel_mode() == ChannelMode::Mono {
return Err(Error::ChannelGainOnMono);
}
let stats = apply_gain_to_data(
&mut data,
gain_steps,
GainMode::Saturating,
Some(channel.index()),
);
fs::write(write_to, &data).map_err(|e| Error::io_write(write_to, e))?;
Ok(stats)
}
fn apply_gain_channel_with_undo(
mut data: Vec<u8>,
write_to: &Path,
channel: Channel,
gain_steps: i32,
) -> Result<SaturationStats> {
let analysis = analyze_data(&data)?;
if analysis.channel_mode() == ChannelMode::Mono {
return Err(Error::ChannelGainOnMono);
}
let mut tag = read_ape_tag(&data).unwrap_or_default();
let (existing_left, existing_right) = parse_undo_values(tag.get(TAG_MP3GAIN_UNDO));
let (new_left, new_right) = match channel {
Channel::Left => (existing_left - gain_steps, existing_right),
Channel::Right => (existing_left, existing_right - gain_steps),
};
tag.set_undo_gain(new_left, new_right, false);
let stats = apply_gain_to_data(
&mut data,
gain_steps,
GainMode::Saturating,
Some(channel.index()),
);
if let Ok((min, max)) = scan_gain_range(&data) {
tag.set_minmax(min, max);
}
let new_data = replace_ape_tag(&data, &tag);
fs::write(write_to, &new_data).map_err(|e| Error::io_write(write_to, e))?;
Ok(stats)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_db_to_steps() {
assert_eq!(db_to_steps(0.0), 0);
assert_eq!(db_to_steps(1.5), 1);
assert_eq!(db_to_steps(3.0), 2);
assert_eq!(db_to_steps(-1.5), -1);
assert_eq!(db_to_steps(2.25), 1);
}
#[test]
fn test_steps_to_db() {
assert_eq!(steps_to_db(0), 0.0);
assert!((steps_to_db(1) - 1.505_149_978).abs() < 1e-9);
assert!((steps_to_db(-2) - -3.010_299_957).abs() < 1e-9);
}
#[test]
fn gain_step_matches_physical_constant() {
assert!((GAIN_STEP_DB - 20.0 * 2.0_f64.log10() / 4.0).abs() < 1e-15);
}
#[test]
fn steps_to_db_roundtrip_has_no_drift() {
for steps in -15..=15 {
let db = steps_to_db(steps);
assert_eq!(db_to_steps(db), steps);
let physical = 20.0 * (2.0_f64.powf(steps as f64 / 4.0)).log10();
assert!((db - physical).abs() < 1e-9, "steps={steps}");
}
}
}