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}
24
25impl PresetKind {
26 pub fn label(self) -> &'static str {
27 match self {
28 PresetKind::PadZimmer => "Pad",
29 PresetKind::DroneSub => "Drone",
30 PresetKind::Shimmer => "Shimmer",
31 PresetKind::Heartbeat => "Heartbeat",
32 PresetKind::BassPulse => "Bass",
33 }
34 }
35}
36
37#[derive(Clone)]
38pub struct GlobalParams {
39 pub bpm: Shared,
40 pub master_gain: Shared,
41 pub brightness: Shared,
50}
51
52impl Default for GlobalParams {
53 fn default() -> Self {
54 Self {
55 bpm: shared(72.0),
56 master_gain: shared(0.7),
57 brightness: shared(0.6),
58 }
59 }
60}
61
62pub const MASTER_SHELF_HZ: f64 = 3500.0;
63pub const MIN_SHELF_GAIN: f64 = 0.2;
64
65#[inline]
67pub fn brightness_to_shelf_gain(b: f64) -> f64 {
68 MIN_SHELF_GAIN + (1.0 - MIN_SHELF_GAIN) * b.clamp(0.0, 1.0)
69}
70
71#[inline]
73pub fn shelf_gain_db(g: f64) -> f64 {
74 20.0 * g.max(1e-6).log10()
75}
76
77#[inline]
82pub fn brightness_to_lp_cutoff(b: f64) -> f64 {
83 3000.0 * 6.0_f64.powf(b.clamp(0.0, 1.0))
84}
85
86pub fn master_bus(brightness: Shared) -> Net {
94 let b_shelf_l = brightness.clone();
95 let b_shelf_r = brightness.clone();
96 let b_lp_l = brightness.clone();
97 let b_lp_r = brightness;
98
99 let sh_f_l = lfo(|_t: f64| MASTER_SHELF_HZ);
101 let sh_f_r = lfo(|_t: f64| MASTER_SHELF_HZ);
102 let sh_q_l = lfo(|_t: f64| 0.7_f64);
103 let sh_q_r = lfo(|_t: f64| 0.7_f64);
104 let sh_g_l = lfo(move |_t: f64| brightness_to_shelf_gain(b_shelf_l.value() as f64));
105 let sh_g_r = lfo(move |_t: f64| brightness_to_shelf_gain(b_shelf_r.value() as f64));
106 let shelf_l = (pass() | sh_f_l | sh_q_l | sh_g_l) >> highshelf();
107 let shelf_r = (pass() | sh_f_r | sh_q_r | sh_g_r) >> highshelf();
108
109 let lp_c_l = lfo(move |_t: f64| brightness_to_lp_cutoff(b_lp_l.value() as f64));
111 let lp_c_r = lfo(move |_t: f64| brightness_to_lp_cutoff(b_lp_r.value() as f64));
112 let lp_q_l = lfo(|_t: f64| 0.5_f64);
113 let lp_q_r = lfo(|_t: f64| 0.5_f64);
114
115 let left = shelf_l >> (pass() | lp_c_l | lp_q_l) >> lowpass();
116 let right = shelf_r >> (pass() | lp_c_r | lp_q_r) >> lowpass();
117 let stereo = left | right;
118
119 let chain = stereo >> limiter_stereo(0.001, 0.3);
120 Net::wrap(Box::new(chain))
121}
122
123pub struct Preset;
124
125impl Preset {
126 pub fn build(kind: PresetKind, p: &TrackParams, g: &GlobalParams) -> Net {
127 match kind {
128 PresetKind::PadZimmer => pad_zimmer(p, g),
129 PresetKind::DroneSub => drone_sub(p, g),
130 PresetKind::Shimmer => shimmer(p, g),
131 PresetKind::Heartbeat => heartbeat(p, g),
132 PresetKind::BassPulse => bass_pulse(p, g),
133 }
134 }
135}
136
137pub const LFO_OFF: u32 = 0;
140pub const LFO_CUTOFF: u32 = 1;
141pub const LFO_GAIN: u32 = 2;
142pub const LFO_FREQ: u32 = 3;
143pub const LFO_REVERB: u32 = 4;
144pub const LFO_TARGETS: u32 = 5;
145
146pub fn lfo_target_name(idx: u32) -> &'static str {
147 match idx {
148 LFO_OFF => "OFF",
149 LFO_CUTOFF => "CUT",
150 LFO_GAIN => "GAIN",
151 LFO_FREQ => "FREQ",
152 LFO_REVERB => "REV",
153 _ => "?",
154 }
155}
156
157#[derive(Clone)]
160pub struct LfoBundle {
161 pub rate: Shared,
162 pub depth: Shared,
163 pub target: Shared,
164}
165
166impl LfoBundle {
167 pub fn from_params(p: &TrackParams) -> Self {
168 Self {
169 rate: p.lfo_rate.clone(),
170 depth: p.lfo_depth.clone(),
171 target: p.lfo_target.clone(),
172 }
173 }
174
175 #[inline]
179 pub fn apply(
180 &self,
181 base: f64,
182 this_target: u32,
183 t: f64,
184 scaler: impl Fn(f64, f64) -> f64,
185 ) -> f64 {
186 let tgt = self.target.value().round() as u32;
187 if tgt != this_target {
188 return base;
189 }
190 let depth = self.depth.value() as f64;
191 if depth < 1.0e-4 {
192 return base;
193 }
194 let rate = self.rate.value() as f64;
195 let lv = (std::f64::consts::TAU * rate * t).sin();
196 scaler(base, lv * depth)
197 }
198}
199
200#[allow(dead_code)]
203fn stereo_from_shared(s: Shared) -> Net {
204 Net::wrap(Box::new(lfo(move |_t: f64| s.value() as f64) >> split::<U2>()))
205}
206
207fn stereo_reverb_mix(base: Shared, lb: LfoBundle) -> Net {
210 let mono = lfo(move |t: f64| {
211 let v = base.value() as f64;
212 lb.apply(v, LFO_REVERB, t, |b, m| (b + m * 0.4).clamp(0.0, 1.0))
213 });
214 Net::wrap(Box::new(mono >> split::<U2>()))
215}
216
217fn supermass_send(amount: Shared) -> Net {
218 let a1 = amount.clone();
219 let a2 = amount;
220 let amount_l = lfo(move |_t: f64| a1.value() as f64);
221 let amount_r = lfo(move |_t: f64| a2.value() as f64);
222 let amount_stereo = Net::wrap(Box::new(amount_l | amount_r));
223
224 let effect = reverb_stereo(35.0, 15.0, 0.88)
227 >> (chorus(3, 0.0, 0.022, 0.28) | chorus(4, 0.0, 0.026, 0.28))
228 >> reverb_stereo(50.0, 28.0, 0.90);
229
230 let wet_scaled = Net::wrap(Box::new(effect)) * amount_stereo;
231 let dry = Net::wrap(Box::new(multipass::<U2>()));
232 dry & wet_scaled
233}
234
235fn stereo_gate_voiced(
236 gain: Shared,
237 mute: Shared,
238 pulse_depth: Shared,
239 bpm: Shared,
240 life_mod: Shared,
241 lb: LfoBundle,
242) -> Net {
243 let raw = lfo(move |t: f64| {
244 let g_raw = (gain.value() * (1.0 - mute.value())) as f64;
245 let g = lb.apply(g_raw, LFO_GAIN, t, |b, m| (b * (1.0 + m * 0.6)).max(0.0));
247 let depth = pulse_depth.value().clamp(0.0, 1.0) as f64;
248 let pulse = pulse_sine(t, bpm.value() as f64);
249 let life = life_mod.value().clamp(0.0, 1.0) as f64;
250 let life_scaled = 0.4 + 0.9 * life;
251 g * (1.0 - depth + depth * pulse) * life_scaled
252 });
253 Net::wrap(Box::new(raw >> follow(0.4) >> split::<U2>()))
254}
255
256fn pad_zimmer(p: &TrackParams, g: &GlobalParams) -> Net {
258 let cut = p.cutoff.clone();
259 let res_s = p.resonance.clone();
260 let det = p.detune.clone();
261
262 let lb = LfoBundle::from_params(p);
263 let f0 = p.freq.clone();
264 let f1 = p.freq.clone();
265 let f2 = p.freq.clone();
266 let f3 = p.freq.clone();
267 let d1 = det.clone();
268 let d2 = det.clone();
269 let (lb0, lb1, lb2, lb3, lb_c) = (
270 lb.clone(),
271 lb.clone(),
272 lb.clone(),
273 lb.clone(),
274 lb.clone(),
275 );
276
277 let osc = ((lfo(move |t: f64| {
278 let b = f0.value() as f64;
279 lb0.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
280 }) >> (sine() * 0.30))
281 + (lfo(move |t: f64| {
282 let b = f1.value() as f64 * 1.501 * (1.0 + d1.value() as f64 * 0.000578);
283 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
284 }) >> (sine() * 0.20))
285 + (lfo(move |t: f64| {
286 let b = f2.value() as f64 * 2.013 * (1.0 + d2.value() as f64 * 0.000578);
287 lb2.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
288 }) >> (sine() * 0.14))
289 + (lfo(move |t: f64| {
290 let b = f3.value() as f64 * 3.007;
291 lb3.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
292 }) >> (sine() * 0.08)))
293 * 0.9;
294
295 let cutoff_mod = lfo(move |t: f64| {
296 let wobble = 1.0 + 0.10 * (0.5 - 0.5 * (t * 0.08).sin());
297 let base = cut.value() as f64 * wobble;
298 lb_c.apply(base, LFO_CUTOFF, t, |b, m| b * 2.0_f64.powf(m))
299 }) >> follow(0.08);
300 let res_mod = lfo(move |_t: f64| res_s.value().min(0.65) as f64) >> follow(0.08);
304
305 let filtered = (osc | cutoff_mod | res_mod) >> moog()
309 >> highshelf_hz(3000.0, 0.7, 0.67);
310
311 let stereo = filtered
312 >> split::<U2>()
313 >> (chorus(0, 0.0, 0.015, 0.35) | chorus(1, 0.0, 0.020, 0.35))
314 >> reverb_stereo(18.0, 4.0, 0.9);
315
316 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
317 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
318 voiced
319 * stereo_gate_voiced(
320 p.gain.clone(),
321 p.mute.clone(),
322 p.pulse_depth.clone(),
323 g.bpm.clone(),
324 p.life_mod.clone(),
325 lb,
326 )
327}
328
329fn drone_sub(p: &TrackParams, g: &GlobalParams) -> Net {
331 let lb = LfoBundle::from_params(p);
332 let cut = p.cutoff.clone();
333 let res_s = p.resonance.clone();
334
335 let f0 = p.freq.clone();
336 let f1 = p.freq.clone();
337 let (lb0, lb1, lb_c) = (lb.clone(), lb.clone(), lb.clone());
338
339 let sub = (lfo(move |t: f64| {
340 let b = f0.value() as f64 * 0.5;
341 lb0.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
342 }) >> (sine() * 0.45))
343 + (lfo(move |t: f64| {
344 let b = f1.value() as f64;
345 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
346 }) >> (sine() * 0.12));
347
348 let noise_cut = lfo(move |t: f64| {
349 let b = cut.value().clamp(40.0, 300.0) as f64;
350 lb_c.apply(b, LFO_CUTOFF, t, |b, m| b * 2.0_f64.powf(m))
351 }) >> follow(0.08);
352 let noise_q = lfo(move |_t: f64| res_s.value() as f64) >> follow(0.08);
353 let noise = (brown() | noise_cut | noise_q) >> moog();
354 let noise_body = noise * 0.28;
355
356 let bpm_am = g.bpm.clone();
357 let am = lfo(move |t: f64| 0.88 + 0.12 * pulse_sine(t, bpm_am.value() as f64));
358 let body = (sub + noise_body) * am;
359
360 let stereo = body >> split::<U2>() >> reverb_stereo(20.0, 5.0, 0.85);
361
362 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
363 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
364 voiced
365 * stereo_gate_voiced(
366 p.gain.clone(),
367 p.mute.clone(),
368 p.pulse_depth.clone(),
369 g.bpm.clone(),
370 p.life_mod.clone(),
371 lb,
372 )
373}
374
375fn shimmer(p: &TrackParams, g: &GlobalParams) -> Net {
377 let lb = LfoBundle::from_params(p);
378 let f0 = p.freq.clone();
379 let f1 = p.freq.clone();
380 let f2 = p.freq.clone();
381 let (lb0, lb1, lb2) = (lb.clone(), lb.clone(), lb.clone());
382
383 let osc = (lfo(move |t: f64| {
384 let b = f0.value() as f64 * 2.0;
385 lb0.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
386 }) >> (sine() * 0.18))
387 + (lfo(move |t: f64| {
388 let b = f1.value() as f64 * 3.0;
389 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
390 }) >> (sine() * 0.12))
391 + (lfo(move |t: f64| {
392 let b = f2.value() as f64 * 4.007;
393 lb2.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
394 }) >> (sine() * 0.08));
395
396 let bright = osc >> highpass_hz(400.0, 0.5);
397 let stereo = bright >> split::<U2>() >> reverb_stereo(22.0, 6.0, 0.85);
398
399 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
400 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
401 voiced
402 * stereo_gate_voiced(
403 p.gain.clone(),
404 p.mute.clone(),
405 p.pulse_depth.clone(),
406 g.bpm.clone(),
407 p.life_mod.clone(),
408 lb,
409 )
410}
411
412fn heartbeat(p: &TrackParams, g: &GlobalParams) -> Net {
417 let bpm = g.bpm.clone();
418
419 let bpm_body_f = bpm.clone();
421 let freq_body = p.freq.clone();
422 let pat_body_f = p.pattern_bits.clone();
423 let body_osc = lfo(move |t: f64| {
424 let bpm_v = bpm_body_f.value() as f64;
425 let bits = pat_body_f.load(Ordering::Relaxed);
426 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
427 let base = freq_body.value() as f64;
428 if active {
429 let drop = (-phi * 40.0).exp();
430 base * (0.7 + 1.5 * drop)
431 } else {
432 base
435 }
436 }) >> sine();
437
438 let bpm_body_e = bpm.clone();
439 let pat_body_e = p.pattern_bits.clone();
440 let body_env = lfo(move |t: f64| {
441 let bpm_v = bpm_body_e.value() as f64;
442 let bits = pat_body_e.load(Ordering::Relaxed);
443 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
444 if active {
445 (-phi * 4.0).exp()
446 } else {
447 0.0
448 }
449 });
450 let body = body_osc * body_env * 0.85;
451
452 let freq_sub = p.freq.clone();
454 let sub_osc = lfo(move |_t: f64| freq_sub.value() as f64 * 0.5) >> sine();
455 let bpm_sub_e = bpm.clone();
456 let pat_sub = p.pattern_bits.clone();
457 let sub_env = lfo(move |t: f64| {
458 let bpm_v = bpm_sub_e.value() as f64;
459 let bits = pat_sub.load(Ordering::Relaxed);
460 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
461 if active {
462 (-phi * 1.5).exp()
463 } else {
464 0.0
465 }
466 });
467 let sub = sub_osc * sub_env * 0.45;
468
469 let bpm_click = bpm.clone();
471 let pat_click = p.pattern_bits.clone();
472 let click_env = lfo(move |t: f64| {
473 let bpm_v = bpm_click.value() as f64;
474 let bits = pat_click.load(Ordering::Relaxed);
475 let (active, phi) = rhythm::step_is_active(bits, t, bpm_v);
476 if active {
477 (-phi * 40.0).exp()
478 } else {
479 0.0
480 }
481 });
482 let click = (brown() >> highpass_hz(1800.0, 0.5)) * click_env * 0.12;
483
484 let kick = body + sub + click;
485
486 let stereo = kick >> split::<U2>() >> reverb_stereo(10.0, 1.5, 0.88);
487
488 let lb = LfoBundle::from_params(p);
489 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
490 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
491 voiced
492 * stereo_gate_voiced(
493 p.gain.clone(),
494 p.mute.clone(),
495 p.pulse_depth.clone(),
496 g.bpm.clone(),
497 p.life_mod.clone(),
498 lb,
499 )
500}
501
502fn bass_pulse(p: &TrackParams, g: &GlobalParams) -> Net {
506 let lb = LfoBundle::from_params(p);
507 let f1 = p.freq.clone();
508 let f2 = p.freq.clone();
509 let f3 = p.freq.clone();
510 let cut = p.cutoff.clone();
511 let res_s = p.resonance.clone();
512 let (lb1, lb2, lb3, lb_c) = (lb.clone(), lb.clone(), lb.clone(), lb.clone());
513
514 let fundamental = lfo(move |t: f64| {
515 let b = f1.value() as f64;
516 lb1.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
517 }) >> (sine() * 0.55);
518 let second = lfo(move |t: f64| {
519 let b = f2.value() as f64 * 2.0;
520 lb2.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
521 }) >> (sine() * 0.22);
522 let sub = lfo(move |t: f64| {
523 let b = f3.value() as f64 * 0.5;
524 lb3.apply(b, LFO_FREQ, t, |b, m| b * 2.0_f64.powf(m / 12.0))
525 }) >> (sine() * 0.35);
526 let osc = fundamental + second + sub;
527
528 let cut_mod = lfo(move |t: f64| {
529 let b = cut.value().min(900.0) as f64;
530 lb_c.apply(b, LFO_CUTOFF, t, |b, m| b * 2.0_f64.powf(m))
531 }) >> follow(0.08);
532 let res_mod = lfo(move |_t: f64| res_s.value().min(0.65) as f64) >> follow(0.08);
533 let filtered = (osc | cut_mod | res_mod) >> moog();
534
535 let bpm_groove = g.bpm.clone();
536 let groove = lfo(move |t: f64| {
537 let pump = pulse_decay(t, bpm_groove.value() as f64, 3.5);
538 0.45 + 0.55 * pump
539 });
540 let grooved = filtered * groove;
541
542 let stereo = grooved >> split::<U2>() >> reverb_stereo(14.0, 2.5, 0.88);
543
544 let with_super = Net::wrap(Box::new(stereo)) >> supermass_send(p.supermass.clone());
545 let voiced = with_super * stereo_reverb_mix(p.reverb_mix.clone(), lb.clone());
546 voiced
547 * stereo_gate_voiced(
548 p.gain.clone(),
549 p.mute.clone(),
550 p.pulse_depth.clone(),
551 g.bpm.clone(),
552 p.life_mod.clone(),
553 lb,
554 )
555}