use std::path::{Path, PathBuf};
use std::fs;
use std::process::ExitCode;
use std::time::Instant;
use taconite_reuse::Reuse;
use taconite_reuse::media;
use taconite_reuse::wav::{self, Audio};
type R<T> = Result<T, Box<dyn std::error::Error>>;
const USAGE: &str = "usage: reuse-clean <in.mp3|m4a|flac|wav> [-o out.{mp3,m4a,wav}] [--bundle DIR] [--rate HZ] \
[--bitrate KBPS] [--mono] [--float]";
#[derive(Clone, Copy, PartialEq, Debug)]
enum Fmt {
Wav,
Mp3,
M4a,
}
impl Fmt {
fn of(p: &Path) -> Fmt {
match p.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).as_deref() {
Some("mp3") => Fmt::Mp3,
Some("m4a" | "mp4" | "aac" | "m4b") => Fmt::M4a,
_ => Fmt::Wav,
}
}
fn ext(self) -> &'static str {
match self {
Fmt::Wav => "wav",
Fmt::Mp3 => "mp3",
Fmt::M4a => "m4a",
}
}
}
fn run() -> R<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let (mut inp, mut out, mut bundle) = (None, None, std::env::var_os("REUSE_BUNDLE").map(PathBuf::from));
let (mut mono, mut float, mut rate, mut kbps) = (false, false, None::<u32>, None::<u32>);
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"-o" | "--out" => out = Some(PathBuf::from(it.next().ok_or("-o <out.mp3|m4a|wav>")?)),
"--bitrate" => kbps = Some(it.next().ok_or("--bitrate <kbps>")?.parse()?),
"--bundle" => bundle = Some(PathBuf::from(it.next().ok_or("--bundle <dir>")?)),
"--rate" => rate = Some(it.next().ok_or("--rate <Hz>")?.parse()?),
"--mono" => mono = true,
"--float" => float = true,
"-h" | "--help" => {
println!("{USAGE}");
return Ok(());
}
s if s.starts_with('-') => return Err(format!("unknown option {s}\n{USAGE}").into()),
_ if inp.is_none() => inp = Some(PathBuf::from(a)),
_ => return Err(USAGE.into()),
}
}
let inp = inp.ok_or(USAGE)?;
let mut out = out.unwrap_or_else(|| inp.with_extension(format!("clean.{}", Fmt::of(&inp).ext())));
let mut fmt = Fmt::of(&out);
if fmt != Fmt::Wav && !cfg!(feature = "encode") {
out = out.with_extension("wav");
eprintln!("(built without the `encode` feature: writing {} instead)", out.display());
fmt = Fmt::Wav;
}
let t0 = Instant::now();
let mut a = media::read(&inp)?;
let secs = a.channels[0].len() as f64 / a.sr as f64;
eprintln!(
"{}: {secs:.2} s, {} channel(s) at {} Hz (decoded in {:.0} ms)",
inp.display(),
a.channels.len(),
a.sr,
t0.elapsed().as_secs_f64() * 1e3
);
if mono && a.channels.len() > 1 {
a = media::mono(&a);
}
let t0 = Instant::now();
let mut m = match &bundle {
Some(b) => Reuse::load(b)?,
None => load_builtin()?,
};
eprintln!("bundle loaded in {:.1} s ({} hardware contexts)", t0.elapsed().as_secs_f64(), m.contexts());
let sr = match rate {
Some(r) if m.cfg.rates.contains(&r) => r,
Some(r) => return Err(format!("--rate {r}: the bundle has kernels for {:?} Hz", m.cfg.rates).into()),
None => media::nearest_rate(a.sr, &m.cfg.rates).ok_or("the bundle lists no rates")?,
};
if sr != a.sr {
eprintln!("running at {sr} Hz (resampled from and back to {} Hz)", a.sr);
}
let same = a.channels.iter().all(|c| c == &a.channels[0]);
let n = if same { 1 } else { a.channels.len() };
if !same && near_mono(&a.channels) {
eprintln!("(the channels are nearly the same: --mono would take 1/{} the time)", a.channels.len());
}
let mut channels = Vec::new();
let mut bar = Bar::new(n);
for (ci, ch) in a.channels.iter().take(n).enumerate() {
let x = media::resample(ch, a.sr, sr);
let t0 = Instant::now();
let y = m.enhance_with(&x, sr, &mut |f| bar.set(ci, f))?;
bar.clear();
let dt = t0.elapsed().as_secs_f64();
eprintln!("channel {ci}: cleaned in {:.0} ms (x{:.1} real time)", dt * 1e3, secs / dt);
let mut y = media::resample(&y, sr, a.sr);
y.resize(ch.len(), 0.0);
channels.push(y);
}
if same && a.channels.len() > 1 {
eprintln!("(the {} channels are identical: enhanced once)", a.channels.len());
channels = vec![channels[0].clone(); a.channels.len()];
}
let clean = Audio { channels, sr: a.sr };
match fmt {
Fmt::Wav => wav::write(&out, &clean, float)?,
_ => {
let est = (fs::metadata(&inp)?.len() as f64 * 8.0 / secs / 1000.0).round() as u32;
let pcm_in = matches!(Fmt::of(&inp), Fmt::Wav);
let k = snap_kbps(kbps.unwrap_or(if pcm_in { 64 * clean.channels.len() as u32 } else { est }));
eprintln!("encoding {} at {k} kbps", fmt.ext());
write_compressed(fmt, &out, &clean, k)?;
}
}
eprintln!("wrote {}", out.display());
Ok(())
}
fn near_mono(ch: &[Vec<f32>]) -> bool {
let e0: f64 = ch[0].iter().map(|&v| (v as f64).powi(2)).sum();
ch[1..].iter().all(|c| {
let d: f64 = c.iter().zip(&ch[0]).map(|(&a, &b)| (a as f64 - b as f64).powi(2)).sum();
d * 100.0 < e0
})
}
struct Bar {
channels: usize,
t0: Instant,
last: Option<Instant>,
first: Option<(f64, f64)>,
tty: bool,
}
impl Bar {
fn new(channels: usize) -> Self {
use std::io::IsTerminal;
Bar { channels, t0: Instant::now(), last: None, first: None, tty: std::io::stderr().is_terminal() }
}
fn set(&mut self, ch: usize, f: f64) {
let done = ((ch as f64 + f.clamp(0.0, 1.0)) / self.channels as f64).min(1.0);
let el = self.t0.elapsed().as_secs_f64();
let (e0, d0) = *self.first.get_or_insert((el, done));
if !self.tty || self.last.is_some_and(|t| t.elapsed().as_millis() < 100 && f < 1.0) {
return;
}
self.last = Some(Instant::now());
const W: usize = 30;
let fill = (done * W as f64).round() as usize;
let eta = if done - d0 > 0.02 {
format!(", ~{:.1} s left", (el - e0) / (done - d0) * (1.0 - done))
} else {
String::new()
};
let chan = if self.channels > 1 { format!(" channel {}/{}", ch + 1, self.channels) } else { String::new() };
eprint!("\r\x1b[2Kcleaning{chan} [{}{}] {:3.0}% {el:.1} s{eta}", "█".repeat(fill), "░".repeat(W - fill), done * 100.0);
}
fn clear(&mut self) {
if self.tty {
eprint!("\r\x1b[2K");
}
}
}
fn snap_kbps(k: u32) -> u32 {
const R: [u32; 14] = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320];
*R.iter().min_by_key(|&&r| r.abs_diff(k)).unwrap()
}
#[cfg(feature = "encode")]
fn write_compressed(fmt: Fmt, out: &Path, a: &Audio, kbps: u32) -> R<()> {
match fmt {
Fmt::Mp3 => taconite_reuse::encode::write_mp3(out, a, kbps)?,
Fmt::M4a => taconite_reuse::encode::write_m4a(out, a, kbps)?,
Fmt::Wav => unreachable!(),
}
Ok(())
}
#[cfg(not(feature = "encode"))]
fn write_compressed(_: Fmt, _: &Path, _: &Audio, _: u32) -> R<()> {
unreachable!("formats other than WAV need the `encode` feature")
}
#[cfg(feature = "embed")]
fn load_builtin() -> R<Reuse> {
Ok(Reuse::load_embedded()?)
}
#[cfg(not(feature = "embed"))]
fn load_builtin() -> R<Reuse> {
Err("no bundle: pass --bundle <dir> or set REUSE_BUNDLE (or build one in: feature `embed`)".into())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}