A (planned) collection of lightweight tools for streaming.
1use anyhow::{Result, anyhow};
2use cpal::{
3 SampleFormat, StreamConfig,
4 traits::{DeviceTrait, HostTrait, StreamTrait},
5};
6use std::{
7 collections::VecDeque,
8 sync::{Arc, Mutex, atomic::AtomicBool},
9 time,
10};
11
12#[derive(Debug, Clone)]
13pub struct MicInfo {
14 pub name: String,
15 pub sample_rate: u32,
16 pub channels: u16,
17}
18
19impl Default for MicInfo {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl MicInfo {
26 pub fn new() -> Self {
27 let host = cpal::default_host();
28 let device = host.default_input_device().expect("no default input device");
29 let supported_config = device.default_input_config().unwrap();
30 let config: cpal::StreamConfig = supported_config.clone().into();
31
32 Self {
33 name: device.name().unwrap_or_else(|_| "Unknown device".into()),
34 sample_rate: config.sample_rate.0,
35 channels: config.channels,
36 }
37 }
38}
39
40pub type SharedLevels = Arc<Mutex<VecDeque<f32>>>;
41
42fn process_input_f32(data: &[f32], active: &AtomicBool, levels: &SharedLevels) {
43 if data.is_empty() {
44 return;
45 }
46
47 let mut sum = 0.0;
48 for &s in data {
49 sum += s * s;
50 }
51 let rms = (sum / data.len() as f32).sqrt();
52
53 let threshold = 0.02;
54 active.store(rms > threshold, std::sync::atomic::Ordering::Relaxed);
55 push_level(levels, rms);
56}
57
58fn process_input_i16(data: &[i16], active: &AtomicBool, levels: &SharedLevels) {
59 if data.is_empty() {
60 return;
61 }
62 let norm = i16::MAX as f32;
63 let mut sum = 0.0;
64 for &s in data {
65 let v = s as f32 / norm;
66 sum += v * v;
67 }
68 let rms = (sum / data.len() as f32).sqrt();
69 let threshold = 0.02;
70 active.store(rms > threshold, std::sync::atomic::Ordering::Relaxed);
71 push_level(levels, rms);
72}
73
74fn process_input_u16(data: &[u16], active: &AtomicBool, levels: &SharedLevels) {
75 if data.is_empty() {
76 return;
77 }
78 let max = u16::MAX as f32;
79 let mid = max / 2.0;
80 let mut sum = 0.0;
81 for &s in data {
82 let v = (s as f32 - mid) / mid;
83 sum += v * v;
84 }
85 let rms = (sum / data.len() as f32).sqrt();
86 let threshold = 0.02;
87 active.store(rms > threshold, std::sync::atomic::Ordering::Relaxed);
88 push_level(levels, rms);
89}
90
91fn push_level(levels: &SharedLevels, rms: f32) {
92 const MAX_SAMPLES: usize = 64;
93
94 if let Ok(mut buf) = levels.lock() {
95 buf.push_back(rms);
96 if buf.len() > MAX_SAMPLES {
97 buf.pop_front();
98 }
99 }
100}
101
102pub fn spawn_mic_listener(active: Arc<AtomicBool>, levels: SharedLevels) -> Result<()> {
103 std::thread::spawn(move || {
104 if let Err(e) = mic_loop(active, levels) {
105 eprintln!("mic loop error: {e:?}");
106 }
107 });
108 Ok(())
109}
110
111pub fn mic_loop(active: Arc<AtomicBool>, levels: SharedLevels) -> Result<()> {
112 let host = cpal::default_host();
113 let device = host
114 .default_input_device()
115 .ok_or_else(|| anyhow::anyhow!("no default input device"))?;
116 println!("Using input device: {}", device.name()?);
117
118 let supported_config = device
119 .default_input_config()
120 .map_err(|e| anyhow::anyhow!("failed to get default input config: {e}"))?;
121 let sample_format = supported_config.sample_format();
122 let config: StreamConfig = supported_config.into();
123
124 let stream = match sample_format {
125 SampleFormat::F32 => {
126 let active = active.clone();
127 let levels = levels.clone();
128 let err_fn = |err| eprintln!("cpal input stream error: {err}");
129 device.build_input_stream(
130 &config,
131 move |data: &[f32], _| process_input_f32(data, &active, &levels),
132 err_fn,
133 None,
134 )?
135 }
136 SampleFormat::I16 => {
137 let active = active.clone();
138 let levels = levels.clone();
139 let err_fn = |err| eprintln!("cpal input stream error: {err}");
140 device.build_input_stream(
141 &config,
142 move |data: &[i16], _| process_input_i16(data, &active, &levels),
143 err_fn,
144 None,
145 )?
146 }
147 SampleFormat::U16 => {
148 let active = active.clone();
149 let levels = levels.clone();
150 let err_fn = |err| eprintln!("cpal input stream error: {err}");
151 device.build_input_stream(
152 &config,
153 move |data: &[u16], _| process_input_u16(data, &active, &levels),
154 err_fn,
155 None,
156 )?
157 }
158
159 other => {
160 return Err(anyhow!("Unsupported sample format: {other:?}"));
161 }
162 };
163
164 stream.play()?;
165
166 loop {
167 std::thread::sleep(time::Duration::from_secs(1));
168 }
169}