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.
561 lines
18 KiB
Rust
561 lines
18 KiB
Rust
use anvil_core::{Engine, Mode, Monitor, Settings, Source};
|
||
use common_dsp::Saw;
|
||
|
||
const RATE: f32 = 48000.;
|
||
|
||
/// Saw notes (start s, end s, Hz, peak) with 1 ms fades.
|
||
fn notes(list: &[(f32, f32, f32, f32)], total: f32) -> Vec<f32> {
|
||
let mut x = vec![0.; (total * RATE) as usize];
|
||
for &(a, b, hz, amp) in list {
|
||
let (a, b) = ((a * RATE) as usize, (b * RATE) as usize);
|
||
let mut saw = Saw::new(RATE, hz);
|
||
let fade = (0.001 * RATE) as usize;
|
||
for i in a..b.min(x.len()) {
|
||
let edge = ((i - a).min(b - 1 - i) as f32 / fade as f32).min(1.);
|
||
x[i] += saw.sample() * amp * edge;
|
||
}
|
||
}
|
||
x
|
||
}
|
||
fn render(e: &mut Engine, x: &[f32], solo: bool) -> Vec<f32> {
|
||
x.iter()
|
||
.map(|&s| {
|
||
e.process_sample(
|
||
[s, s],
|
||
Monitor {
|
||
solo,
|
||
..Monitor::default()
|
||
},
|
||
)[0]
|
||
})
|
||
.collect()
|
||
}
|
||
fn rms(x: &[f32]) -> f32 {
|
||
(x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32).sqrt()
|
||
}
|
||
fn db(x: f32) -> f32 {
|
||
20. * x.max(1e-9).log10()
|
||
}
|
||
/// Frequency from interpolated rising zero crossings.
|
||
fn frequency(x: &[f32]) -> f32 {
|
||
let mut first = None;
|
||
let mut last = 0.;
|
||
let mut count = 0;
|
||
for i in 1..x.len() {
|
||
if x[i - 1] < 0. && x[i] >= 0. {
|
||
let t = (i - 1) as f32 + x[i - 1] / (x[i - 1] - x[i]);
|
||
if first.is_none() {
|
||
first = Some(t);
|
||
} else {
|
||
count += 1;
|
||
}
|
||
last = t;
|
||
}
|
||
}
|
||
count as f32 * RATE / (last - first.unwrap())
|
||
}
|
||
fn seconds(t: f32) -> usize {
|
||
(t * RATE) as usize
|
||
}
|
||
fn engine(s: Settings) -> (Engine, usize) {
|
||
let mut e = Engine::new(RATE, 2).unwrap();
|
||
e.set_settings(s).unwrap();
|
||
let l = e.latency_samples() as usize;
|
||
(e, l)
|
||
}
|
||
|
||
#[test]
|
||
fn every_note_gets_the_same_sub_level() {
|
||
let (mut e, l) = engine(Settings::default());
|
||
let x = notes(&[(0.1, 0.6, 55., 0.5), (0.8, 1.3, 55., 0.1)], 1.6);
|
||
let y = render(&mut e, &x, true);
|
||
let loud = rms(&y[seconds(0.3) + l..seconds(0.5) + l]);
|
||
let quiet = rms(&y[seconds(1.0) + l..seconds(1.2) + l]);
|
||
assert!(
|
||
(db(loud) - db(quiet)).abs() < 0.5,
|
||
"{} vs {}",
|
||
db(loud),
|
||
db(quiet)
|
||
);
|
||
// −9 dBFS peak sine.
|
||
assert!((db(loud * 2f32.sqrt()) + 9.).abs() < 0.5, "{}", db(loud));
|
||
}
|
||
|
||
#[test]
|
||
fn sub_follows_the_detected_pitch_and_octave() {
|
||
for (octave, expect) in [(0, 43.654), (-1, 21.827)] {
|
||
let (mut e, l) = engine(Settings {
|
||
octave,
|
||
..Settings::default()
|
||
});
|
||
let x = notes(&[(0.1, 1.1, 43.65, 0.5)], 1.3);
|
||
let y = render(&mut e, &x, true);
|
||
let f = frequency(&y[seconds(0.3) + l..seconds(1.0) + l]);
|
||
assert!((f / expect - 1.).abs() < 0.005, "octave {octave}: {f}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_pitch_change_without_a_gap_is_a_new_note() {
|
||
let (mut e, l) = engine(Settings::default());
|
||
let x = notes(&[(0.1, 0.6, 55., 0.5), (0.6, 1.1, 82.41, 0.5)], 1.3);
|
||
let y = render(&mut e, &x, true);
|
||
let f = frequency(&y[seconds(0.75) + l..seconds(1.05) + l]);
|
||
assert!((f / 82.41 - 1.).abs() < 0.005, "{f}");
|
||
}
|
||
|
||
#[test]
|
||
fn every_note_restarts_at_the_start_phase() {
|
||
let (mut e, l) = engine(Settings {
|
||
phase: 90.,
|
||
..Settings::default()
|
||
});
|
||
let x = notes(&[(0.1, 0.4, 55., 0.5), (0.6, 0.9, 55., 0.3)], 1.1);
|
||
let y = render(&mut e, &x, true);
|
||
let period = (RATE / 55.) as usize;
|
||
let a = &y[seconds(0.1) + l..seconds(0.1) + l + 2 * period];
|
||
let b = &y[seconds(0.6) + l..seconds(0.6) + l + 2 * period];
|
||
let peak = y.iter().fold(0f32, |m, v| m.max(v.abs()));
|
||
let worst = a.iter().zip(b).fold(0f32, |m, (p, q)| m.max((p - q).abs()));
|
||
assert!(worst < 0.1 * peak, "{worst} of {peak}");
|
||
// Fit the phase over the second period, after the 2 ms attack.
|
||
let w = std::f32::consts::TAU * 55. / RATE;
|
||
let (mut s, mut c) = (0., 0.);
|
||
for (k, v) in a.iter().enumerate().skip(period) {
|
||
s += v * (w * k as f32).sin();
|
||
c += v * (w * k as f32).cos();
|
||
}
|
||
let phase = c.atan2(s).to_degrees();
|
||
assert!((phase - 90.).abs() < 20., "{phase}");
|
||
}
|
||
|
||
#[test]
|
||
fn midi_notes_set_pitch_and_are_delayed_by_the_latency() {
|
||
let (mut e, l) = engine(Settings {
|
||
source: Source::Midi,
|
||
..Settings::default()
|
||
});
|
||
let mut y = Vec::new();
|
||
for i in 0..seconds(0.8) {
|
||
if i == 1000 {
|
||
e.note_on(33);
|
||
}
|
||
if i == seconds(0.6) {
|
||
e.note_off(33);
|
||
}
|
||
y.push(
|
||
e.process_sample(
|
||
[0.; 2],
|
||
Monitor {
|
||
solo: true,
|
||
..Monitor::default()
|
||
},
|
||
)[0],
|
||
);
|
||
}
|
||
assert!(y[..1000 + l].iter().all(|v| *v == 0.));
|
||
assert!(y[1000 + l + 50].abs() > 0.);
|
||
let f = frequency(&y[seconds(0.2) + l..seconds(0.5)]);
|
||
assert!((f / 55. - 1.).abs() < 0.001, "{f}");
|
||
assert!(y[seconds(0.6) + l + seconds(0.05)..]
|
||
.iter()
|
||
.all(|v| v.abs() < 1e-6));
|
||
}
|
||
|
||
#[test]
|
||
fn note_changes_do_not_click() {
|
||
let (mut e, _) = engine(Settings::default());
|
||
let x = notes(
|
||
&[
|
||
(0.1, 0.4, 55., 0.5),
|
||
(0.4, 0.7, 73.42, 0.5),
|
||
(0.7, 1.0, 41.2, 0.5),
|
||
],
|
||
1.3,
|
||
);
|
||
let y = render(&mut e, &x, true);
|
||
let step = y.windows(2).fold(0f32, |m, w| m.max((w[1] - w[0]).abs()));
|
||
// A −9 dBFS sine at 73 Hz moves at most 0.0034 per sample.
|
||
assert!(step < 0.012, "{step}");
|
||
}
|
||
|
||
#[test]
|
||
fn keep_without_notes_is_an_allpass() {
|
||
let (mut e, _) = engine(Settings {
|
||
source: Source::Midi,
|
||
keep: 100.,
|
||
..Settings::default()
|
||
});
|
||
for hz in [40., 90., 300., 3000.] {
|
||
e.reset();
|
||
let x: Vec<f32> = (0..seconds(1.))
|
||
.map(|i| 0.3 * (std::f32::consts::TAU * hz * i as f32 / RATE).sin())
|
||
.collect();
|
||
let y = render(&mut e, &x, false);
|
||
let d = db(rms(&y[seconds(0.5)..])) - db(rms(&x[seconds(0.5)..]));
|
||
assert!(d.abs() < 0.1, "{hz}: {d}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn split_removes_original_sub_and_keeps_mids() {
|
||
let (mut e, l) = engine(Settings {
|
||
source: Source::Midi,
|
||
..Settings::default()
|
||
});
|
||
let x: Vec<f32> = (0..seconds(1.))
|
||
.map(|i| 0.3 * (std::f32::consts::TAU * 40. * i as f32 / RATE).sin())
|
||
.collect();
|
||
let y = render(&mut e, &x, false);
|
||
assert!(db(rms(&y[seconds(0.5) + l..])) < db(0.3) - 18.);
|
||
}
|
||
|
||
#[test]
|
||
fn settings_are_validated_and_serialise() {
|
||
let mut e = Engine::new(RATE, 1).unwrap();
|
||
assert!(e
|
||
.set_settings(Settings {
|
||
level: 3.,
|
||
..Settings::default()
|
||
})
|
||
.is_err());
|
||
let json = serde_json::to_string(&Settings::default()).unwrap();
|
||
assert_eq!(
|
||
serde_json::from_str::<Settings>(&json).unwrap(),
|
||
Settings::default()
|
||
);
|
||
assert!(Engine::new(1000., 2).is_err());
|
||
assert!(Engine::new(48000., 3).is_err());
|
||
}
|
||
|
||
/// Peak amplitude of the component at `hz`, over a window of whole cycles.
|
||
fn amplitude(x: &[f32], hz: f32) -> f32 {
|
||
let w = std::f32::consts::TAU * hz / RATE;
|
||
let (re, im) = x.iter().enumerate().fold((0., 0.), |(re, im), (n, v)| {
|
||
(re + v * (w * n as f32).cos(), im - v * (w * n as f32).sin())
|
||
});
|
||
2. * (re * re + im * im).sqrt() / x.len() as f32
|
||
}
|
||
/// Sines (Hz, peak) from 0.1 s to `total` − 0.1 s, with 1 ms fades.
|
||
fn tones(list: &[(f32, f32)], total: f32) -> Vec<f32> {
|
||
let n = (total * RATE) as usize;
|
||
let (a, b, fade) = (seconds(0.1), n - seconds(0.1), seconds(0.001));
|
||
(0..n)
|
||
.map(|i| {
|
||
if i < a || i >= b {
|
||
return 0.;
|
||
}
|
||
let edge = ((i - a).min(b - 1 - i) as f32 / fade as f32).min(1.);
|
||
let t = (i - a) as f32 / RATE;
|
||
edge * list
|
||
.iter()
|
||
.map(|&(hz, amp)| amp * (std::f32::consts::TAU * hz * t).sin())
|
||
.sum::<f32>()
|
||
})
|
||
.collect()
|
||
}
|
||
fn ghost(amount: f32) -> Settings {
|
||
Settings {
|
||
ghost: amount,
|
||
ghost_from: 300.,
|
||
..Settings::default()
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ghost_fills_the_harmonics_a_clean_sub_lacks() {
|
||
let (mut e, l) = engine(ghost(100.));
|
||
let x = tones(&[(55., 0.5)], 1.);
|
||
let y = render(&mut e, &x, true);
|
||
// 0.2 s holds exactly 11 cycles of 55 Hz.
|
||
let w = &y[seconds(0.5) + l..seconds(0.7) + l];
|
||
let sub = amplitude(w, 55.);
|
||
assert!((db(sub) + 9.).abs() < 0.5, "{}", db(sub));
|
||
for k in 2..=5 {
|
||
// Below Ghost from: nothing added, so the sub stays a clean sine there.
|
||
let a = amplitude(w, 55. * k as f32);
|
||
assert!(db(a / sub) < -60., "harmonic {k}: {}", db(a / sub));
|
||
}
|
||
for k in 8..=10 {
|
||
let a = amplitude(w, 55. * k as f32);
|
||
let expect = 20. * (2. / k as f32).log10();
|
||
assert!(
|
||
(db(a / sub) - expect).abs() < 1.,
|
||
"harmonic {k}: {}",
|
||
db(a / sub)
|
||
);
|
||
}
|
||
// Harmonic 6 (330 Hz) sits in the fade above 300 Hz: present, but quieter.
|
||
let six = db(amplitude(w, 330.) / sub);
|
||
assert!(six < 20. * (2f32 / 6.).log10() - 3. && six > -40., "{six}");
|
||
}
|
||
|
||
#[test]
|
||
fn ghost_leaves_harmonics_the_patch_already_has() {
|
||
let (mut e, l) = engine(ghost(100.));
|
||
// The patch carries its 8th harmonic (440 Hz) louder than Ghost would add it.
|
||
let x = tones(&[(55., 0.5), (440., 0.2)], 1.);
|
||
let y = render(&mut e, &x, false);
|
||
let w = &y[seconds(0.5) + l..seconds(0.7) + l];
|
||
let eighth = amplitude(w, 440.);
|
||
assert!((db(eighth) - db(0.2)).abs() < 0.5, "{}", db(eighth));
|
||
// The 9th is missing from the patch, so Ghost fills it.
|
||
let ninth = amplitude(w, 495.) / amplitude(w, 55.);
|
||
assert!(
|
||
(db(ninth) - 20. * (2f32 / 9.).log10()).abs() < 1.,
|
||
"{}",
|
||
db(ninth)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn ghost_off_adds_nothing_and_phone_listen_drops_the_sub() {
|
||
let x = tones(&[(55., 0.5)], 1.);
|
||
let (mut e, l) = engine(ghost(0.));
|
||
let y = render(&mut e, &x, true);
|
||
let w = &y[seconds(0.5) + l..seconds(0.7) + l];
|
||
for k in 2..=10 {
|
||
assert!(amplitude(w, 55. * k as f32) < 1e-4, "harmonic {k}");
|
||
}
|
||
let phone = |amount: f32| {
|
||
let (mut e, l) = engine(ghost(amount));
|
||
let y: Vec<f32> = x
|
||
.iter()
|
||
.map(|&s| {
|
||
e.process_sample(
|
||
[s, s],
|
||
Monitor {
|
||
solo: true,
|
||
phone: true,
|
||
audition: false,
|
||
},
|
||
)[0]
|
||
})
|
||
.collect();
|
||
rms(&y[seconds(0.5) + l..seconds(0.7) + l])
|
||
};
|
||
// A clean 55 Hz sub all but vanishes on a phone; Ghost makes the note heard.
|
||
let (plain, ghosted) = (phone(0.), phone(100.));
|
||
assert!(db(plain) < -50., "{}", db(plain));
|
||
assert!(
|
||
db(ghosted) - db(plain) > 25.,
|
||
"{} vs {}",
|
||
db(ghosted),
|
||
db(plain)
|
||
);
|
||
}
|
||
|
||
/// A kick-like burst: a 55 Hz sine starting at phase 0 with a 60 ms decay, at each time.
|
||
fn kicks(times: &[f32], total: f32) -> Vec<f32> {
|
||
let mut k = vec![0.; seconds(total)];
|
||
for &t in times {
|
||
for i in 0..seconds(0.2) {
|
||
let n = seconds(t) + i;
|
||
if n < k.len() {
|
||
let t = i as f32 / RATE;
|
||
k[n] += 0.8 * (std::f32::consts::TAU * 55. * t).sin() * (-t / 0.06).exp();
|
||
}
|
||
}
|
||
}
|
||
k
|
||
}
|
||
fn render_kick(e: &mut Engine, x: &[f32], k: &[f32], monitor: Monitor) -> Vec<f32> {
|
||
x.iter()
|
||
.zip(k)
|
||
.map(|(&s, &k)| e.process_with_kick([s, s], k, monitor)[0])
|
||
.collect()
|
||
}
|
||
/// Normalised correlation of `y[a..b]` with a sine of `hz` at phase 0 at sample `zero`.
|
||
fn sine_match(y: &[f32], a: usize, b: usize, hz: f32, zero: f32) -> f32 {
|
||
let w = std::f32::consts::TAU * hz / RATE;
|
||
let (mut xy, mut xx, mut yy) = (0., 0., 0.);
|
||
for (i, &v) in y.iter().enumerate().take(b).skip(a) {
|
||
let r = (w * (i as f32 - zero)).sin();
|
||
xy += v * r;
|
||
xx += r * r;
|
||
yy += v * v;
|
||
}
|
||
xy / (xx * yy).sqrt()
|
||
}
|
||
const SOLO: Monitor = Monitor {
|
||
solo: true,
|
||
phone: false,
|
||
audition: false,
|
||
};
|
||
fn kick_lock(offset: f32) -> Settings {
|
||
Settings {
|
||
kick_lock: true,
|
||
kick_offset: offset,
|
||
..Settings::default()
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_kick_restarts_the_sub_at_the_start_phase_and_ducks_it() {
|
||
// A bass note that started off the kick: before the hit, the sub runs at its own phase.
|
||
let x = tones(&[(55., 0.5)], 1.4);
|
||
let k = kicks(&[0.613], 1.4);
|
||
let (mut e, l) = engine(kick_lock(0.));
|
||
let y = render_kick(&mut e, &x, &k, SOLO);
|
||
let hit = seconds(0.613) + l;
|
||
let after = sine_match(
|
||
&y,
|
||
hit + seconds(0.004),
|
||
hit + seconds(0.04),
|
||
55.,
|
||
hit as f32,
|
||
);
|
||
assert!(after > 0.99, "{after}");
|
||
let before = rms(&y[hit - seconds(0.1)..hit - seconds(0.01)]);
|
||
let ducked = rms(&y[hit + seconds(0.004)..hit + seconds(0.022)]);
|
||
let back = rms(&y[hit + seconds(0.4)..hit + seconds(0.5)]);
|
||
assert!(
|
||
db(before) - db(ducked) > 2.,
|
||
"{} vs {}",
|
||
db(before),
|
||
db(ducked)
|
||
);
|
||
assert!(
|
||
(db(back) - db(before)).abs() < 0.3,
|
||
"{} vs {}",
|
||
db(back),
|
||
db(before)
|
||
);
|
||
assert_eq!(e.take_analyzer().kick_hits, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn the_offset_restarts_the_sub_before_or_after_the_kick() {
|
||
let x = tones(&[(55., 0.5)], 1.2);
|
||
let k = kicks(&[0.613], 1.2);
|
||
for offset in [-10., 7.] {
|
||
// No duck: only the phase is checked here.
|
||
let (mut e, l) = engine(Settings {
|
||
kick_duck: 0.,
|
||
..kick_lock(offset)
|
||
});
|
||
let y = render_kick(&mut e, &x, &k, SOLO);
|
||
let start = (seconds(0.613) + l) as f32 + offset * 0.001 * RATE;
|
||
let a = start as usize + seconds(0.004);
|
||
let m = sine_match(&y, a, a + seconds(0.03), 55., start);
|
||
assert!(m > 0.99, "{offset}: {m}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn without_kick_lock_the_sidechain_changes_nothing() {
|
||
let x = tones(&[(55., 0.5)], 1.);
|
||
let k = kicks(&[0.3, 0.6], 1.);
|
||
let (mut a, _) = engine(Settings::default());
|
||
let (mut b, _) = engine(Settings::default());
|
||
let with = render_kick(&mut a, &x, &k, SOLO);
|
||
let without = render(&mut b, &x, true);
|
||
assert_eq!(with, without);
|
||
}
|
||
|
||
#[test]
|
||
fn the_audition_finds_where_kick_and_sub_sum() {
|
||
use anvil_core::{AUDITION, AUDITION_HITS};
|
||
let hits = AUDITION.len() * AUDITION_HITS as usize;
|
||
let total = 0.5 + hits as f32 * 0.2 + 0.4;
|
||
let x = tones(&[(55., 0.5)], total);
|
||
let times: Vec<f32> = (0..hits).map(|i| 0.5 + i as f32 * 0.2).collect();
|
||
let k = kicks(×, total);
|
||
let (mut e, _) = engine(kick_lock(0.));
|
||
let monitor = Monitor {
|
||
audition: true,
|
||
..SOLO
|
||
};
|
||
render_kick(&mut e, &x, &k, monitor);
|
||
let f = e.take_analyzer();
|
||
assert_eq!(f.kick_hits as usize, hits);
|
||
let sums = f.audition_sums;
|
||
assert!(sums.iter().all(|v| v.is_finite()), "{sums:?}");
|
||
// The kick is a 55 Hz burst at phase 0, like the sub: they sum fully at 0 ms and
|
||
// cancel half a period (9 ms) away.
|
||
let best = (0..sums.len())
|
||
.max_by(|&a, &b| sums[a].total_cmp(&sums[b]))
|
||
.unwrap();
|
||
assert_eq!(AUDITION[best], 0., "{sums:?}");
|
||
let zero = sums[6];
|
||
assert!(zero > 2., "{zero}");
|
||
for i in [4, 8] {
|
||
assert!(
|
||
zero - sums[i] > 4.,
|
||
"{} ms: {} vs {zero}",
|
||
AUDITION[i],
|
||
sums[i]
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn unsure_tracking_brings_back_the_original_sub() {
|
||
let (mut e, l) = engine(Settings::default());
|
||
// A clear note, then low noise the tracker cannot read.
|
||
let mut x = notes(&[(0.1, 0.6, 55., 0.5)], 0.6);
|
||
let mut seed = 3u32;
|
||
let mut lp = 0f32;
|
||
for _ in 0..seconds(1.5) {
|
||
seed ^= seed << 13;
|
||
seed ^= seed >> 17;
|
||
seed ^= seed << 5;
|
||
lp += 0.05 * ((seed as f32 / u32::MAX as f32 - 0.5) - lp);
|
||
x.push(4. * lp);
|
||
}
|
||
let solo = render(&mut e, &x, true);
|
||
let sine = rms(&solo[seconds(0.3) + l..seconds(0.5) + l]);
|
||
let unsure = rms(&solo[seconds(1.5) + l..seconds(2.0) + l]);
|
||
assert!(
|
||
db(unsure) < db(sine) - 40.,
|
||
"{} vs {}",
|
||
db(unsure),
|
||
db(sine)
|
||
);
|
||
// The full output carries the original low band again.
|
||
let (mut e, l) = engine(Settings::default());
|
||
let y = render(&mut e, &x, false);
|
||
let a = rms(&x[seconds(1.5)..seconds(2.0)]);
|
||
let b = rms(&y[seconds(1.5) + l..seconds(2.0) + l]);
|
||
assert!((db(a) - db(b)).abs() < 1., "{} vs {}", db(a), db(b));
|
||
}
|
||
|
||
#[test]
|
||
fn enhance_keeps_the_original_and_adds_the_sine() {
|
||
let x = tones(&[(55., 0.3)], 1.2);
|
||
for (mode, keeps) in [(Mode::Replace, false), (Mode::Enhance, true)] {
|
||
let (mut e, l) = engine(Settings {
|
||
mode,
|
||
octave: -1,
|
||
..Settings::default()
|
||
});
|
||
let y = render(&mut e, &x, false);
|
||
// Whole cycles of 27.5 Hz: 0.4 s.
|
||
let w = &y[seconds(0.5) + l..seconds(0.9) + l];
|
||
let original = amplitude(w, 55.);
|
||
let added = amplitude(w, 27.5);
|
||
assert!((db(added) + 9.).abs() < 1., "{mode:?}: {}", db(added));
|
||
if keeps {
|
||
assert!((db(original) - db(0.3)).abs() < 1., "{}", db(original));
|
||
} else {
|
||
assert!(db(original) < db(0.3) - 12., "{}", db(original));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn note_mode_plays_the_set_note_whatever_the_audio() {
|
||
let (mut e, l) = engine(Settings {
|
||
source: Source::Note,
|
||
pitch: common_dsp::tracker::Pitch {
|
||
note: 29,
|
||
..Default::default()
|
||
},
|
||
..Settings::default()
|
||
});
|
||
let x = notes(&[(0.1, 1.1, 61.74, 0.5)], 1.3);
|
||
let y = render(&mut e, &x, true);
|
||
let f = frequency(&y[seconds(0.3) + l..seconds(1.0) + l]);
|
||
assert!((f / 43.654 - 1.).abs() < 0.005, "{f}");
|
||
}
|