1use anyhow::{Context, Result};
9use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
10use cpal::{Device, Stream, StreamConfig};
11use fundsp::hacker::*;
12use parking_lot::Mutex;
13use std::collections::VecDeque;
14use std::sync::Arc;
15
16use super::preset::{master_bus, GlobalParams, Preset, PresetKind};
17use super::track::Track;
18use crate::math::harmony::golden_freq;
19use crate::recording::RecorderState;
20
21pub const MAX_TRACKS: usize = 8;
23
24pub const SCOPE_CAPACITY: usize = 512;
26pub const SCOPE_DECIMATION: usize = 32;
28
29pub type ScopeBuffer = Arc<Mutex<VecDeque<(f32, f32)>>>;
30pub type SharedGraph = Arc<Mutex<Net>>;
31
32pub struct EngineHandle {
34 pub tracks: Arc<Mutex<Vec<Track>>>,
35 pub global: GlobalParams,
36 pub peak_l: Shared,
37 pub peak_r: Shared,
38 pub sample_rate: f32,
39 pub scope: ScopeBuffer,
40 pub phase_clock: Shared,
41 pub recorder: Arc<RecorderState>,
42 graph: SharedGraph,
47 _stream: Stream,
48}
49
50pub struct AudioEngine;
51
52impl AudioEngine {
53 pub fn start(initial_tracks: Vec<Track>) -> Result<EngineHandle> {
54 assert!(
55 initial_tracks.len() == MAX_TRACKS,
56 "expected exactly {MAX_TRACKS} pre-allocated tracks, got {}",
57 initial_tracks.len()
58 );
59
60 let host = cpal::default_host();
61 let device = host
62 .default_output_device()
63 .context("no default output audio device")?;
64 let config: StreamConfig = device.default_output_config()?.into();
65 let sample_rate = config.sample_rate.0 as f32;
66 let channels = config.channels as usize;
67
68 let global = GlobalParams::default();
69 let peak_l = shared(0.0);
70 let peak_r = shared(0.0);
71 let phase_clock = shared(0.0);
72 let scope: ScopeBuffer = Arc::new(Mutex::new(VecDeque::with_capacity(SCOPE_CAPACITY)));
73 let tracks = Arc::new(Mutex::new(initial_tracks));
74 let recorder = RecorderState::new(sample_rate as u32);
75
76 let mut graph = build_master(&tracks.lock(), &global);
77 graph.set_sample_rate(sample_rate as f64);
78 let graph: SharedGraph = Arc::new(Mutex::new(graph));
79
80 let stream = start_stream(
81 device,
82 config,
83 channels,
84 sample_rate,
85 graph.clone(),
86 global.master_gain.clone(),
87 peak_l.clone(),
88 peak_r.clone(),
89 scope.clone(),
90 phase_clock.clone(),
91 recorder.clone(),
92 )?;
93
94 Ok(EngineHandle {
95 tracks,
96 global,
97 peak_l,
98 peak_r,
99 sample_rate,
100 scope,
101 phase_clock,
102 recorder,
103 graph,
104 _stream: stream,
105 })
106 }
107}
108
109impl EngineHandle {
110 pub fn rebuild_graph(&self) {
118 let tracks = self.tracks.lock();
119 let mut new_graph = build_master(&tracks, &self.global);
120 drop(tracks);
121 new_graph.set_sample_rate(self.sample_rate as f64);
122 *self.graph.lock() = new_graph;
123 }
124}
125
126fn build_master(tracks: &[Track], g: &GlobalParams) -> Net {
127 let mut summed: Option<Net> = None;
128 for t in tracks {
129 let node = Preset::build(t.kind, &t.params, g);
130 summed = Some(match summed {
131 Some(acc) => acc + node,
132 None => node,
133 });
134 }
135 let summed = summed.unwrap_or_else(|| Net::wrap(Box::new(zero() | zero())));
136
137 summed >> master_bus(g.brightness.clone())
141}
142
143#[allow(clippy::too_many_arguments)]
144fn start_stream(
145 device: Device,
146 config: StreamConfig,
147 channels: usize,
148 sample_rate: f32,
149 graph: SharedGraph,
150 master: Shared,
151 peak_l: Shared,
152 peak_r: Shared,
153 scope: ScopeBuffer,
154 phase_clock: Shared,
155 recorder: Arc<RecorderState>,
156) -> Result<Stream> {
157 let err_fn = |err| tracing::error!("audio stream error: {err}");
158 let mut env_l = 0.0f32;
159 let mut env_r = 0.0f32;
160 let fall = 0.9995f32;
161 let dt: f64 = 1.0 / sample_rate as f64;
162 let mut t: f64 = 0.0;
163 let mut decim = 0usize;
164
165 let stream = device.build_output_stream(
166 &config,
167 move |data: &mut [f32], _| {
168 let m = master.value();
169 let mut pending: [(f32, f32); 32] = [(0.0, 0.0); 32];
170 let mut pending_n = 0usize;
171 let mut graph = graph.lock();
174
175 for frame in data.chunks_mut(channels) {
176 let (lo, ro) = graph.get_stereo();
177 let l = lo * m;
178 let r = ro * m;
179 env_l = (env_l * fall).max(l.abs());
180 env_r = (env_r * fall).max(r.abs());
181
182 for (ch, slot) in frame.iter_mut().enumerate() {
183 *slot = if ch & 1 == 0 { l } else { r };
184 }
185
186 recorder.push_frame(l, r);
189
190 decim = decim.wrapping_add(1);
191 if decim.is_multiple_of(SCOPE_DECIMATION) && pending_n < pending.len() {
192 pending[pending_n] = (l, r);
193 pending_n += 1;
194 }
195
196 t += dt;
197 }
198
199 if pending_n > 0 {
201 let mut scope = scope.lock();
202 for &s in &pending[..pending_n] {
203 if scope.len() == SCOPE_CAPACITY {
204 scope.pop_front();
205 }
206 scope.push_back(s);
207 }
208 }
209
210 peak_l.set_value(env_l);
211 peak_r.set_value(env_r);
212 phase_clock.set_value(t as f32);
213 },
214 err_fn,
215 None,
216 )?;
217 stream.play()?;
218 Ok(stream)
219}
220
221pub fn default_track_set() -> Vec<Track> {
223 let root = 55.0f32; let mut tracks = Vec::with_capacity(MAX_TRACKS);
225
226 tracks.push(Track::new(0, "Pad", PresetKind::PadZimmer, golden_freq(root, 0)));
228 tracks.push(Track::new(1, "Bass", PresetKind::BassPulse, golden_freq(root, 0)));
229 tracks.push(Track::new(2, "Heartbeat", PresetKind::Heartbeat, golden_freq(root, 0)));
230 tracks.push(Track::new(3, "Drone", PresetKind::DroneSub, golden_freq(root, -1)));
231 tracks[3].params.gain.set_value(0.32);
232 tracks[3].params.reverb_mix.set_value(0.7);
233
234 tracks.push(Track::dormant(4, "Shimmer", PresetKind::Shimmer, golden_freq(root, 1)));
238 tracks.push(Track::dormant(5, "Bell", PresetKind::Bell, golden_freq(root, 2)));
239 tracks.push(Track::dormant(6, "SuperSaw", PresetKind::SuperSaw, golden_freq(root, -2)));
240 tracks.push(Track::dormant(7, "Pluck", PresetKind::PluckSaw, golden_freq(root, 1)));
241
242 tracks
243}