Producers found the tracked plugins jumpy: the shared tracker switched notes after 10 ms and retuned every 5 ms, so growls and formant basses flickered and every tracked plugin acted on wrong notes. Shared tracker (common-dsp): - A new note needs a steady read for 40 ms; an octave down 80 ms, an octave up 200 ms (an octave-up read still fits the note's period and counts as agreeing). Inside a note the tuning only moves past a 0.25 semitone deadband. Follow bends keeps the old fast behaviour as an option. - A trust score turns the note unsure while reads disagree; Tracker::active and NoteState report it, and Fallback latches "step back" across note ends. - Notes end when the window start stays below the gate's release level for 20 ms, so short gaps between notes are seen without waiting for the gate. - PitchSource replaces the old (midi, gate, glide) arguments: Audio, MIDI or Note mode (a set note, no detection), plus a key lock (minor, major, root only) that snaps reads to the key. - stems.rs: synthetic stems (growls, wobble, reese, formant, 808 slide, layered drop, noise riser) and a score; examples/tracker_report scores the corpus or real WAV stems. On the corpus the old behaviour makes up to 28 extra note starts per second; the stable default makes none. Plugins: - Anvil, Lathe, Temper, Knurl, Planish, Braze and Forge gain the NOTE pitch source, the key lock and Follow bends (shared common-params crate and a shared header switch in common-ui). - Unsure tracking: Anvil brings the original sub back in place of the sine, Lathe passes the audio through, Knurl fades its texture out; Temper, Planish and Braze hold their last sure tuning. Readouts show the note only when it is sure. - Anvil gains Mode: Replace or Enhance (keeps the original sub and adds the sine under it). - Planish slides its band edges over 6 ms on a new note instead of snapping. - Ballast defaults to Grid parts, which need no tracking.
119 lines
3.8 KiB
Rust
119 lines
3.8 KiB
Rust
//! 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<f32> = wave
|
|
.samples
|
|
.chunks(channels)
|
|
.map(|c| c.iter().sum::<f32>() / 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),
|
|
);
|
|
}
|
|
}
|