use crate::analysis::{analyze_data, ChannelMode};
use crate::ape::{
parse_undo_values, parse_undo_wrap, read_ape_tag, replace_ape_tag, 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.5;
pub const MAX_GAIN: u8 = 255;
pub const MIN_GAIN: u8 = 0;
#[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 other(&self) -> Self {
match self {
Channel::Left => Channel::Right,
Channel::Right => Channel::Left,
}
}
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>,
}
impl GainOptions {
pub fn new(steps: i32) -> Self {
Self {
steps,
wrap: false,
undo: false,
channel: 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 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> {
let same_path = read_from == write_to;
if self.steps == 0 {
if !same_path {
fs::copy(read_from, write_to).map_err(|e| Error::io_write(write_to, e))?;
}
return Ok(SaturationStats::default());
}
if let Some(channel) = self.channel {
if self.undo {
apply_gain_channel_with_undo(read_from, write_to, channel, self.steps)
} else {
apply_gain_channel_impl(read_from, write_to, channel, self.steps)
}
} else if self.undo {
let mode = if self.wrap {
GainMode::Wrapping
} else {
GainMode::Saturating
};
apply_gain_with_undo_impl_to_path(read_from, write_to, self.steps, mode)
} else {
let mode = if self.wrap {
GainMode::Wrapping
} else {
GainMode::Saturating
};
apply_gain_simple_to_path(read_from, write_to, self.steps, mode)
}
}
}
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);
let new_data = replace_ape_tag(&data, &tag);
fs::write(file_path, &new_data).map_err(|e| Error::io_write(file_path, e))?;
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(
read_from: &Path,
write_to: &Path,
gain_steps: i32,
mode: GainMode,
) -> Result<SaturationStats> {
let mut data = fs::read(read_from).map_err(|e| Error::io_read(read_from, e))?;
let stats = apply_gain_to_data(&mut data, gain_steps, mode, None);
fs::write(write_to, &data).map_err(|e| Error::io_write(write_to, e))?;
Ok(stats)
}
fn apply_gain_with_undo_impl_to_path(
read_from: &Path,
write_to: &Path,
gain_steps: i32,
mode: GainMode,
) -> Result<SaturationStats> {
let mut data = fs::read(read_from).map_err(|e| Error::io_read(read_from, e))?;
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 - gain_steps,
existing_right - gain_steps,
wrap,
);
let stats = apply_gain_to_data(&mut data, gain_steps, mode, None);
let (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)
}
fn apply_gain_channel_impl(
read_from: &Path,
write_to: &Path,
channel: Channel,
gain_steps: i32,
) -> Result<SaturationStats> {
let mut data = fs::read(read_from).map_err(|e| Error::io_read(read_from, e))?;
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(
read_from: &Path,
write_to: &Path,
channel: Channel,
gain_steps: i32,
) -> Result<SaturationStats> {
let mut data = fs::read(read_from).map_err(|e| Error::io_read(read_from, e))?;
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), 2);
}
#[test]
fn test_steps_to_db() {
assert_eq!(steps_to_db(0), 0.0);
assert_eq!(steps_to_db(1), 1.5);
assert_eq!(steps_to_db(-2), -3.0);
}
}