//! Scores the note tracker on bass stems. //! //! ```text //! cargo run -p common-dsp --release --example tracker_report //! cargo run -p common-dsp --release --example tracker_report -- [--sub] [--bends] //! [--note F1] [--key free|minor|major|root] stem.wav ... //! ``` //! Without files it scores the synthetic corpus in `common_dsp::stems`, once with the stable //! default and once with Follow bends. With files, `--note` names the one note each file //! plays (for a wrong-note share); without it only note starts and unsure time are reported. use common_dsp::parse_note; use common_dsp::stems::{self, Score, Truth}; use common_dsp::tracker::{PitchSource, Scale, TrackerConfig}; use std::{env, fs, process}; fn row(name: &str, s: Score) { println!( "{name:<14} {:>6.1}s {:>8.2} {:>8.2} {:>7.1}% {:>7.1}%", s.seconds, s.starts, s.extra_starts, s.wrong * 100., s.unsure * 100. ); } fn header() { println!( "{:<14} {:>7} {:>8} {:>8} {:>8} {:>8}", "stem", "length", "starts/s", "extra/s", "wrong", "unsure" ); } fn main() { let mut cfg = TrackerConfig::BASS; let mut source = PitchSource::default(); let mut note = None; let mut files = vec![]; let mut args = env::args().skip(1); while let Some(a) = args.next() { match a.as_str() { "--sub" => cfg = TrackerConfig::SUB, "--bends" => source.bends = true, "--note" => { let n = args.next().and_then(|n| parse_note(&n)); let Some(n) = n else { eprintln!("--note needs a note name such as F1"); process::exit(2); }; note = Some(n); source.note = n; } "--key" => { source.scale = match args.next().as_deref() { Some("free") => Scale::Free, Some("minor") => Scale::Minor, Some("major") => Scale::Major, Some("root") => Scale::Root, _ => { eprintln!("--key needs free, minor, major or root"); process::exit(2); } }; } _ => files.push(a), } } if files.is_empty() { let rate = 48000.; for bends in [false, true] { println!( "\n{}", if bends { "Follow bends" } else { "Stable (default)" } ); header(); let s = PitchSource { bends, ..source }; for stem in stems::all(rate) { row( stem.name, stems::score(rate, cfg, s, &stem.samples, &stem.truth), ); } } return; } header(); for path in files { let wave = match fs::read(&path) .map_err(|e| e.to_string()) .and_then(|b| common_dsp::wav::decode(&b).map_err(str::to_string)) { Ok(w) => w, Err(e) => { eprintln!("{path}: {e}"); continue; } }; let channels = wave.channels as usize; let mono: Vec = wave .samples .chunks(channels) .map(|c| c.iter().sum::() / channels as f32) .collect(); // With a known note, quiet samples are skipped rather than counted as silence. let truth = match note { Some(n) => vec![Truth::Note(n as f32); mono.len()], None => vec![], }; let name = path.rsplit('/').next().unwrap_or(&path); row( name, stems::score(wave.rate as f32, cfg, source, &mono, &truth), ); } }