limnus_audio_device/
low_level.rs1use cpal::traits::{DeviceTrait, HostTrait};
6use cpal::{Device, Host, StreamConfig};
7use std::fmt::Debug;
8use std::io;
9
10use tracing::{debug, error, info, trace};
11
12use limnus_local_resource::prelude::*;
13
14#[derive(LocalResource)]
15pub struct Audio {
16 #[allow(dead_code)]
17 device: Device,
18 config: StreamConfig,
19 sample_rate: u32,
20}
21
22impl Debug for Audio {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "Audio")
25 }
26}
27
28#[allow(unused)]
29fn debug_output(host: Host) {
30 for device in host.devices().expect("should have a device") {
31 let desc = device.description().ok();
32 info!(
33 "Found device: {:?}",
34 desc.as_ref()
35 .map_or("unknown", cpal::DeviceDescription::name)
36 );
37
38 let configs = device.supported_output_configs();
39 if configs.is_err() {
40 continue;
41 }
42
43 for config in configs.unwrap() {
44 info!(
45 " Channels: {}, Sample Rate: {} - {} Hz, Sample Format: {:?}",
46 config.channels(),
47 config.min_sample_rate(),
48 config.max_sample_rate(),
49 config.sample_format()
50 );
51 }
52 }
53}
54
55const PREFERRED_SAMPLE_RATE: u32 = 44100;
56const MAX_SUPPORTED_RATE: u32 = 48000;
57const REQUIRED_CHANNELS: u16 = 2;
58
59impl Audio {
60 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
61 let host = cpal::default_host();
62
63 let default_device = host.default_output_device();
64 if default_device.is_none() {
65 return Err(Box::new(std::io::Error::new(
66 std::io::ErrorKind::NotFound,
67 "no ",
68 )));
69 }
70
71 let device = default_device.unwrap();
72 let device_name = device
73 .description()
74 .ok()
75 .map_or("unknown".to_string(), |d| d.name().to_string());
76 debug!(device = device_name, "default output device");
77
78 let all_supported_configs = device.supported_output_configs()?.collect::<Vec<_>>();
79
80 for config in &all_supported_configs {
81 debug!("Supported config: {:?}", config);
82 }
83
84 let supported_configs: Vec<_> = all_supported_configs
85 .into_iter()
86 .filter(|config| {
87 matches!(
88 config.sample_format(),
89 cpal::SampleFormat::I16 | cpal::SampleFormat::F32
90 ) && config.channels() == REQUIRED_CHANNELS
91 && config.min_sample_rate() <= MAX_SUPPORTED_RATE
92 && config.max_sample_rate() >= PREFERRED_SAMPLE_RATE
93 })
94 .collect();
95
96 for config in &supported_configs {
97 debug!(
98 "Valid config - Format: {:?}, Channels: {}, Rate range: {} - {}",
99 config.sample_format(),
100 config.channels(),
101 config.min_sample_rate(),
102 config.max_sample_rate()
103 );
104 }
105
106 let supported_config = supported_configs
107 .into_iter()
108 .min_by_key(|config| {
109 let format_priority = match config.sample_format() {
110 cpal::SampleFormat::I16 => 0,
111 _ => 1,
112 };
113 (format_priority, config.max_sample_rate())
114 })
115 .ok_or_else(|| {
116 error!("No supported output configurations with stereo I16/F32 format found");
117 io::Error::new(
118 io::ErrorKind::NotFound,
119 "no supported stereo output configurations found",
120 )
121 })?;
122
123 let sample_rate = if supported_config.min_sample_rate() <= PREFERRED_SAMPLE_RATE
125 && supported_config.max_sample_rate() >= PREFERRED_SAMPLE_RATE
126 {
127 PREFERRED_SAMPLE_RATE } else {
129 MAX_SUPPORTED_RATE };
131
132 let supported_config =
133 supported_config.with_sample_rate(cpal::SampleRate::from(sample_rate));
134
135 trace!(config=?supported_config, "Selected output config");
136
137 let config: StreamConfig = supported_config.into();
138
139 info!(device=device_name, sample_rate, config=?&config, "selected device and configuration");
140
141 Ok(Self {
142 device,
143 config,
144 sample_rate,
145 })
146 }
147
148 #[must_use]
149 pub const fn device(&self) -> &Device {
150 &self.device
151 }
152
153 #[must_use]
154 pub const fn config(&self) -> &StreamConfig {
155 &self.config
156 }
157
158 #[must_use]
159 pub const fn sample_rate(&self) -> u32 {
160 self.sample_rate
161 }
162}