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}
29
30impl LivePlayService {
31    pub fn new(logger: Arc<Logger>, builder: ProjectBuilder) -> Result<Self> {
32        let playback = LivePlaybackEngine::new(logger.clone())
33            .context("failed to initialise audio playback engine")?;
34        Ok(Self {
35            logger,
36            playback,
37            builder,
38        })
39    }
40
41    pub async fn run(&self, request: LivePlayRequest) -> Result<()> {
42        if request.live_mode {
43            self.run_live(request).await
44        } else {
45            self.run_offline(request).await
46        }
47    }
48
49    async fn run_offline(&self, request: LivePlayRequest) -> Result<()> {
50        let artifacts = self.builder.build(&request.build)?;
51        self.logger
52            .debug(format!("Build RMS: {:.4}", artifacts.rms));
53        self.logger.watch(format!(
54            "Audio regenerated in {} (total build {})",
55            format_duration(artifacts.audio_render_time),
56            format_duration(artifacts.total_duration)
57        ));
58        self.logger.info(format!(
59            "Loop length ≈ {}",
60            format_duration(artifacts.audio_length)
61        ));
62        self.logger.success(format!(
63            "Artifacts written: AST={}, audio={}",
64            artifacts.ast_path.display(),
65            artifacts.primary_audio_path.display()
66        ));
67
68        let source = LiveAudioSource::from_artifacts(&artifacts);
69        self.playback.play_once(source, request.volume).await?;
70        self.logger.info("Playback finished.");
71        Ok(())
72    }
73
74    async fn run_live(&self, request: LivePlayRequest) -> Result<()> {
75        let mut artifacts = match self.builder.build(&request.build) {
76            Ok(artifacts) => artifacts,
77            Err(err) => {
78                self.logger.error(format!("Initial build failed: {err}"));
79                return Err(err);
80            }
81        };
82        self.logger
83            .debug(format!("Build RMS: {:.4}", artifacts.rms));
84        self.logger.watch(format!(
85            "Audio regenerated in {} (total build {})",
86            format_duration(artifacts.audio_render_time),
87            format_duration(artifacts.total_duration)
88        ));
89        self.logger.info(format!(
90            "Loop length ≈ {}",
91            format_duration(artifacts.audio_length)
92        ));
93        let poll = Duration::from_millis(request.crossfade_ms.max(10));
94        let options = LivePlaybackOptions::new(poll).with_volume(request.volume);
95
96        let initial_source = LiveAudioSource::from_artifacts(&artifacts);
97        let session = self
98            .playback
99            .start_live_session(initial_source, options)
100            .await?;
101        let mut best_audio_render_time = artifacts.audio_render_time;
102
103        self.logger.watch(format!(
104            "Live mode watching {}",
105            request.build.entry_path.display()
106        ));
107        let watcher = FileWatcher::new(self.logger.clone());
108        let mut stream = watcher
109            .watch(request.build.entry_path.clone(), WatchOptions::default())
110            .await
111            .context("failed to initialise file watcher")?;
112
113        loop {
114            select! {
115                change = stream.next_change() => {
116                    match change {
117                        Some(path) => {
118                            self.logger.watch(format!("Rebuilding after change at {}", path.display()));
119                            match self.builder.build(&request.build) {
120                                Ok(new_artifacts) => {
121                                    self.logger
122                                        .debug(format!("Build RMS: {:.4}", new_artifacts.rms));
123                                    self.logger.watch(format!(
124                                        "Audio regenerated in {} (total build {})",
125                                        format_duration(new_artifacts.audio_render_time),
126                                        format_duration(new_artifacts.total_duration)
127                                    ));
128                                    self.logger.info(format!(
129                                        "Loop length ≈ {}",
130                                        format_duration(new_artifacts.audio_length)
131                                    ));
132                                    if new_artifacts.audio_render_time < best_audio_render_time {
133                                        best_audio_render_time = new_artifacts.audio_render_time;
134                                        self.logger.success(format!(
135                                            "⏱️ New best audio regen time: {}",
136                                            format_duration(best_audio_render_time)
137                                        ));
138                                    } else {
139                                        self.logger.info(format!(
140                                            "Best audio regen time so far: {}",
141                                            format_duration(best_audio_render_time)
142                                        ));
143                                    }
144                                    artifacts = new_artifacts;
145                                    let next_source = LiveAudioSource::from_artifacts(&artifacts);
146                                    if let Err(err) = session.queue_source(next_source) {
147                                        self.logger.error(format!("Failed to queue live buffer: {err}"));
148                                    }
149                                }
150                                Err(err) => {
151                                    self.logger.error(format!("Build failed after change: {err}"));
152                                }
153                            }
154                        }
155                        None => {
156                            self.logger.warn("Watch stream ended; shutting down live playback");
157                            break;
158                        }
159                    }
160                }
161                _ = session.heartbeat() => {}
162            }
163        }
164
165        session.finish().await
166    }
167}
168
169impl LiveAudioSource {
170    fn from_artifacts(artifacts: &BuildArtifacts) -> Self {
171        LiveAudioSource::with_path(
172            artifacts.primary_audio_path.clone(),
173            artifacts.primary_format,
174            artifacts.bit_depth,
175            artifacts.channels,
176            artifacts.sample_rate,
177            artifacts.resample_quality,
178            artifacts.audio_length,
179        )
180    }
181}
182
183fn format_duration(duration: Duration) -> String {
184    if duration.as_secs() >= 1 {
185        format!("{:.2}s", duration.as_secs_f64())
186    } else {
187        let ms = duration.as_secs_f64() * 1000.0;
188        if ms >= 100.0 {
189            format!("{:.0}ms", ms)
190        } else {
191            format!("{:.1}ms", ms)
192        }
193    }
194}