Skip to main content

phosphor_core/
project.rs

1//! Shared domain models for the audio engine and UI.
2//!
3//! These types live in phosphor-core so both the audio thread (mixer)
4//! and the UI thread (TUI/GUI) can reference the same data without
5//! duplicating definitions. Audio-thread-safe state uses atomics.
6
7use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
8
9use crate::engine::VuLevels;
10
11/// Identifies a track by index.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct TrackId(pub usize);
14
15/// What kind of track this is — determines routing and capabilities.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TrackKind {
18    /// Has a synth/plugin, receives MIDI.
19    Instrument,
20    /// Plays back audio clips.
21    Audio,
22    /// Send bus A.
23    SendA,
24    /// Send bus B.
25    SendB,
26    /// Master output bus.
27    Master,
28}
29
30/// Audio-thread-safe track configuration.
31///
32/// Written by the UI thread, read by the audio thread — all fields
33/// are atomic so no locks are needed.
34#[derive(Debug)]
35pub struct TrackConfig {
36    pub muted: AtomicBool,
37    pub soloed: AtomicBool,
38    pub armed: AtomicBool,
39    /// Whether this track is currently selected for MIDI input.
40    /// Only one track should be selected at a time.
41    pub midi_active: AtomicBool,
42    /// Volume stored as f32 bits in an AtomicU32.
43    pub volume: AtomicU32,
44}
45
46impl TrackConfig {
47    pub fn new() -> Self {
48        Self {
49            muted: AtomicBool::new(false),
50            soloed: AtomicBool::new(false),
51            armed: AtomicBool::new(false),
52            midi_active: AtomicBool::new(false),
53            volume: AtomicU32::new(0.75f32.to_bits()),
54        }
55    }
56
57    pub fn get_volume(&self) -> f32 {
58        f32::from_bits(self.volume.load(Ordering::Relaxed))
59    }
60
61    pub fn set_volume(&self, v: f32) {
62        self.volume.store(v.to_bits(), Ordering::Relaxed);
63    }
64
65    pub fn is_muted(&self) -> bool {
66        self.muted.load(Ordering::Relaxed)
67    }
68
69    pub fn is_soloed(&self) -> bool {
70        self.soloed.load(Ordering::Relaxed)
71    }
72
73    pub fn is_armed(&self) -> bool {
74        self.armed.load(Ordering::Relaxed)
75    }
76
77    pub fn is_midi_active(&self) -> bool {
78        self.midi_active.load(Ordering::Relaxed)
79    }
80}
81
82impl Default for TrackConfig {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88/// Shared handle for a track — the UI holds an `Arc<TrackHandle>` to
89/// read VU levels and write mute/solo/arm/volume.
90#[derive(Debug)]
91pub struct TrackHandle {
92    pub id: usize,
93    pub kind: TrackKind,
94    pub config: TrackConfig,
95    pub vu: VuLevels,
96}
97
98impl TrackHandle {
99    pub fn new(id: usize, kind: TrackKind) -> Self {
100        Self {
101            id,
102            kind,
103            config: TrackConfig::new(),
104            vu: VuLevels::new(),
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn track_config_defaults() {
115        let cfg = TrackConfig::new();
116        assert!(!cfg.is_muted());
117        assert!(!cfg.is_soloed());
118        assert!(!cfg.is_armed());
119        assert!((cfg.get_volume() - 0.75).abs() < 0.001);
120    }
121
122    #[test]
123    fn track_config_volume_round_trip() {
124        let cfg = TrackConfig::new();
125        cfg.set_volume(0.42);
126        assert!((cfg.get_volume() - 0.42).abs() < 0.001);
127    }
128
129    #[test]
130    fn track_config_atomics() {
131        let cfg = TrackConfig::new();
132        cfg.muted.store(true, Ordering::Relaxed);
133        assert!(cfg.is_muted());
134        cfg.soloed.store(true, Ordering::Relaxed);
135        assert!(cfg.is_soloed());
136        cfg.armed.store(true, Ordering::Relaxed);
137        assert!(cfg.is_armed());
138    }
139
140    #[test]
141    fn track_handle_new() {
142        let h = TrackHandle::new(0, TrackKind::Instrument);
143        assert_eq!(h.id, 0);
144        assert_eq!(h.kind, TrackKind::Instrument);
145        assert!(!h.config.is_muted());
146    }
147
148    #[test]
149    fn track_kind_variants() {
150        assert_ne!(TrackKind::Instrument, TrackKind::Audio);
151        assert_ne!(TrackKind::SendA, TrackKind::SendB);
152        assert_ne!(TrackKind::Master, TrackKind::Audio);
153    }
154}