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}
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}