audio_visualizer/dynamic/
live_input.rs

1/*
2MIT License
3
4Copyright (c) 2021 Philipp Schuster
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24//! This module enables to record audio and store the latest audio data in a synchronized
25//! ringbuffer. See [`setup_audio_input_loop`].
26//!
27//! It uses the [`cpal`] crate to record audio.
28
29use cpal::traits::{DeviceTrait, HostTrait};
30use cpal::Device;
31use ringbuffer::AllocRingBuffer;
32use std::fmt::{Debug, Formatter};
33use std::sync::{Arc, Mutex};
34
35/// Describes the audio input device that should be used and the config for the input stream.
36/// Caller must be certain, that the config works for the given device on the current platform.
37pub struct AudioDevAndCfg {
38    /// The input device.
39    dev: cpal::Device,
40    /// Desired configuration for the input stream.
41    cfg: cpal::StreamConfig,
42}
43
44impl AudioDevAndCfg {
45    /// Creates an instance. If no device is passed, it falls back to the default input
46    /// device of the system.
47    pub fn new(
48        preferred_dev: Option<cpal::Device>,
49        preferred_cfg: Option<cpal::StreamConfig>,
50    ) -> Self {
51        let dev = preferred_dev.unwrap_or_else(|| {
52            let host = cpal::default_host();
53            host.default_input_device().unwrap_or_else(|| {
54                panic!(
55                    "No default audio input device found for host {}",
56                    host.id().name()
57                )
58            })
59        });
60        let cfg = preferred_cfg.unwrap_or_else(|| dev.default_input_config().unwrap().config());
61        Self { dev, cfg }
62    }
63
64    /// Getter for audio device.
65    pub const fn dev(&self) -> &cpal::Device {
66        &self.dev
67    }
68
69    /// Getter for audio input stream config.
70    pub const fn cfg(&self) -> &cpal::StreamConfig {
71        &self.cfg
72    }
73}
74
75impl Debug for AudioDevAndCfg {
76    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("AudioDevAndCfg")
78            .field(
79                "dev",
80                &self
81                    .dev
82                    .name()
83                    .as_ref()
84                    .map(|x| x.as_str())
85                    .unwrap_or("<unknown>"),
86            )
87            .field("cfg", &self.cfg)
88            .finish()
89    }
90}
91
92/// Sets up audio recording with the [`cpal`] library on the given audio input device.
93///
94/// If no input device is given, it uses the default input device. Panics, if it not present.
95/// Returns the stream plus the chosen config for the device.
96///
97/// Appends all audio data to the ringbuffer `latest_audio_data`.
98///
99/// Works on Windows (WASAPI), Linux (ALSA) and MacOS (coreaudio).
100pub fn setup_audio_input_loop(
101    latest_audio_data: Arc<Mutex<AllocRingBuffer<f32>>>,
102    audio_dev_and_cfg: AudioDevAndCfg,
103) -> cpal::Stream {
104    let dev = audio_dev_and_cfg.dev();
105    let cfg = audio_dev_and_cfg.cfg();
106
107    eprintln!(
108        "Using input device '{}' with config: {:?}",
109        dev.name()
110            .as_ref()
111            .map(|x| x.as_str())
112            .unwrap_or("<unknown>"),
113        cfg
114    );
115
116    assert!(
117        cfg.channels == 1 || cfg.channels == 2,
118        "only supports Mono or Stereo channels!"
119    );
120
121    if cfg.sample_rate.0 != 44100 && cfg.sample_rate.0 != 48000 {
122        eprintln!(
123            "WARN: sampling rate is {}, but the crate was only tested with 44,1/48khz.",
124            cfg.sample_rate.0
125        );
126    }
127
128    let is_mono = cfg.channels == 1;
129
130    let stream = dev
131        .build_input_stream(
132            // This is not as easy as it might look. Even if the supported configs show, that a
133            // input device supports a given fixed buffer size, ALSA but also WASAPI tend to
134            // fail with unclear error messages. I found out, that using the default option is the
135            // only variant that is working on all platforms (Windows, Mac, Linux). The buffer
136            // size tends to be not as small as it would be optimal (for super low latency)
137            // but is still good enough (for example ~10ms on Windows) or ~6ms on ALSA (in my
138            // tests).
139            audio_dev_and_cfg.cfg(),
140            // this is pretty cool by "cpal"; we can use u16, i16 or f32 and
141            // the type system does all the magic behind the scenes. f32 also works
142            // on Windows (WASAPI), MacOS (coreaudio), and Linux (ALSA).
143            // TODO: I found out that we probably can't rely on the fact, that every audio input device
144            //  supports f32. I guess, I need to check this in the supported audio stream config too..
145            move |data: &[f32], _info| {
146                let mut audio_buf = latest_audio_data.lock().unwrap();
147                // Audio buffer only contains Mono data
148                if is_mono {
149                    audio_buf.extend(data.iter().copied());
150                } else {
151                    // interleaving for stereo is LRLR (de-facto standard?)
152                    audio_buf.extend(data.chunks_exact(2).map(|vals| (vals[0] + vals[1]) / 2.0))
153                }
154            },
155            |err| {
156                eprintln!("got stream error: {:#?}", err);
157            },
158            None,
159        )
160        .unwrap();
161
162    stream
163}
164
165/// Lists all input devices for [`cpal`]. Can be used to select a device for
166/// [`setup_audio_input_loop`].
167pub fn list_input_devs() -> Vec<(String, cpal::Device)> {
168    let host = cpal::default_host();
169    type DeviceName = String;
170    let mut devs: Vec<(DeviceName, Device)> = host
171        .input_devices()
172        .unwrap()
173        .map(|dev| {
174            (
175                dev.name().unwrap_or_else(|_| String::from("<unknown>")),
176                dev,
177            )
178        })
179        .collect();
180    devs.sort_by(|(n1, _), (n2, _)| n1.cmp(n2));
181    devs
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_list_input_devs() {
190        dbg!(list_input_devs()
191            .iter()
192            .map(|(n, d)| (n, d.default_input_config()))
193            .collect::<Vec<_>>());
194    }
195}