A tool to sync music with your favorite devices
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

*: write replaygain/soundcheck information when transcoding

Gee Sawra 6ce59f9b 645057be

+143 -40
+21 -10
src/cmd/sync.rs
··· 1 1 use crate::cmd::*; 2 2 use crate::db; 3 3 use crate::ffmpeg; 4 + use crate::ffmpeg::TranscodeResult; 4 5 use crate::gpod; 5 6 use crate::model; 6 7 use anyhow::Ok; ··· 276 277 false => (), 277 278 } 278 279 let dest_path = ipm.path_for_file(fp.as_path()).unwrap(); 279 - let copied_track = copy(&track, dest_db, dest_path.clone(), trs).await?; 280 + let (copied_track, tr) = copy(&track, dest_db, dest_path.clone(), trs).await?; 280 281 281 - Ok((copied_track, dest_path)) 282 + Ok((copied_track, dest_path, tr.unwrap())) 282 283 } 283 284 }) 284 285 .buffer_unordered(threads) ··· 286 287 .await?; 287 288 288 289 log::info!("finalizing..."); 289 - for (track, path) in converted { 290 - ipod_manager.finalize_track(&track.clone(), path.clone(), track.extension, 44100, 256)?; 290 + for (track, path, trans_res) in converted { 291 + ipod_manager.finalize_track( 292 + &track.clone(), 293 + path.clone(), 294 + track.extension, 295 + 44100, 296 + 256, 297 + trans_res.replaygain.map(|r| r.soundcheck_value()), 298 + )?; 291 299 } 292 300 293 301 return Ok(()); ··· 384 392 dest_db: &db::Instance, 385 393 dest_fname: PathBuf, 386 394 transcoding_settings: Option<ffmpeg::Quality>, 387 - ) -> Result<model::Track> { 395 + ) -> Result<(model::Track, Option<TranscodeResult>)> { 388 396 // We replicate src database on dest 1:1 389 397 // We should create the same logical database (tracks with the same tags have the same hash) 390 398 // ignoring the format. ··· 424 432 425 433 let opts = CopyOptions::new().overwrite(true); 426 434 427 - async_std::task::spawn_blocking({ 435 + let maybe_transcode_result = async_std::task::spawn_blocking({ 428 436 let track_for_thread = track.clone(); 429 437 let ts_for_thread = transcoding_settings.clone(); 430 438 ··· 435 443 log::info!("transcoding & copying {} ({})", track_for_thread, ts); 436 444 log::debug!("transcoding path: {}", track_dest_path.display()); 437 445 438 - ffmpeg::convert(&track_for_thread, track_dest_path, &ts)?; 439 - return Ok(()); 446 + return Ok(Some(ffmpeg::convert( 447 + &track_for_thread, 448 + track_dest_path, 449 + &ts, 450 + )?)); 440 451 } 441 452 442 453 log::info!("copying {}", track_for_thread); ··· 460 471 } 461 472 }; 462 473 463 - Ok(()) 474 + Ok(None) 464 475 } 465 476 }) 466 477 .await?; ··· 471 482 .await 472 483 .with_context(|| "Cannot insert copy finished track in destination database")?; 473 484 474 - Ok(track.clone()) 485 + Ok((track.clone(), maybe_transcode_result)) 475 486 }
+117 -3
src/ffmpeg.rs
··· 1 1 use anyhow::{Result, anyhow}; 2 2 use clap::{builder::TypedValueParser, error::ErrorKind}; 3 - use std::{path::PathBuf, str::FromStr}; 3 + use regex::Regex; 4 + use std::{path::PathBuf, str::FromStr, sync::LazyLock}; 4 5 5 6 use crate::model::Track; 6 7 ··· 245 246 } 246 247 } 247 248 248 - pub fn convert(track: &Track, dest: PathBuf, quality: &Quality) -> Result<()> { 249 + pub struct TranscodeResult { 250 + pub replaygain: Option<ReplayGain>, 251 + } 252 + 253 + pub fn convert(track: &Track, dest: PathBuf, quality: &Quality) -> Result<TranscodeResult> { 249 254 let mut cmd = std::process::Command::new("ffmpeg"); 250 255 let res = cmd 251 256 .arg("-y") ··· 273 278 .split(" ") 274 279 .collect::<Vec<&str>>(), 275 280 ) 281 + .args(["-filter:a", "replaygain"]) 276 282 .arg(dest.display().to_string()) 277 283 .args(quality.codec.ffmpeg_flags()) 278 284 .output()?; 285 + 286 + let rg = ReplayGain::parse_from_str(&String::from_utf8(res.stderr.clone()).unwrap()); 287 + 288 + log::debug!("replaygain metadata: {:?}", rg); 279 289 280 290 match res.status.success() { 281 - true => Ok(()), 291 + true => Ok(TranscodeResult { replaygain: rg }), 282 292 false => Err(anyhow!( 283 293 "{}: {}", 284 294 res.status, ··· 286 296 )), 287 297 } 288 298 } 299 + 300 + #[derive(Debug)] 301 + pub struct ReplayGain { 302 + pub track_gain: f64, 303 + } 304 + 305 + static RG_REGEXP: LazyLock<Regex> = 306 + std::sync::LazyLock::new(|| regex::Regex::new("track_gain = ((.+) dB)").unwrap()); 307 + 308 + impl ReplayGain { 309 + fn parse_from_str(out: &str) -> Option<Self> { 310 + let gain_str = match RG_REGEXP.captures(out) { 311 + Some(g) => g, 312 + None => return None, 313 + }; 314 + 315 + Some(ReplayGain { 316 + track_gain: gain_str.get(2).unwrap().as_str().parse().unwrap(), 317 + }) 318 + } 319 + 320 + // From iPodLinux iTunesDB's page: 321 + // 4 The SoundCheck value to apply to the song, when SoundCheck is switched on in the iPod settings. 322 + // The value to put in this field can be determined by the equation: X = 1000 * 10 ^ (-.1 * Y) 323 + // where Y is the adjustment value in dB and X is the value that goes into the SoundCheck field. 324 + // The value 0 is special, the equation is not used and it is treated as "no Soundcheck" (basically the same as the value 1000). 325 + // This equation works perfectly well with ReplayGain derived data instead of the iTunes SoundCheck derived information. 326 + pub fn soundcheck_value(&self) -> u32 { 327 + let ten: f64 = 10.0; 328 + let res = (1000.0 * ten.powf(-0.1 * self.track_gain)).trunc(); 329 + res as u32 330 + } 331 + } 332 + 333 + #[cfg(test)] 334 + mod tests { 335 + use super::*; 336 + 337 + #[test] 338 + fn replaygain_parse() { 339 + let out = r#"ffmpeg version 7.1.3 Copyright (c) 2000-2025 the FFmpeg developers 340 + built with gcc 14 (Debian 14.2.0-19) 341 + configuration: --disable-decoder=amrnb --disable-gnutls --disable-liblensfun --disable-libopencv --disable-podpages --disable-sndio --disable-stripping --disable-omx --enable-avfilter --enable-chromaprint --enable-frei0r --enable-gcrypt --enable-gpl --enable-ladspa --enable-libaom --enable-libaribb24 --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libdavs2 --enable-libdc1394 --enable-libdrm --enable-libdvdnav --enable-libdvdread --enable-libfdk-aac --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libharfbuzz --enable-libiec61883 --enable-libilbc --enable-libjack --enable-libjxl --enable-libklvanc --enable-libkvazaar --enable-libmp3lame --enable-libmysofa --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libplacebo --enable-libpulse --enable-librabbitmq --enable-librist --enable-librsvg --enable-librubberband --enable-libshine --enable-libsmbclient --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvmaf --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-nonfree --enable-openal --enable-opencl --enable-opengl --enable-openssl --enable-postproc --enable-pthreads --enable-shared --enable-version3 --incdir=/usr/include/aarch64-linux-gnu --libdir=/usr/lib/aarch64-linux-gnu --prefix=/usr --toolchain=hardened --cc=aarch64-linux-gnu-gcc --cxx=aarch64-linux-gnu-g++ --disable-altivec --shlibdir=/usr/lib/aarch64-linux-gnu 342 + libavutil 59. 39.100 / 59. 39.100 343 + libavcodec 61. 19.101 / 61. 19.101 344 + libavformat 61. 7.100 / 61. 7.100 345 + libavdevice 61. 3.100 / 61. 3.100 346 + libavfilter 10. 4.100 / 10. 4.100 347 + libswscale 8. 3.100 / 8. 3.100 348 + libswresample 5. 3.100 / 5. 3.100 349 + libpostproc 58. 3.100 / 58. 3.100 350 + Input #0, flac, from 'elegy.flac': 351 + Metadata: 352 + TITLE : Elegy 353 + ARTIST : Architects 354 + album_artist : Architects 355 + ALBUM : The Sky, The Earth & All Between 356 + disc : 1 357 + track : 1 358 + DATE : 2025 359 + TRACKTOTAL : 12 360 + DISCTOTAL : 1 361 + ISRC : USEP42418003 362 + Duration: 00:03:35.57, start: 0.000000, bitrate: 1003 kb/s 363 + Stream #0:0: Audio: flac, 44100 Hz, stereo, s16 364 + File 'elegy.opus' already exists. Overwrite? [y/N] y 365 + Stream mapping: 366 + Stream #0:0 -> #0:0 (flac (native) -> opus (libopus)) 367 + Press [q] to stop, [?] for help 368 + Output #0, opus, to 'elegy.opus': 369 + Metadata: 370 + TITLE : Elegy 371 + ARTIST : Architects 372 + album_artist : Architects 373 + ALBUM : The Sky, The Earth & All Between 374 + disc : 1 375 + track : 1 376 + DATE : 2025 377 + TRACKTOTAL : 12 378 + DISCTOTAL : 1 379 + ISRC : USEP42418003 380 + encoder : Lavf61.7.100 381 + Stream #0:0: Audio: opus, 48000 Hz, stereo, flt, 64 kb/s 382 + Metadata: 383 + encoder : Lavc61.19.101 libopus 384 + TITLE : Elegy 385 + ARTIST : Architects 386 + ALBUMARTIST : Architects 387 + ALBUM : The Sky, The Earth & All Between 388 + DISCNUMBER : 1 389 + TRACKNUMBER : 1 390 + DATE : 2025 391 + TRACKTOTAL : 12 392 + DISCTOTAL : 1 393 + ISRC : USEP42418003 394 + [Parsed_replaygain_0 @ 0xffff6c002ed0] track_gain = -10.61 dB=72.2x 395 + [Parsed_replaygain_0 @ 0xffff6c002ed0] track_peak = 1.020527 396 + [out#0/opus @ 0xaaaad66a9100] video:0KiB audio:1637KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 1.022192% 397 + size= 1654KiB time=00:03:35.56 bitrate= 62.8kbits/s speed=72.3x 398 + "#; 399 + let rp = ReplayGain::parse_from_str(out).unwrap(); 400 + println!("rp: {} sc: {}", rp.track_gain, rp.soundcheck_value()); 401 + } 402 + }
+5 -27
src/gpod/gpod.rs
··· 71 71 } 72 72 } 73 73 74 - // TODO: refactor this to write track to ipod while transcoding. 75 - /* 76 - * this is how libgpod cp_track_to_ipod does it: 77 - * 78 - // we can pass NULL as track, since we're copying a new track for the first time 79 - // and the expected perf optimization won't apply. 80 - // conversely, the second parameter must be the ipod mount_point, not NULL. 81 - dest_filename = itdb_cp_get_dest_filename (track, NULL, filename, error); 82 - 83 - if (dest_filename) 84 - { 85 - if (itdb_cp (filename, dest_filename, error)) 86 - { 87 - if (itdb_cp_finalize (track, NULL, dest_filename, error)) 88 - { 89 - result = TRUE; 90 - } 91 - } 92 - g_free (dest_filename); 93 - } 94 - 95 - we can return the dest filename calculated based on the filename, give it to ffmpeg, 96 - then finalize? 97 - 98 - requires refactoring how we do copies, but we don't litter the hosts' temp dir 99 - and should be faster 100 - */ 101 74 pub fn path_for_file(&self, path: &Path) -> Result<PathBuf> { 102 75 unsafe { 103 76 let mp = std::ffi::CString::new(self.mountpoint.clone()).unwrap(); ··· 134 107 codec: String, 135 108 sample_rate: u16, 136 109 bitrate: i32, 110 + soundcheck: Option<u32>, 137 111 ) -> Result<()> { 138 112 unsafe { 139 113 let mp = std::ffi::CString::new(self.mountpoint.clone()).unwrap(); ··· 156 130 .st_size() 157 131 .try_into() 158 132 .unwrap(); 133 + 134 + if let Some(sc) = soundcheck { 135 + (*ipod_track).soundcheck = sc; 136 + } 159 137 160 138 let mut error: *mut GError = ptr::null_mut(); 161 139 let path = CString::new(file.display().to_string()).unwrap();