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)>>>;
30
31pub struct EngineHandle {
33 pub tracks: Arc<Mutex<Vec<Track>>>,
34 pub global: GlobalParams,
35 pub peak_l: Shared,
36 pub peak_r: Shared,
37 pub sample_rate: f32,
38 pub scope: ScopeBuffer,
39 pub phase_clock: Shared,
40 pub recorder: Arc<RecorderState>,
41 _stream: Stream,
42}
43
44pub struct AudioEngine;
45
46impl AudioEngine {
47 pub fn start(initial_tracks: Vec<Track>) -> Result<EngineHandle> {
48 assert!(
49 initial_tracks.len() == MAX_TRACKS,
50 "expected exactly {MAX_TRACKS} pre-allocated tracks, got {}",
51 initial_tracks.len()
52 );
53
54 let host = cpal::default_host();
55 let device = host
56 .default_output_device()
57 .context("no default output audio device")?;
58 let config: StreamConfig = device.default_output_config()?.into();
59 let sample_rate = config.sample_rate.0 as f32;
60 let channels = config.channels as usize;
61
62 let global = GlobalParams::default();
63 let peak_l = shared(0.0);
64 let peak_r = shared(0.0);
65 let phase_clock = shared(0.0);
66 let scope: ScopeBuffer = Arc::new(Mutex::new(VecDeque::with_capacity(SCOPE_CAPACITY)));
67 let tracks = Arc::new(Mutex::new(initial_tracks));
68 let recorder = RecorderState::new(sample_rate as u32);
69
70 let mut graph = build_master(&tracks.lock(), &global);
71 graph.set_sample_rate(sample_rate as f64);
72
73 let stream = start_stream(
74 device,
75 config,
76 channels,
77 sample_rate,
78 graph,
79 global.master_gain.clone(),
80 peak_l.clone(),
81 peak_r.clone(),
82 scope.clone(),
83 phase_clock.clone(),
84 recorder.clone(),
85 )?;
86
87 Ok(EngineHandle {
88 tracks,
89 global,
90 peak_l,
91 peak_r,
92 sample_rate,
93 scope,
94 phase_clock,
95 recorder,
96 _stream: stream,
97 })
98 }
99}
100
101fn build_master(tracks: &[Track], g: &GlobalParams) -> Net {
102 let mut summed: Option<Net> = None;
103 for t in tracks {
104 let node = Preset::build(t.kind, &t.params, g);
105 summed = Some(match summed {
106 Some(acc) => acc + node,
107 None => node,
108 });
109 }
110 let summed = summed.unwrap_or_else(|| Net::wrap(Box::new(zero() | zero())));
111
112 summed >> master_bus(g.brightness.clone())
116}
117
118#[allow(clippy::too_many_arguments)]
119fn start_stream(
120 device: Device,
121 config: StreamConfig,
122 channels: usize,
123 sample_rate: f32,
124 mut graph: Net,
125 master: Shared,
126 peak_l: Shared,
127 peak_r: Shared,
128 scope: ScopeBuffer,
129 phase_clock: Shared,
130 recorder: Arc<RecorderState>,
131) -> Result<Stream> {
132 let err_fn = |err| tracing::error!("audio stream error: {err}");
133 let mut env_l = 0.0f32;
134 let mut env_r = 0.0f32;
135 let fall = 0.9995f32;
136 let dt: f64 = 1.0 / sample_rate as f64;
139 let mut t: f64 = 0.0;
140 let mut decim = 0usize;
141
142 let stream = device.build_output_stream(
143 &config,
144 move |data: &mut [f32], _| {
145 let m = master.value();
146 let mut pending: [(f32, f32); 32] = [(0.0, 0.0); 32];
147 let mut pending_n = 0usize;
148
149 for frame in data.chunks_mut(channels) {
150 let (lo, ro) = graph.get_stereo();
151 let l = lo * m;
154 let r = ro * m;
155 env_l = (env_l * fall).max(l.abs());
156 env_r = (env_r * fall).max(r.abs());
157
158 for (ch, slot) in frame.iter_mut().enumerate() {
159 *slot = if ch & 1 == 0 { l } else { r };
160 }
161
162 recorder.push_frame(l, r);
165
166 decim = decim.wrapping_add(1);
167 if decim.is_multiple_of(SCOPE_DECIMATION) && pending_n < pending.len() {
168 pending[pending_n] = (l, r);
169 pending_n += 1;
170 }
171
172 t += dt;
173 }
174
175 if pending_n > 0 {
177 let mut scope = scope.lock();
178 for &s in &pending[..pending_n] {
179 if scope.len() == SCOPE_CAPACITY {
180 scope.pop_front();
181 }
182 scope.push_back(s);
183 }
184 }
185
186 peak_l.set_value(env_l);
187 peak_r.set_value(env_r);
188 phase_clock.set_value(t as f32);
189 },
190 err_fn,
191 None,
192 )?;
193 stream.play()?;
194 Ok(stream)
195}
196
197pub fn default_track_set() -> Vec<Track> {
199 let root = 55.0f32; let mut tracks = Vec::with_capacity(MAX_TRACKS);
201
202 tracks.push(Track::new(0, "Pad · root", PresetKind::PadZimmer, golden_freq(root, 0)));
204 tracks.push(Track::new(1, "Bass", PresetKind::BassPulse, golden_freq(root, 0)));
205 tracks.push(Track::new(2, "Heartbeat", PresetKind::Heartbeat, golden_freq(root, 0)));
206 tracks.push(Track::new(3, "Sub Drone", PresetKind::DroneSub, golden_freq(root, -1)));
207 tracks[3].params.gain.set_value(0.32);
209 tracks[3].params.reverb_mix.set_value(0.7);
210
211 tracks.push(Track::dormant(4, "— empty", PresetKind::Shimmer, golden_freq(root, 1)));
213 tracks.push(Track::dormant(5, "— empty", PresetKind::BassPulse, golden_freq(root, -1)));
214 tracks.push(Track::dormant(6, "— empty", PresetKind::Shimmer, golden_freq(root, 2)));
215 tracks.push(Track::dormant(7, "— empty", PresetKind::PadZimmer, golden_freq(root, 1)));
216
217 tracks
218}