Skip to main content

rust_synth/audio/
track.rs

1//! Track = one voice in the mix.
2
3use fundsp::hacker::*;
4use std::sync::atomic::AtomicU32;
5use std::sync::Arc;
6
7use super::preset::PresetKind;
8use crate::math::rhythm;
9
10#[derive(Clone)]
11pub struct TrackParams {
12    pub gain: Shared,
13    pub cutoff: Shared,
14    pub resonance: Shared,
15    pub detune: Shared,
16    pub sweep_k: Shared,
17    pub sweep_center: Shared,
18    pub reverb_mix: Shared,
19    pub supermass: Shared,
20    pub pulse_depth: Shared,
21    pub mute: Shared,
22    pub freq: Shared,
23    pub life_mod: Shared,
24    /// 16-step Euclidean rhythm pattern as a bitmask. Drum presets
25    /// (currently only Heartbeat) read this every sample to decide
26    /// whether to fire on the current step. Recomputed in the TUI loop
27    /// from `pattern_hits` + `pattern_rotation`.
28    pub pattern_bits: Arc<AtomicU32>,
29    /// Hits per 16 steps, [0.0, 16.0]. Fed into euclidean_bits().
30    pub pattern_hits: Shared,
31    /// Pattern rotation, [0.0, 15.0].
32    pub pattern_rotation: Shared,
33    /// Per-track LFO rate in Hz (0.01..20).
34    pub lfo_rate: Shared,
35    /// LFO depth [0..1]. Depth 0 = LFO off regardless of target.
36    pub lfo_depth: Shared,
37    /// LFO target index (quantised):
38    ///   0 OFF · 1 CUT · 2 GAIN · 3 FREQ · 4 REV
39    pub lfo_target: Shared,
40    /// Per-preset "character" knob in [0.0, 1.0]. Each preset interprets
41    /// this differently — Pad stretches partials, Bell shifts FM ratio,
42    /// Heartbeat scales the pitch drop, etc. Default 0.5 reproduces the
43    /// original hand-tuned formula; 0 and 1 are the two extremes.
44    pub character: Shared,
45}
46
47impl TrackParams {
48    pub fn default_for(freq: f32) -> Self {
49        Self {
50            gain: shared(0.45),
51            cutoff: shared(1600.0),
52            resonance: shared(0.30),
53            detune: shared(7.0),
54            sweep_k: shared(1.2),
55            sweep_center: shared(1.5),
56            reverb_mix: shared(0.6),
57            supermass: shared(0.0),
58            pulse_depth: shared(0.0),
59            mute: shared(0.0),
60            freq: shared(freq),
61            life_mod: shared(1.0),
62            pattern_bits: Arc::new(AtomicU32::new(rhythm::euclidean_bits(4, 0))),
63            pattern_hits: shared(4.0),
64            pattern_rotation: shared(0.0),
65            lfo_rate: shared(0.5),
66            lfo_depth: shared(0.0),
67            lfo_target: shared(1.0), // CUT by default (only audible when depth > 0)
68            character: shared(0.5),  // neutral — reproduces the hand-tuned formula
69        }
70    }
71
72    pub fn dormant(freq: f32) -> Self {
73        let p = Self::default_for(freq);
74        p.mute.set_value(1.0);
75        p.gain.set_value(0.3);
76        p
77    }
78
79    /// TUI-facing snapshot — narrowed to f32 where only display
80    /// precision matters. Audio still runs on f64 internally.
81    pub fn snapshot(&self) -> TrackSnapshot {
82        TrackSnapshot {
83            gain: self.gain.value(),
84            cutoff: self.cutoff.value(),
85            resonance: self.resonance.value(),
86            detune: self.detune.value(),
87            sweep_k: self.sweep_k.value(),
88            sweep_center: self.sweep_center.value(),
89            reverb_mix: self.reverb_mix.value(),
90            supermass: self.supermass.value(),
91            pulse_depth: self.pulse_depth.value(),
92            freq: self.freq.value(),
93            life_mod: self.life_mod.value(),
94            pattern_bits: self.pattern_bits.load(std::sync::atomic::Ordering::Relaxed),
95            pattern_hits: self.pattern_hits.value(),
96            pattern_rotation: self.pattern_rotation.value(),
97            lfo_rate: self.lfo_rate.value(),
98            lfo_depth: self.lfo_depth.value(),
99            lfo_target: self.lfo_target.value(),
100            character: self.character.value(),
101            muted: self.mute.value() > 0.5,
102        }
103    }
104}
105
106pub struct TrackSnapshot {
107    pub gain: f32,
108    pub cutoff: f32,
109    pub resonance: f32,
110    pub detune: f32,
111    pub sweep_k: f32,
112    pub sweep_center: f32,
113    pub reverb_mix: f32,
114    pub supermass: f32,
115    pub pulse_depth: f32,
116    pub freq: f32,
117    pub life_mod: f32,
118    pub pattern_bits: u32,
119    pub pattern_hits: f32,
120    pub pattern_rotation: f32,
121    pub lfo_rate: f32,
122    pub lfo_depth: f32,
123    pub lfo_target: f32,
124    pub character: f32,
125    pub muted: bool,
126}
127
128pub struct Track {
129    pub id: usize,
130    pub name: String,
131    pub kind: PresetKind,
132    pub params: TrackParams,
133}
134
135impl Track {
136    pub fn new(id: usize, name: impl Into<String>, kind: PresetKind, freq: f32) -> Self {
137        Self {
138            id,
139            name: name.into(),
140            kind,
141            params: TrackParams::default_for(freq),
142        }
143    }
144
145    pub fn dormant(id: usize, name: impl Into<String>, kind: PresetKind, freq: f32) -> Self {
146        Self {
147            id,
148            name: name.into(),
149            kind,
150            params: TrackParams::dormant(freq),
151        }
152    }
153}