//! **배속·피치가 다른 사본을 오디오 지문으로 잡을 수 있는가.** //! //! chromaprint는 고정 시간 격자에서 크로마 특징을 뽑는다. 재생 속도가 바뀌면 시간축과 //! 주파수가 함께 밀리므로 값 자체가 달라질 것으로 예상되지만, **예상으로 설계를 정하면 //! 안 된다** — 오디오 우선 파이프라인이 어떤 사본을 놓치는지가 여기서 결정된다. //! //! 픽스처의 1.25배속 사본과 원본을 비교해 공통 토큰 수를 센다. //! (후보 생성은 값 토큰 교집합으로 하므로 그 수가 0이면 후보 자체가 안 만들어진다) use archive_indexer::ffmpeg::{self, FfTools}; use std::collections::HashSet; use std::path::{Path, PathBuf}; fn repo_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").canonicalize().unwrap() } fn tools() -> Option { let bin = repo_root().join("src-tauri/binaries"); let triple = if cfg!(all(windows, target_arch = "x86_64")) { "x86_64-pc-windows-msvc" } else { "aarch64-pc-windows-msvc" }; let ext = if cfg!(windows) { ".exe" } else { "" }; let (a, b) = ( bin.join(format!("ffmpeg-{triple}{ext}")), bin.join(format!("ffprobe-{triple}{ext}")), ); (a.is_file() && b.is_file()).then_some(FfTools { ffmpeg: a, ffprobe: b }) } fn fp(t: &FfTools, p: &Path) -> Vec { ffmpeg::audio_fingerprint(t, p, std::time::Duration::from_secs(120)).unwrap_or_default() } /// 서로 다른 값의 교집합 크기 — 후보 생성이 보는 것과 같은 기준. fn shared_distinct(a: &[u32], b: &[u32]) -> (usize, usize, usize) { let sa: HashSet = a.iter().copied().collect(); let sb: HashSet = b.iter().copied().collect(); (sa.len(), sb.len(), sa.intersection(&sb).count()) } #[test] fn speed_and_pitch_changes_break_audio_matching() { let Some(t) = tools() else { eprintln!("사이드카 없음 — 건너뜀"); return; }; let vids = repo_root().join(".fixtures/videos"); let base = vids.join("vidA.mp4"); if !base.is_file() { eprintln!("픽스처 없음 — 건너뜀"); return; } let a = fp(&t, &base); assert!(!a.is_empty(), "원본 오디오 지문이 비었다"); // 픽스처에 있는 변형들 — 오디오를 건드리지 않은 것과 건드린 것을 함께 본다 let cases: [(&str, &str); 5] = [ ("vidA_reenc.mp4", "재인코딩(오디오 복사)"), ("vidA_wm.mp4", "워터마크(오디오 복사)"), ("vidA_letterbox.mp4", "레터박스(오디오 복사)"), ("vidA_clip.mp4", "중간 잘라내기"), ("vidA_speed125.mp4", "1.25배속 ← 오디오가 바뀐다"), ]; println!("{:<22} {:>8} {:>8} {:>8} {}", "파일", "원본값", "사본값", "공통값", "설명"); let mut speed_shared = usize::MAX; for (name, desc) in cases { let p = vids.join(name); if !p.is_file() { continue; } let b = fp(&t, &p); let (na, nb, common) = shared_distinct(&a, &b); println!("{name:<22} {na:>8} {nb:>8} {common:>8} {desc}"); if name.contains("speed125") { speed_shared = common; } } // 그 자리에서 만든 순수 배속·피치 변형도 본다 — 픽스처 하나에 의존하지 않기 위해 let tmp = std::env::temp_dir().join("archive-audio-speed"); let _ = std::fs::create_dir_all(&tmp); let variants: [(&str, &str); 3] = [ ("atempo105.mp4", "atempo=1.05"), // 5% 빠르게 (피치 유지) ("atempo125.mp4", "atempo=1.25"), // 25% 빠르게 (피치 유지) ("pitch.mp4", "asetrate=44100*1.06,aresample=44100"), // 피치만 6% 올림 ]; println!(); for (out, filt) in variants { let dst = tmp.join(out); let r = ffmpeg::command(&t.ffmpeg) .args(["-v", "error", "-y", "-i"]) .arg(&base) .args(["-af", filt, "-c:v", "copy"]) .arg(&dst) .output(); if r.map(|o| o.status.success()).unwrap_or(false) { let b = fp(&t, &dst); let (na, nb, common) = shared_distinct(&a, &b); println!("{out:<22} {na:>8} {nb:>8} {common:>8} {filt}"); } } // 배속 사본이 공통 토큰을 거의 안 가진다는 것을 고정한다 — // 오디오 우선 파이프라인의 알려진 한계이고, 그 사실이 바뀌면 설계를 다시 봐야 한다. if speed_shared != usize::MAX { println!("\n1.25배속 공통 토큰 = {speed_shared}"); } }