1use super::controls::{ControlParameter, Values};
3use super::prepared::BeatClock;
4use super::{Envelope, Oscillator, Waveform};
5use crate::math::{cos_turns, exp2, sin_turns};
6use crate::{Error, Result};
7
8#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
10pub enum FilterMode {
11 LowPass,
12 HighPass,
13 BandPass,
14}
15#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub struct VoiceFilter {
19 pub mode: FilterMode,
20 pub cutoff_hz: u32,
22 pub resonance: u16,
24 pub keytrack: u16,
26 pub envelope: Envelope,
28 pub envelope_cents: i16,
30}
31impl Default for VoiceFilter {
32 fn default() -> Self {
33 Self {
34 mode: FilterMode::LowPass,
35 cutoff_hz: 2000,
36 resonance: 71,
37 keytrack: 0,
38 envelope: Envelope::default(),
39 envelope_cents: 0,
40 }
41 }
42}
43#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
45pub enum LfoRate {
46 MilliHertz(u32),
48 Beats { numerator: u16, denominator: u16 },
50}
51#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
54pub struct Lfo {
55 pub rate: LfoRate,
56 pub depth: u16,
58}
59impl Lfo {
60 fn validate(self, max: u16) -> Result<()> {
61 let rate = match self.rate {
62 LfoRate::MilliHertz(n) => (1..=20000).contains(&n),
63 LfoRate::Beats {
64 numerator,
65 denominator,
66 } => (1..=64).contains(&numerator) && (1..=64).contains(&denominator),
67 };
68 if !rate || self.depth > max {
69 return Err(Error::invalid("Invalid LFO rate or depth"));
70 }
71 Ok(())
72 }
73 fn value(self, clock: u64, rate: u32, tempo: u16) -> f64 {
74 let (num, den) = match self.rate {
75 LfoRate::MilliHertz(n) => (n as u128, rate as u128 * 1000),
76 LfoRate::Beats {
77 numerator,
78 denominator,
79 } => (
80 tempo as u128 * denominator as u128,
81 rate as u128 * 60 * numerator as u128,
82 ),
83 };
84 let phase = match ((clock as u128).checked_mul(num), u64::try_from(den)) {
87 (Some(product), Ok(den64)) if product <= u64::MAX as u128 => {
88 (product as u64 % den64) as u128
89 }
90 _ => (clock as u128 * num) % den,
91 };
92 sin_turns(phase as f64 / den as f64) * self.depth as f64
93 }
94 fn value_clock(self, clock: u64, rate: u32, tempo: u16, musical: Option<BeatClock>) -> f64 {
95 if let (
96 Some(c),
97 LfoRate::Beats {
98 numerator,
99 denominator,
100 },
101 ) = (musical, self.rate)
102 {
103 let den = c.denominator * numerator as u128;
104 let phase = c.numerator * denominator as u128 % den;
105 sin_turns(phase as f64 / den as f64) * self.depth as f64
106 } else {
107 self.value(clock, rate, tempo)
108 }
109 }
110}
111#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
116pub struct Synthwave {
117 pub unison: u8,
119 pub detune: u16,
121 pub spread: u8,
123 pub pan: i8,
125 pub voices: u8,
128 pub glide_ms: u32,
131 pub legato: bool,
134 pub filter: Option<VoiceFilter>,
135 pub pitch_lfo: Option<Lfo>,
136 pub cutoff_lfo: Option<Lfo>,
137 pub amplitude_lfo: Option<Lfo>,
138 pub pan_lfo: Option<Lfo>,
139}
140impl Default for Synthwave {
141 fn default() -> Self {
142 Self {
143 unison: 1,
144 detune: 0,
145 spread: 0,
146 pan: 0,
147 voices: 8,
148 glide_ms: 0,
149 legato: false,
150 filter: None,
151 pitch_lfo: None,
152 cutoff_lfo: None,
153 amplitude_lfo: None,
154 pan_lfo: None,
155 }
156 }
157}
158impl Synthwave {
159 pub fn validate(&self) -> Result<()> {
160 if !(1..=8).contains(&self.unison)
161 || self.detune > 100
162 || self.spread > 100
163 || !(-100..=100).contains(&self.pan)
164 || !(1..=16).contains(&self.voices)
165 || self.glide_ms > 60000
166 || ((self.glide_ms != 0 || self.legato) && self.voices != 1)
167 {
168 return Err(Error::invalid(
169 "Invalid unison, spread, pan, voice limit or mono glide/legato settings",
170 ));
171 }
172 if let Some(f) = self.filter {
173 let e = f.envelope;
174 if !(1..=96000).contains(&f.cutoff_hz)
175 || !(50..=1000).contains(&f.resonance)
176 || f.keytrack > 200
177 || !(-9600..=9600).contains(&f.envelope_cents)
178 || e.sustain > 100
179 || [e.attack_ms, e.decay_ms, e.release_ms]
180 .iter()
181 .any(|&t| t > 60000)
182 {
183 return Err(Error::invalid("Invalid voice filter or filter envelope"));
184 }
185 } else if self.cutoff_lfo.is_some() {
186 return Err(Error::invalid("Cutoff LFO requires a filter"));
187 }
188 for (lfo, max) in [
189 (self.pitch_lfo, 2400),
190 (self.cutoff_lfo, 9600),
191 (self.amplitude_lfo, 100),
192 (self.pan_lfo, 100),
193 ] {
194 if let Some(lfo) = lfo {
195 lfo.validate(max)?;
196 }
197 }
198 Ok(())
199 }
200}
201#[derive(Clone, Debug)]
202pub(super) struct WidePlan {
203 pub settings: Synthwave,
204 rate: u32,
205 tempo: u16,
206 ratios: [f64; 8],
207 pans: [f64; 8],
208 glide: u64,
209}
210impl WidePlan {
211 pub fn new(settings: Synthwave, rate: u32, tempo: u16) -> Self {
212 let mut ratios = [1.0; 8];
213 let mut pans = [0.0; 8];
214 for i in 0..settings.unison as usize {
215 let offset = if settings.unison == 1 {
216 0.0
217 } else {
218 2.0 * i as f64 / (settings.unison - 1) as f64 - 1.0
219 };
220 ratios[i] = exp2(offset * settings.detune as f64 / 1200.0);
221 pans[i] = offset * settings.spread as f64 / 100.0;
222 }
223 Self {
224 settings,
225 rate,
226 tempo,
227 ratios,
228 pans,
229 glide: (settings.glide_ms as u64 * rate as u64 + 500) / 1000,
230 }
231 }
232}
233#[derive(Clone, Copy, Debug, Default)]
236struct FilterState {
237 ic1: f64,
238 ic2: f64,
239}
240impl FilterState {
241 fn sample(&mut self, x: f64, mode: FilterMode, g: f64, k: f64) -> f64 {
242 let a1 = 1.0 / (1.0 + g * (g + k));
243 let a2 = g * a1;
244 let a3 = g * a2;
245 let v3 = x - self.ic2;
246 let v1 = a1 * self.ic1 + a2 * v3;
247 let v2 = self.ic2 + a2 * self.ic1 + a3 * v3;
248 self.ic1 = 2.0 * v1 - self.ic1;
249 self.ic2 = 2.0 * v2 - self.ic2;
250 match mode {
251 FilterMode::LowPass => v2,
252 FilterMode::HighPass => x - k * v1 - v2,
253 FilterMode::BandPass => k * v1,
254 }
255 }
256}
257#[derive(Clone, Debug)]
258pub(super) struct WideVoice {
259 oscillators: [Oscillator; 8],
260 filters: [FilterState; 2],
261 age: u64,
262 released: Option<(u64, f64)>,
263 filter_level: f64,
264 pub pitch: f64,
265 from: f64,
266 target: f64,
267 glide_age: u64,
268 hz: (u64, f64),
270 coefficient: (u64, u64, u32, f64),
272}
273impl WideVoice {
274 pub fn new() -> Self {
275 Self {
276 oscillators: std::array::from_fn(|_| Oscillator::new(super::Aliasing::BandLimited)),
277 filters: [FilterState::default(); 2],
278 age: 0,
279 released: None,
280 filter_level: 0.0,
281 pitch: 0.0,
282 from: 0.0,
283 target: 0.0,
284 glide_age: 0,
285 hz: (f64::NAN.to_bits(), 0.0),
286 coefficient: (f64::NAN.to_bits(), 0, 0, 0.0),
287 }
288 }
289 pub fn trigger(&mut self, plan: &WidePlan, cents: f64, active: bool, legato: bool) {
290 self.from = if active && plan.settings.voices == 1 && plan.glide > 0 {
291 self.pitch
292 } else {
293 cents
294 };
295 self.target = cents;
296 self.glide_age = 0;
297 self.pitch = self.from;
298 if !legato {
299 self.age = 0;
300 self.released = None;
301 self.filter_level = 0.0;
302 self.filters = [FilterState::default(); 2];
303 for (i, osc) in self.oscillators.iter_mut().enumerate() {
304 *osc = Oscillator::new(super::Aliasing::BandLimited);
305 osc.phase_offset(i as f64 * 0.037);
308 }
309 }
310 }
311 pub fn release(&mut self) {
312 if self.released.is_none() {
313 self.released = Some((0, self.filter_level));
314 }
315 }
316 pub fn sample(
317 &mut self,
318 plan: &WidePlan,
319 wave: Waveform,
320 cents_offset: f64,
321 clock: u64,
322 musical: Option<BeatClock>,
323 controls: Option<Values>,
324 ) -> [f64; 2] {
325 let s = plan.settings;
326 self.pitch = if plan.glide == 0 {
327 self.target
328 } else {
329 self.from
330 + (self.target - self.from)
331 * (self.glide_age.min(plan.glide) as f64 / plan.glide as f64)
332 };
333 self.glide_age = self.glide_age.saturating_add(1);
334 let get = |p| controls.and_then(|c| c.get(p));
335 let lfo = |l: Option<Lfo>, parameter: ControlParameter| {
336 l.map_or(0.0, |l| {
337 if let Some(depth) = get(parameter) {
338 Lfo { depth: 1, ..l }.value_clock(clock, plan.rate, plan.tempo, musical) * depth
339 } else {
340 l.value_clock(clock, plan.rate, plan.tempo, musical)
341 }
342 })
343 };
344 let mut cents =
345 self.pitch + cents_offset + lfo(s.pitch_lfo, ControlParameter::PitchLfoDepth);
346 if let Some(offset) = get(ControlParameter::PitchCents) {
347 cents += offset;
348 }
349 if self.hz.0 != cents.to_bits() {
350 self.hz = (cents.to_bits(), crate::math::cents_to_hz(cents));
351 }
352 let hz = self.hz.1;
353 let mut out = [0.0; 2];
354 for i in 0..s.unison as usize {
355 let sample =
356 self.oscillators[i].next(wave, hz * plan.ratios[i], plan.rate) / s.unison as f64;
357 let pan = plan.pans[i];
358 out[0] += sample * (1.0 - pan.max(0.0));
359 out[1] += sample * (1.0 + pan.min(0.0));
360 }
361 if let Some(f) = s.filter {
362 self.filter_level = if let Some((age, from)) = &mut self.released {
363 let release = frames(f.envelope.release_ms, plan.rate);
364 let level = if release == 0 {
365 0.0
366 } else {
367 *from * (1.0 - (*age as f64 / release as f64).min(1.0))
368 };
369 *age += 1;
370 level
371 } else {
372 envelope(f.envelope, self.age, plan.rate)
373 };
374 let cents = f.envelope_cents as f64 * self.filter_level
375 + (self.pitch + 900.0) * f.keytrack as f64 / 100.0
376 + lfo(s.cutoff_lfo, ControlParameter::CutoffLfoDepth);
377 let base = get(ControlParameter::CutoffHz).unwrap_or(f.cutoff_hz as f64);
378 let key = (cents.to_bits(), base.to_bits(), plan.rate);
379 if (self.coefficient.0, self.coefficient.1, self.coefficient.2) != key {
380 let cutoff = (base * exp2(cents / 1200.0)).clamp(1.0, plan.rate as f64 * 0.45);
381 let turns = cutoff / (2.0 * plan.rate as f64);
382 self.coefficient = (key.0, key.1, key.2, sin_turns(turns) / cos_turns(turns));
383 }
384 let g = self.coefficient.3;
385 let k = get(ControlParameter::ResonanceQ)
386 .map_or_else(|| 100.0 / f.resonance as f64, |q| 1.0 / q);
387 for (state, value) in self.filters.iter_mut().zip(&mut out) {
388 *value = state.sample(*value, f.mode, g, k);
389 }
390 }
391 let amp = s.amplitude_lfo.map_or(1.0, |l| {
392 1.0 - (get(ControlParameter::AmplitudeLfoDepth).unwrap_or(l.depth as f64)
393 + lfo(Some(l), ControlParameter::AmplitudeLfoDepth))
394 / 200.0
395 });
396 let pan_lfo = lfo(s.pan_lfo, ControlParameter::PanLfoDepth);
397 let pan = if let Some(p) = get(ControlParameter::Pan) {
398 (p + pan_lfo / 100.0).clamp(-1.0, 1.0)
399 } else {
400 ((s.pan as f64 + pan_lfo) / 100.0).clamp(-1.0, 1.0)
401 };
402 out[0] *= amp * (1.0 - pan.max(0.0));
403 out[1] *= amp * (1.0 + pan.min(0.0));
404 self.age += 1;
405 out
406 }
407}
408fn frames(ms: u32, rate: u32) -> u64 {
409 (ms as u64 * rate as u64 + 500) / 1000
410}
411fn envelope(e: Envelope, age: u64, rate: u32) -> f64 {
412 let attack = frames(e.attack_ms, rate);
413 let decay = frames(e.decay_ms, rate);
414 let sustain = e.sustain as f64 / 100.0;
415 if age < attack {
416 age as f64 / attack as f64
417 } else if age - attack < decay {
418 1.0 - (1.0 - sustain) * (age - attack) as f64 / decay as f64
419 } else {
420 sustain
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 #[test]
428 fn filters_have_measured_pass_stop_and_resonance_responses() {
429 let rate = 48000.0;
430 let cutoff = 1000.0;
431 let g = sin_turns(cutoff / (2.0 * rate)) / cos_turns(cutoff / (2.0 * rate));
432 let response = |mode, q, freq| {
433 let mut state = FilterState::default();
434 let mut input = 0.0;
435 let mut output = 0.0;
436 for n in 0..24000 {
437 let x = sin_turns(n as f64 * freq / rate);
438 let y = state.sample(x, mode, g, 1.0 / q);
439 if n > 12000 {
440 input += x * x;
441 output += y * y;
442 }
443 }
444 (output / input).sqrt()
445 };
446 for mode in [
447 FilterMode::LowPass,
448 FilterMode::HighPass,
449 FilterMode::BandPass,
450 ] {
451 for q in [0.5, 1.0, 4.0, 10.0] {
452 let centre = response(mode, q, cutoff);
453 let expected = if mode == FilterMode::BandPass { 1.0 } else { q };
454 assert!(
455 (centre / expected - 1.0).abs() < 0.003,
456 "{mode:?} Q={q}: {centre}"
457 );
458 }
459 }
460 assert!(response(FilterMode::LowPass, 0.71, 125.0) > 0.98);
461 assert!(response(FilterMode::LowPass, 0.71, 8000.0) < 0.02);
462 assert!(response(FilterMode::HighPass, 0.71, 125.0) < 0.02);
463 assert!(response(FilterMode::HighPass, 0.71, 8000.0) > 0.98);
464 for freq in [125.0, 8000.0] {
465 assert!(response(FilterMode::BandPass, 4.0, freq) < 0.04);
466 }
467 }
468 #[test]
469 fn glide_reaches_target_at_the_exact_prepared_duration() {
470 let plan = WidePlan::new(
471 Synthwave {
472 voices: 1,
473 glide_ms: 100,
474 legato: true,
475 ..Default::default()
476 },
477 1000,
478 120,
479 );
480 let mut voice = WideVoice::new();
481 voice.trigger(&plan, 0.0, false, false);
482 voice.sample(&plan, Waveform::Triangle, 0.0, 0, None, None);
483 voice.trigger(&plan, 1200.0, true, true);
484 for frame in 0..=100 {
485 voice.sample(&plan, Waveform::Triangle, 0.0, frame, None, None);
486 assert!((voice.pitch - frame as f64 * 12.0).abs() < 1e-9);
487 }
488 voice.sample(&plan, Waveform::Triangle, 0.0, 101, None, None);
489 assert_eq!(voice.pitch, 1200.0);
490 voice.trigger(&plan, -1200.0, true, true);
491 voice.sample(&plan, Waveform::Triangle, 0.0, 102, None, None);
492 assert_eq!(voice.pitch, 1200.0);
493 }
494 #[test]
495 fn modulation_extremes_remain_finite_at_supported_rate_edges() {
496 for rate in [1000, 8000, 48000, 768000] {
497 for mode in [
498 FilterMode::LowPass,
499 FilterMode::HighPass,
500 FilterMode::BandPass,
501 ] {
502 let plan = WidePlan::new(
503 Synthwave {
504 unison: 8,
505 detune: 100,
506 spread: 100,
507 filter: Some(VoiceFilter {
508 mode,
509 cutoff_hz: 96000,
510 resonance: 1000,
511 keytrack: 200,
512 envelope_cents: 9600,
513 envelope: Envelope {
514 attack_ms: 1,
515 decay_ms: 2,
516 sustain: 0,
517 release_ms: 1,
518 },
519 }),
520 cutoff_lfo: Some(Lfo {
521 rate: LfoRate::MilliHertz(20000),
522 depth: 9600,
523 }),
524 pitch_lfo: Some(Lfo {
525 rate: LfoRate::MilliHertz(20000),
526 depth: 2400,
527 }),
528 ..Default::default()
529 },
530 rate,
531 999,
532 );
533 let mut voice = WideVoice::new();
534 voice.trigger(&plan, -6900.0, false, false);
535 for n in 0..4000 {
536 let frame = voice.sample(&plan, Waveform::Sawtooth, 0.0, n, None, None);
537 assert!(
538 frame.iter().all(|x| x.is_finite() && x.abs() < 1000.0),
539 "rate={rate} {mode:?}: {frame:?}"
540 );
541 }
542 }
543 }
544 }
545}