use std::f32::consts::TAU;
use crate::spec::{HonkSpec, Mood};
use crate::style::{Voice, Wave};
pub const SAMPLE_RATE: u32 = 44_100;
const SR: f32 = SAMPLE_RATE as f32;
const SAD_STRETCH: f32 = 1.6;
const SAD_END_PITCH: f32 = 0.6;
const HAPPY_FROM: f32 = 0.6;
const HAPPY_END_PITCH: f32 = 1.3;
const DRIVE: f32 = 1.5;
fn secs_to_samples(secs: f32) -> usize {
(secs * SR).round() as usize
}
fn honk_secs(spec: &HonkSpec, voice: &Voice, last: bool) -> f32 {
let mut secs = voice.honk_secs;
if spec.long {
secs *= 2.0;
}
if last && spec.mood == Mood::Sad {
secs *= SAD_STRETCH;
}
secs
}
pub fn duration_secs(spec: &HonkSpec) -> f32 {
let voice = spec.style.voice();
let samples: usize = (0..spec.times)
.map(|i| secs_to_samples(honk_secs(spec, &voice, i + 1 == spec.times)))
.sum::<usize>()
+ spec.times.saturating_sub(1) as usize * secs_to_samples(voice.gap_secs);
samples as f32 / SR
}
fn mood_bend(mood: Mood, t: f32) -> f32 {
match mood {
Mood::Neutral => 1.0,
Mood::Sad => 1.0 + (SAD_END_PITCH - 1.0) * t,
Mood::Happy if t > HAPPY_FROM => {
1.0 + (HAPPY_END_PITCH - 1.0) * ((t - HAPPY_FROM) / (1.0 - HAPPY_FROM))
}
Mood::Happy => 1.0,
}
}
fn oscillator(wave: Wave, phase: f32) -> f32 {
match wave {
Wave::Saw => 2.0 * phase - 1.0,
Wave::Square => {
if phase < 0.5 {
1.0
} else {
-1.0
}
}
}
}
struct Noise(u32);
impl Noise {
fn next(&mut self) -> f32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 17;
self.0 ^= self.0 << 5;
(self.0 as f32 / u32::MAX as f32) * 2.0 - 1.0
}
}
pub fn render(spec: &HonkSpec) -> Vec<f32> {
let voice = spec.style.voice();
let gap = secs_to_samples(voice.gap_secs);
let mut out = Vec::new();
for i in 0..spec.times {
let last = i + 1 == spec.times;
if i > 0 {
out.extend(std::iter::repeat_n(0.0, gap));
}
let mood = if last { spec.mood } else { Mood::Neutral };
let n = secs_to_samples(honk_secs(spec, &voice, last));
render_honk(&mut out, spec, &voice, n, mood);
}
out
}
fn render_honk(out: &mut Vec<f32>, spec: &HonkSpec, voice: &Voice, n: usize, mood: Mood) {
let attack = secs_to_samples(voice.attack_secs).max(1);
let release = secs_to_samples(voice.release_secs).max(1);
let alpha = 1.0 - (-TAU * voice.cutoff_hz / SR).exp();
let norm = DRIVE.tanh();
let mut phases = vec![0.0f32; voice.freqs.len()];
let mut noise = Noise(0x9E37_79B9);
let mut lp = 0.0f32;
for j in 0..n {
let t = j as f32 / n as f32;
let vibrato = 1.0 + voice.vibrato_depth * (TAU * voice.vibrato_hz * j as f32 / SR).sin();
let pitch = spec.pitch * voice.pitch_at(t) * mood_bend(mood, t) * vibrato;
let mut x = 0.0;
for (phase, &f) in phases.iter_mut().zip(voice.freqs) {
x += oscillator(voice.wave, *phase);
*phase = (*phase + f * pitch / SR).fract();
}
x /= voice.freqs.len() as f32;
x = x * (1.0 - voice.noise) + noise.next() * voice.noise;
lp += alpha * (x - lp);
let env = if j < attack {
j as f32 / attack as f32
} else if j + release >= n {
(n - 1 - j) as f32 / release as f32
} else {
1.0
};
out.push((lp * DRIVE).tanh() / norm * env * spec.volume);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{HonkSpec, Mood};
use crate::style::Style;
fn spec(style: Style) -> HonkSpec {
HonkSpec {
style,
..HonkSpec::default()
}
}
fn bursts(samples: &[f32]) -> Vec<(usize, usize)> {
let mut out = Vec::new();
let (mut start, mut zeros) = (None, 0usize);
for (i, &s) in samples.iter().enumerate() {
if s == 0.0 {
zeros += 1;
if zeros == 100
&& let Some(st) = start.take()
{
out.push((st, i + 1 - 100));
}
} else {
zeros = 0;
start.get_or_insert(i);
}
}
if let Some(st) = start {
out.push((st, samples.len()));
}
out
}
fn crossings(samples: &[f32], a: usize, b: usize) -> usize {
samples[a..b]
.windows(2)
.filter(|w| w[0] < 0.0 && w[1] >= 0.0)
.count()
}
#[test]
fn bulb_has_the_expected_length() {
assert_eq!(render(&spec(Style::Bulb)).len(), 2 * 9702 + 5292);
}
#[test]
fn long_doubles_each_honk() {
let s = HonkSpec {
long: true,
..spec(Style::Bulb)
};
assert_eq!(render(&s).len(), 2 * 19404 + 5292);
}
#[test]
fn duration_matches_render() {
for style in Style::ALL {
let s = HonkSpec {
times: 3,
mood: Mood::Sad,
..spec(style)
};
let secs = render(&s).len() as f32 / SAMPLE_RATE as f32;
assert!((secs - duration_secs(&s)).abs() < 0.001, "{style:?}");
}
}
#[test]
fn every_style_stays_within_volume_and_is_finite() {
for style in Style::ALL {
for volume in [0.0, 0.3, 1.0] {
let s = HonkSpec {
volume,
times: 2,
..spec(style)
};
let samples = render(&s);
assert!(!samples.is_empty());
assert!(samples.iter().all(|x| x.is_finite()), "{style:?}");
let peak = samples.iter().fold(0.0f32, |m, x| m.max(x.abs()));
assert!(peak <= volume + 1e-6, "{style:?} peak {peak} > {volume}");
if volume > 0.0 {
assert!(peak > volume * 0.3, "{style:?} is too quiet: {peak}");
}
}
}
}
#[test]
fn times_gives_that_many_bursts() {
for style in Style::ALL {
for times in [1, 3, 5] {
let s = HonkSpec {
times,
..spec(style)
};
assert_eq!(
bursts(&render(&s)).len(),
times as usize,
"{style:?} x{times}"
);
}
}
}
#[test]
fn rendering_is_deterministic() {
let s = spec(Style::Clown);
assert_eq!(render(&s), render(&s));
}
#[test]
fn sad_bends_down_and_is_longer() {
let neutral = render(&HonkSpec {
times: 1,
..spec(Style::Bulb)
});
let sad = render(&HonkSpec {
times: 1,
mood: Mood::Sad,
..spec(Style::Bulb)
});
assert!(sad.len() > neutral.len());
let n = sad.len();
let start = crossings(&sad, n / 10, n * 3 / 10);
let end = crossings(&sad, n * 7 / 10, n * 9 / 10);
assert!(
(end as f32) < start as f32 * 0.85,
"start {start}, end {end}"
);
}
#[test]
fn happy_bends_up_at_the_end() {
let happy = render(&HonkSpec {
times: 1,
mood: Mood::Happy,
..spec(Style::Bulb)
});
let n = happy.len();
let start = crossings(&happy, n / 10, n * 3 / 10);
let end = crossings(&happy, n * 75 / 100, n * 95 / 100);
assert!(end as f32 > start as f32 * 1.08, "start {start}, end {end}");
}
#[test]
fn only_the_last_honk_carries_the_mood() {
let neutral = render(&HonkSpec {
times: 2,
..spec(Style::Car)
});
let sad = render(&HonkSpec {
times: 2,
mood: Mood::Sad,
..spec(Style::Car)
});
let first = bursts(&neutral)[0];
assert_eq!(&neutral[first.0..first.1], &sad[first.0..first.1]);
}
#[test]
fn higher_pitch_means_more_crossings() {
let low = render(&HonkSpec {
times: 1,
pitch: 0.5,
..spec(Style::Bulb)
});
let high = render(&HonkSpec {
times: 1,
pitch: 2.0,
..spec(Style::Bulb)
});
assert!(crossings(&high, 0, high.len()) > 3 * crossings(&low, 0, low.len()));
}
}