Skip to main content

limnus_audio_device/
low_level.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/limnus
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use 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        // Always try to use 44.1kHz unless it's not supported
124        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 // Use 44.1kHz if supported
128        } else {
129            MAX_SUPPORTED_RATE // Otherwise use 48kHz
130        };
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}