Ghost adds the sub's harmonics 2–10 above Ghost from, so phones hear the note. The band above the split is measured per harmonic and only missing harmonics are filled, so added ones cannot cancel the patch's own. The sub level does not change. Phone monitors the output in mono, 400 Hz–7 kHz.
338 lines
11 KiB
Rust
338 lines
11 KiB
Rust
use anvil_core::{Engine, 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, phone: false })[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,
|
||
phone: false,
|
||
},
|
||
)[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,
|
||
},
|
||
)[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)
|
||
);
|
||
}
|