1use std::f32::consts::TAU;
8
9use crate::spec::{HonkSpec, Mood};
10use crate::style::{Voice, Wave};
11
12pub const SAMPLE_RATE: u32 = 44_100;
13
14const SR: f32 = SAMPLE_RATE as f32;
15const SAD_STRETCH: f32 = 1.6;
17const SAD_END_PITCH: f32 = 0.6;
19const HAPPY_FROM: f32 = 0.6;
21const HAPPY_END_PITCH: f32 = 1.3;
22const DRIVE: f32 = 1.5;
24
25fn secs_to_samples(secs: f32) -> usize {
26 (secs * SR).round() as usize
27}
28
29fn honk_secs(spec: &HonkSpec, voice: &Voice, last: bool) -> f32 {
30 let mut secs = voice.honk_secs;
31 if spec.long {
32 secs *= 2.0;
33 }
34 if last && spec.mood == Mood::Sad {
35 secs *= SAD_STRETCH;
36 }
37 secs
38}
39
40pub fn duration_secs(spec: &HonkSpec) -> f32 {
42 let voice = spec.style.voice();
43 let samples: usize = (0..spec.times)
44 .map(|i| secs_to_samples(honk_secs(spec, &voice, i + 1 == spec.times)))
45 .sum::<usize>()
46 + spec.times.saturating_sub(1) as usize * secs_to_samples(voice.gap_secs);
47 samples as f32 / SR
48}
49
50fn mood_bend(mood: Mood, t: f32) -> f32 {
52 match mood {
53 Mood::Neutral => 1.0,
54 Mood::Sad => 1.0 + (SAD_END_PITCH - 1.0) * t,
55 Mood::Happy if t > HAPPY_FROM => {
56 1.0 + (HAPPY_END_PITCH - 1.0) * ((t - HAPPY_FROM) / (1.0 - HAPPY_FROM))
57 }
58 Mood::Happy => 1.0,
59 }
60}
61
62fn oscillator(wave: Wave, phase: f32) -> f32 {
63 match wave {
64 Wave::Saw => 2.0 * phase - 1.0,
65 Wave::Square => {
66 if phase < 0.5 {
67 1.0
68 } else {
69 -1.0
70 }
71 }
72 }
73}
74
75struct Noise(u32);
77
78impl Noise {
79 fn next(&mut self) -> f32 {
80 self.0 ^= self.0 << 13;
81 self.0 ^= self.0 >> 17;
82 self.0 ^= self.0 << 5;
83 (self.0 as f32 / u32::MAX as f32) * 2.0 - 1.0
84 }
85}
86
87pub fn render(spec: &HonkSpec) -> Vec<f32> {
89 let voice = spec.style.voice();
90 let gap = secs_to_samples(voice.gap_secs);
91 let mut out = Vec::new();
92 for i in 0..spec.times {
93 let last = i + 1 == spec.times;
94 if i > 0 {
95 out.extend(std::iter::repeat_n(0.0, gap));
96 }
97 let mood = if last { spec.mood } else { Mood::Neutral };
98 let n = secs_to_samples(honk_secs(spec, &voice, last));
99 render_honk(&mut out, spec, &voice, n, mood);
100 }
101 out
102}
103
104fn render_honk(out: &mut Vec<f32>, spec: &HonkSpec, voice: &Voice, n: usize, mood: Mood) {
105 let attack = secs_to_samples(voice.attack_secs).max(1);
106 let release = secs_to_samples(voice.release_secs).max(1);
107 let alpha = 1.0 - (-TAU * voice.cutoff_hz / SR).exp();
108 let norm = DRIVE.tanh();
109 let mut phases = vec![0.0f32; voice.freqs.len()];
110 let mut noise = Noise(0x9E37_79B9);
111 let mut lp = 0.0f32;
112
113 for j in 0..n {
114 let t = j as f32 / n as f32;
115 let vibrato = 1.0 + voice.vibrato_depth * (TAU * voice.vibrato_hz * j as f32 / SR).sin();
116 let pitch = spec.pitch * voice.pitch_at(t) * mood_bend(mood, t) * vibrato;
117
118 let mut x = 0.0;
119 for (phase, &f) in phases.iter_mut().zip(voice.freqs) {
120 x += oscillator(voice.wave, *phase);
121 *phase = (*phase + f * pitch / SR).fract();
122 }
123 x /= voice.freqs.len() as f32;
124 x = x * (1.0 - voice.noise) + noise.next() * voice.noise;
125
126 lp += alpha * (x - lp);
127
128 let env = if j < attack {
129 j as f32 / attack as f32
130 } else if j + release >= n {
131 (n - 1 - j) as f32 / release as f32
132 } else {
133 1.0
134 };
135
136 out.push((lp * DRIVE).tanh() / norm * env * spec.volume);
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::spec::{HonkSpec, Mood};
144 use crate::style::Style;
145
146 fn spec(style: Style) -> HonkSpec {
147 HonkSpec {
148 style,
149 ..HonkSpec::default()
150 }
151 }
152
153 fn bursts(samples: &[f32]) -> Vec<(usize, usize)> {
155 let mut out = Vec::new();
156 let (mut start, mut zeros) = (None, 0usize);
157 for (i, &s) in samples.iter().enumerate() {
158 if s == 0.0 {
159 zeros += 1;
160 if zeros == 100
161 && let Some(st) = start.take()
162 {
163 out.push((st, i + 1 - 100));
164 }
165 } else {
166 zeros = 0;
167 start.get_or_insert(i);
168 }
169 }
170 if let Some(st) = start {
171 out.push((st, samples.len()));
172 }
173 out
174 }
175
176 fn crossings(samples: &[f32], a: usize, b: usize) -> usize {
178 samples[a..b]
179 .windows(2)
180 .filter(|w| w[0] < 0.0 && w[1] >= 0.0)
181 .count()
182 }
183
184 #[test]
185 fn bulb_has_the_expected_length() {
186 assert_eq!(render(&spec(Style::Bulb)).len(), 2 * 9702 + 5292);
188 }
189
190 #[test]
191 fn long_doubles_each_honk() {
192 let s = HonkSpec {
193 long: true,
194 ..spec(Style::Bulb)
195 };
196 assert_eq!(render(&s).len(), 2 * 19404 + 5292);
197 }
198
199 #[test]
200 fn duration_matches_render() {
201 for style in Style::ALL {
202 let s = HonkSpec {
203 times: 3,
204 mood: Mood::Sad,
205 ..spec(style)
206 };
207 let secs = render(&s).len() as f32 / SAMPLE_RATE as f32;
208 assert!((secs - duration_secs(&s)).abs() < 0.001, "{style:?}");
209 }
210 }
211
212 #[test]
213 fn every_style_stays_within_volume_and_is_finite() {
214 for style in Style::ALL {
215 for volume in [0.0, 0.3, 1.0] {
216 let s = HonkSpec {
217 volume,
218 times: 2,
219 ..spec(style)
220 };
221 let samples = render(&s);
222 assert!(!samples.is_empty());
223 assert!(samples.iter().all(|x| x.is_finite()), "{style:?}");
224 let peak = samples.iter().fold(0.0f32, |m, x| m.max(x.abs()));
225 assert!(peak <= volume + 1e-6, "{style:?} peak {peak} > {volume}");
226 if volume > 0.0 {
227 assert!(peak > volume * 0.3, "{style:?} is too quiet: {peak}");
228 }
229 }
230 }
231 }
232
233 #[test]
234 fn times_gives_that_many_bursts() {
235 for style in Style::ALL {
236 for times in [1, 3, 5] {
237 let s = HonkSpec {
238 times,
239 ..spec(style)
240 };
241 assert_eq!(
242 bursts(&render(&s)).len(),
243 times as usize,
244 "{style:?} x{times}"
245 );
246 }
247 }
248 }
249
250 #[test]
251 fn rendering_is_deterministic() {
252 let s = spec(Style::Clown);
253 assert_eq!(render(&s), render(&s));
254 }
255
256 #[test]
257 fn sad_bends_down_and_is_longer() {
258 let neutral = render(&HonkSpec {
259 times: 1,
260 ..spec(Style::Bulb)
261 });
262 let sad = render(&HonkSpec {
263 times: 1,
264 mood: Mood::Sad,
265 ..spec(Style::Bulb)
266 });
267 assert!(sad.len() > neutral.len());
268 let n = sad.len();
269 let start = crossings(&sad, n / 10, n * 3 / 10);
270 let end = crossings(&sad, n * 7 / 10, n * 9 / 10);
271 assert!(
272 (end as f32) < start as f32 * 0.85,
273 "start {start}, end {end}"
274 );
275 }
276
277 #[test]
278 fn happy_bends_up_at_the_end() {
279 let happy = render(&HonkSpec {
280 times: 1,
281 mood: Mood::Happy,
282 ..spec(Style::Bulb)
283 });
284 let n = happy.len();
285 let start = crossings(&happy, n / 10, n * 3 / 10);
286 let end = crossings(&happy, n * 75 / 100, n * 95 / 100);
287 assert!(end as f32 > start as f32 * 1.08, "start {start}, end {end}");
288 }
289
290 #[test]
291 fn only_the_last_honk_carries_the_mood() {
292 let neutral = render(&HonkSpec {
293 times: 2,
294 ..spec(Style::Car)
295 });
296 let sad = render(&HonkSpec {
297 times: 2,
298 mood: Mood::Sad,
299 ..spec(Style::Car)
300 });
301 let first = bursts(&neutral)[0];
302 assert_eq!(&neutral[first.0..first.1], &sad[first.0..first.1]);
303 }
304
305 #[test]
306 fn higher_pitch_means_more_crossings() {
307 let low = render(&HonkSpec {
308 times: 1,
309 pitch: 0.5,
310 ..spec(Style::Bulb)
311 });
312 let high = render(&HonkSpec {
313 times: 1,
314 pitch: 2.0,
315 ..spec(Style::Bulb)
316 });
317 assert!(crossings(&high, 0, high.len()) > 3 * crossings(&low, 0, low.len()));
318 }
319}