use crate::analysis::{analyze, ChannelMode};
use crate::ape::{
delete_ape_tag, parse_undo_values, read_ape_tag_from_file, write_ape_tag, ApeTag,
TAG_MP3GAIN_MINMAX, TAG_MP3GAIN_UNDO,
};
use crate::error::{Error, Result};
use crate::frame::{apply_gain_to_channel_data, apply_gain_to_data, GainMode};
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> {
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(0);
}
if let Some(channel) = self.channel {
if !same_path {
fs::copy(read_from, write_to).map_err(|e| Error::io_write(write_to, e))?;
}
if self.undo {
apply_gain_channel_with_undo(write_to, channel, self.steps)
} else {
apply_gain_channel_impl(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 fn undo_gain(file_path: &Path) -> Result<usize> {
let tag = read_ape_tag_from_file(file_path)?.ok_or(Error::NoApeTag)?;
let undo_gain = tag.get_undo_gain().ok_or(Error::NoUndoTag)?;
if undo_gain == 0 {
return Ok(0);
}
let frames = apply_gain(file_path, -undo_gain)?;
let mut new_tag = tag.clone();
new_tag.remove(TAG_MP3GAIN_UNDO);
new_tag.remove(TAG_MP3GAIN_MINMAX);
if new_tag.is_empty() {
delete_ape_tag(file_path)?;
} else {
write_ape_tag(file_path, &new_tag)?;
}
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<usize> {
let mut data = fs::read(read_from).map_err(|e| Error::io_read(read_from, e))?;
let modified_frames = apply_gain_to_data(&mut data, gain_steps, mode);
fs::write(write_to, &data).map_err(|e| Error::io_write(write_to, e))?;
Ok(modified_frames)
}
fn apply_gain_with_undo_impl_to_path(
read_from: &Path,
write_to: &Path,
gain_steps: i32,
mode: GainMode,
) -> Result<usize> {
let analysis = analyze(read_from)?;
let mut tag = read_ape_tag_from_file(read_from)?.unwrap_or_else(ApeTag::new);
let existing_undo = tag.get_undo_gain().unwrap_or(0);
let new_undo = existing_undo + gain_steps;
let wrap = mode == GainMode::Wrapping;
tag.set_undo_gain(new_undo, new_undo, wrap);
if tag.get(TAG_MP3GAIN_MINMAX).is_none() {
tag.set_minmax(analysis.min_gain(), analysis.max_gain());
}
let frames = apply_gain_simple_to_path(read_from, write_to, gain_steps, mode)?;
write_ape_tag(write_to, &tag)?;
Ok(frames)
}
fn apply_gain_channel_impl(file_path: &Path, channel: Channel, gain_steps: i32) -> Result<usize> {
let analysis = analyze(file_path)?;
if analysis.channel_mode() == ChannelMode::Mono {
return Err(Error::ChannelGainOnMono);
}
let mut data = fs::read(file_path).map_err(|e| Error::io_read(file_path, e))?;
let modified_frames = apply_gain_to_channel_data(&mut data, channel.index(), gain_steps);
fs::write(file_path, &data).map_err(|e| Error::io_write(file_path, e))?;
Ok(modified_frames)
}
fn apply_gain_channel_with_undo(
file_path: &Path,
channel: Channel,
gain_steps: i32,
) -> Result<usize> {
let analysis = analyze(file_path)?;
if analysis.channel_mode() == ChannelMode::Mono {
return Err(Error::ChannelGainOnMono);
}
let mut tag = read_ape_tag_from_file(file_path)?.unwrap_or_else(ApeTag::new);
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);
if tag.get(TAG_MP3GAIN_MINMAX).is_none() {
tag.set_minmax(analysis.min_gain(), analysis.max_gain());
}
let frames = apply_gain_channel_impl(file_path, channel, gain_steps)?;
write_ape_tag(file_path, &tag)?;
Ok(frames)
}
#[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);
}
}