1use anyhow::{anyhow, Context, Result};
9use parking_lot::Mutex;
10use std::num::NonZeroU8;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use std::time::Instant;
14
15const BITS_PER_SAMPLE: usize = 24;
16const CHANNELS: usize = 2;
17pub const MAX_MINUTES: u32 = 15;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RecordFormat {
24 Flac,
26 Ogg,
29}
30
31impl RecordFormat {
32 pub fn label(self) -> &'static str {
33 match self {
34 RecordFormat::Flac => "flac",
35 RecordFormat::Ogg => "ogg",
36 }
37 }
38
39 pub fn extension(self) -> &'static str {
40 match self {
41 RecordFormat::Flac => "flac",
42 RecordFormat::Ogg => "ogg",
43 }
44 }
45
46 pub fn toggle(self) -> Self {
47 match self {
48 RecordFormat::Flac => RecordFormat::Ogg,
49 RecordFormat::Ogg => RecordFormat::Flac,
50 }
51 }
52}
53
54pub struct RecorderState {
59 buffer: Mutex<Option<Vec<f32>>>,
60 pub started_at: Mutex<Option<Instant>>,
61 pub sample_rate: u32,
62 pub max_samples: usize,
63 pub format: Mutex<RecordFormat>,
66}
67
68impl RecorderState {
69 pub fn new(sample_rate: u32) -> Arc<Self> {
70 let max_samples = MAX_MINUTES as usize * 60 * sample_rate as usize * CHANNELS;
71 Arc::new(Self {
72 buffer: Mutex::new(None),
73 started_at: Mutex::new(None),
74 sample_rate,
75 max_samples,
76 format: Mutex::new(RecordFormat::Flac),
77 })
78 }
79
80 pub fn is_recording(&self) -> bool {
81 self.buffer.lock().is_some()
82 }
83
84 pub fn elapsed_seconds(&self) -> f32 {
85 self.started_at
86 .lock()
87 .map(|t| t.elapsed().as_secs_f32())
88 .unwrap_or(0.0)
89 }
90
91 pub fn start(&self) {
92 let mut buf = self.buffer.lock();
93 if buf.is_none() {
94 *buf = Some(Vec::with_capacity(self.sample_rate as usize * CHANNELS * 30));
95 }
96 *self.started_at.lock() = Some(Instant::now());
97 }
98
99 pub fn push_frame(&self, l: f32, r: f32) {
101 let mut guard = self.buffer.lock();
102 if let Some(buf) = guard.as_mut() {
103 if buf.len() + 2 <= self.max_samples {
104 buf.push(l);
105 buf.push(r);
106 }
107 }
108 }
109
110 pub fn stop_and_encode(&self, dir: &Path) -> Result<PathBuf> {
112 std::fs::create_dir_all(dir).context("create recordings dir")?;
113 let samples = self.buffer.lock().take().ok_or_else(|| anyhow!("not recording"))?;
114 *self.started_at.lock() = None;
115 let format = *self.format.lock();
116
117 let name = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
118 let path = dir.join(format!("{name}.{}", format.extension()));
119 let sr = self.sample_rate;
120 let target = path.clone();
121
122 std::thread::spawn(move || {
123 let result = match format {
124 RecordFormat::Flac => encode_flac(&samples, sr, &target),
125 RecordFormat::Ogg => encode_ogg(&samples, sr, &target),
126 };
127 match result {
128 Ok(()) => tracing::info!(
129 "wrote {} ({:.1}s, {:.1} MB)",
130 target.display(),
131 samples.len() as f32 / (sr as f32 * CHANNELS as f32),
132 std::fs::metadata(&target)
133 .map(|m| m.len() as f32 / 1_048_576.0)
134 .unwrap_or(0.0),
135 ),
136 Err(e) => tracing::error!(
137 "{} encode failed for {}: {e}",
138 format.label().to_uppercase(),
139 target.display()
140 ),
141 }
142 });
143
144 Ok(path)
145 }
146
147 pub fn toggle_format(&self) -> RecordFormat {
148 let mut f = self.format.lock();
149 *f = f.toggle();
150 *f
151 }
152
153 pub fn current_format(&self) -> RecordFormat {
154 *self.format.lock()
155 }
156}
157
158fn encode_flac(samples: &[f32], sample_rate: u32, path: &Path) -> Result<()> {
159 let scale = ((1i32 << (BITS_PER_SAMPLE - 1)) - 1) as f32;
161 let int_samples: Vec<i32> = samples
162 .iter()
163 .map(|&s| (s.clamp(-1.0, 1.0) * scale) as i32)
164 .collect();
165
166 use flacenc::error::Verify;
167 let config = flacenc::config::Encoder::default()
168 .into_verified()
169 .map_err(|(_, e)| anyhow!("flacenc config verify: {e:?}"))?;
170
171 let source = flacenc::source::MemSource::from_samples(
172 &int_samples,
173 CHANNELS,
174 BITS_PER_SAMPLE,
175 sample_rate as usize,
176 );
177 let stream = flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
178 .map_err(|e| anyhow!("flacenc encode: {e:?}"))?;
179
180 use flacenc::component::BitRepr;
181 let mut sink = flacenc::bitsink::ByteSink::new();
182 stream
183 .write(&mut sink)
184 .map_err(|e| anyhow!("flacenc write: {e:?}"))?;
185
186 std::fs::write(path, sink.as_slice())
187 .with_context(|| format!("write flac to {}", path.display()))?;
188 Ok(())
189}
190
191fn encode_ogg(samples: &[f32], sample_rate: u32, path: &Path) -> Result<()> {
196 let frames = samples.len() / CHANNELS;
197 let mut left = Vec::with_capacity(frames);
198 let mut right = Vec::with_capacity(frames);
199 for frame in samples.chunks_exact(CHANNELS) {
200 left.push(frame[0]);
201 right.push(frame[1]);
202 }
203
204 let file = std::fs::File::create(path)
205 .with_context(|| format!("create {}", path.display()))?;
206
207 let sr = std::num::NonZeroU32::new(sample_rate)
208 .ok_or_else(|| anyhow!("sample rate must be non-zero"))?;
209 let channels = NonZeroU8::new(CHANNELS as u8)
210 .ok_or_else(|| anyhow!("channels must be non-zero"))?;
211
212 let mut encoder = vorbis_rs::VorbisEncoderBuilder::new(sr, channels, file)
213 .map_err(|e| anyhow!("vorbis builder: {e}"))?
214 .build()
215 .map_err(|e| anyhow!("vorbis build: {e}"))?;
216
217 encoder
218 .encode_audio_block([&left[..], &right[..]])
219 .map_err(|e| anyhow!("vorbis encode: {e}"))?;
220 encoder
221 .finish()
222 .map_err(|e| anyhow!("vorbis finish: {e}"))?;
223
224 Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 fn synth_samples(seconds: f32, sr: u32) -> Vec<f32> {
232 let n = (seconds * sr as f32) as usize;
233 let mut samples = Vec::with_capacity(n * 2);
234 for i in 0..n {
235 let v = (i as f32 / sr as f32 * 440.0 * std::f32::consts::TAU).sin() * 0.5;
236 samples.push(v);
237 samples.push(v);
238 }
239 samples
240 }
241
242 #[test]
243 fn encodes_short_buffer_to_valid_ogg() {
244 let sr = 48_000u32;
245 let samples = synth_samples(0.1, sr);
246 let dir = tempfile::tempdir().unwrap();
247 let path = dir.path().join("t.ogg");
248 encode_ogg(&samples, sr, &path).unwrap();
249 let bytes = std::fs::read(&path).unwrap();
250 assert!(bytes.len() > 100, "ogg too small: {}", bytes.len());
251 assert_eq!(&bytes[..4], b"OggS");
253 }
254
255 #[test]
256 fn encodes_short_buffer_to_valid_flac() {
257 let sr = 48_000u32;
259 let n = sr as usize / 10;
260 let mut samples = Vec::with_capacity(n * 2);
261 for i in 0..n {
262 let v = (i as f32 / sr as f32 * 440.0 * std::f32::consts::TAU).sin() * 0.5;
263 samples.push(v);
264 samples.push(v);
265 }
266 let dir = tempfile::tempdir().unwrap();
267 let path = dir.path().join("t.flac");
268 encode_flac(&samples, sr, &path).unwrap();
269 let bytes = std::fs::read(&path).unwrap();
270 assert!(bytes.len() > 100, "flac too small: {}", bytes.len());
271 assert_eq!(&bytes[..4], b"fLaC");
273 }
274}