1mod convert_impl;
17#[cfg(not(target_arch = "wasm32"))]
18mod null;
19mod sys;
20#[cfg(target_arch = "wasm32")]
21pub mod web;
22#[cfg(windows)]
23mod windows;
24
25use std::sync::{Arc, Mutex};
26
27use shuyi_sound::Channels;
28
29pub mod convert {
31 pub use crate::convert_impl::{Boundary, DeviceSample};
32}
33
34pub use sys::{HandOut, HandTo, handover};
35
36type Callback = Arc<Mutex<dyn FnMut(&mut [i16]) + Send>>;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43#[non_exhaustive]
44pub struct OutputConfig {
45 pub rate: u32,
47 pub channels: Channels,
49 pub frames: u32,
51}
52
53impl OutputConfig {
54 pub const fn new(rate: u32, channels: Channels, frames: u32) -> Self {
56 Self {
57 rate,
58 channels,
59 frames,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
66#[non_exhaustive]
67pub enum OutputError {
68 #[error("no sound device")]
70 NoDevice,
71 #[error("no sound output here: {0}")]
73 Unavailable(String),
74 #[error("the sound device refused: {0}")]
76 Device(String),
77}
78
79pub struct Output {
81 inner: Inner,
82 rate: u32,
83}
84
85enum Inner {
86 #[cfg(not(target_arch = "wasm32"))]
87 Null(null::NullOutput),
88 #[cfg(windows)]
89 Windows(windows::WindowsOutput),
90 #[cfg(target_arch = "wasm32")]
91 Web(web::WebOutput),
92}
93
94impl std::fmt::Debug for Output {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("Output")
97 .field("rate", &self.rate)
98 .finish_non_exhaustive()
99 }
100}
101
102impl Output {
103 pub fn preferred_rate() -> Option<u32> {
106 #[cfg(windows)]
107 return windows::preferred_rate();
108 #[cfg(not(windows))]
109 None
110 }
111
112 pub fn open(
114 config: OutputConfig,
115 callback: impl FnMut(&mut [i16]) + Send + 'static,
116 ) -> Result<Self, OutputError> {
117 Self::open_shared(config, Arc::new(Mutex::new(callback)))
118 }
119
120 fn open_shared(config: OutputConfig, callback: Callback) -> Result<Self, OutputError> {
121 #[cfg(windows)]
122 return windows::WindowsOutput::open(config, callback).map(|output| Self {
123 inner: Inner::Windows(output),
124 rate: config.rate,
125 });
126 #[cfg(target_arch = "wasm32")]
127 return web::WebOutput::open(config, callback, false).map(|output| Self {
128 rate: output.rate(),
129 inner: Inner::Web(output),
130 });
131 #[cfg(not(any(windows, target_arch = "wasm32")))]
132 {
133 let _ = (config, callback);
134 Err(OutputError::Unavailable(
135 "this system's output isn't written yet".to_owned(),
136 ))
137 }
138 }
139
140 pub fn null(config: OutputConfig, callback: impl FnMut(&mut [i16]) + Send + 'static) -> Self {
142 Self::null_shared(config, Arc::new(Mutex::new(callback)))
143 }
144
145 fn null_shared(config: OutputConfig, callback: Callback) -> Self {
146 #[cfg(target_arch = "wasm32")]
147 {
148 let output = web::WebOutput::open(config, callback, true);
150 match output {
151 Ok(output) => Self {
152 rate: output.rate(),
153 inner: Inner::Web(output),
154 },
155 Err(_) => Self {
156 rate: config.rate,
157 inner: Inner::Web(web::WebOutput::idle(config.rate)),
158 },
159 }
160 }
161 #[cfg(not(target_arch = "wasm32"))]
162 Self {
163 rate: config.rate,
164 inner: Inner::Null(null::NullOutput::open(config, callback)),
165 }
166 }
167
168 pub fn open_or_null(
171 config: OutputConfig,
172 callback: impl FnMut(&mut [i16]) + Send + 'static,
173 ) -> (Self, Option<OutputError>) {
174 let callback: Callback = Arc::new(Mutex::new(callback));
175 match Self::open_shared(config, Arc::clone(&callback)) {
176 Ok(output) => (output, None),
177 Err(error) => (Self::null_shared(config, callback), Some(error)),
178 }
179 }
180
181 pub const fn rate(&self) -> u32 {
183 self.rate
184 }
185
186 #[cfg_attr(not(windows), allow(clippy::unused_self))]
191 pub fn stats(&self) -> (u32, u32) {
192 #[cfg(windows)]
193 if let Inner::Windows(output) = &self.inner {
194 return output.stats();
195 }
196 (0, 0)
197 }
198
199 pub fn suspend(&self) {
201 self.hold(true);
202 }
203
204 pub fn resume(&self) {
206 self.hold(false);
207 }
208
209 fn hold(&self, on: bool) {
210 match &self.inner {
211 #[cfg(not(target_arch = "wasm32"))]
212 Inner::Null(output) => output.hold(on),
213 #[cfg(windows)]
214 Inner::Windows(output) => output.hold(on),
215 #[cfg(target_arch = "wasm32")]
216 Inner::Web(output) => output.hold(on),
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use std::sync::atomic::{AtomicUsize, Ordering};
224 use std::time::{Duration, Instant};
225
226 use super::*;
227
228 const CONFIG: OutputConfig = OutputConfig::new(48_000, Channels::Stereo, 48);
229
230 fn counting() -> (Arc<AtomicUsize>, impl FnMut(&mut [i16]) + Send + 'static) {
231 let count = Arc::new(AtomicUsize::new(0));
232 let inner = Arc::clone(&count);
233 (count, move |out: &mut [i16]| {
234 inner.fetch_add(out.len(), Ordering::AcqRel);
235 })
236 }
237
238 fn wait_for(count: &AtomicUsize, more_than: usize) {
239 let start = Instant::now();
240 while count.load(Ordering::Acquire) <= more_than {
241 assert!(start.elapsed() < Duration::from_secs(20), "it never asked");
242 std::thread::sleep(Duration::from_millis(1));
243 }
244 }
245
246 #[test]
247 fn the_null_output_runs_at_the_rate_it_was_asked_for_and_can_be_held() {
248 let (count, callback) = counting();
249 let output = Output::null(CONFIG, callback);
250 assert_eq!(output.rate(), 48_000);
251 assert_eq!(output.stats(), (0, 0), "it has no stream to build or lose");
252 assert!(format!("{output:?}").contains("48000"));
253 wait_for(&count, 0);
254 assert_eq!(
255 count.load(Ordering::Acquire) % 96,
256 0,
257 "whole blocks of stereo"
258 );
259 output.suspend();
260 std::thread::sleep(Duration::from_millis(20));
261 let held = count.load(Ordering::Acquire);
262 std::thread::sleep(Duration::from_millis(30));
263 assert_eq!(count.load(Ordering::Acquire), held, "it stopped asking");
264 output.resume();
265 wait_for(&count, held);
266 }
267
268 #[cfg(not(windows))]
271 #[test]
272 fn a_system_without_its_output_is_quiet_and_says_why() {
273 assert_eq!(Output::preferred_rate(), None);
274 let (_, callback) = counting();
275 assert!(matches!(
276 Output::open(CONFIG, callback),
277 Err(OutputError::Unavailable(_))
278 ));
279 let (count, callback) = counting();
280 let (output, problem) = Output::open_or_null(CONFIG, callback);
281 assert!(matches!(problem, Some(OutputError::Unavailable(_))));
282 assert_eq!(output.rate(), 48_000);
283 wait_for(&count, 0);
284 }
285
286 #[test]
287 fn errors_say_what_went_wrong() {
288 assert_eq!(OutputError::NoDevice.to_string(), "no sound device");
289 let refused = OutputError::Device("busy".to_owned());
290 assert_eq!(refused.to_string(), "the sound device refused: busy");
291 }
292
293 #[test]
296 fn the_lints_are_the_workspace_lints_except_unsafe_code() {
297 let manifest = include_str!("../Cargo.toml");
298 let root = include_str!("../../../Cargo.toml");
299 let section = |text: &'static str, name: &str| -> Vec<&'static str> {
300 text.lines()
301 .skip_while(|line| *line != name)
302 .skip(1)
303 .take_while(|line| !line.starts_with('['))
304 .filter(|line| !line.is_empty() && !line.starts_with('#'))
305 .collect()
306 };
307 let rust = section(manifest, "[lints.rust]");
308 assert!(rust.contains(&"unsafe_code = \"deny\""), "{rust:?}");
309 let workspace_rust: Vec<String> = section(root, "[workspace.lints.rust]")
310 .iter()
311 .map(|line| line.replace("\"forbid\"", "\"deny\""))
312 .collect();
313 assert_eq!(rust, workspace_rust);
314 let mut clippy = section(manifest, "[lints.clippy]");
315 clippy.retain(|line| !line.starts_with("undocumented_unsafe_blocks"));
316 assert_eq!(clippy, section(root, "[workspace.lints.clippy]"));
317 }
318}