Skip to main content

rust_synth/audio/
vibe.rs

1//! Named whole-mix presets that reconfigure every track + global at once.
2//! Trigger with the `V` / `v` keys — graph is rebuilt afterwards so the
3//! new voice kinds take effect immediately.
4
5use super::engine::EngineHandle;
6use super::preset::PresetKind;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VibeKind {
10    Default,
11    BladeRunner,
12    Cathedral,
13    DanceFloor,
14}
15
16impl VibeKind {
17    pub fn label(self) -> &'static str {
18        match self {
19            VibeKind::Default => "Default",
20            VibeKind::BladeRunner => "Blade Runner",
21            VibeKind::Cathedral => "Cathedral",
22            VibeKind::DanceFloor => "DanceFloor",
23        }
24    }
25
26    pub fn next(self) -> Self {
27        match self {
28            VibeKind::Default => VibeKind::BladeRunner,
29            VibeKind::BladeRunner => VibeKind::Cathedral,
30            VibeKind::Cathedral => VibeKind::DanceFloor,
31            VibeKind::DanceFloor => VibeKind::Default,
32        }
33    }
34}
35
36/// Apply a named vibe and rebuild the master graph. All eight slots are
37/// touched so the mix reflects exactly what the vibe wants — no leftover
38/// state from whatever was playing before.
39pub fn apply(engine: &EngineHandle, vibe: VibeKind) {
40    match vibe {
41        VibeKind::Default => apply_default(engine),
42        VibeKind::BladeRunner => apply_blade_runner(engine),
43        VibeKind::Cathedral => apply_cathedral(engine),
44        VibeKind::DanceFloor => apply_dance_floor(engine),
45    }
46    engine.rebuild_graph();
47}
48
49// ── Vibe: Blade Runner (Vangelis, 1982) ─────────────────────────────────
50// Slow tempo, dark brightness, minor pentatonic, cathedral reverb on
51// everything. CS-80-style SuperSaw and a lone metallic Bell playing the
52// main motif. Heartbeat is sparse and soft — film-score, not dance.
53fn apply_blade_runner(engine: &EngineHandle) {
54    engine.global.bpm.set_value(66.0);
55    engine.global.brightness.set_value(0.45);
56    engine.global.master_gain.set_value(0.65);
57    engine.global.scale_mode.set_value(1.0); // minor pentatonic
58
59    let tracks = engine.tracks.lock();
60    let root = 55.0_f32; // A1
61
62    set_track(&tracks, 0, PresetKind::PadZimmer, root, |p| {
63        p.gain.set_value(0.48);
64        p.cutoff.set_value(2400.0);
65        p.resonance.set_value(0.30);
66        p.detune.set_value(22.0);
67        p.character.set_value(0.70); // inharmonic partials (metallic pad)
68        p.reverb_mix.set_value(0.80);
69        p.supermass.set_value(0.70);
70        p.arp.set_value(0.18);
71        p.lfo_rate.set_value(0.12);
72        p.lfo_depth.set_value(0.45);
73        p.lfo_target.set_value(1.0); // CUT
74    });
75    set_track(&tracks, 1, PresetKind::BassPulse, root, |p| {
76        p.gain.set_value(0.55);
77        p.cutoff.set_value(380.0);
78        p.resonance.set_value(0.45);
79        p.character.set_value(0.40);
80        p.reverb_mix.set_value(0.55);
81        p.supermass.set_value(0.30);
82        p.arp.set_value(0.25);
83    });
84    set_track(&tracks, 2, PresetKind::Heartbeat, root, |p| {
85        p.gain.set_value(0.65);
86        // Low character → almost no click, all 808-style sub boom.
87        // No more "knock on wood in the distance."
88        p.character.set_value(0.18);
89        p.pattern_hits.set_value(3.0); // sparse
90        p.pattern_rotation.set_value(0.0);
91        p.reverb_mix.set_value(0.45); // up-front, not cavernous
92        p.supermass.set_value(0.15);
93    });
94    set_track(&tracks, 3, PresetKind::DroneSub, root * 0.5, |p| {
95        p.gain.set_value(0.32);
96        p.cutoff.set_value(180.0);
97        p.reverb_mix.set_value(0.85);
98        p.supermass.set_value(0.60);
99    });
100    set_track(&tracks, 4, PresetKind::Shimmer, root * 2.0, |p| {
101        p.gain.set_value(0.28);
102        p.character.set_value(0.70);
103        p.reverb_mix.set_value(0.90);
104        p.supermass.set_value(0.80);
105        p.arp.set_value(0.22);
106    });
107    set_track(&tracks, 5, PresetKind::Bell, root * 1.5, |p| {
108        p.gain.set_value(0.30);
109        p.resonance.set_value(0.50); // FM depth — CS-80 metallic stab
110        p.character.set_value(0.65); // FM ratio ≈ 3.1
111        p.reverb_mix.set_value(0.85);
112        p.supermass.set_value(0.70);
113        p.arp.set_value(0.30);
114    });
115    set_track(&tracks, 6, PresetKind::SuperSaw, root, |p| {
116        p.gain.set_value(0.32);
117        p.cutoff.set_value(1800.0);
118        p.resonance.set_value(0.35);
119        p.detune.set_value(18.0); // CS-80-flavoured chorus spread
120        p.reverb_mix.set_value(0.70);
121        p.supermass.set_value(0.50);
122        p.arp.set_value(0.20);
123        p.lfo_rate.set_value(0.08);
124        p.lfo_depth.set_value(0.30);
125        p.lfo_target.set_value(1.0); // CUT
126    });
127    mute_slot(&tracks, 7); // Pluck stays dormant by default
128}
129
130// ── Vibe: Cathedral (everything drenched in supermass) ──────────────────
131fn apply_cathedral(engine: &EngineHandle) {
132    engine.global.bpm.set_value(54.0);
133    engine.global.brightness.set_value(0.55);
134    engine.global.master_gain.set_value(0.60);
135    engine.global.scale_mode.set_value(0.0);
136
137    let tracks = engine.tracks.lock();
138    let root = 55.0_f32;
139
140    set_track(&tracks, 0, PresetKind::PadZimmer, root, |p| {
141        p.gain.set_value(0.45);
142        p.cutoff.set_value(2000.0);
143        p.character.set_value(0.80);
144        p.reverb_mix.set_value(0.95);
145        p.supermass.set_value(1.00);
146        p.arp.set_value(0.10);
147    });
148    set_track(&tracks, 1, PresetKind::Shimmer, root * 2.0, |p| {
149        p.gain.set_value(0.30);
150        p.reverb_mix.set_value(0.95);
151        p.supermass.set_value(1.00);
152        p.arp.set_value(0.15);
153    });
154    set_track(&tracks, 2, PresetKind::Bell, root, |p| {
155        p.gain.set_value(0.28);
156        p.resonance.set_value(0.40);
157        p.character.set_value(0.50);
158        p.reverb_mix.set_value(0.95);
159        p.supermass.set_value(1.00);
160        p.arp.set_value(0.25);
161    });
162    set_track(&tracks, 3, PresetKind::DroneSub, root * 0.5, |p| {
163        p.gain.set_value(0.30);
164        p.cutoff.set_value(160.0);
165        p.reverb_mix.set_value(0.95);
166        p.supermass.set_value(0.90);
167    });
168    mute_slot(&tracks, 4);
169    mute_slot(&tracks, 5);
170    mute_slot(&tracks, 6);
171    mute_slot(&tracks, 7);
172}
173
174// ── Vibe: DanceFloor (tight low-end, fast arp, little reverb) ──────────
175fn apply_dance_floor(engine: &EngineHandle) {
176    engine.global.bpm.set_value(128.0);
177    engine.global.brightness.set_value(0.75);
178    engine.global.master_gain.set_value(0.70);
179    engine.global.scale_mode.set_value(0.0);
180
181    let tracks = engine.tracks.lock();
182    let root = 55.0_f32;
183
184    set_track(&tracks, 0, PresetKind::BassPulse, root, |p| {
185        p.gain.set_value(0.62);
186        p.cutoff.set_value(500.0);
187        p.resonance.set_value(0.55);
188        p.reverb_mix.set_value(0.25);
189        p.supermass.set_value(0.0);
190        p.arp.set_value(0.30);
191    });
192    set_track(&tracks, 1, PresetKind::Heartbeat, root, |p| {
193        p.gain.set_value(0.75);
194        p.character.set_value(0.75); // punchy kick
195        p.pattern_hits.set_value(4.0);
196        p.pattern_rotation.set_value(0.0);
197        p.reverb_mix.set_value(0.40);
198        p.supermass.set_value(0.0);
199    });
200    set_track(&tracks, 2, PresetKind::PluckSaw, root * 2.0, |p| {
201        p.gain.set_value(0.45);
202        p.cutoff.set_value(3000.0);
203        p.resonance.set_value(0.45);
204        p.pattern_hits.set_value(11.0);
205        p.reverb_mix.set_value(0.35);
206        p.arp.set_value(0.40);
207    });
208    set_track(&tracks, 3, PresetKind::SuperSaw, root * 1.5, |p| {
209        p.gain.set_value(0.38);
210        p.cutoff.set_value(2400.0);
211        p.detune.set_value(30.0);
212        p.reverb_mix.set_value(0.50);
213        p.supermass.set_value(0.25);
214        p.arp.set_value(0.20);
215        p.lfo_rate.set_value(0.25);
216        p.lfo_depth.set_value(0.35);
217        p.lfo_target.set_value(1.0);
218    });
219    mute_slot(&tracks, 4);
220    mute_slot(&tracks, 5);
221    mute_slot(&tracks, 6);
222    mute_slot(&tracks, 7);
223}
224
225// ── Vibe: Default (reset to launch layout) ─────────────────────────────
226fn apply_default(engine: &EngineHandle) {
227    engine.global.bpm.set_value(72.0);
228    engine.global.brightness.set_value(0.6);
229    engine.global.master_gain.set_value(0.7);
230    engine.global.scale_mode.set_value(0.0);
231
232    let tracks = engine.tracks.lock();
233    let root = 55.0_f32;
234
235    set_track(&tracks, 0, PresetKind::PadZimmer, root, |p| {
236        reset_neutral(p);
237    });
238    set_track(&tracks, 1, PresetKind::BassPulse, root, |p| {
239        reset_neutral(p);
240    });
241    set_track(&tracks, 2, PresetKind::Heartbeat, root, |p| {
242        reset_neutral(p);
243        p.pulse_depth.set_value(0.0);
244    });
245    set_track(&tracks, 3, PresetKind::DroneSub, root * 0.5, |p| {
246        reset_neutral(p);
247        p.gain.set_value(0.32);
248        p.reverb_mix.set_value(0.7);
249    });
250    mute_slot(&tracks, 4);
251    mute_slot(&tracks, 5);
252    mute_slot(&tracks, 6);
253    mute_slot(&tracks, 7);
254}
255
256// ── Helpers ─────────────────────────────────────────────────────────────
257
258fn set_track<F>(
259    tracks: &parking_lot::MutexGuard<'_, Vec<super::track::Track>>,
260    idx: usize,
261    kind: PresetKind,
262    freq: f32,
263    config: F,
264) where
265    F: FnOnce(&super::track::TrackParams),
266{
267    // SAFETY: we own the MutexGuard for the caller's lifetime; the audio
268    // thread reads `kind` only at rebuild_graph() which is called after
269    // this function returns via engine.rebuild_graph(). Direct pointer
270    // mutation avoids the mutability dance through a `&mut Vec<Track>`.
271    let track = &tracks[idx];
272    let track_ptr = track as *const super::track::Track as *mut super::track::Track;
273    unsafe {
274        (*track_ptr).kind = kind;
275    }
276    track.params.freq.set_value(freq);
277    track.params.mute.set_value(0.0);
278    config(&track.params);
279}
280
281fn mute_slot(
282    tracks: &parking_lot::MutexGuard<'_, Vec<super::track::Track>>,
283    idx: usize,
284) {
285    if let Some(t) = tracks.get(idx) {
286        t.params.mute.set_value(1.0);
287    }
288}
289
290/// Reset one track to the neutral defaults used at fresh launch.
291fn reset_neutral(p: &super::track::TrackParams) {
292    p.gain.set_value(0.45);
293    p.cutoff.set_value(1600.0);
294    p.resonance.set_value(0.30);
295    p.detune.set_value(7.0);
296    p.sweep_k.set_value(1.2);
297    p.sweep_center.set_value(1.5);
298    p.reverb_mix.set_value(0.6);
299    p.supermass.set_value(0.0);
300    p.pulse_depth.set_value(0.0);
301    p.character.set_value(0.5);
302    p.arp.set_value(0.0);
303    p.lfo_rate.set_value(0.5);
304    p.lfo_depth.set_value(0.0);
305    p.lfo_target.set_value(1.0);
306    p.pattern_hits.set_value(4.0);
307    p.pattern_rotation.set_value(0.0);
308}