Skip to main content

phosphor_core/
engine.rs

1//! The audio engine: owns the transport, synth, MIDI routing,
2//! and drives the audio callback.
3
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
6
7use crossbeam_channel::Sender;
8use phosphor_midi::message::{MidiMessage, MidiMessageType};
9use phosphor_midi::ring::MidiRingReceiver;
10use phosphor_plugin::{MidiEvent, Plugin};
11
12use crate::mixer::{Mixer, MixerCommand, mixer_command_channel};
13use crate::project::TrackHandle;
14use crate::transport::Transport;
15use crate::EngineConfig;
16
17/// Shared VU meter levels — written by audio thread, read by UI thread.
18/// Stored as f32 bits in AtomicU32 (lock-free, no allocation).
19#[derive(Debug)]
20pub struct VuLevels {
21    /// Peak level (0.0..1.0) — decays over time.
22    pub peak_l: AtomicU32,
23    pub peak_r: AtomicU32,
24}
25
26impl Default for VuLevels {
27    fn default() -> Self { Self::new() }
28}
29
30impl VuLevels {
31    pub fn new() -> Self {
32        Self {
33            peak_l: AtomicU32::new(0),
34            peak_r: AtomicU32::new(0),
35        }
36    }
37
38    pub fn set(&self, l: f32, r: f32) {
39        self.peak_l.store(l.to_bits(), Ordering::Relaxed);
40        self.peak_r.store(r.to_bits(), Ordering::Relaxed);
41    }
42
43    pub fn get(&self) -> (f32, f32) {
44        (
45            f32::from_bits(self.peak_l.load(Ordering::Relaxed)),
46            f32::from_bits(self.peak_r.load(Ordering::Relaxed)),
47        )
48    }
49}
50
51/// The core audio engine. Lives on the audio thread.
52///
53/// The engine is split into two parts:
54/// - `EngineShared`: read from any thread (transport, config)
55/// - `EngineAudio`: owned by the audio callback (synth, MIDI receiver)
56pub struct EngineShared {
57    pub config: EngineConfig,
58    pub transport: Arc<Transport>,
59    pub panic_flag: Arc<AtomicBool>,
60    /// Real-time VU meter levels from the audio thread (master bus).
61    pub vu_levels: Arc<VuLevels>,
62    /// Send commands to the mixer (add/remove tracks, set instruments).
63    pub mixer_command_tx: Sender<MixerCommand>,
64    /// Per-track handles for UI to read VU / write mute/solo/arm/volume.
65    pub track_handles: Vec<Arc<TrackHandle>>,
66}
67
68impl EngineShared {
69    pub fn new(config: EngineConfig) -> Self {
70        let (tx, _rx) = mixer_command_channel();
71        Self {
72            config,
73            transport: Arc::new(Transport::new(120.0)),
74            panic_flag: Arc::new(AtomicBool::new(false)),
75            vu_levels: Arc::new(VuLevels::new()),
76            mixer_command_tx: tx,
77            track_handles: Vec::new(),
78        }
79    }
80
81    /// Create shared state with a specific command sender (for wiring to a mixer).
82    pub fn with_command_tx(config: EngineConfig, tx: Sender<MixerCommand>) -> Self {
83        Self {
84            config,
85            transport: Arc::new(Transport::new(120.0)),
86            panic_flag: Arc::new(AtomicBool::new(false)),
87            vu_levels: Arc::new(VuLevels::new()),
88            mixer_command_tx: tx,
89            track_handles: Vec::new(),
90        }
91    }
92
93    /// Trigger a panic — kills all sound on next audio callback.
94    pub fn panic(&self) {
95        self.panic_flag.store(true, Ordering::Relaxed);
96    }
97}
98
99/// Audio-thread-only state. NOT Send — lives inside the audio callback closure.
100pub struct EngineAudio {
101    channels: u16,
102    sample_rate: u32,
103    synth: Box<dyn Plugin>,
104    midi_rx: Option<MidiRingReceiver>,
105    panic_flag: Arc<AtomicBool>,
106    vu_levels: Arc<VuLevels>,
107    /// Scratch buffer for MIDI events (reused to avoid allocation).
108    midi_scratch: Vec<MidiMessage>,
109    /// Plugin-format MIDI events for the current buffer.
110    plugin_events: Vec<MidiEvent>,
111    /// Scratch audio buffers for plugin output.
112    plugin_buf_l: Vec<f32>,
113    plugin_buf_r: Vec<f32>,
114    /// Per-track mixer. Processes all tracks and mixes to master.
115    mixer: Option<Mixer>,
116}
117
118impl EngineAudio {
119    pub fn new(
120        config: &EngineConfig,
121        synth: Box<dyn Plugin>,
122        midi_rx: Option<MidiRingReceiver>,
123        panic_flag: Arc<AtomicBool>,
124        vu_levels: Arc<VuLevels>,
125    ) -> Self {
126        let buf_size = config.buffer_size as usize;
127        let mut s = Self {
128            channels: 2,
129            sample_rate: config.sample_rate,
130            synth,
131            midi_rx,
132            panic_flag,
133            vu_levels,
134            midi_scratch: Vec::with_capacity(256),
135            plugin_events: Vec::with_capacity(256),
136            plugin_buf_l: vec![0.0; buf_size],
137            plugin_buf_r: vec![0.0; buf_size],
138            mixer: None,
139        };
140        s.synth.init(config.sample_rate as f64, buf_size);
141        s
142    }
143
144    /// Create an EngineAudio with a mixer instead of a single synth.
145    pub fn with_mixer(
146        config: &EngineConfig,
147        mixer: Mixer,
148        midi_rx: Option<MidiRingReceiver>,
149        panic_flag: Arc<AtomicBool>,
150        vu_levels: Arc<VuLevels>,
151    ) -> Self {
152        let buf_size = config.buffer_size as usize;
153        // We still need a dummy synth for the legacy code path;
154        // when the mixer is present, the synth is not used.
155        use phosphor_plugin::{ParameterInfo, PluginCategory, PluginInfo};
156        struct NullPlugin;
157        impl Plugin for NullPlugin {
158            fn info(&self) -> PluginInfo {
159                PluginInfo {
160                    name: "null".into(),
161                    version: "0".into(),
162                    author: "".into(),
163                    category: PluginCategory::Utility,
164                }
165            }
166            fn init(&mut self, _sr: f64, _bs: usize) {}
167            fn process(
168                &mut self,
169                _i: &[&[f32]],
170                _o: &mut [&mut [f32]],
171                _m: &[MidiEvent],
172            ) {
173            }
174            fn parameter_count(&self) -> usize { 0 }
175            fn parameter_info(&self, _: usize) -> Option<ParameterInfo> { None }
176            fn get_parameter(&self, _: usize) -> f32 { 0.0 }
177            fn set_parameter(&mut self, _: usize, _: f32) {}
178            fn reset(&mut self) {}
179        }
180
181        Self {
182            channels: 2,
183            sample_rate: config.sample_rate,
184            synth: Box::new(NullPlugin),
185            midi_rx,
186            panic_flag,
187            vu_levels,
188            midi_scratch: Vec::with_capacity(256),
189            plugin_events: Vec::with_capacity(256),
190            plugin_buf_l: vec![0.0; buf_size],
191            plugin_buf_r: vec![0.0; buf_size],
192            mixer: Some(mixer),
193        }
194    }
195
196    /// Drain and discard any pending MIDI events. Call before starting
197    /// the audio stream to flush controller init bursts.
198    pub fn flush_midi(&mut self) {
199        if let Some(rx) = &mut self.midi_rx {
200            self.midi_scratch.clear();
201            rx.drain_into(&mut self.midi_scratch);
202            if !self.midi_scratch.is_empty() {
203                tracing::info!("Flushed {} stale MIDI events", self.midi_scratch.len());
204            }
205            self.midi_scratch.clear();
206        }
207    }
208
209    /// Process one interleaved audio buffer. Called from the audio thread.
210    ///
211    /// `output` is interleaved: [L0, R0, L1, R1, ...]
212    pub fn process(&mut self, output: &mut [f32], transport: &Transport) {
213        // Check panic flag — kill all sound immediately
214        if self.panic_flag.swap(false, Ordering::Relaxed) {
215            self.synth.reset();
216            if let Some(ref mut mixer) = self.mixer {
217                mixer.reset_all();
218            }
219            output.fill(0.0);
220            return;
221        }
222
223        let num_frames = output.len() / self.channels as usize;
224
225        // Drain MIDI from ring buffer
226        self.midi_scratch.clear();
227        if let Some(rx) = &mut self.midi_rx {
228            rx.drain_into(&mut self.midi_scratch);
229        }
230
231        // If we have a mixer, delegate to it
232        if let Some(ref mut mixer) = self.mixer {
233            mixer.process(output, &self.midi_scratch, transport);
234            // Advance transport after processing (so playback reads the pre-advance position)
235            transport.advance(num_frames as u32, self.sample_rate);
236            return;
237        }
238
239        // Legacy single-synth path (for tests and backward compat)
240        // Ensure scratch buffers are big enough
241        if self.plugin_buf_l.len() < num_frames {
242            self.plugin_buf_l.resize(num_frames, 0.0);
243            self.plugin_buf_r.resize(num_frames, 0.0);
244        }
245
246        // Convert MIDI to plugin events
247        self.plugin_events.clear();
248        for msg in &self.midi_scratch {
249            if let Some(ev) = midi_to_plugin_event(msg) {
250                self.plugin_events.push(ev);
251            }
252        }
253
254        // Clear plugin output buffers
255        self.plugin_buf_l[..num_frames].fill(0.0);
256        self.plugin_buf_r[..num_frames].fill(0.0);
257
258        // Process synth
259        {
260            let mut outputs: [&mut [f32]; 2] = [
261                &mut self.plugin_buf_l[..num_frames],
262                &mut self.plugin_buf_r[..num_frames],
263            ];
264            self.synth.process(&[], &mut outputs, &self.plugin_events);
265        }
266
267        // Interleave into output and compute peak levels
268        let mut peak_l = 0.0f32;
269        let mut peak_r = 0.0f32;
270        for i in 0..num_frames {
271            let idx = i * self.channels as usize;
272            let l = self.plugin_buf_l[i];
273            output[idx] = l;
274            peak_l = peak_l.max(l.abs());
275            if self.channels >= 2 {
276                let r = self.plugin_buf_r[i];
277                output[idx + 1] = r;
278                peak_r = peak_r.max(r.abs());
279            }
280        }
281
282        // Smooth VU: fast attack, slow decay
283        let (old_l, old_r) = self.vu_levels.get();
284        let decay = 0.85f32; // ~60ms decay at 44.1k/64
285        let new_l = if peak_l > old_l { peak_l } else { old_l * decay };
286        let new_r = if peak_r > old_r { peak_r } else { old_r * decay };
287        self.vu_levels.set(new_l, new_r);
288
289        // Advance transport
290        transport.advance(num_frames as u32, self.sample_rate);
291    }
292}
293
294/// Convert a phosphor-midi MidiMessage to a phosphor-plugin MidiEvent.
295fn midi_to_plugin_event(msg: &MidiMessage) -> Option<MidiEvent> {
296    // For now, all events get sample_offset 0 (within the current buffer).
297    // Future: use msg.timestamp for sample-accurate positioning.
298    match msg.message_type {
299        MidiMessageType::NoteOn { .. }
300        | MidiMessageType::NoteOff { .. }
301        | MidiMessageType::ControlChange { .. }
302        | MidiMessageType::PitchBend { .. } => Some(MidiEvent {
303            sample_offset: 0,
304            status: msg.raw[0],
305            data1: msg.raw[1],
306            data2: msg.raw[2],
307        }),
308        _ => None,
309    }
310}
311
312// ── Legacy Engine (for tests and simple use) ──
313
314/// Simple combined engine for tests and the TUI.
315pub struct Engine {
316    pub shared: EngineShared,
317}
318
319impl Engine {
320    pub fn new(config: EngineConfig) -> Self {
321        let (tx, _rx) = mixer_command_channel();
322        Self {
323            shared: EngineShared::with_command_tx(config, tx),
324        }
325    }
326
327    /// Create an engine wired to a mixer command channel.
328    pub fn with_command_tx(config: EngineConfig, tx: Sender<MixerCommand>) -> Self {
329        Self {
330            shared: EngineShared::with_command_tx(config, tx),
331        }
332    }
333
334    /// Access the transport.
335    pub fn transport(&self) -> &Transport {
336        &self.shared.transport
337    }
338}
339
340// Re-export for backward compat with TUI code
341impl std::ops::Deref for Engine {
342    type Target = EngineShared;
343    fn deref(&self) -> &Self::Target {
344        &self.shared
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use phosphor_dsp::synth::PhosphorSynth;
352    use phosphor_midi::ring::midi_ring_buffer;
353    use phosphor_midi::message::MidiMessage;
354
355    fn test_engine(midi_rx: Option<MidiRingReceiver>) -> (EngineAudio, Arc<Transport>) {
356        let config = EngineConfig { buffer_size: 64, sample_rate: 44100 };
357        let transport = Arc::new(Transport::new(120.0));
358        let panic_flag = Arc::new(AtomicBool::new(false));
359        let vu_levels = Arc::new(VuLevels::new());
360        let synth = Box::new(PhosphorSynth::new());
361        let engine = EngineAudio::new(&config, synth, midi_rx, panic_flag, vu_levels);
362        (engine, transport)
363    }
364
365    #[test]
366    fn engine_produces_silence_with_no_midi() {
367        let (mut engine, transport) = test_engine(None);
368        let mut output = vec![0.0f32; 128]; // 64 frames stereo
369        engine.process(&mut output, &transport);
370        assert!(output.iter().all(|&s| s == 0.0));
371    }
372
373    #[test]
374    fn engine_produces_sound_from_midi() {
375        let (mut tx, rx) = midi_ring_buffer();
376        let (mut engine, transport) = test_engine(Some(rx));
377
378        // Send note on
379        let msg = MidiMessage::from_bytes(&[0x90, 60, 100], 0).unwrap();
380        tx.push(msg);
381
382        let mut output = vec![0.0f32; 512]; // 256 frames stereo
383        engine.process(&mut output, &transport);
384
385        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
386        assert!(peak > 0.01, "Should produce sound from MIDI, peak={peak}");
387    }
388
389    #[test]
390    fn engine_output_is_stereo() {
391        let (mut tx, rx) = midi_ring_buffer();
392        let (mut engine, transport) = test_engine(Some(rx));
393
394        let msg = MidiMessage::from_bytes(&[0x90, 60, 100], 0).unwrap();
395        tx.push(msg);
396
397        let mut output = vec![0.0f32; 128];
398        engine.process(&mut output, &transport);
399
400        // Left and right channels should be identical (mono synth)
401        for i in 0..64 {
402            assert_eq!(output[i * 2], output[i * 2 + 1], "L/R mismatch at frame {i}");
403        }
404    }
405
406    #[test]
407    fn engine_output_always_finite() {
408        let (mut tx, rx) = midi_ring_buffer();
409        let (mut engine, transport) = test_engine(Some(rx));
410
411        let msg = MidiMessage::from_bytes(&[0x90, 60, 127], 0).unwrap();
412        tx.push(msg);
413
414        for _ in 0..1000 {
415            let mut output = vec![0.0f32; 128];
416            engine.process(&mut output, &transport);
417            assert!(output.iter().all(|s| s.is_finite()));
418        }
419    }
420
421    #[test]
422    fn engine_note_off_leads_to_silence() {
423        let (mut tx, rx) = midi_ring_buffer();
424        let (mut engine, transport) = test_engine(Some(rx));
425
426        // Note on
427        tx.push(MidiMessage::from_bytes(&[0x90, 60, 100], 0).unwrap());
428        let mut output = vec![0.0f32; 128];
429        engine.process(&mut output, &transport);
430
431        // Note off
432        tx.push(MidiMessage::from_bytes(&[0x80, 60, 0], 0).unwrap());
433
434        // Process enough for release to finish (exponential decay needs more buffers)
435        for _ in 0..500 {
436            output.fill(0.0);
437            engine.process(&mut output, &transport);
438        }
439
440        // Should be silent now
441        let peak = output.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
442        assert!(peak < 0.001, "Should be silent after release, peak={peak}");
443    }
444
445    #[test]
446    fn engine_advances_transport() {
447        let (mut engine, transport) = test_engine(None);
448        transport.play();
449        let mut output = vec![0.0f32; 128];
450        engine.process(&mut output, &transport);
451        assert!(transport.position_ticks() > 0);
452    }
453}