Skip to main content

devalang_wasm/services/live/play/
mod.rs

1#![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    /// Guard clone to keep bg_rx alive for the duration of a live session
29    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        // Spawn a persistent interpreter thread to keep "loop pass" background workers
110        // alive and to print realtime messages while the live session is active.
111        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        // Create a single background channel for the entire live session and wrap the
116        // receiver in an Arc<Mutex<...>> so the receiver's lifetime can be owned by the
117        // LivePlaybackSession and shared (by reference) with the persistent thread.
118        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        // Also emit via the structured logger so it appears in normal log outputs
122        self.logger.debug(format!(
123            "[LIVE] created bg channel; bg_rx strong_count={}",
124            StdArc::strong_count(&bg_rx)
125        ));
126
127        // Keep a guardian clone in the service to avoid accidental drop during live session
128        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        // Helper closure to spawn a persistent interpreter that will run collect_events once
136        // to register groups and spawn background workers, then wait until signalled to stop.
137        // It accepts the bg_tx and an Arc<Mutex<Receiver>> owned by the session.
138        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            // Stop signal channel for the persistent thread
148            let (stop_tx, stop_rx) = std_mpsc::channel::<()>();
149
150            let handle = std::thread::spawn(move || {
151                use std::time::Duration as StdDuration;
152                // Run the persistent interpreter inside a panic-catch so we can
153                // log if it exits unexpectedly (panics will otherwise be silent
154                // inside spawned threads and lead to the bg receiver being
155                // dropped, causing worker sends to fail).
156                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                    // Suppress immediate prints in the persistent interpreter so that
167                    // the live playback engine (which replays the build's .printlog)
168                    // is the single source of truth for timed PRINT output. This
169                    // avoids duplicate prints (once from the interpreter and once
170                    // from the playback sidecar) while keeping scheduled logs in
171                    // the merged events for rendering.
172                    interp.suppress_print = true;
173                    interp.suppress_beat_emit = true;
174
175                    // Install the sender so that loop pass workers will reuse it instead of
176                    // creating their own channel and receiver owned by the interpreter.
177                    interp.background_event_tx = Some(bg_tx.clone());
178
179                    // Collect events once to register groups and spawn background 'pass' workers
180                    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                    // Drain and merge background worker batches continuously until stop signal.
187                    loop {
188                        // Check for stop signal without blocking forever
189                        match stop_rx.try_recv() {
190                            Ok(_) | Err(std_mpsc::TryRecvError::Disconnected) => break,
191                            Err(std_mpsc::TryRecvError::Empty) => {}
192                        }
193
194                        // Receive one batch from the bg_rx with timeout and merge
195                        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                                // Debug: report merge and earliest time
212                                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                                // Merge produced events into persistent interpreter's event list
250                                interp.events.merge(events);
251                            }
252                            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
253                                // nothing to merge now, continue loop
254                            }
255                            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
256                                // workers are gone
257                                break;
258                            }
259                        }
260                    }
261                    logger.debug(
262                        "[PERSISTENT] persistent interpreter exiting (dropping interp)".to_string(),
263                    );
264                    // dropping interp will stop background workers
265                });
266
267                // If the persistent interpreter panicked, log the payload so we can
268                // diagnose unexpected thread exits that would drop the bg receiver.
269                if let Err(err) = r {
270                    logger.error(format!(
271                        "[PERSISTENT] persistent thread panicked: {:?}",
272                        err
273                    ));
274                }
275            });
276
277            // Return the stop sender so callers can request the thread to stop, and the join handle
278            (stop_tx, handle)
279        };
280
281        // Start persistent interpreter for initial artifacts
282        {
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                                    // Stop previous persistent interpreter (if any) and spawn a new one for updated statements
342                                    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        // Wait for session completion, then clear our guardian clone so the receiver may be dropped
375        let res = session.finish().await;
376        // clear guard
377        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}