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    /// Arpeggiator depth [0..1]. 0 → pitch stays on `freq`.
46    /// Above 0, every 2 beats the pitch jumps to a pentatonic-scale note
47    /// (glided via follow() so it sounds like portamento, not steps).
48    pub arp: Shared,
49}
50
51impl TrackParams {
52    pub fn default_for(freq: f32) -> Self {
53        Self {
54            gain: shared(0.45),
55            cutoff: shared(1600.0),
56            resonance: shared(0.30),
57            detune: shared(7.0),
58            sweep_k: shared(1.2),
59            sweep_center: shared(1.5),
60            reverb_mix: shared(0.6),
61            supermass: shared(0.0),
62            pulse_depth: shared(0.0),
63            mute: shared(0.0),
64            freq: shared(freq),
65            life_mod: shared(1.0),
66            pattern_bits: Arc::new(AtomicU32::new(rhythm::euclidean_bits(4, 0))),
67            pattern_hits: shared(4.0),
68            pattern_rotation: shared(0.0),
69            lfo_rate: shared(0.5),
70            lfo_depth: shared(0.0),
71            lfo_target: shared(1.0), // CUT by default (only audible when depth > 0)
72            character: shared(0.5),  // neutral — reproduces the hand-tuned formula
73            arp: shared(0.0),        // static pitch by default
74        }
75    }
76
77    pub fn dormant(freq: f32) -> Self {
78        let p = Self::default_for(freq);
79        p.mute.set_value(1.0);
80        p.gain.set_value(0.3);
81        p
82    }
83
84    /// TUI-facing snapshot — narrowed to f32 where only display
85    /// precision matters. Audio still runs on f64 internally.
86    pub fn snapshot(&self) -> TrackSnapshot {
87        TrackSnapshot {
88            gain: self.gain.value(),
89            cutoff: self.cutoff.value(),
90            resonance: self.resonance.value(),
91            detune: self.detune.value(),
92            sweep_k: self.sweep_k.value(),
93            sweep_center: self.sweep_center.value(),
94            reverb_mix: self.reverb_mix.value(),
95            supermass: self.supermass.value(),
96            pulse_depth: self.pulse_depth.value(),
97            freq: self.freq.value(),
98            life_mod: self.life_mod.value(),
99            pattern_bits: self.pattern_bits.load(std::sync::atomic::Ordering::Relaxed),
100            pattern_hits: self.pattern_hits.value(),
101            pattern_rotation: self.pattern_rotation.value(),
102            lfo_rate: self.lfo_rate.value(),
103            lfo_depth: self.lfo_depth.value(),
104            lfo_target: self.lfo_target.value(),
105            character: self.character.value(),
106            arp: self.arp.value(),
107            muted: self.mute.value() > 0.5,
108        }
109    }
110}
111
112pub struct TrackSnapshot {
113    pub gain: f32,
114    pub cutoff: f32,
115    pub resonance: f32,
116    pub detune: f32,
117    pub sweep_k: f32,
118    pub sweep_center: f32,
119    pub reverb_mix: f32,
120    pub supermass: f32,
121    pub pulse_depth: f32,
122    pub freq: f32,
123    pub life_mod: f32,
124    pub pattern_bits: u32,
125    pub pattern_hits: f32,
126    pub pattern_rotation: f32,
127    pub lfo_rate: f32,
128    pub lfo_depth: f32,
129    pub lfo_target: f32,
130    pub character: f32,
131    pub arp: f32,
132    pub muted: bool,
133}
134
135pub struct Track {
136    pub id: usize,
137    pub name: String,
138    pub kind: PresetKind,
139    pub params: TrackParams,
140}
141
142impl Track {
143    pub fn new(id: usize, name: impl Into<String>, kind: PresetKind, freq: f32) -> Self {
144        Self {
145            id,
146            name: name.into(),
147            kind,
148            params: TrackParams::default_for(freq),
149        }
150    }
151
152    pub fn dormant(id: usize, name: impl Into<String>, kind: PresetKind, freq: f32) -> Self {
153        Self {
154            id,
155            name: name.into(),
156            kind,
157            params: TrackParams::dormant(freq),
158        }
159    }
160}