Skip to content

Commit 6e953dc

Browse files
m_igashim_igashi
authored andcommitted
feat: add -i option for track index selection (issue #25)
- Add -i <n> option to specify which audio track to process in multi-track files - Default to track index 0 (first audio track) - Useful for MP4/M4V files with multiple audio tracks (e.g., different languages) - Add aacgain to acknowledgements in README - Bump version to 1.1.0
1 parent e1314a6 commit 6e953dc

File tree

4 files changed

+96
-8
lines changed

4 files changed

+96
-8
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mp3rgain"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
edition = "2021"
55
authors = ["Masanari Higashi <M-Igashi@users.noreply.github.com>"]
66
description = "Lossless MP3 volume adjustment - a modern mp3gain replacement written in Rust"

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ mp3rgain -r *.mp3 *.m4a
123123

124124
# Recursive directory processing includes M4A files
125125
mp3rgain -R /path/to/music
126+
127+
# Process specific audio track in multi-track files (e.g., video files)
128+
mp3rgain -i 1 movie.m4v # Process second audio track
129+
mp3rgain -i 0 song.m4a # Process first track (default)
126130
```
127131

128132
Note: For M4A files, mp3rgain writes ReplayGain tags (iTunes freeform format) but does not modify the audio data, as AAC doesn't have a lossless gain adjustment mechanism like MP3's `global_gain` field.
@@ -255,6 +259,7 @@ Example JSON output:
255259
| `-r` | Apply Track gain (ReplayGain analysis) |
256260
| `-a` | Apply Album gain (ReplayGain analysis) |
257261
| `-e` | Skip album analysis (even with multiple files) |
262+
| `-i <n>` | Specify which audio track to process (default: 0) |
258263
| `-u` | Undo gain changes (restore from APEv2 tag) |
259264
| `-x` | Only find max amplitude of file |
260265
| `-s <mode>` | Stored tag handling: `c` (check), `d` (delete), `s` (skip), `r` (recalc), `i` (ID3v2), `a` (APEv2) |
@@ -364,6 +369,7 @@ println!("Headroom: {} steps", info.headroom_steps);
364369

365370
- [symphonia](https://github.com/pdrat/symphonia) - Pure Rust audio decoding library (used for ReplayGain analysis)
366371
- [Original mp3gain](http://mp3gain.sourceforge.net/) - The original C implementation that inspired this project
372+
- [aacgain](https://aacgain.altosdesign.com/) - AAC/MP4 ReplayGain implementation that inspired the `-i` track index option
367373

368374
## Contributing
369375

src/main.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ struct Options {
6969
album_gain: bool, // -a (apply album gain)
7070
skip_album: bool, // -e: skip album analysis
7171
max_amplitude_only: bool, // -x: only find max amplitude
72+
track_index: Option<u32>, // -i <index>: track index for multi-track files
7273

7374
// Behavior options
7475
preserve_timestamp: bool, // -p
@@ -323,6 +324,18 @@ fn parse_args(args: &[String]) -> Result<Options> {
323324
"a" => opts.album_gain = true,
324325
"e" => opts.skip_album = true,
325326
"x" => opts.max_amplitude_only = true,
327+
"i" => {
328+
i += 1;
329+
if i >= args.len() {
330+
eprintln!("{}: -i requires an argument", "error".red().bold());
331+
std::process::exit(1);
332+
}
333+
opts.track_index = Some(
334+
args[i]
335+
.parse()
336+
.map_err(|_| anyhow::anyhow!("invalid track index: {}", args[i]))?,
337+
);
338+
}
326339
"u" => opts.undo = true,
327340
"p" => opts.preserve_timestamp = true,
328341
"c" => opts.ignore_clipping = true,
@@ -386,6 +399,14 @@ fn parse_args(args: &[String]) -> Result<Options> {
386399
.parse()
387400
.map_err(|_| anyhow::anyhow!("invalid modifier value: {}", val))?;
388401
}
402+
// Handle -i with attached value (e.g., -i1)
403+
_ if flag.starts_with('i') => {
404+
let val = &flag[1..];
405+
opts.track_index = Some(
406+
val.parse()
407+
.map_err(|_| anyhow::anyhow!("invalid track index: {}", val))?,
408+
);
409+
}
389410
_ => {
390411
eprintln!("{}: unknown option: -{}", "warning".yellow().bold(), flag);
391412
}
@@ -1308,7 +1329,7 @@ fn cmd_album_gain(files: &[PathBuf], opts: &Options) -> Result<()> {
13081329

13091330
let file_refs: Vec<&std::path::Path> = files.iter().map(|p| p.as_path()).collect();
13101331

1311-
match replaygain::analyze_album(&file_refs) {
1332+
match replaygain::analyze_album_with_index(&file_refs, opts.track_index) {
13121333
Ok(album_result) => {
13131334
// Apply gain modifier
13141335
let modified_gain_steps = album_result.album_gain_steps() + opts.gain_modifier;
@@ -1893,7 +1914,7 @@ fn process_track_gain(file: &PathBuf, opts: &Options) -> Result<JsonFileResult>
18931914
);
18941915
}
18951916

1896-
match replaygain::analyze_track(file) {
1917+
match replaygain::analyze_track_with_index(file, opts.track_index) {
18971918
Ok(result) => {
18981919
// Apply gain modifier
18991920
let base_steps = result.gain_steps();
@@ -2229,6 +2250,7 @@ fn print_usage() {
22292250
println!(" -r Apply Track gain (ReplayGain analysis)");
22302251
println!(" -a Apply Album gain (ReplayGain analysis)");
22312252
println!(" -e Skip album analysis (even with multiple files)");
2253+
println!(" -i <n> Specify which audio track to process (default: 0)");
22322254
println!(" -u Undo gain changes (restore from APEv2 tag)");
22332255
println!(" -x Only find max amplitude of file");
22342256
println!(" -s <mode> Stored tag handling:");

src/replaygain.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,15 @@ fn detect_file_type(file_path: &Path) -> AudioFileType {
361361
/// Analyze a single track and calculate ReplayGain
362362
#[cfg(feature = "replaygain")]
363363
pub fn analyze_track(file_path: &Path) -> Result<ReplayGainResult> {
364+
analyze_track_with_index(file_path, None)
365+
}
366+
367+
/// Analyze a single track with optional track index selection
368+
#[cfg(feature = "replaygain")]
369+
pub fn analyze_track_with_index(
370+
file_path: &Path,
371+
track_index: Option<u32>,
372+
) -> Result<ReplayGainResult> {
364373
// Detect file type
365374
let file_type = detect_file_type(file_path);
366375

@@ -387,12 +396,32 @@ pub fn analyze_track(file_path: &Path) -> Result<ReplayGainResult> {
387396

388397
let mut format = probed.format;
389398

390-
// Find the first audio track
391-
let track = format
399+
// Find audio tracks
400+
let audio_tracks: Vec<_> = format
392401
.tracks()
393402
.iter()
394-
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
395-
.ok_or_else(|| anyhow::anyhow!("No audio track found"))?;
403+
.filter(|t| t.codec_params.codec != CODEC_TYPE_NULL)
404+
.collect();
405+
406+
if audio_tracks.is_empty() {
407+
anyhow::bail!("No audio track found");
408+
}
409+
410+
// Select track by index or default to first
411+
let track = match track_index {
412+
Some(idx) => {
413+
let idx = idx as usize;
414+
if idx >= audio_tracks.len() {
415+
anyhow::bail!(
416+
"Track index {} out of range (file has {} audio track(s))",
417+
idx,
418+
audio_tracks.len()
419+
);
420+
}
421+
audio_tracks[idx]
422+
}
423+
None => audio_tracks[0],
424+
};
396425

397426
let track_id = track.id;
398427
let sample_rate = track
@@ -538,12 +567,21 @@ fn process_audio_buffer(
538567
/// Analyze multiple tracks for album gain
539568
#[cfg(feature = "replaygain")]
540569
pub fn analyze_album(files: &[&Path]) -> Result<AlbumGainResult> {
570+
analyze_album_with_index(files, None)
571+
}
572+
573+
/// Analyze multiple tracks for album gain with optional track index selection
574+
#[cfg(feature = "replaygain")]
575+
pub fn analyze_album_with_index(
576+
files: &[&Path],
577+
track_index: Option<u32>,
578+
) -> Result<AlbumGainResult> {
541579
let mut track_results = Vec::with_capacity(files.len());
542580
let mut album_peak: f64 = 0.0;
543581

544582
for file in files {
545583
// Analyze each track
546-
let result = analyze_track(file)?;
584+
let result = analyze_track_with_index(file, track_index)?;
547585
album_peak = album_peak.max(result.peak);
548586

549587
// We need to re-analyze to get raw RMS values for album calculation
@@ -582,6 +620,17 @@ pub fn analyze_track(_file_path: &Path) -> Result<ReplayGainResult> {
582620
)
583621
}
584622

623+
#[cfg(not(feature = "replaygain"))]
624+
pub fn analyze_track_with_index(
625+
_file_path: &Path,
626+
_track_index: Option<u32>,
627+
) -> Result<ReplayGainResult> {
628+
anyhow::bail!(
629+
"ReplayGain analysis requires the 'replaygain' feature.\n\
630+
Install with: cargo install mp3rgain --features replaygain"
631+
)
632+
}
633+
585634
#[cfg(not(feature = "replaygain"))]
586635
pub fn analyze_album(_files: &[&Path]) -> Result<AlbumGainResult> {
587636
anyhow::bail!(
@@ -590,6 +639,17 @@ pub fn analyze_album(_files: &[&Path]) -> Result<AlbumGainResult> {
590639
)
591640
}
592641

642+
#[cfg(not(feature = "replaygain"))]
643+
pub fn analyze_album_with_index(
644+
_files: &[&Path],
645+
_track_index: Option<u32>,
646+
) -> Result<AlbumGainResult> {
647+
anyhow::bail!(
648+
"ReplayGain analysis requires the 'replaygain' feature.\n\
649+
Install with: cargo install mp3rgain --features replaygain"
650+
)
651+
}
652+
593653
/// Check if ReplayGain feature is available
594654
pub fn is_available() -> bool {
595655
cfg!(feature = "replaygain")

0 commit comments

Comments
 (0)