use anyhow::Result;
use colored::*;
use mp3rgain::apply::{apply_with_options, predict_apply, ApplyOptions};
use mp3rgain::replaygain::AudioFileType;
use mp3rgain::{mp4meta, steps_to_db, Channel};
use std::fmt::Write as _;
use std::path::Path;
use crate::cli::options::{Options, OutputFormat, StoredTagMode};
use crate::json_output::{FileStatus, JsonFileResult};
use crate::util::get_filename;
use super::utils::{
emit_clipping_warning, emit_file_warning, report_file_error, warn_aac_multi_track,
};
pub fn process_apply(file: &Path, steps: i32, opts: &Options) -> Result<(JsonFileResult, String)> {
let mut out = String::new();
let result = process_apply_into(file, steps, opts, &mut out)?;
Ok((result, out))
}
fn process_apply_into(
file: &Path,
steps: i32,
opts: &Options,
out: &mut String,
) -> Result<JsonFileResult> {
let filename = get_filename(file);
let is_aac = mp4meta::is_aac_file(file);
let file_type = Some(if is_aac {
AudioFileType::Aac
} else {
AudioFileType::Mp3
});
if is_aac {
warn_aac_multi_track(file, filename, opts);
}
if opts.dry_run {
let (actual_steps, warning_msg) =
if !opts.prevent_clipping && (opts.ignore_clipping || opts.quiet) {
(steps, None)
} else {
let mut apply_opts = ApplyOptions::new(steps);
apply_opts.prevent_clipping = opts.prevent_clipping;
apply_opts.wrap = opts.wrap_gain;
apply_opts.file_type = file_type;
match predict_apply(file, &apply_opts) {
Ok(report) => {
let warning = emit_clipping_warning(steps, &report, opts, filename, None);
(report.actual_steps, warning)
}
Err(e) => {
return Ok(JsonFileResult {
dry_run: Some(true),
..report_file_error(file, filename, e, opts)
});
}
}
};
if opts.output_format == OutputFormat::Text && !opts.quiet {
writeln!(
out,
" {} [DRY RUN] {} (would apply {} steps)",
"~".cyan(),
filename,
actual_steps
)?;
}
return Ok(JsonFileResult {
file: file.display().to_string(),
status: Some(FileStatus::DryRun),
gain_applied_steps: Some(actual_steps),
gain_applied_db: Some(steps_to_db(actual_steps)),
warning: warning_msg,
dry_run: Some(true),
..Default::default()
});
}
let mut apply_opts = ApplyOptions::new(steps);
apply_opts.prevent_clipping = opts.prevent_clipping;
apply_opts.wrap = opts.wrap_gain;
apply_opts.preserve_timestamp = opts.preserve_timestamp;
apply_opts.write_undo = opts.stored_tag_mode != StoredTagMode::Skip;
apply_opts.tag_layout = opts.tag_layout;
apply_opts.skip_clipping_check = !opts.prevent_clipping && (opts.ignore_clipping || opts.quiet);
apply_opts.file_type = file_type;
match apply_with_options(file, &apply_opts) {
Ok(report) => {
let clip_warn = emit_clipping_warning(steps, &report, opts, filename, None);
let sat_warn = emit_saturation_warning(&report, opts, filename);
let warning_msg = combine_warnings(clip_warn, sat_warn);
if opts.output_format == OutputFormat::Text && !opts.quiet {
if is_aac {
writeln!(
out,
" {} {} ({} gains modified)",
"v".green(),
filename,
report.modified
)?;
} else {
writeln!(
out,
" {} {} ({} frames)",
"v".green(),
filename,
report.modified
)?;
}
}
Ok(JsonFileResult {
file: file.display().to_string(),
status: Some(FileStatus::Success),
frames: Some(report.modified),
gain_applied_steps: Some(report.actual_steps),
gain_applied_db: Some(steps_to_db(report.actual_steps)),
max_gain: report.gain_range.map(|(max, _)| max),
min_gain: report.gain_range.map(|(_, min)| min),
warning: warning_msg,
..Default::default()
})
}
Err(e) => Ok(report_file_error(file, filename, e, opts)),
}
}
fn emit_saturation_warning(
report: &mp3rgain::ApplyReport,
opts: &Options,
filename: &str,
) -> Option<String> {
let (low, high) = (report.saturated_low, report.saturated_high);
if low == 0 && high == 0 {
return None;
}
let detail = match (low, high) {
(l, 0) => format!("{l} gain value(s) clamped at 0 (silence)"),
(0, h) => format!("{h} gain value(s) clamped at 255 (distortion)"),
(l, h) => format!("{l} gain value(s) clamped at 0 (silence), {h} at 255 (distortion)"),
};
let msg = format!("{detail} - saturated gain is not losslessly reversible");
emit_file_warning(opts, filename, &msg, None);
Some(msg)
}
fn combine_warnings(clip: Option<String>, saturation: Option<String>) -> Option<String> {
match (clip, saturation) {
(Some(a), Some(b)) => Some(format!("{a}; {b}")),
(a, b) => a.or(b),
}
}
pub fn process_apply_channel(
file: &Path,
channel: Channel,
steps: i32,
opts: &Options,
) -> Result<(JsonFileResult, String)> {
let mut out = String::new();
let result = process_apply_channel_into(file, channel, steps, opts, &mut out)?;
Ok((result, out))
}
fn process_apply_channel_into(
file: &Path,
channel: Channel,
steps: i32,
opts: &Options,
out: &mut String,
) -> Result<JsonFileResult> {
let filename = get_filename(file);
let channel_name = channel.name();
if opts.output_format == OutputFormat::Text && !opts.quiet {
if let Ok(mp3rgain::ChannelMode::JointStereo) = mp3rgain::analysis::read_channel_mode(file)
{
emit_file_warning(
opts,
filename,
"Joint Stereo file: channel-specific gain may not work as expected",
None,
);
}
}
if opts.dry_run {
if opts.output_format == OutputFormat::Text && !opts.quiet {
writeln!(
out,
" {} [DRY RUN] {} (would apply {} steps to {} channel)",
"~".cyan(),
filename,
steps,
channel_name
)?;
}
return Ok(JsonFileResult {
file: file.display().to_string(),
status: Some(FileStatus::DryRun),
gain_applied_steps: Some(steps),
gain_applied_db: Some(steps_to_db(steps)),
dry_run: Some(true),
..Default::default()
});
}
let mut apply_opts = ApplyOptions::new(steps);
apply_opts.channel = Some(channel);
apply_opts.preserve_timestamp = opts.preserve_timestamp;
apply_opts.write_undo = opts.stored_tag_mode != StoredTagMode::Skip;
apply_opts.tag_layout = opts.tag_layout;
apply_opts.skip_clipping_check = true;
apply_opts.file_type = Some(AudioFileType::Mp3);
match apply_with_options(file, &apply_opts) {
Ok(report) => {
let warning_msg = emit_saturation_warning(&report, opts, filename);
if opts.output_format == OutputFormat::Text && !opts.quiet {
writeln!(
out,
" {} {} ({} frames, {} channel)",
"v".green(),
filename,
report.modified,
channel_name
)?;
}
Ok(JsonFileResult {
file: file.display().to_string(),
status: Some(FileStatus::Success),
frames: Some(report.modified),
gain_applied_steps: Some(steps),
gain_applied_db: Some(steps_to_db(steps)),
warning: warning_msg,
..Default::default()
})
}
Err(e) => Ok(report_file_error(file, filename, e, opts)),
}
}