返回 CodeWhale
audio.rs
根目录 / crates / tui / src / tui / pet_watch / audio.rs
1 //! A bounded presentation sink for the shared core's stereo PCM. No score,
2 //! event interpretation or simulation clock lives in the audio player.
3 use std::io::{self, Write};
4 use std::process::{Child, Command, Stdio};
5 use std::sync::atomic::{AtomicBool, Ordering};
6 use std::sync::{Arc, Mutex, mpsc};
7 use std::time::{Duration, Instant};
8
9 pub const SAMPLE_RATE: usize = 48_000;
10 pub const MAX_FRAMES: usize = SAMPLE_RATE / 2;
11
12 pub(super) struct Packet {
13 pub(super) bytes: Vec<u8>,
14 created: Instant,
15 }
16
17 #[derive(Default)]
18 struct Control {
19 child: Mutex<Option<Child>>,
20 cancelled: AtomicBool,
21 failed: AtomicBool,
22 }
23
24 #[derive(Clone)]
25 pub struct Target {
26 tx: mpsc::SyncSender<Packet>,
27 control: Arc<Control>,
28 requested: Instant,
29 }
30
31 impl Target {
32 pub fn same_stream(&self, other: &Self) -> bool {
33 Arc::ptr_eq(&self.control, &other.control)
34 }
35
36 pub fn active(&self) -> bool {
37 !self.control.cancelled.load(Ordering::Acquire)
38 && !self.control.failed.load(Ordering::Acquire)
39 }
40
41 pub fn fail(&self) {
42 self.control.failed.store(true, Ordering::Release);
43 }
44
45 pub fn current(&self) -> bool {
46 self.requested.elapsed() <= Duration::from_millis(500)
47 }
48
49 pub fn send(&self, channels: [Vec<f32>; 2]) -> Result<(), ()> {
50 let length = channels[0].len();
51 if length > MAX_FRAMES
52 || channels[1].len() != length
53 || channels
54 .iter()
55 .flatten()
56 .any(|s| !s.is_finite() || s.abs() > 1.0)
57 {
58 self.fail();
59 return Err(());
60 }
61 if !self.active() {
62 return Err(());
63 }
64 if !self.current() {
65 return Ok(());
66 }
67 let mut bytes = Vec::with_capacity(length * 8);
68 for (left, right) in channels[0].iter().zip(&channels[1]) {
69 bytes.extend_from_slice(&left.to_le_bytes());
70 bytes.extend_from_slice(&right.to_le_bytes());
71 }
72 match self.tx.try_send(Packet {
73 bytes,
74 created: self.requested,
75 }) {
76 Ok(()) | Err(mpsc::TrySendError::Full(_)) => Ok(()),
77 Err(mpsc::TrySendError::Disconnected(_)) => {
78 self.fail();
79 Err(())
80 }
81 }
82 }
83 }
84
85 pub struct Output {
86 target: Target,
87 }
88
89 impl Output {
90 #[cfg(test)]
91 pub(super) fn capture() -> (Self, mpsc::Receiver<Packet>) {
92 let (tx, rx) = mpsc::sync_channel(4);
93 (
94 Self {
95 target: Target {
96 tx,
97 control: Arc::new(Control::default()),
98 requested: Instant::now(),
99 },
100 },
101 rx,
102 )
103 }
104
105 #[cfg(not(test))]
106 pub fn start() -> io::Result<Self> {
107 let mut command = Command::new("ffplay");
108 command.args([
109 "-nodisp",
110 "-autoexit",
111 "-loglevel",
112 "error",
113 "-probesize",
114 "32",
115 "-analyzeduration",
116 "0",
117 "-f",
118 "f32le",
119 "-sample_rate",
120 "48000",
121 "-ch_layout",
122 "stereo",
123 "-i",
124 "pipe:0",
125 ]);
126 #[cfg(windows)]
127 {
128 use std::os::windows::process::CommandExt;
129 command.creation_flags(0x08000000); // CREATE_NO_WINDOW
130 }
131 Self::spawn(command)
132 }
133
134 #[cfg(test)]
135 pub fn start() -> io::Result<Self> {
136 // Product/library tests never open the user's audio output.
137 Err(io::Error::new(
138 io::ErrorKind::Unsupported,
139 "audio disabled in tests",
140 ))
141 }
142
143 fn spawn(mut command: Command) -> io::Result<Self> {
144 let (tx, rx) = mpsc::sync_channel(4);
145 let control = Arc::new(Control::default());
146 let thread_control = Arc::clone(&control);
147 std::thread::Builder::new()
148 .name("pet-audio".into())
149 .spawn(move || {
150 let result = play(&mut command, rx, &thread_control);
151 if result.is_err() && !thread_control.cancelled.load(Ordering::Acquire) {
152 thread_control.failed.store(true, Ordering::Release);
153 }
154 let child = thread_control
155 .child
156 .lock()
157 .ok()
158 .and_then(|mut slot| slot.take());
159 if let Some(mut child) = child {
160 let _ = child.kill();
161 let _ = child.wait();
162 }
163 })?;
164 Ok(Self {
165 target: Target {
166 tx,
167 control,
168 requested: Instant::now(),
169 },
170 })
171 }
172
173 pub fn target(&self) -> Target {
174 Target {
175 requested: Instant::now(),
176 ..self.target.clone()
177 }
178 }
179 pub fn failed(&self) -> bool {
180 self.target.control.failed.load(Ordering::Acquire)
181 }
182 }
183
184 impl Drop for Output {
185 fn drop(&mut self) {
186 self.target.control.cancelled.store(true, Ordering::Release);
187 // Closing or muting Watch interrupts even a blocked pipe write. Reaping
188 // happens in the audio thread, never in the terminal event loop.
189 if let Ok(mut slot) = self.target.control.child.lock()
190 && let Some(child) = slot.as_mut()
191 {
192 let _ = child.kill();
193 }
194 }
195 }
196
197 fn play(command: &mut Command, rx: mpsc::Receiver<Packet>, control: &Control) -> io::Result<()> {
198 if control.cancelled.load(Ordering::Acquire) {
199 return Ok(());
200 }
201 let mut child = command
202 .stdin(Stdio::piped())
203 .stdout(Stdio::null())
204 .stderr(Stdio::null())
205 .spawn()?;
206 let Some(mut input) = child.stdin.take() else {
207 let _ = child.kill();
208 let _ = child.wait();
209 return Err(io::Error::other("audio pipe unavailable"));
210 };
211 match control.child.lock() {
212 Ok(mut slot) => *slot = Some(child),
213 Err(_) => {
214 let _ = child.kill();
215 let _ = child.wait();
216 return Err(io::Error::other("audio lock failed"));
217 }
218 }
219 while !control.cancelled.load(Ordering::Acquire) && !control.failed.load(Ordering::Acquire) {
220 if control
221 .child
222 .lock()
223 .map_err(|_| io::Error::other("audio lock failed"))?
224 .as_mut()
225 .is_none_or(|c| c.try_wait().map_or(true, |status| status.is_some()))
226 {
227 return Err(io::Error::other("audio player exited"));
228 }
229 match rx.recv_timeout(Duration::from_millis(50)) {
230 Ok(packet) => {
231 if packet.created.elapsed() > Duration::from_millis(500) {
232 continue;
233 }
234 input.write_all(&packet.bytes)?;
235 }
236 Err(mpsc::RecvTimeoutError::Timeout) => {}
237 Err(mpsc::RecvTimeoutError::Disconnected) => break,
238 }
239 }
240 Ok(())
241 }
242
243 #[cfg(test)]
244 mod tests {
245 use super::*;
246
247 fn wait_until(mut ready: impl FnMut() -> bool) {
248 let until = Instant::now() + Duration::from_secs(5);
249 while !ready() {
250 assert!(Instant::now() < until, "audio process did not settle");
251 std::thread::sleep(Duration::from_millis(5));
252 }
253 }
254
255 fn receiver() -> (Target, mpsc::Receiver<Packet>) {
256 let (tx, rx) = mpsc::sync_channel(4);
257 (
258 Target {
259 tx,
260 control: Arc::new(Control::default()),
261 requested: Instant::now(),
262 },
263 rx,
264 )
265 }
266
267 #[test]
268 fn pcm_boundary_preserves_samples_rejects_invalid_input_and_discards_backlog() {
269 let (target, rx) = receiver();
270 target.send([vec![0.125, -0.5], vec![0.25, 0.75]]).unwrap();
271 let bytes: Vec<_> = [0.125_f32, 0.25, -0.5, 0.75]
272 .into_iter()
273 .flat_map(f32::to_le_bytes)
274 .collect();
275 assert_eq!(rx.recv().unwrap().bytes, bytes);
276 for channels in [
277 [vec![0.0], vec![]],
278 [vec![f32::NAN], vec![0.0]],
279 [vec![1.01], vec![0.0]],
280 [vec![0.0; MAX_FRAMES + 1], vec![0.0; MAX_FRAMES + 1]],
281 ] {
282 let (target, rx) = receiver();
283 assert!(target.send(channels).is_err());
284 assert!(!target.active());
285 assert!(rx.try_recv().is_err());
286 }
287 let (mut target, rx) = receiver();
288 target.requested = Instant::now() - Duration::from_secs(1);
289 assert!(target.send([vec![0.0], vec![0.0]]).is_ok());
290 assert!(target.active());
291 assert!(rx.try_recv().is_err());
292 let (target, rx) = receiver();
293 for _ in 0..4 {
294 target.send([vec![0.0], vec![0.0]]).unwrap();
295 }
296 assert!(target.send([vec![0.0], vec![0.0]]).is_ok());
297 assert!(target.active());
298 assert_eq!(rx.try_iter().count(), 4);
299 }
300
301 /// Only a test subprocess with this explicit environment enters the sink.
302 /// Ordinary library/nextest runs return without launching or playing audio.
303 #[test]
304 fn pet_audio_fake_player() {
305 let Some(path) = std::env::var_os("CODEWHALE_TEST_PET_AUDIO_OUTPUT") else {
306 return;
307 };
308 let mut file = std::fs::File::create(path).unwrap();
309 if std::env::var_os("CODEWHALE_TEST_PET_AUDIO_HOLD").is_some() {
310 std::thread::sleep(Duration::from_secs(30));
311 } else {
312 std::io::copy(&mut std::io::stdin().lock(), &mut file).unwrap();
313 }
314 }
315
316 fn fake_player(path: &std::path::Path, hold: bool) -> Command {
317 let module = module_path!().split_once("::").unwrap().1;
318 let mut command = Command::new(std::env::current_exe().unwrap());
319 command
320 .args([
321 "--exact",
322 &format!("{module}::pet_audio_fake_player"),
323 "--nocapture",
324 ])
325 .env("CODEWHALE_TEST_PET_AUDIO_OUTPUT", path);
326 if hold {
327 command.env("CODEWHALE_TEST_PET_AUDIO_HOLD", "1");
328 }
329 command
330 }
331
332 #[test]
333 fn muting_reaps_the_player_even_with_a_retained_target_and_blocked_pipe() {
334 for hold in [false, true] {
335 let dir = tempfile::tempdir().unwrap();
336 let path = dir.path().join("received.f32");
337 let output = Output::spawn(fake_player(&path, hold)).unwrap();
338 wait_until(|| path.exists());
339 let target = output.target();
340 let control = Arc::clone(&target.control);
341 target
342 .send([vec![0.125; MAX_FRAMES], vec![-0.25; MAX_FRAMES]])
343 .unwrap();
344 if !hold {
345 wait_until(|| std::fs::metadata(&path).unwrap().len() == (MAX_FRAMES * 8) as u64);
346 }
347 drop(output);
348 assert!(!target.active());
349 assert!(target.send([vec![0.0], vec![0.0]]).is_err());
350 // Only this test and the deliberately retained target remain. The
351 // player thread must have returned after killing and reaping it.
352 wait_until(|| Arc::strong_count(&control) == 2);
353 assert!(control.child.lock().unwrap().is_none());
354 }
355 }
356
357 #[test]
358 fn missing_player_fails_without_opening_a_device_or_blocking_the_caller() {
359 assert!(Output::start().is_err());
360 let dir = tempfile::tempdir().unwrap();
361 let output = Output::spawn(Command::new(dir.path().join("missing-player"))).unwrap();
362 wait_until(|| output.failed());
363 assert!(!output.target().active());
364 }
365 }
366
366 lines RUST