devalang_wasm/services/live/play/
mod.rs1#![cfg(feature = "cli")]
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result};
7use tokio::select;
8
9use crate::engine::audio::playback::live::{
10 LiveAudioSource, LivePlaybackEngine, LivePlaybackOptions,
11};
12use crate::services::build::pipeline::{BuildArtifacts, BuildRequest, ProjectBuilder};
13use crate::services::watch::file::{FileWatcher, WatchOptions};
14use crate::tools::logger::Logger;
15
16#[derive(Debug, Clone)]
17pub struct LivePlayRequest {
18 pub build: BuildRequest,
19 pub live_mode: bool,
20 pub crossfade_ms: u64,
21 pub volume: f32,
22}
23
24pub struct LivePlayService {
25 logger: Arc<Logger>,
26 playback: LivePlaybackEngine,
27 builder: ProjectBuilder,
28 bg_rx_guard: std::sync::Mutex<
30 Option<
31 std::sync::Arc<
32 std::sync::Mutex<
33 std::sync::mpsc::Receiver<crate::engine::audio::events::AudioEventList>,
34 >,
35 >,
36 >,
37 >,
38}
39
40impl LivePlayService {
41 pub fn new(logger: Arc<Logger>, builder: ProjectBuilder) -> Result<Self> {
42 let playback = LivePlaybackEngine::new(logger.clone())
43 .context("failed to initialise audio playback engine")?;
44 Ok(Self {
45 logger,
46 playback,
47 builder,
48 bg_rx_guard: std::sync::Mutex::new(None),
49 })
50 }
51
52 pub async fn run(&self, request: LivePlayRequest) -> Result<()> {
53 if request.live_mode {
54 self.run_live(request).await
55 } else {
56 self.run_offline(request).await
57 }
58 }
59
60 async fn run_offline(&self, request: LivePlayRequest) -> Result<()> {
61 let artifacts = self.builder.build(&request.build)?;
62 self.logger
63 .debug(format!("Build RMS: {:.4}", artifacts.rms));
64 self.logger.watch(format!(
65 "Audio regenerated in {} (total build {})",
66 format_duration(artifacts.audio_render_time),
67 format_duration(artifacts.total_duration)
68 ));
69 self.logger.info(format!(
70 "Loop length ≈ {}",
71 format_duration(artifacts.audio_length)
72 ));
73 self.logger.success(format!(
74 "Artifacts written: AST={}, audio={}",
75 artifacts.ast_path.display(),
76 artifacts.primary_audio_path.display()
77 ));
78
79 let source = LiveAudioSource::from_artifacts(&artifacts);
80 self.playback.play_once(source, request.volume).await?;
81 self.logger.info("Playback finished.");
82 Ok(())
83 }
84
85 async fn run_live(&self, request: LivePlayRequest) -> Result<()> {
86 let mut artifacts = match self.builder.build(&request.build) {
87 Ok(artifacts) => artifacts,
88 Err(err) => {
89 self.logger.error(format!("Initial build failed: {err}"));
90 return Err(err);
91 }
92 };
93 self.logger
94 .debug(format!("Build RMS: {:.4}", artifacts.rms));
95 self.logger.watch(format!(
96 "Audio regenerated in {} (total build {})",
97 format_duration(artifacts.audio_render_time),
98 format_duration(artifacts.total_duration)
99 ));
100 self.logger.info(format!(
101 "Loop length ≈ {}",
102 format_duration(artifacts.audio_length)
103 ));
104 let poll = Duration::from_millis(request.crossfade_ms.max(10));
105 let options = LivePlaybackOptions::new(poll).with_volume(request.volume);
106
107 let initial_source = LiveAudioSource::from_artifacts(&artifacts);
108
109 use crate::engine::audio::interpreter::driver::AudioInterpreter;
112 use std::sync::mpsc as std_mpsc;
113 use std::sync::{Arc as StdArc, Mutex as StdMutex};
114
115 let (bg_tx, bg_rx_std) =
119 std_mpsc::channel::<crate::engine::audio::events::AudioEventList>();
120 let bg_rx = StdArc::new(StdMutex::new(bg_rx_std));
121 self.logger.debug(format!(
123 "[LIVE] created bg channel; bg_rx strong_count={}",
124 StdArc::strong_count(&bg_rx)
125 ));
126
127 if let Ok(mut g) = self.bg_rx_guard.lock() {
129 *g = Some(bg_rx.clone());
130 }
131
132 let mut persistent_stop_tx: Option<std_mpsc::Sender<()>>;
133 let mut persistent_handle: Option<std::thread::JoinHandle<()>>;
134
135 let spawn_persistent = |stmts: Vec<crate::language::syntax::ast::Statement>,
139 sample_rate: u32,
140 bg_tx: std_mpsc::Sender<
141 crate::engine::audio::events::AudioEventList,
142 >,
143 bg_rx: StdArc<
144 StdMutex<std::sync::mpsc::Receiver<crate::engine::audio::events::AudioEventList>>,
145 >,
146 logger: Arc<Logger>| {
147 let (stop_tx, stop_rx) = std_mpsc::channel::<()>();
149
150 let handle = std::thread::spawn(move || {
151 use std::time::Duration as StdDuration;
152 let r = std::panic::catch_unwind(|| {
157 logger.debug(format!(
158 "[PERSISTENT] starting persistent interpreter (sample_rate={})",
159 sample_rate
160 ));
161 logger.debug(format!(
162 "[PERSISTENT] bg_rx strong_count at thread start={}",
163 StdArc::strong_count(&bg_rx)
164 ));
165 let mut interp = AudioInterpreter::new(sample_rate);
166 interp.suppress_print = true;
173 interp.suppress_beat_emit = true;
174
175 interp.background_event_tx = Some(bg_tx.clone());
178
179 let _ = interp.collect_events(&stmts);
181 logger.debug(format!(
182 "[PERSISTENT] collect_events() returned; background_event_tx present={}",
183 interp.background_event_tx.is_some()
184 ));
185
186 loop {
188 match stop_rx.try_recv() {
190 Ok(_) | Err(std_mpsc::TryRecvError::Disconnected) => break,
191 Err(std_mpsc::TryRecvError::Empty) => {}
192 }
193
194 logger.debug(format!(
196 "[PERSISTENT] bg_rx strong_count before recv_timeout={}",
197 StdArc::strong_count(&bg_rx)
198 ));
199 let recv_result = match bg_rx.lock() {
200 Ok(rx) => rx.recv_timeout(StdDuration::from_millis(200)),
201 Err(err) => {
202 logger.error(format!(
203 "[PERSISTENT] background event channel lock poisoned: {err}"
204 ));
205 break;
206 }
207 };
208
209 match recv_result {
210 Ok(events) => {
211 let cnt = events.events.len();
213 let mut times: Vec<f32> = events
214 .events
215 .iter()
216 .map(|e| match e {
217 crate::engine::audio::events::AudioEvent::Note {
218 start_time,
219 ..
220 } => *start_time,
221 crate::engine::audio::events::AudioEvent::Chord {
222 start_time,
223 ..
224 } => *start_time,
225 crate::engine::audio::events::AudioEvent::Sample {
226 start_time,
227 ..
228 } => *start_time,
229 crate::engine::audio::events::AudioEvent::Stop {
230 time,
231 ..
232 } => *time,
233 })
234 .collect();
235 for (t, _m) in &events.logs {
236 times.push(*t);
237 }
238 let earliest = times.into_iter().min_by(|a, b| {
239 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
240 });
241 if let Some(t) = earliest {
242 logger.debug(format!(
243 "[PERSISTENT] merging {} bg events, earliest_start_time={}",
244 cnt, t
245 ));
246 } else {
247 logger.debug(format!("[PERSISTENT] merging {} bg events", cnt));
248 }
249 interp.events.merge(events);
251 }
252 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
253 }
255 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
256 break;
258 }
259 }
260 }
261 logger.debug(
262 "[PERSISTENT] persistent interpreter exiting (dropping interp)".to_string(),
263 );
264 });
266
267 if let Err(err) = r {
270 logger.error(format!(
271 "[PERSISTENT] persistent thread panicked: {:?}",
272 err
273 ));
274 }
275 });
276
277 (stop_tx, handle)
279 };
280
281 {
283 let (tx, handle) = spawn_persistent(
284 artifacts.statements.clone(),
285 artifacts.sample_rate,
286 bg_tx.clone(),
287 bg_rx.clone(),
288 self.logger.clone(),
289 );
290 persistent_stop_tx = Some(tx);
291 persistent_handle = Some(handle);
292 }
293
294 let session = self
295 .playback
296 .start_live_session(initial_source, options, Some(bg_rx.clone()))
297 .await?;
298 let mut best_audio_render_time = artifacts.audio_render_time;
299
300 self.logger.watch(format!(
301 "Live mode watching {}",
302 request.build.entry_path.display()
303 ));
304 let watcher = FileWatcher::new(self.logger.clone());
305 let mut stream = watcher
306 .watch(request.build.entry_path.clone(), WatchOptions::default())
307 .await
308 .context("failed to initialise file watcher")?;
309
310 loop {
311 select! {
312 change = stream.next_change() => {
313 match change {
314 Some(path) => {
315 self.logger.watch(format!("Rebuilding after change at {}", path.display()));
316 match self.builder.build(&request.build) {
317 Ok(new_artifacts) => {
318 self.logger
319 .debug(format!("Build RMS: {:.4}", new_artifacts.rms));
320 self.logger.watch(format!(
321 "Audio regenerated in {} (total build {})",
322 format_duration(new_artifacts.audio_render_time),
323 format_duration(new_artifacts.total_duration)
324 ));
325 self.logger.info(format!(
326 "Loop length ≈ {}",
327 format_duration(new_artifacts.audio_length)
328 ));
329 if new_artifacts.audio_render_time < best_audio_render_time {
330 best_audio_render_time = new_artifacts.audio_render_time;
331 self.logger.success(format!(
332 "⏱️ New best audio regen time: {}",
333 format_duration(best_audio_render_time)
334 ));
335 } else {
336 self.logger.info(format!(
337 "Best audio regen time so far: {}",
338 format_duration(best_audio_render_time)
339 ));
340 }
341 if let Some(tx) = persistent_stop_tx.take() {
343 let _ = tx.send(());
344 }
345 if let Some(handle) = persistent_handle.take() {
346 let _ = handle.join();
347 }
348
349 artifacts = new_artifacts;
350 let (tx, handle) = spawn_persistent(artifacts.statements.clone(), artifacts.sample_rate, bg_tx.clone(), bg_rx.clone(), self.logger.clone());
351 persistent_stop_tx = Some(tx);
352 persistent_handle = Some(handle);
353
354 let next_source = LiveAudioSource::from_artifacts(&artifacts);
355 if let Err(err) = session.queue_source(next_source) {
356 self.logger.error(format!("Failed to queue live buffer: {err}"));
357 }
358 }
359 Err(err) => {
360 self.logger.error(format!("Build failed after change: {err}"));
361 }
362 }
363 }
364 None => {
365 self.logger.warn("Watch stream ended; shutting down live playback");
366 break;
367 }
368 }
369 }
370 _ = session.heartbeat() => {}
371 }
372 }
373
374 let res = session.finish().await;
376 if let Ok(mut g) = self.bg_rx_guard.lock() {
378 let _ = g.take();
379 }
380 res
381 }
382}
383
384impl LiveAudioSource {
385 fn from_artifacts(artifacts: &BuildArtifacts) -> Self {
386 LiveAudioSource::with_path(
387 artifacts.primary_audio_path.clone(),
388 artifacts.primary_format,
389 artifacts.bit_depth,
390 artifacts.channels,
391 artifacts.sample_rate,
392 artifacts.resample_quality,
393 artifacts.audio_length,
394 )
395 }
396}
397
398fn format_duration(duration: Duration) -> String {
399 if duration.as_secs() >= 1 {
400 format!("{:.2}s", duration.as_secs_f64())
401 } else {
402 let ms = duration.as_secs_f64() * 1000.0;
403 if ms >= 100.0 {
404 format!("{:.0}ms", ms)
405 } else {
406 format!("{:.1}ms", ms)
407 }
408 }
409}