amico_hal/os/common/audio/
control.rs

1use cpal::traits::{HostTrait, StreamTrait};
2use lame::Lame;
3use rodio::{Decoder, OutputStream};
4use rodio::{DeviceTrait, Sink};
5use std::fs::File;
6use std::io::{BufReader, BufWriter, Write};
7use std::path::Path;
8use std::sync::{Arc, Mutex};
9use std::thread;
10use std::time::Duration;
11
12#[derive(Debug, thiserror::Error)]
13pub enum AudioPlaybackError {
14    #[error("Failed to create audio file")]
15    CreateError(#[from] std::io::Error),
16
17    #[error("Failed to create output stream")]
18    StreamError(#[from] rodio::StreamError),
19
20    #[error("Failed to decode audio file")]
21    DecodeError(#[from] rodio::decoder::DecoderError),
22
23    #[error("Failed to play audio source")]
24    PlayError(#[from] rodio::PlayError),
25
26    #[error("Failed to join playback handle")]
27    SpawnError(#[from] tokio::task::JoinError),
28}
29
30pub async fn playback(filepath: &str) -> Result<(), AudioPlaybackError> {
31    let filepath = filepath.to_string();
32    // Spawn blocking operation in a separate thread since audio playback uses non-Send types
33    tokio::task::spawn_blocking(move || {
34        // Get an output stream handle to the default physical sound device
35        let (_stream, stream_handle) =
36            OutputStream::try_default().map_err(AudioPlaybackError::StreamError)?;
37
38        // Load a sound from a file, using a path relative to Cargo.toml
39        let file = BufReader::new(File::open(filepath).map_err(AudioPlaybackError::CreateError)?);
40
41        // Decode that sound file into a source
42        let source = Decoder::new(file).map_err(AudioPlaybackError::DecodeError)?;
43
44        // _stream must live as long as the sink
45        let sink = Sink::try_new(&stream_handle).map_err(AudioPlaybackError::PlayError)?;
46        sink.append(source);
47
48        // The sound plays in a separate thread. This call will block the current thread until the sink
49        // has finished playing all its queued sounds.
50        sink.sleep_until_end();
51
52        Ok(())
53    })
54    .await
55    .map_err(AudioPlaybackError::SpawnError)?
56}
57
58#[derive(Debug, thiserror::Error)]
59pub enum AudioRecordingError {
60    #[error("Failed to join recording handle")]
61    SpawnError(#[from] tokio::task::JoinError),
62
63    #[error("Failed to create MP3 encoder")]
64    EncoderError(String),
65
66    #[error("Failed to write sample")]
67    SampleWriteError,
68
69    #[error("Failed to finalize MP3 file")]
70    FinalizationError,
71
72    #[error("IO error: {0}")]
73    IoError(#[from] std::io::Error),
74}
75
76// Since we can't use lame directly in a thread-safe context, we'll use a simpler approach
77// We'll record to a temporary WAV file and then convert it to MP3 after recording
78pub async fn record_blocking(filepath: &str) -> Result<(), AudioRecordingError> {
79    // Create a temporary WAV file path
80    let temp_wav_path = format!("{}.temp.wav", filepath);
81
82    // Record to the temporary WAV file
83    record_to_wav(&temp_wav_path)?;
84
85    // Convert WAV to MP3
86    convert_wav_to_mp3(&temp_wav_path, filepath)?;
87
88    // Remove the temporary WAV file
89    std::fs::remove_file(&temp_wav_path).map_err(AudioRecordingError::IoError)?;
90
91    Ok(())
92}
93
94// Function to record audio to a WAV file
95fn record_to_wav(filepath: &str) -> Result<(), AudioRecordingError> {
96    let host = cpal::default_host();
97    let device = host
98        .default_input_device()
99        .expect("Failed to get input device");
100    let config = device
101        .default_input_config()
102        .expect("Failed to get default input config");
103
104    let sample_rate = config.sample_rate().0;
105    let channels = config.channels() as usize;
106
107    // Ensure directory exists
108    if let Some(parent) = Path::new(filepath).parent() {
109        std::fs::create_dir_all(parent).map_err(AudioRecordingError::IoError)?;
110    }
111
112    // Create a buffer to store the recorded samples
113    let samples = Arc::new(Mutex::new(Vec::<f32>::new()));
114    let samples_clone = Arc::clone(&samples);
115
116    let err_fn = |err| eprintln!("Stream error: {}", err);
117
118    let stream = device
119        .build_input_stream(
120            &config.into(),
121            move |data: &[f32], _: &cpal::InputCallbackInfo| {
122                if let Ok(mut samples) = samples_clone.lock() {
123                    samples.extend_from_slice(data);
124                }
125            },
126            err_fn,
127            None,
128        )
129        .expect("Failed to build input stream");
130
131    println!("Recording for 3 seconds...");
132    stream.play().unwrap();
133
134    // Use a blocking sleep instead of tokio's async sleep
135    thread::sleep(Duration::from_secs(3));
136
137    drop(stream);
138
139    // Get the recorded samples
140    let samples = Arc::try_unwrap(samples)
141        .map_err(|_| AudioRecordingError::FinalizationError)?
142        .into_inner()
143        .map_err(|_| AudioRecordingError::FinalizationError)?;
144
145    // Write the samples to a WAV file
146    let spec = hound::WavSpec {
147        channels: channels as u16,
148        sample_rate,
149        bits_per_sample: 32,
150        sample_format: hound::SampleFormat::Float,
151    };
152
153    let mut writer = hound::WavWriter::create(filepath, spec).map_err(|_| {
154        AudioRecordingError::EncoderError("Failed to create WAV writer".to_string())
155    })?;
156
157    for sample in samples {
158        writer
159            .write_sample(sample)
160            .map_err(|_| AudioRecordingError::SampleWriteError)?;
161    }
162
163    writer
164        .finalize()
165        .map_err(|_| AudioRecordingError::FinalizationError)?;
166
167    Ok(())
168}
169
170// Function to convert WAV to MP3
171fn convert_wav_to_mp3(wav_path: &str, mp3_path: &str) -> Result<(), AudioRecordingError> {
172    // Read the WAV file
173    let mut reader = hound::WavReader::open(wav_path)
174        .map_err(|_| AudioRecordingError::EncoderError("Failed to open WAV file".to_string()))?;
175
176    let spec = reader.spec();
177    let channels = spec.channels as usize;
178    let sample_rate = spec.sample_rate;
179
180    // Create the MP3 encoder
181    let mut lame = Lame::new().ok_or_else(|| {
182        AudioRecordingError::EncoderError("Failed to create LAME encoder".to_string())
183    })?;
184
185    // Configure the encoder
186    lame.set_channels(channels as u8)
187        .map_err(|_| AudioRecordingError::EncoderError("Failed to set channels".to_string()))?;
188    lame.set_sample_rate(sample_rate)
189        .map_err(|_| AudioRecordingError::EncoderError("Failed to set sample rate".to_string()))?;
190    lame.set_quality(5)
191        .map_err(|_| AudioRecordingError::EncoderError("Failed to set quality".to_string()))?;
192    lame.init_params().map_err(|_| {
193        AudioRecordingError::EncoderError("Failed to initialize parameters".to_string())
194    })?;
195
196    // Create the MP3 file
197    let mp3_file = File::create(mp3_path).map_err(AudioRecordingError::IoError)?;
198    let mut mp3_writer = BufWriter::new(mp3_file);
199
200    // Read all samples from the WAV file and convert from f32 to i16
201    let samples: Vec<i16> = reader
202        .samples::<f32>()
203        .filter_map(Result::ok)
204        .map(|sample| (sample * 32767.0) as i16) // Convert f32 [-1.0, 1.0] to i16 range
205        .collect();
206
207    // Process the samples in chunks
208    let chunk_size = 1024 * channels;
209    for chunk in samples.chunks(chunk_size) {
210        // Split the interleaved samples into left and right channels
211        let mut left = Vec::with_capacity(chunk.len() / channels);
212        let mut right = Vec::with_capacity(chunk.len() / channels);
213
214        for i in (0..chunk.len()).step_by(channels) {
215            left.push(chunk[i]);
216            right.push(if channels > 1 { chunk[i + 1] } else { chunk[i] });
217        }
218
219        // Encode to MP3
220        let mut mp3_buffer = vec![0u8; chunk.len() * 4]; // Allocate enough space for MP3 data
221        let encoded_size = lame
222            .encode(&left, &right, &mut mp3_buffer)
223            .map_err(|_| AudioRecordingError::EncoderError("Failed to encode MP3".to_string()))?;
224
225        // Write the MP3 data
226        if encoded_size > 0 {
227            mp3_writer
228                .write_all(&mp3_buffer[..encoded_size])
229                .map_err(AudioRecordingError::IoError)?;
230        }
231    }
232
233    // Flush the MP3 encoder
234    let mut mp3_buffer = vec![0u8; 7200]; // Buffer for flush data
235    let encoded_size = lame.encode(&[], &[], &mut mp3_buffer).map_err(|_| {
236        AudioRecordingError::EncoderError("Failed to flush MP3 encoder".to_string())
237    })?;
238
239    if encoded_size > 0 {
240        mp3_writer
241            .write_all(&mp3_buffer[..encoded_size])
242            .map_err(AudioRecordingError::IoError)?;
243    }
244
245    // Flush the file writer
246    mp3_writer.flush().map_err(AudioRecordingError::IoError)?;
247
248    Ok(())
249}