sound: macOS-like two-hit shutter, Balatro clatter, bell arpeggio tail, even loudness, volume 0.9
This commit is contained in:
1 parent
58c8c57b1f
commit
5a06890c0c
4 files changed
+243
-39
No files matched your search
@@ -116,7 +116,7 @@ backend = "builtin" # or "satty"
|
|||||||
|
|
||||||
[sound]
|
[sound]
|
||||||
enabled = true
|
enabled = true
|
||||||
volume = 0.6
|
volume = 0.9
|
||||||
combo_reset_secs = 5
|
combo_reset_secs = 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ image, a cancel brought the thumbnail back, and a swipe still dismissed it.
|
|||||||
- [ ] Region: while dragging, a `W × H` label follows the pointer (physical pixels, same as the saved PNG), flipping near the edges.
|
- [ ] Region: while dragging, a `W × H` label follows the pointer (physical pixels, same as the saved PNG), flipping near the edges.
|
||||||
- [ ] Shift locks the dimension you move first; Alt grows from the centre; Space held moves the selection, and after letting go resizing continues without a jump; Space before a drag still switches to window mode.
|
- [ ] Shift locks the dimension you move first; Alt grows from the centre; Space held moves the selection, and after letting go resizing continues without a jump; Space before a drag still switches to window mode.
|
||||||
- [ ] `[capture] window_shadow = true`: window shots get a soft shadow following the rounded corners; preview, clipboard and file all have it.
|
- [ ] `[capture] window_shadow = true`: window shots get a soft shadow following the rounded corners; preview, clipboard and file all have it.
|
||||||
- [ ] Shots in quick succession play sounds 1 → 5 getting faster and brighter, then 6, 7, 6, 7 rising endlessly; after a 5 s pause it starts at 1 again. `valw __combo-demo` plays the whole sequence for tuning.
|
- [ ] Each shot is a two-hit "ka-chunk" (macOS-like) with a metallic clatter that grows from shot 2; shots in quick succession play sounds 1 → 5 getting faster and brighter, then 6, 7, 6, 7 (a climbing bell arpeggio over a glide) rising endlessly; every shot about as loud; after a 5 s pause it starts at 1 again. `valw __combo-demo` plays the whole sequence for tuning.
|
||||||
- [ ] `[sound] enabled = false`, `volume`, `combo_reset_secs` take effect; the toolbar's Play sound option overrides the config; no `pw-play` → no sound, capture still works (warning in the log).
|
- [ ] `[sound] enabled = false`, `volume`, `combo_reset_secs` take effect; the toolbar's Play sound option overrides the config; no `pw-play` → no sound, capture still works (warning in the log).
|
||||||
|
|
||||||
### Known minors (deferred from the polish review)
|
### Known minors (deferred from the polish review)
|
||||||
@@ -152,7 +152,6 @@ image, a cancel brought the thumbnail back, and a swipe still dismissed it.
|
|||||||
- `__combo-demo` and a capture regenerating the same WAV at once can race on the temp file (one silent shot).
|
- `__combo-demo` and a capture regenerating the same WAV at once can race on the temp file (one silent shot).
|
||||||
- Without `XDG_RUNTIME_DIR` the sound cache and combo state in `/tmp` are shared between users.
|
- Without `XDG_RUNTIME_DIR` the sound cache and combo state in `/tmp` are shared between users.
|
||||||
- `pipewire` on the wrapper's PATH adds its whole closure to the package.
|
- `pipewire` on the wrapper's PATH adds its whole closure to the package.
|
||||||
- Tuning by ear: shots 6–7 are about 9–10 dB louder than 1–5; adjust with `__combo-demo`.
|
|
||||||
|
|
||||||
## home-manager module
|
## home-manager module
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -84,7 +84,7 @@ impl Default for Sound {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
volume: 0.6,
|
volume: 0.9,
|
||||||
combo_reset_secs: 5,
|
combo_reset_secs: 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +292,7 @@ mod tests {
|
|||||||
fn sound_defaults_and_bounds() {
|
fn sound_defaults_and_bounds() {
|
||||||
let s = Config::default().sound;
|
let s = Config::default().sound;
|
||||||
assert!(s.enabled);
|
assert!(s.enabled);
|
||||||
assert_eq!((s.volume, s.combo_reset_secs), (0.6, 5));
|
assert_eq!((s.volume, s.combo_reset_secs), (0.9, 5));
|
||||||
for bad in ["volume = 1.5", "volume = nan", "volume = -0.1"] {
|
for bad in ["volume = 1.5", "volume = nan", "volume = -0.1"] {
|
||||||
let err = parse(&format!("[sound]\n{bad}\n")).unwrap_err();
|
let err = parse(&format!("[sound]\n{bad}\n")).unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+239
-34
@@ -1,11 +1,33 @@
|
|||||||
//! The shutter sounds, synthesised: no samples, no licences.
|
//! The shutter sounds, synthesised: no samples, no licences.
|
||||||
|
//!
|
||||||
|
//! A two-hit mechanical shutter with a pitched body ("ka-chunk"), a metallic
|
||||||
|
//! clatter that grows with the combo, and on shots 6 and 7 a bell
|
||||||
|
//! arpeggio over a Shepard glide that seems to rise forever.
|
||||||
|
|
||||||
use std::f32::consts::TAU;
|
use std::f32::consts::TAU;
|
||||||
|
|
||||||
pub const RATE: u32 = 44_100;
|
pub const RATE: u32 = 44_100;
|
||||||
const SPEEDS: [f32; 5] = [1.00, 1.12, 1.25, 1.40, 1.60];
|
const SPEEDS: [f32; 5] = [1.00, 1.12, 1.25, 1.40, 1.60];
|
||||||
const LOOP_SPEED: f32 = 1.80;
|
const LOOP_SPEED: f32 = 1.80;
|
||||||
const GLIDE_SECS: f32 = 0.4;
|
/// The shutter's second hit, after the first.
|
||||||
|
const GAP: f32 = 0.150;
|
||||||
|
/// How much clatter each of shots 1–5 carries (6 and 7 take the last).
|
||||||
|
const CLATTER: [f32; 5] = [0.0, 0.08, 0.15, 0.22, 0.30];
|
||||||
|
const CLATTER_HZ: f32 = 900.0;
|
||||||
|
/// The clatter's partials: (ratio to its fundamental, amplitude, decay s).
|
||||||
|
const CLATTER_PARTIALS: [(f32, f32, f32); 4] = [
|
||||||
|
(1.0, 1.0, 0.060),
|
||||||
|
(1.50, 0.7, 0.050),
|
||||||
|
(4.40, 0.9, 0.030),
|
||||||
|
(22.1, 0.25, 0.012),
|
||||||
|
];
|
||||||
|
/// Every sound is scaled to this short-term loudness (RMS).
|
||||||
|
const LOUDNESS: f32 = 0.30;
|
||||||
|
/// The tail of shots 6 and 7, relative to [`LOUDNESS`].
|
||||||
|
const TAIL_LOUDNESS: f32 = 0.8;
|
||||||
|
const TAIL_SECS: f32 = 0.30;
|
||||||
|
const ARPEGGIO_NOTES: usize = 3;
|
||||||
|
const ARPEGGIO_STEP: f32 = 0.075;
|
||||||
/// The Shepard tone: octave-spaced partials from BASE Hz under a bell
|
/// The Shepard tone: octave-spaced partials from BASE Hz under a bell
|
||||||
/// centred on CENTRE Hz (log scale).
|
/// centred on CENTRE Hz (log scale).
|
||||||
const BASE: f32 = 55.0;
|
const BASE: f32 = 55.0;
|
||||||
@@ -29,32 +51,138 @@ fn secs(n: usize) -> f32 {
|
|||||||
n as f32 / RATE as f32
|
n as f32 / RATE as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A two-part mechanical "tak-shk", about 150 ms.
|
fn samples(secs_long: f32) -> usize {
|
||||||
pub fn shutter() -> Vec<f32> {
|
(secs_long * RATE as f32) as usize
|
||||||
let len = (0.150 * RATE as f32) as usize;
|
|
||||||
let mut noise = Noise(0x1234_5678);
|
|
||||||
let (mut low, mut high_prev, mut high) = (0.0f32, 0.0f32, 0.0f32);
|
|
||||||
let mut out = Vec::with_capacity(len);
|
|
||||||
for i in 0..len {
|
|
||||||
let t = secs(i);
|
|
||||||
let n = noise.next();
|
|
||||||
// Click: bright, 0.5 ms attack, ~6 ms decay.
|
|
||||||
let click = n * (t / 0.0005).min(1.0) * (-t / 0.006).exp();
|
|
||||||
// "shk": band-passed noise from 60 ms, ~30 ms decay, softer.
|
|
||||||
low += 0.25 * (n - low);
|
|
||||||
high = 0.9 * (high + low - high_prev);
|
|
||||||
high_prev = low;
|
|
||||||
let u = t - 0.060;
|
|
||||||
let shk = if u >= 0.0 {
|
|
||||||
high * 0.9 * (u / 0.002).min(1.0) * (-u / 0.03).exp()
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
out.push(click * 0.8 + shk);
|
|
||||||
}
|
}
|
||||||
fade_out(&mut out, 0.010);
|
|
||||||
normalise(&mut out, 0.9);
|
/// Adds `src` times `gain` into `dst` from `at` seconds, growing `dst`.
|
||||||
out
|
fn mix(dst: &mut Vec<f32>, src: &[f32], at: f32, gain: f32) {
|
||||||
|
let start = samples(at);
|
||||||
|
if dst.len() < start + src.len() {
|
||||||
|
dst.resize(start + src.len(), 0.0);
|
||||||
|
}
|
||||||
|
for (d, s) in dst[start..].iter_mut().zip(src) {
|
||||||
|
*d += s * gain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A damped resonance at `f` Hz with a small downward pitch blip and an
|
||||||
|
/// inharmonic overtone: the body of a hit.
|
||||||
|
fn thunk(f: f32, decay: f32) -> Vec<f32> {
|
||||||
|
let (mut p1, mut p2) = (0.0f32, 0.0f32);
|
||||||
|
(0..samples(decay * 7.0))
|
||||||
|
.map(|i| {
|
||||||
|
let t = secs(i);
|
||||||
|
let env = (t / 0.0008).min(1.0) * (-t / decay).exp();
|
||||||
|
let fi = f * (1.0 + 0.25 * (-t / 0.006).exp());
|
||||||
|
p1 = (p1 + TAU * fi / RATE as f32) % TAU;
|
||||||
|
p2 = (p2 + TAU * fi * 2.76 / RATE as f32) % TAU;
|
||||||
|
env * (p1.sin() + 0.35 * p2.sin() * (-t / (decay * 0.4)).exp())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A burst of high-passed noise; `bright` (0–1) tames the very top.
|
||||||
|
fn burst(decay: f32, seed: u32, bright: f32) -> Vec<f32> {
|
||||||
|
let mut noise = Noise(seed);
|
||||||
|
let (mut low, mut high_prev, mut high) = (0.0f32, 0.0f32, 0.0f32);
|
||||||
|
(0..samples(decay * 7.0))
|
||||||
|
.map(|i| {
|
||||||
|
let t = secs(i);
|
||||||
|
low += bright * (noise.next() - low);
|
||||||
|
high = 0.92 * (high + low - high_prev);
|
||||||
|
high_prev = low;
|
||||||
|
high * (t / 0.0005).min(1.0) * (-t / decay).exp()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hit(f: f32, seed: u32, noise: f32) -> Vec<f32> {
|
||||||
|
let mut s = thunk(f, 0.035);
|
||||||
|
mix(&mut s, &burst(0.018, seed, 0.9), 0.0, noise);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mechanical "ka-chunk": two hits [`GAP`] apart, the second a fifth up.
|
||||||
|
pub fn shutter() -> Vec<f32> {
|
||||||
|
let mut s = hit(260.0, 0x0123_4567, 0.55);
|
||||||
|
mix(&mut s, &hit(390.0, 0x0765_4321, 0.65), GAP, 1.1);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One metallic strike of the clatter at `f` Hz.
|
||||||
|
fn ping(f: f32) -> Vec<f32> {
|
||||||
|
let mut phases = [0f32; CLATTER_PARTIALS.len()];
|
||||||
|
(0..samples(0.35))
|
||||||
|
.map(|i| {
|
||||||
|
let t = secs(i);
|
||||||
|
let mut v = 0.0;
|
||||||
|
for (p, (ratio, amp, decay)) in phases.iter_mut().zip(CLATTER_PARTIALS) {
|
||||||
|
*p = (*p + TAU * f * ratio / RATE as f32) % TAU;
|
||||||
|
v += amp * p.sin() * (-t / decay).exp();
|
||||||
|
}
|
||||||
|
v * (t / 0.0004).min(1.0)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A coin-like clatter: four quick, fading metallic strikes.
|
||||||
|
fn clatter(f: f32) -> Vec<f32> {
|
||||||
|
let mut s = Vec::new();
|
||||||
|
for (j, (at, amp)) in [(0.0, 1.0), (0.035, 0.7), (0.080, 0.5), (0.135, 0.35)]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
mix(&mut s, &ping(f * (1.0 + 0.012 * j as f32)), at, amp);
|
||||||
|
mix(
|
||||||
|
&mut s,
|
||||||
|
&burst(0.006, 0x9e37_79b9 + j as u32, 0.6),
|
||||||
|
at,
|
||||||
|
0.35 * amp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shutter with clatter `level` (1–5): more of it, and two semitones
|
||||||
|
/// higher, each level.
|
||||||
|
fn shot(level: usize) -> Vec<f32> {
|
||||||
|
let mut s = shutter();
|
||||||
|
let amount = CLATTER[level - 1];
|
||||||
|
if amount > 0.0 {
|
||||||
|
let f = CLATTER_HZ * 2f32.powf((level - 1) as f32 * 2.0 / 12.0);
|
||||||
|
mix(&mut s, &clatter(f), GAP, amount);
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The loudest 20 ms of `s` (RMS): what the ear compares between shots.
|
||||||
|
pub fn loudness(s: &[f32]) -> f32 {
|
||||||
|
let window = samples(0.020).min(s.len()).max(1);
|
||||||
|
let mut sum: f32 = s[..window.min(s.len())].iter().map(|v| v * v).sum();
|
||||||
|
let mut best = sum;
|
||||||
|
for i in window..s.len() {
|
||||||
|
sum += s[i] * s[i] - s[i - window] * s[i - window];
|
||||||
|
best = best.max(sum);
|
||||||
|
}
|
||||||
|
(best.max(0.0) / window as f32).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scales `s` to `target` loudness through a soft limiter, keeps the peak
|
||||||
|
/// under 0.95 and fades the end.
|
||||||
|
fn finish(mut s: Vec<f32>, target: f32) -> Vec<f32> {
|
||||||
|
let gain = target / loudness(&s).max(1e-9);
|
||||||
|
for v in s.iter_mut() {
|
||||||
|
*v = (*v * gain * 1.2).tanh() / 1.2;
|
||||||
|
}
|
||||||
|
let max = s.iter().fold(0.0f32, |m, v| m.max(v.abs()));
|
||||||
|
if max > 0.95 {
|
||||||
|
for v in s.iter_mut() {
|
||||||
|
*v *= 0.95 / max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fade_out(&mut s, 0.010);
|
||||||
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `samples` played `factor` times faster (shorter and higher).
|
/// `samples` played `factor` times faster (shorter and higher).
|
||||||
@@ -106,14 +234,50 @@ pub fn glide(from: f32, to: f32, secs_long: f32) -> Vec<f32> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One bell note of the Shepard scale at `position` octaves: octave
|
||||||
|
/// partials with a short metallic overtone.
|
||||||
|
fn bell(position: f32) -> Vec<f32> {
|
||||||
|
let partials = partials(position);
|
||||||
|
let mut phases = vec![(0f32, 0f32); partials.len()];
|
||||||
|
(0..samples(0.16))
|
||||||
|
.map(|i| {
|
||||||
|
let t = secs(i);
|
||||||
|
let mut v = 0.0;
|
||||||
|
for ((p, q), (f, a)) in phases.iter_mut().zip(&partials) {
|
||||||
|
*p = (*p + TAU * f / RATE as f32) % TAU;
|
||||||
|
*q = (*q + TAU * f * 4.4 / RATE as f32) % TAU;
|
||||||
|
v += a * (p.sin() + 0.25 * q.sin() * (-t / 0.015).exp());
|
||||||
|
}
|
||||||
|
v * (t / 0.0015).min(1.0) * (-t / 0.05).exp()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`ARPEGGIO_NOTES`] bell notes climbing from `from` towards `to`
|
||||||
|
/// octaves, over a quiet Shepard glide.
|
||||||
|
fn tail(from: f32, to: f32) -> Vec<f32> {
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
for j in 0..ARPEGGIO_NOTES {
|
||||||
|
let position = from + (to - from) * j as f32 / ARPEGGIO_NOTES as f32;
|
||||||
|
mix(&mut notes, &bell(position), j as f32 * ARPEGGIO_STEP, 1.0);
|
||||||
|
}
|
||||||
|
let pad = glide(from, to, TAIL_SECS);
|
||||||
|
let gain = 0.45 / loudness(&pad);
|
||||||
|
let mut s: Vec<f32> = pad.iter().map(|v| v * gain).collect();
|
||||||
|
mix(&mut s, ¬es, 0.0, 1.0 / loudness(¬es));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
/// The sound for shot `n` of a combo (1–7).
|
/// The sound for shot `n` of a combo (1–7).
|
||||||
pub fn sound(n: u8) -> Vec<f32> {
|
pub fn sound(n: u8) -> Vec<f32> {
|
||||||
match n {
|
match n {
|
||||||
1..=5 => speed(&shutter(), SPEEDS[n as usize - 1]),
|
1..=5 => finish(speed(&shot(n as usize), SPEEDS[n as usize - 1]), LOUDNESS),
|
||||||
6 | 7 => {
|
6 | 7 => {
|
||||||
let mut s = speed(&shutter(), LOOP_SPEED);
|
let mut s = finish(speed(&shot(5), LOOP_SPEED), LOUDNESS);
|
||||||
let half = if n == 6 { (0.0, 0.5) } else { (0.5, 1.0) };
|
let half = if n == 6 { (0.0, 0.5) } else { (0.5, 1.0) };
|
||||||
s.extend(glide(half.0, half.1, GLIDE_SECS));
|
let t = tail(half.0, half.1);
|
||||||
|
let gain = LOUDNESS * TAIL_LOUDNESS / loudness(&t);
|
||||||
|
s.extend(t.iter().map(|v| v * gain));
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
_ => sound(1),
|
_ => sound(1),
|
||||||
@@ -175,15 +339,48 @@ mod tests {
|
|||||||
s.iter().fold(0.0, |m, v| m.max(v.abs()))
|
s.iter().fold(0.0, |m, v| m.max(v.abs()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rms(s: &[f32], from_ms: f32, to_ms: f32) -> f32 {
|
||||||
|
let at = |ms: f32| ((ms / 1000.0 * RATE as f32) as usize).min(s.len());
|
||||||
|
let seg = &s[at(from_ms)..at(to_ms)];
|
||||||
|
(seg.iter().map(|v| v * v).sum::<f32>() / seg.len().max(1) as f32).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_shutter_is_short_and_never_clips() {
|
fn the_shutter_is_two_hits_and_never_clips() {
|
||||||
let s = shutter();
|
let s = shutter();
|
||||||
let ms = s.len() as f32 * 1000.0 / RATE as f32;
|
let ms = s.len() as f32 * 1000.0 / RATE as f32;
|
||||||
assert!((140.0..=160.0).contains(&ms), "{ms} ms");
|
assert!((300.0..=450.0).contains(&ms), "{ms} ms");
|
||||||
assert!(peak(&s) <= 0.95 && peak(&s) > 0.3);
|
let (first, between, second) = (
|
||||||
|
rms(&s, 0.0, 20.0),
|
||||||
|
rms(&s, 115.0, 140.0),
|
||||||
|
rms(&s, 150.0, 170.0),
|
||||||
|
);
|
||||||
|
assert!(first > 3.0 * between, "{first} vs {between}");
|
||||||
|
assert!(
|
||||||
|
second > 3.0 * between,
|
||||||
|
"a second hit at 150 ms: {second} vs {between}"
|
||||||
|
);
|
||||||
assert!(s.last().unwrap().abs() < 0.01, "ends quietly (no pop)");
|
assert!(s.last().unwrap().abs() < 0.01, "ends quietly (no pop)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_shot_is_about_as_loud() {
|
||||||
|
let db: Vec<f32> = (1..=7)
|
||||||
|
.map(|n| 20.0 * loudness(&sound(n)).log10())
|
||||||
|
.collect();
|
||||||
|
let (lo, hi) = db
|
||||||
|
.iter()
|
||||||
|
.fold((f32::MAX, f32::MIN), |(lo, hi), v| (lo.min(*v), hi.max(*v)));
|
||||||
|
assert!(hi - lo < 1.5, "{db:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_shot_ends_quietly() {
|
||||||
|
for n in 1..=7 {
|
||||||
|
assert!(sound(n).last().unwrap().abs() < 0.01, "sound {n} pops");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn faster_is_shorter() {
|
fn faster_is_shorter() {
|
||||||
let s = shutter();
|
let s = shutter();
|
||||||
@@ -192,8 +389,16 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sounds_escalate_then_loop() {
|
fn sounds_escalate_then_loop() {
|
||||||
let lens: Vec<usize> = (1..=5).map(|n| sound(n).len()).collect();
|
assert!(SPEEDS.windows(2).all(|w| w[1] > w[0]) && LOOP_SPEED > SPEEDS[4]);
|
||||||
assert!(lens.windows(2).all(|w| w[1] < w[0]), "{lens:?}");
|
for n in 1..=5u8 {
|
||||||
|
let s = sound(n);
|
||||||
|
let at = GAP * 1000.0 / SPEEDS[n as usize - 1];
|
||||||
|
let (before, hit) = (rms(&s, at - 25.0, at - 5.0), rms(&s, at, at + 15.0));
|
||||||
|
assert!(
|
||||||
|
hit > 2.0 * before,
|
||||||
|
"shot {n}: second hit at {at} ms ({hit} vs {before})"
|
||||||
|
);
|
||||||
|
}
|
||||||
assert!(sound(6).len() > sound(5).len(), "6 adds the glide");
|
assert!(sound(6).len() > sound(5).len(), "6 adds the glide");
|
||||||
assert_eq!(sound(6).len(), sound(7).len());
|
assert_eq!(sound(6).len(), sound(7).len());
|
||||||
for n in 1..=7 {
|
for n in 1..=7 {
|
||||||
|
|||||||
Reference in new issue
Block a user