1use fundsp::hacker::*;
9
10use std::sync::atomic::Ordering;
11
12use super::track::TrackParams;
13use crate::math::pulse::{pulse_decay, pulse_sine};
14use crate::math::rhythm;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PresetKind {
18 PadZimmer,
19 DroneSub,
20 Shimmer,
21 Heartbeat,
22 BassPulse,
23 Bell,
24}
25
26pub const ALL_KINDS: [PresetKind; 6] = [
28 PresetKind::PadZimmer,
29 PresetKind::BassPulse,
30 PresetKind::Heartbeat,
31 PresetKind::DroneSub,
32 PresetKind::Shimmer,
33 PresetKind::Bell,
34];
35
36impl PresetKind {
37 pub fn label(self) -> &'static str {
38 match self {
39 PresetKind::PadZimmer => "Pad",
40 PresetKind::DroneSub => "Drone",
41 PresetKind::Shimmer => "Shimmer",
42 PresetKind::Heartbeat => "Heartbeat",
43 PresetKind::BassPulse => "Bass",
44 PresetKind::Bell => "Bell",
45 }
46 }
47
48 pub fn next(self) -> Self {
49 let i = ALL_KINDS.iter().position(|&k| k == self).unwrap_or(0);
50 ALL_KINDS[(i + 1) % ALL_KINDS.len()]
51 }
52
53 pub fn prev(self) -> Self {
54 let i = ALL_KINDS.iter().position(|&k| k == self).unwrap_or(0);
55 ALL_KINDS[(i + ALL_KINDS.len() - 1) % ALL_KINDS.len()]
56 }
57}
58
59#[derive(Clone)]
60pub struct GlobalParams {
61 pub bpm: Shared,
62 pub master_gain: Shared,
63 pub brightness: Shared,
72}
73
74impl Default for GlobalParams {
75 fn default() -> Self {
76 Self {
77 bpm: shared(72.0),
78 master_gain: shared(0.7),
79 brightness: shared(0.6),
80 }
81 }
82}
83
84pub const MASTER_SHELF_HZ: f64 = 3500.0;
85pub const MIN_SHELF_GAIN: f64 = 0.2;
86
87#[inline]
89pub fn brightness_to_shelf_gain(b: f64) -> f64 {
90 MIN_SHELF_GAIN + (1.0 - MIN_SHELF_GAIN) * b.clamp(0.0, 1.0)
91}
92
93#[inline]
95pub fn shelf_gain_db(g: f64) -> f64 {
96 20.0 * g.max(1e-6).log10()
97}
98
99#[inline]
104pub fn brightness_to_lp_cutoff(b: f64) -> f64 {
105 3000.0 * 6.0_f64.powf(b.clamp(0.0, 1.0))
106}
107
108pub fn master_bus(brightness: Shared) -> Net {
116 let b_shelf_l = brightness.clone();
117 let b_shelf_r = brightness.clone();
118 let b_lp_l = brightness.clone();
119 let b_lp_r = brightness;
120
121 let sh_f_l = lfo(|_t: f64| MASTER_SHELF_HZ);
123 let sh_f_r = lfo(|_t: f64| MASTER_SHELF_HZ);
124 let sh_q_l = lfo(|_t: f64| 0.7_f64);
125 let sh_q_r = lfo(|_t: f64| 0.7_f64);
126 let sh_g_l = lfo(move |_t: f64| brightness_to_shelf_gain(b_shelf_l.value() as f64));
127 let sh_g_r = lfo(move |_t: f64| brightness_to_shelf_gain(b_shelf_r.value() as f64));
128 let shelf_l = (pass() | sh_f_l | sh_q_l | sh_g_l) >> highshelf();
129 let shelf_r = (pass() | sh_f_r | sh_q_r | sh_g_r) >> highshelf();
130
131 let lp_c_l = lfo(move |_t: f64| brightness_to_lp_cutoff(b_lp_l.value() as f64));
133 let lp_c_r = lfo(move |_t: f64| brightness_to_lp_cutoff(b_lp_r.value() as f64));
134 let lp_q_l = lfo(|_t: f64| 0.5_f64);
135 let lp_q_r = lfo(|_t: f64| 0.5_f64);
136
137 let left = shelf_l >> (pass() | lp_c_l | lp_q_l) >> lowpass();
138 let right = shelf_r >> (pass() | lp_c_r | lp_q_r) >> lowpass();
139 let stereo = left | right;
140
141 let chain = stereo >> limiter_stereo(0.001, 0.3);
142 Net::wrap(Box::new(chain))
143}
144
145pub struct Preset;
146
147impl Preset {
148 pub fn build(kind: PresetKind, p: &TrackParams, g: &GlobalParams) -> Net {
149 match kind {
150 PresetKind::PadZimmer => pad_zimmer(p, g),
151 PresetKind::DroneSub => drone_sub(p, g),
152 PresetKind::Shimmer => shimmer(p, g),
153 PresetKind::Heartbeat => heartbeat(p, g),
154 PresetKind::BassPulse => bass_pulse(p, g),
155 PresetKind::Bell => bell_preset(p, g),
156 }
157 }
158}
159
160pub const LFO_OFF: u32 = 0;
163pub const LFO_CUTOFF: u32 = 1;
164pub const LFO_GAIN: u32 = 2;
165pub const LFO_FREQ: u32 = 3;
166pub const LFO_REVERB: u32 = 4;
167pub const LFO_TARGETS: u32 = 5;
168
169pub fn lfo_target_name(idx: u32) -> &'static str {
170 match idx {
171 LFO_OFF => "OFF",
172 LFO_CUTOFF => "CUT",
173 LFO_GAIN => "GAIN",
174 LFO_FREQ => "FREQ",
175 LFO_REVERB => "REV",
176 _ => "?",
177 }
178}
179
180#[derive(Clone)]
183pub struct LfoBundle {
184 pub rate: Shared,
185 pub depth: Shared,
186 pub target: Shared,
187}
188
189impl LfoBundle {
190 pub fn from_params(p: &TrackParams) -> Self {
191 Self {
192 rate: p.lfo_rate.clone(),
193 depth: p.lfo_depth.clone(),
194 target: p.lfo_target.clone(),
195 }
196 }
197
198 #[inline]
202 pub fn apply(
203 &self,
204 base: f64,
205 this_target: u32,
206 t: f64,
207 scaler: impl Fn(f64, f64) -> f64,
208 ) -> f64 {
209 let tgt = self.target.value().round() as u32;
210 if tgt != this_target {
211 return base;
212 }
213 let depth = self.depth.value() as f64;
214 if depth < 1.0e-4 {
215 return base;
216 }
217 let rate = self.rate.value() as f64;
218 let lv = (std::f64::consts::TAU * rate * t).sin();
219 scaler(base, lv * depth)
220 }
221}
222
223#[allow(dead_code)]
226fn stereo_from_shared(s: Shared) -> Net {
227 Net::wrap(Box::new(lfo(move |_t: f64| s.value() as f64) >> split::<U2>()))
228}
229
230fn stereo_reverb_mix(base: Shared, lb: LfoBundle) -> Net {
233 let mono = lfo(move |t: f64| {
234 let v = base.value() as f64;
235 lb.apply(v, LFO_REVERB, t, |b, m| (b + m * 0.4).clamp(0.0, 1.0))
236 });
237 Net::wrap(Box::new(mono >> split::<U2>()))
238}
239
240fn supermass_send(amount: Shared) -> Net {
241 let a1 = amount.clone();
242 let a2 = amount;
243 let amount_l = lfo(move |_t: f64| a1.value() as f64);
244 let amount_r = lfo(move |_t: f64| a2.value() as f64);
245 let amount_stereo = Net::wrap(Box::new(amount_l | amount_r));
246
247 let effect = reverb_stereo(35.0, 15.0, 0.88)
250 >> (chorus(3, 0.0, 0.022, 0.28) | chorus(4, 0.0, 0.026, 0.28))
251 >> reverb_stereo(50.0, 28.0, 0.90);
252
253 let wet_scaled = Net::wrap(Box::new(effect)) * amount_stereo;
254 let dry = Net::wrap(Box::new(multipass::<U2>()));
255 dry & wet_scaled
256}
257
258fn stereo_gate_voiced(
259 gain: Shared,
260 mute: Shared,
261 pulse_depth: Shared,
262 bpm: Shared,
263 life_mod: Shared,
264 lb: LfoBundle,
265) -> Net {
266 let raw = lfo(move |t: f64| {
267 let g_raw = (gain.value() * (1.0 - mute.value())) as f64;
268 let g = lb.apply(g_raw, LFO_GAIN, t, |b, m| (b * (1.0 + m * 0.6)).max(0.0));
270 let depth = pulse_depth.value().clamp(0.0, 1.0) as f64;
271 let pulse = pulse_sine(t, bpm.value() as f64);
272 let life = life_mod.value().clamp(0.0, 1.0) as f64;
273 let life_scaled = 0.4 + 0.9 * life;
274 g * (1.0 - depth + depth * pulse) * life_scaled
275 });
276 Net::wrap(Box::new(raw >> follow(0.4) >> split::<U2>()))
277}
278
279fn pad_zimmer(p: &TrackParams, g: &GlobalParams) -> Net {
281 let cut = p.cutoff.clone();
282 let res_s = p.resonance.clone();
283 let det = p.detune.clone();
284
285 let lb = LfoBundle::from_params(p);
286 let f0 = p.freq.clone();
287 let f1 = p.freq.clone();
288 let f2 = p.freq.clone();
289 let f3 = p.freq.clone();
290 let d1 = det.clone();
291 let d2 = det.clone();
292 let (lb0, lb1, lb2, lb3, lb_c) = (
293 lb.clone(),
294 lb.clone(),
295 lb.clone(),
296 lb.clone(),
297 lb.clone(),
298 );
299
300 let osc = ((lfo(move |t: f64| {
301 let b = f0.value() as f64;
302 lb0.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
303 }) >> (sine() * 0.30))
304 + (lfo(move |t: f64| {
305 let b = f1.value() as f64 * 1.501 * (1.0 + d1.value() as f64 * 0.000578);
306 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
307 }) >> (sine() * 0.20))
308 + (lfo(move |t: f64| {
309 let b = f2.value() as f64 * 2.013 * (1.0 + d2.value() as f64 * 0.000578);
310 lb2.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
311 }) >> (sine() * 0.14))
312 + (lfo(move |t: f64| {
313 let b = f3.value() as f64 * 3.007;
314 lb3.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
315 }) >> (sine() * 0.08)))
316 * 0.9;
317
318 let cutoff_mod = lfo(move |t: f64| {
319 let wobble = 1.0 + 0.10 * (0.5 - 0.5 * (t * 0.08).sin());
320 let base = cut.value() as f64 * wobble;
321 lb_c.apply(base, LFO_CUTOFF, t, |b, m| b * 2.0_f64.powf(m))
322 }) >> follow(0.08);
323 let res_mod = lfo(move |_t: f64| res_s.value().min(0.65) as f64) >> follow(0.08);
327
328 let filtered = (osc | cutoff_mod | res_mod) >> moog()
332 >> highshelf_hz(3000.0, 0.7, 0.67);
333
334 let stereo = filtered
335 >> split::<U2>()
336 >> (chorus(0, 0.0, 0.015, 0.35) | chorus(1, 0.0, 0.020, 0.35))
337 >> reverb_stereo(18.0, 4.0, 0.9);
338
339 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
340 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
341 voiced
342 * stereo_gate_voiced(
343 p.gain.clone(),
344 p.mute.clone(),
345 p.pulse_depth.clone(),
346 g.bpm.clone(),
347 p.life_mod.clone(),
348 lb,
349 )
350}
351
352fn drone_sub(p: &TrackParams, g: &GlobalParams) -> Net {
354 let lb = LfoBundle::from_params(p);
355 let cut = p.cutoff.clone();
356 let res_s = p.resonance.clone();
357
358 let f0 = p.freq.clone();
359 let f1 = p.freq.clone();
360 let (lb0, lb1, lb_c) = (lb.clone(), lb.clone(), lb.clone());
361
362 let sub = (lfo(move |t: f64| {
363 let b = f0.value() as f64 * 0.5;
364 lb0.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
365 }) >> (sine() * 0.45))
366 + (lfo(move |t: f64| {
367 let b = f1.value() as f64;
368 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
369 }) >> (sine() * 0.12));
370
371 let noise_cut = lfo(move |t: f64| {
372 let b = cut.value().clamp(40.0, 300.0) as f64;
373 lb_c.apply(b, LFO_CUTOFF, t, |b, m| b * 2.0_f64.powf(m))
374 }) >> follow(0.08);
375 let noise_q = lfo(move |_t: f64| res_s.value() as f64) >> follow(0.08);
376 let noise = (brown() | noise_cut | noise_q) >> moog();
377 let noise_body = noise * 0.28;
378
379 let bpm_am = g.bpm.clone();
380 let am = lfo(move |t: f64| 0.88 + 0.12 * pulse_sine(t, bpm_am.value() as f64));
381 let body = (sub + noise_body) * am;
382
383 let stereo = body >> split::<U2>() >> reverb_stereo(20.0, 5.0, 0.85);
384
385 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
386 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
387 voiced
388 * stereo_gate_voiced(
389 p.gain.clone(),
390 p.mute.clone(),
391 p.pulse_depth.clone(),
392 g.bpm.clone(),
393 p.life_mod.clone(),
394 lb,
395 )
396}
397
398fn shimmer(p: &TrackParams, g: &GlobalParams) -> Net {
400 let lb = LfoBundle::from_params(p);
401 let f0 = p.freq.clone();
402 let f1 = p.freq.clone();
403 let f2 = p.freq.clone();
404 let (lb0, lb1, lb2) = (lb.clone(), lb.clone(), lb.clone());
405
406 let osc = (lfo(move |t: f64| {
407 let b = f0.value() as f64 * 2.0;
408 lb0.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
409 }) >> (sine() * 0.18))
410 + (lfo(move |t: f64| {
411 let b = f1.value() as f64 * 3.0;
412 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
413 }) >> (sine() * 0.12))
414 + (lfo(move |t: f64| {
415 let b = f2.value() as f64 * 4.007;
416 lb2.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
417 }) >> (sine() * 0.08));
418
419 let bright = osc >> highpass_hz(400.0, 0.5);
420 let stereo = bright >> split::<U2>() >> reverb_stereo(22.0, 6.0, 0.85);
421
422 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
423 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
424 voiced
425 * stereo_gate_voiced(
426 p.gain.clone(),
427 p.mute.clone(),
428 p.pulse_depth.clone(),
429 g.bpm.clone(),
430 p.life_mod.clone(),
431 lb,
432 )
433}
434
435fn heartbeat(p: &TrackParams, g: &GlobalParams) -> Net {
440 let bpm = g.bpm.clone();
441
442 let bpm_body_f = bpm.clone();
444 let freq_body = p.freq.clone();
445 let pat_body_f = p.pattern_bits.clone();
446 let body_osc = lfo(move |t: f64| {
447 let bpm_v = bpm_body_f.value() as f64;
448 let bits = pat_body_f.load(Ordering::Relaxed);
449 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
450 let base = freq_body.value() as f64;
451 if active {
452 let drop = (-phi * 40.0).exp();
453 base * (0.7 + 1.5 * drop)
454 } else {
455 base
458 }
459 }) >> sine();
460
461 let bpm_body_e = bpm.clone();
462 let pat_body_e = p.pattern_bits.clone();
463 let body_env = lfo(move |t: f64| {
464 let bpm_v = bpm_body_e.value() as f64;
465 let bits = pat_body_e.load(Ordering::Relaxed);
466 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
467 if active {
468 (-phi * 4.0).exp()
469 } else {
470 0.0
471 }
472 });
473 let body = body_osc * body_env * 0.85;
474
475 let freq_sub = p.freq.clone();
477 let sub_osc = lfo(move |_t: f64| freq_sub.value() as f64 * 0.5) >> sine();
478 let bpm_sub_e = bpm.clone();
479 let pat_sub = p.pattern_bits.clone();
480 let sub_env = lfo(move |t: f64| {
481 let bpm_v = bpm_sub_e.value() as f64;
482 let bits = pat_sub.load(Ordering::Relaxed);
483 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
484 if active {
485 (-phi * 1.5).exp()
486 } else {
487 0.0
488 }
489 });
490 let sub = sub_osc * sub_env * 0.45;
491
492 let bpm_click = bpm.clone();
494 let pat_click = p.pattern_bits.clone();
495 let click_env = lfo(move |t: f64| {
496 let bpm_v = bpm_click.value() as f64;
497 let bits = pat_click.load(Ordering::Relaxed);
498 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
499 if active {
500 (-phi * 40.0).exp()
501 } else {
502 0.0
503 }
504 });
505 let click = (brown() >> highpass_hz(1800.0, 0.5)) * click_env * 0.12;
506
507 let kick = body + sub + click;
508
509 let stereo = kick >> split::<U2>() >> reverb_stereo(10.0, 1.5, 0.88);
510
511 let lb = LfoBundle::from_params(p);
512 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
513 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
514 voiced
515 * stereo_gate_voiced(
516 p.gain.clone(),
517 p.mute.clone(),
518 p.pulse_depth.clone(),
519 g.bpm.clone(),
520 p.life_mod.clone(),
521 lb,
522 )
523}
524
525fn bass_pulse(p: &TrackParams, g: &GlobalParams) -> Net {
529 let lb = LfoBundle::from_params(p);
530 let f1 = p.freq.clone();
531 let f2 = p.freq.clone();
532 let f3 = p.freq.clone();
533 let cut = p.cutoff.clone();
534 let res_s = p.resonance.clone();
535 let (lb1, lb2, lb3, lb_c) = (lb.clone(), lb.clone(), lb.clone(), lb.clone());
536
537 let fundamental = lfo(move |t: f64| {
538 let b = f1.value() as f64;
539 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
540 }) >> (sine() * 0.55);
541 let second = lfo(move |t: f64| {
542 let b = f2.value() as f64 * 2.0;
543 lb2.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
544 }) >> (sine() * 0.22);
545 let sub = lfo(move |t: f64| {
546 let b = f3.value() as f64 * 0.5;
547 lb3.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
548 }) >> (sine() * 0.35);
549 let osc = fundamental + second + sub;
550
551 let cut_mod = lfo(move |t: f64| {
552 let b = cut.value().min(900.0) as f64;
553 lb_c.apply(b, LFO_CUTOFF, t, |b, m| b * 2.0_f64.powf(m))
554 }) >> follow(0.08);
555 let res_mod = lfo(move |_t: f64| res_s.value().min(0.65) as f64) >> follow(0.08);
556 let filtered = (osc | cut_mod | res_mod) >> moog();
557
558 let bpm_groove = g.bpm.clone();
559 let groove = lfo(move |t: f64| {
560 let pump = pulse_decay(t, bpm_groove.value() as f64, 3.5);
561 0.45 + 0.55 * pump
562 });
563 let grooved = filtered * groove;
564
565 let stereo = grooved >> split::<U2>() >> reverb_stereo(14.0, 2.5, 0.88);
566
567 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
568 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
569 voiced
570 * stereo_gate_voiced(
571 p.gain.clone(),
572 p.mute.clone(),
573 p.pulse_depth.clone(),
574 g.bpm.clone(),
575 p.life_mod.clone(),
576 lb,
577 )
578}
579
580fn bell_preset(p: &TrackParams, g: &GlobalParams) -> Net {
585 let lb = LfoBundle::from_params(p);
586 let fc = p.freq.clone();
587 let fm = p.freq.clone();
588 let fm_depth = p.resonance.clone();
589 let (lb_c, lb_m) = (lb.clone(), lb.clone());
590
591 let modulator_freq = lfo(move |t: f64| {
592 let b = fm.value() as f64 * 2.76;
593 lb_m.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
594 });
595 let modulator = modulator_freq >> sine();
596 let mod_scale = lfo(move |_t: f64| fm_depth.value().min(0.65) as f64 * 450.0);
597 let modulator_scaled = modulator * mod_scale;
598
599 let carrier_base = lfo(move |t: f64| {
600 let b = fc.value() as f64;
601 lb_c.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
602 });
603 let bell_sig = (carrier_base + modulator_scaled) >> sine();
604
605 let bpm_am = g.bpm.clone();
606 let am = lfo(move |t: f64| 0.85 + 0.15 * pulse_sine(t, bpm_am.value() as f64 * 0.25));
607 let body = bell_sig * am * 0.30;
608
609 let stereo = body >> split::<U2>() >> reverb_stereo(25.0, 8.0, 0.85);
610
611 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
612 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
613 voiced
614 * stereo_gate_voiced(
615 p.gain.clone(),
616 p.mute.clone(),
617 p.pulse_depth.clone(),
618 g.bpm.clone(),
619 p.life_mod.clone(),
620 lb,
621 )
622}