ssloc/
realtime.rs

1use std::fmt::Debug;
2use std::str::FromStr;
3
4use alsa::pcm::{self, Access, HwParams, IoFormat};
5use alsa::{Direction, ValueOr, PCM};
6use derive_more::Display;
7use forr::forr;
8use num::{Num, ToPrimitive};
9use serde::{Deserialize, Serialize};
10
11use crate::{Audio, F};
12
13#[derive(Clone, Copy, Display, Deserialize, Serialize, Debug, PartialEq, Eq)]
14#[serde(rename_all = "lowercase")]
15pub enum Format {
16    S8,
17    U8,
18    S16,
19    U16,
20    S32,
21    U32,
22    F32,
23    F64,
24}
25impl FromStr for Format {
26    type Err = String;
27
28    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
29        Ok(match s.to_ascii_lowercase().as_str() {
30            "s8" => Format::S8,
31            "u8" => Format::U8,
32            "s16" => Format::S16,
33            "u16" => Format::U16,
34            "s32" => Format::S32,
35            "u32" => Format::U32,
36            "f32" => Format::F32,
37            "f64" => Format::F64,
38            e => return Err(format!("Unsupported audio format {e:?}")),
39        })
40    }
41}
42
43impl From<Format> for pcm::Format {
44    fn from(value: Format) -> Self {
45        match value {
46            Format::S8 => Self::S8,
47            Format::U8 => Self::U8,
48            Format::S16 => Self::s16(),
49            Format::U16 => Self::u16(),
50            Format::S32 => Self::s32(),
51            Format::U32 => Self::u32(),
52            Format::F32 => Self::float(),
53            Format::F64 => Self::float64(),
54        }
55    }
56}
57
58impl PartialEq<str> for Format {
59    fn eq(&self, other: &str) -> bool {
60        matches!(
61            (other, self),
62            ("s8" | "S8", Format::S8)
63                | ("u8" | "U8", Format::U8)
64                | ("s16" | "S16", Format::S16)
65                | ("u16" | "U16", Format::U16)
66                | ("s32" | "S32", Format::S32)
67                | ("u32" | "U32", Format::U32)
68                | ("f32" | "F32", Format::F32)
69                | ("f64" | "F64", Format::F64)
70        )
71    }
72}
73
74impl Format {
75    /// Returns the supported `Format`s for a device's [`HwParams`]
76    pub fn supported<'a>(params: &'a HwParams) -> impl Iterator<Item = Format> + 'a {
77        [
78            Format::S8,
79            Format::U8,
80            Format::S16,
81            Format::U16,
82            Format::S32,
83            Format::U32,
84            Format::F32,
85            Format::F64,
86        ]
87        .into_iter()
88        .filter(|&format| params.test_format(format.into()).is_ok())
89    }
90}
91
92pub struct AudioRecorder<T> {
93    pcm: PCM,
94    channels: usize,
95    rate: u32,
96    buffer: Vec<T>,
97    // audio: Audio,
98}
99
100pub trait Normalize {
101    fn normalize(self) -> F;
102}
103
104impl Normalize for f32 {
105    fn normalize(self) -> F {
106        self.into()
107    }
108}
109
110impl Normalize for F {
111    fn normalize(self) -> F {
112        self
113    }
114}
115
116forr! { $signed:ty, $unsigned:ty in [i8, u8, i16, u16, i32, u32] $*
117    impl Normalize for $signed {
118        fn normalize(self) -> F {
119            self as F / (Self::MAX as F - 1.)
120        }
121    }
122    impl Normalize for $unsigned {
123        fn normalize(self) -> F {
124            (self as F - $signed::MAX as F) / ($signed::MAX as F - 1.)
125        }
126    }
127}
128
129impl<T: Copy + IoFormat + Num + ToPrimitive + Normalize> AudioRecorder<T> {
130    #[allow(clippy::missing_errors_doc)]
131    pub fn new(
132        name: impl AsRef<str>,
133        channels: usize,
134        rate: u32,
135        format: Format,
136        duration: f64,
137    ) -> Result<Self, alsa::Error> {
138        let pcm = PCM::new(name.as_ref(), Direction::Capture, false)?;
139        {
140            let hwp = HwParams::any(&pcm)?;
141            #[allow(clippy::cast_possible_truncation)]
142            hwp.set_channels(channels as u32)?;
143            hwp.set_rate(rate, ValueOr::Nearest)?;
144            hwp.set_format(format.into())?;
145            hwp.set_access(Access::RWInterleaved)?;
146            pcm.hw_params(&hwp)?;
147        }
148        pcm.start()?;
149        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
150        Ok(AudioRecorder {
151            pcm,
152            channels,
153            rate,
154            // must be a full channels wide so only rate as float
155            buffer: vec![T::zero(); (duration * f64::from(rate)) as usize * channels],
156            // audio: Audio::empty(rate as F),
157        })
158    }
159
160    // TODO borrow and reuse audio
161    #[allow(clippy::missing_errors_doc)]
162    pub fn record(&mut self) -> Result<Audio, alsa::Error> {
163        _ = self.pcm.prepare();
164        let io = self.pcm.io_checked()?;
165
166        // if recording stopped discard end of buffer
167        let len = io.readi(&mut self.buffer)? * self.channels;
168        Ok(Audio::from_interleaved(
169            self.rate.into(),
170            self.channels,
171            self.buffer[..len].iter().copied().map(Normalize::normalize),
172        ))
173    }
174}
175
176#[macro_export]
177macro_rules! for_format {
178    ($format:expr, $expr:expr) => {
179        match $format {
180            Format::S8 => {
181                type FORMAT = i8;
182                $expr
183            }
184            Format::U8 => {
185                type FORMAT = u8;
186                $expr
187            }
188            Format::S16 => {
189                type FORMAT = i16;
190                $expr
191            }
192            Format::U16 => {
193                type FORMAT = u16;
194                $expr
195            }
196            Format::S32 => {
197                type FORMAT = i32;
198                $expr
199            }
200            Format::U32 => {
201                type FORMAT = u32;
202                $expr
203            }
204            Format::F32 => {
205                type FORMAT = f32;
206                $expr
207            }
208            Format::F64 => {
209                type FORMAT = f64;
210                $expr
211            }
212        }
213    };
214}