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}
41
42impl TrackParams {
43    pub fn default_for(freq: f32) -> Self {
44        Self {
45            gain: shared(0.45),
46            cutoff: shared(1600.0),
47            resonance: shared(0.30),
48            detune: shared(7.0),
49            sweep_k: shared(1.2),
50            sweep_center: shared(1.5),
51            reverb_mix: shared(0.6),
52            supermass: shared(0.0),
53            pulse_depth: shared(0.0),
54            mute: shared(0.0),
55            freq: shared(freq),
56            life_mod: shared(1.0),
57            pattern_bits: Arc::new(AtomicU32::new(rhythm::euclidean_bits(4, 0))),
58            pattern_hits: shared(4.0),
59            pattern_rotation: shared(0.0),
60            lfo_rate: shared(0.5),
61            lfo_depth: shared(0.0),
62            lfo_target: shared(1.0), // CUT by default (only audible when depth > 0)
63        }
64    }
65
66    pub fn dormant(freq: f32) -> Self {
67        let p = Self::default_for(freq);
68        p.mute.set_value(1.0);
69        p.gain.set_value(0.3);
70        p
71    }
72
73    /// TUI-facing snapshot — narrowed to f32 where only display
74    /// precision matters. Audio still runs on f64 internally.
75    pub fn snapshot(&self) -> TrackSnapshot {
76        TrackSnapshot {
77            gain: self.gain.value(),
78            cutoff: self.cutoff.value(),
79            resonance: self.resonance.value(),
80            detune: self.detune.value(),
81            sweep_k: self.sweep_k.value(),
82            sweep_center: self.sweep_center.value(),
83            reverb_mix: self.reverb_mix.value(),
84            supermass: self.supermass.value(),
85            pulse_depth: self.pulse_depth.value(),
86            freq: self.freq.value(),
87            life_mod: self.life_mod.value(),
88            pattern_bits: self.pattern_bits.load(std::sync::atomic::Ordering::Relaxed),
89            pattern_hits: self.pattern_hits.value(),
90            pattern_rotation: self.pattern_rotation.value(),
91            lfo_rate: self.lfo_rate.value(),
92            lfo_depth: self.lfo_depth.value(),
93            lfo_target: self.lfo_target.value(),
94            muted: self.mute.value() > 0.5,
95        }
96    }
97}
98
99pub struct TrackSnapshot {
100    pub gain: f32,
101    pub cutoff: f32,
102    pub resonance: f32,
103    pub detune: f32,
104    pub sweep_k: f32,
105    pub sweep_center: f32,
106    pub reverb_mix: f32,
107    pub supermass: f32,
108    pub pulse_depth: f32,
109    pub freq: f32,
110    pub life_mod: f32,
111    pub pattern_bits: u32,
112    pub pattern_hits: f32,
113    pub pattern_rotation: f32,
114    pub lfo_rate: f32,
115    pub lfo_depth: f32,
116    pub lfo_target: f32,
117    pub muted: bool,
118}
119
120pub struct Track {
121    pub id: usize,
122    pub name: String,
123    pub kind: PresetKind,
124    pub params: TrackParams,
125}
126
127impl Track {
128    pub fn new(id: usize, name: impl Into<String>, kind: PresetKind, freq: f32) -> Self {
129        Self {
130            id,
131            name: name.into(),
132            kind,
133            params: TrackParams::default_for(freq),
134        }
135    }
136
137    pub fn dormant(id: usize, name: impl Into<String>, kind: PresetKind, freq: f32) -> Self {
138        Self {
139            id,
140            name: name.into(),
141            kind,
142            params: TrackParams::dormant(freq),
143        }
144    }
145}