#[cfg(feature = "aac")]
pub mod aac;
#[cfg(feature = "aac")]
mod aac_codebooks;
pub mod analysis;
pub mod ape;
pub mod apply;
pub mod error;
mod frame;
pub mod gain;
pub mod id3v2;
pub mod mp4meta;
pub mod replaygain;
pub use analysis::{
analyze, find_max_amplitude, is_mono, ChannelMode, MaxAmplitudeResult, Mp3Analysis, MpegVersion,
};
pub use ape::{
delete_ape_tag, read_ape_tag, read_ape_tag_from_file, write_ape_tag, ApeItem, ApeTag,
TAG_MP3GAIN_ALBUM_MINMAX, TAG_MP3GAIN_MINMAX, TAG_MP3GAIN_UNDO, TAG_REPLAYGAIN_ALBUM_GAIN,
TAG_REPLAYGAIN_ALBUM_PEAK, TAG_REPLAYGAIN_TRACK_GAIN, TAG_REPLAYGAIN_TRACK_PEAK,
};
pub use apply::{
apply_with_options, predict_apply, AacAlbumInfo, ApplyOptions, ApplyReport, ClippingDetection,
};
pub use error::{Error, Result};
pub use gain::{
apply_gain, apply_gain_db, apply_gain_to_peak, db_to_linear, db_to_steps, peak_to_headroom_db,
peak_to_pcm_sample, steps_to_db, undo_gain, would_clip, Channel, GainOptions, GAIN_STEP_DB,
MAX_GAIN, MIN_GAIN,
};
pub use id3v2::{
delete_id3v2_replaygain, read_id3v2_replaygain, undo_gain_id3v2, write_id3v2_replaygain,
write_id3v2_undo, Id3v2ReplayGain,
};
use std::path::{Path, PathBuf};
pub const SUPPORTED_EXTENSIONS: &[&str] = &["mp3", "m4a", "aac", "mp4"];
pub fn is_supported_audio_path(path: &Path) -> bool {
if path
.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("._"))
{
return false;
}
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|ext| {
SUPPORTED_EXTENSIONS
.iter()
.any(|s| ext.eq_ignore_ascii_case(s))
})
}
pub fn collect_audio_files(dir: &Path, recursive: bool) -> Result<Vec<PathBuf>> {
let mut result = Vec::new();
collect_audio_files_into(dir, recursive, &mut result)?;
Ok(result)
}
pub fn apply_gain_db_auto(file_path: &Path, gain_db: f64) -> Result<usize> {
#[cfg(feature = "aac")]
{
if mp4meta::is_aac_file(file_path) {
return aac::apply_aac_gain_to_path(file_path, file_path, gain::db_to_steps(gain_db));
}
}
gain::apply_gain_db(file_path, gain_db)
}
pub fn undo_gain_auto(file_path: &Path, use_id3v2: bool) -> Result<usize> {
#[cfg(feature = "aac")]
{
if mp4meta::is_aac_file(file_path) {
return aac::undo_aac_gain(file_path);
}
}
if use_id3v2 {
id3v2::undo_gain_id3v2(file_path)
} else {
gain::undo_gain(file_path)
}
}
pub fn delete_gain_tags_auto(file_path: &Path, use_id3v2: bool) -> Result<()> {
#[cfg(feature = "aac")]
{
if mp4meta::is_aac_file(file_path) {
mp4meta::delete_replaygain_tags(file_path)?;
return mp4meta::delete_undo_tags(file_path);
}
}
if use_id3v2 {
id3v2::delete_id3v2_replaygain(file_path)
} else {
ape::delete_ape_tag(file_path)
}
}
pub fn read_undo_steps(file_path: &Path, use_id3v2: bool) -> Option<i32> {
#[cfg(feature = "aac")]
{
if mp4meta::is_aac_file(file_path) {
let undo_tags = mp4meta::read_undo_tags(file_path).ok()?;
return Some(ape::parse_undo_values(undo_tags.undo()).0);
}
}
if use_id3v2 {
let rg = id3v2::read_id3v2_replaygain(file_path).ok()?;
return Some(ape::parse_undo_values(rg.undo.as_deref()).0);
}
let tag = ape::read_ape_tag_from_file(file_path).ok()??;
tag.get_undo_gain()
}
fn collect_audio_files_into(dir: &Path, recursive: bool, result: &mut Vec<PathBuf>) -> Result<()> {
let entries = std::fs::read_dir(dir).map_err(|e| Error::io_read(dir, e))?;
for entry in entries {
let entry = entry.map_err(|e| Error::io_read(dir, e))?;
let file_type = entry.file_type().map_err(|e| Error::io_read(dir, e))?;
let path = entry.path();
if file_type.is_dir() {
if recursive {
collect_audio_files_into(&path, recursive, result)?;
}
} else if is_supported_audio_path(&path) {
result.push(path);
}
}
Ok(())
}
#[cfg(all(test, feature = "aac"))]
mod auto_dispatch_tests {
use super::*;
use std::io::Write;
#[test]
fn auto_dispatch_does_not_corrupt_mp4_when_payload_mimics_mp3_frame() {
let dir = std::env::temp_dir().join("mp3rgain_issue_149");
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("fake.m4a");
let mut bytes = vec![
0x00, 0x00, 0x00, 0x14, b'f', b't', b'y', b'p', b'M', b'4', b'A', b' ', 0x00, 0x00,
0x00, 0x00, b'M', b'4', b'A', b' ', ];
let frame_header = [0xFFu8, 0xE3, 0x10, 0x00];
bytes.extend_from_slice(&frame_header);
bytes.resize(20 + 52, 0x55); bytes.extend_from_slice(&frame_header);
bytes.resize(20 + 52 + 52, 0x55); let original = bytes.clone();
std::fs::File::create(&path)
.unwrap()
.write_all(&bytes)
.unwrap();
let _ = apply_gain_db_auto(&path, 3.0);
let after = std::fs::read(&path).unwrap();
assert_eq!(
after, original,
"MP4 bytes must be untouched by auto dispatch"
);
let _ = std::fs::remove_dir_all(&dir);
}
}