Skip to main content

audio_visualizer/live/
input.rs

1/*
2MIT License
3
4Copyright (c) 2026 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//! Audio recording via [`cpal`]: [`AudioInput`] selects an input device and
25//! stream config, recording appends mono samples to a shared [`AudioBuffer`].
26//!
27//! Works cross-platform: Windows (WASAPI), Linux (ALSA), macOS (coreaudio).
28
29use crate::Error;
30use cpal::traits::{DeviceTrait, HostTrait};
31use ringbuffer::{AllocRingBuffer, RingBuffer};
32use std::fmt::{Debug, Formatter};
33use std::sync::{Arc, Mutex};
34
35/// Callback size in frames to request when the stream config leaves the
36/// buffer size to the device.
37///
38/// The visualization only moves when a callback delivers new samples, so
39/// the callback rate caps the perceived frame rate regardless of how fast
40/// the window renders. Device defaults are coarse: PipeWire's default
41/// quantum is 1024 frames, i.e. ~47 callbacks/s at 48 kHz, which visibly
42/// stutters on a 100 Hz display (one update every ~2 frames). 256 frames is
43/// 5.3 ms at 48 kHz, enough for displays well beyond 144 Hz, and cheap: the
44/// callback only appends to the ringbuffer.
45const PREFERRED_BUFFER_FRAMES: u32 = 256;
46
47/// The latest recorded samples together with the number of samples recorded
48/// in total.
49///
50/// The total is what makes the visualization stable: it turns an index into
51/// the ringbuffer into an absolute position in the audio stream, which does
52/// not move when new samples arrive.
53pub(crate) struct AudioBuffer {
54    samples: AllocRingBuffer<f32>,
55    total: u64,
56}
57
58impl AudioBuffer {
59    /// Creates a buffer holding the latest `capacity` samples, pre-filled
60    /// with silence so that the waveform covers the whole time axis right
61    /// from the start.
62    ///
63    /// The pre-filled silence counts towards the total: the buffer is full
64    /// from the very first frame, so the stream has to start `capacity`
65    /// samples before the first recorded one. Starting the count at zero
66    /// would make the stream position of the oldest buffered sample
67    /// negative for as long as the recording is shorter than the buffer.
68    ///
69    /// `capacity` must be a power of two (ringbuffer requirement).
70    pub(crate) fn new(capacity: usize) -> Self {
71        let mut samples = AllocRingBuffer::new(capacity);
72        samples.fill(0.0);
73        Self {
74            samples,
75            total: capacity as u64,
76        }
77    }
78
79    /// The buffered samples (oldest first) and the absolute stream position
80    /// just past the newest one.
81    pub(crate) fn snapshot(&self) -> (Vec<f32>, u64) {
82        (self.samples.to_vec(), self.total)
83    }
84
85    fn extend(&mut self, samples: impl ExactSizeIterator<Item = f32>) {
86        self.total += samples.len() as u64;
87        self.samples.extend(samples);
88    }
89}
90
91/// The audio input device and stream configuration used for recording.
92///
93/// The caller must be certain that the config works for the given device on
94/// the current platform; [`AudioInput::default_device`] and
95/// [`AudioInput::from_device`] pick configs that do.
96pub struct AudioInput {
97    dev: cpal::Device,
98    cfg: cpal::StreamConfig,
99}
100
101impl AudioInput {
102    /// Uses the system default input device with its default configuration.
103    pub fn default_device() -> Result<Self, Error> {
104        let host = cpal::default_host();
105        let dev = host.default_input_device().ok_or_else(|| {
106            Error::Audio(format!(
107                "no default audio input device found for host {}",
108                host.id().name()
109            ))
110        })?;
111        Self::from_device(dev)
112    }
113
114    /// Uses the given device with a mono input configuration if it offers
115    /// one, otherwise with its default input configuration.
116    ///
117    /// Everything is visualized as mono anyway, so recording mono directly
118    /// halves the data the device has to deliver and saves the downmix. For
119    /// a device that only offers stereo, recording averages the two
120    /// channels.
121    pub fn from_device(dev: cpal::Device) -> Result<Self, Error> {
122        let default = dev
123            .default_input_config()
124            .map_err(|e| Error::Audio(format!("no default input config: {e}")))?;
125        let cfg = mono_config(&dev, &default).unwrap_or_else(|| default.config());
126        Ok(Self { dev, cfg })
127    }
128
129    /// Uses the given device and stream configuration.
130    ///
131    /// A `buffer_size` of [`cpal::BufferSize::Default`] does not mean the
132    /// device default: recording then asks for 256 frames per callback,
133    /// which keeps the visualization moving every frame on high refresh
134    /// rate displays, and only falls back to the device default if the
135    /// device rejects that. A fixed size is used as given.
136    #[must_use]
137    pub const fn new(dev: cpal::Device, cfg: cpal::StreamConfig) -> Self {
138        Self { dev, cfg }
139    }
140
141    /// All available input devices of the default host, sorted by name.
142    pub fn devices() -> Result<Vec<(String, cpal::Device)>, Error> {
143        let host = cpal::default_host();
144        let mut devs: Vec<(String, cpal::Device)> = host
145            .input_devices()
146            .map_err(|e| Error::Audio(format!("can't enumerate input devices: {e}")))?
147            .map(|dev| (dev.to_string(), dev))
148            .collect();
149        devs.sort_by(|(n1, _), (n2, _)| n1.cmp(n2));
150        Ok(devs)
151    }
152
153    /// The input device.
154    #[must_use]
155    pub const fn device(&self) -> &cpal::Device {
156        &self.dev
157    }
158
159    /// The stream configuration.
160    #[must_use]
161    pub const fn config(&self) -> &cpal::StreamConfig {
162        &self.cfg
163    }
164
165    /// Builds an input stream that continuously appends the recorded audio
166    /// to `latest_audio_data` as mono samples (stereo is averaged to mono).
167    ///
168    /// The stream still has to be started with
169    /// [`cpal::traits::StreamTrait::play`] and records until dropped.
170    pub(crate) fn build_stream(
171        &self,
172        latest_audio_data: Arc<Mutex<AudioBuffer>>,
173    ) -> Result<cpal::Stream, Error> {
174        let channels = self.cfg.channels;
175        if channels != 1 && channels != 2 {
176            return Err(Error::Audio(format!(
177                "only mono or stereo input is supported, device has {channels} channels"
178            )));
179        }
180        let is_mono = channels == 1;
181
182        let build = |cfg: cpal::StreamConfig| {
183            let latest_audio_data = latest_audio_data.clone();
184            self.dev.build_input_stream(
185                cfg,
186                move |data: &[f32], _info| {
187                    let mut audio_buf = latest_audio_data.lock().unwrap();
188                    if is_mono {
189                        audio_buf.extend(data.iter().copied());
190                    } else {
191                        // interleaving for stereo is LRLR (de-facto standard)
192                        let (pairs, _) = data.as_chunks::<2>();
193                        audio_buf.extend(pairs.iter().map(|[l, r]| (l + r) / 2.0));
194                    }
195                },
196                |err| eprintln!("audio stream error: {err:#?}"),
197                None,
198            )
199        };
200
201        if matches!(self.cfg.buffer_size, cpal::BufferSize::Default) {
202            let preferred = cpal::StreamConfig {
203                buffer_size: cpal::BufferSize::Fixed(PREFERRED_BUFFER_FRAMES),
204                ..self.cfg
205            };
206            // Some devices only support their own period size and reject
207            // this right here (e.g. "not in the supported range
208            // 1024..=1024"); the default size then still records, just with
209            // coarser updates.
210            if let Ok(stream) = build(preferred) {
211                return Ok(stream);
212            }
213        }
214        build(self.cfg).map_err(|e| Error::Audio(format!("can't build input stream: {e}")))
215    }
216}
217
218/// A mono configuration of `dev` with the sample rate of `default`, if the
219/// device supports one.
220///
221/// Only `f32` is considered, because that is what recording asks the device
222/// for.
223fn mono_config(
224    dev: &cpal::Device,
225    default: &cpal::SupportedStreamConfig,
226) -> Option<cpal::StreamConfig> {
227    let sample_rate = default.sample_rate();
228    dev.supported_input_configs()
229        .ok()?
230        .find(|cfg| {
231            cfg.channels() == 1
232                && cfg.sample_format() == cpal::SampleFormat::F32
233                && (cfg.min_sample_rate()..=cfg.max_sample_rate()).contains(&sample_rate)
234        })
235        .map(|cfg| cfg.with_sample_rate(sample_rate).config())
236}
237
238impl Debug for AudioInput {
239    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
240        f.debug_struct("AudioInput")
241            .field("dev", &self.dev.to_string())
242            .field("cfg", &self.cfg)
243            .finish()
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_list_input_devs() {
253        dbg!(
254            AudioInput::devices()
255                .unwrap()
256                .iter()
257                .map(|(n, d)| (n, d.default_input_config()))
258                .collect::<Vec<_>>()
259        );
260    }
261}