1 use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
2 use crate::config::AudioFormat;
3 use crate::convert::Converter;
4 use crate::decoder::AudioPacket;
5 use crate::{NUM_CHANNELS, SAMPLE_RATE};
6 use alsa::device_name::HintIter;
7 use alsa::pcm::{Access, Format, HwParams, PCM};
8 use alsa::{Direction, ValueOr};
9 use std::cmp::min;
10 use std::process::exit;
11 use std::time::Duration;
12 use thiserror::Error;
13 
14 // 0.5 sec buffer.
15 const PERIOD_TIME: Duration = Duration::from_millis(100);
16 const BUFFER_TIME: Duration = Duration::from_millis(500);
17 
18 #[derive(Debug, Error)]
19 enum AlsaError {
20     #[error("<AlsaSink> Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")]
21     UnsupportedFormat {
22         device: String,
23         alsa_format: Format,
24         format: AudioFormat,
25         e: alsa::Error,
26     },
27 
28     #[error("<AlsaSink> Device {device} Unsupported Channel Count {channel_count}, {e}")]
29     UnsupportedChannelCount {
30         device: String,
31         channel_count: u8,
32         e: alsa::Error,
33     },
34 
35     #[error("<AlsaSink> Device {device} Unsupported Sample Rate {samplerate}, {e}")]
36     UnsupportedSampleRate {
37         device: String,
38         samplerate: u32,
39         e: alsa::Error,
40     },
41 
42     #[error("<AlsaSink> Device {device} Unsupported Access Type RWInterleaved, {e}")]
43     UnsupportedAccessType { device: String, e: alsa::Error },
44 
45     #[error("<AlsaSink> Device {device} May be Invalid, Busy, or Already in Use, {e}")]
46     PcmSetUp { device: String, e: alsa::Error },
47 
48     #[error("<AlsaSink> Failed to Drain PCM Buffer, {0}")]
49     DrainFailure(alsa::Error),
50 
51     #[error("<AlsaSink> {0}")]
52     OnWrite(alsa::Error),
53 
54     #[error("<AlsaSink> Hardware, {0}")]
55     HwParams(alsa::Error),
56 
57     #[error("<AlsaSink> Software, {0}")]
58     SwParams(alsa::Error),
59 
60     #[error("<AlsaSink> PCM, {0}")]
61     Pcm(alsa::Error),
62 
63     #[error("<AlsaSink> Could Not Parse Ouput Name(s) and/or Description(s)")]
64     Parsing,
65 
66     #[error("<AlsaSink>")]
67     NotConnected,
68 }
69 
70 impl From<AlsaError> for SinkError {
from(e: AlsaError) -> SinkError71     fn from(e: AlsaError) -> SinkError {
72         use AlsaError::*;
73         let es = e.to_string();
74         match e {
75             DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
76             PcmSetUp { .. } => SinkError::ConnectionRefused(es),
77             NotConnected => SinkError::NotConnected(es),
78             _ => SinkError::InvalidParams(es),
79         }
80     }
81 }
82 
83 pub struct AlsaSink {
84     pcm: Option<PCM>,
85     format: AudioFormat,
86     device: String,
87     period_buffer: Vec<u8>,
88 }
89 
list_outputs() -> SinkResult<()>90 fn list_outputs() -> SinkResult<()> {
91     println!("Listing available Alsa outputs:");
92     for t in &["pcm", "ctl", "hwdep"] {
93         println!("{} devices:", t);
94 
95         let i = HintIter::new_str(None, t).map_err(|_| AlsaError::Parsing)?;
96 
97         for a in i {
98             if let Some(Direction::Playback) = a.direction {
99                 // mimic aplay -L
100                 let name = a.name.ok_or(AlsaError::Parsing)?;
101                 let desc = a.desc.ok_or(AlsaError::Parsing)?;
102 
103                 println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t"));
104             }
105         }
106     }
107 
108     Ok(())
109 }
110 
open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>111 fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> {
112     let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp {
113         device: dev_name.to_string(),
114         e,
115     })?;
116 
117     let alsa_format = match format {
118         AudioFormat::F64 => Format::float64(),
119         AudioFormat::F32 => Format::float(),
120         AudioFormat::S32 => Format::s32(),
121         AudioFormat::S24 => Format::s24(),
122         AudioFormat::S16 => Format::s16(),
123 
124         #[cfg(target_endian = "little")]
125         AudioFormat::S24_3 => Format::S243LE,
126         #[cfg(target_endian = "big")]
127         AudioFormat::S24_3 => Format::S243BE,
128     };
129 
130     let bytes_per_period = {
131         let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?;
132 
133         hwp.set_access(Access::RWInterleaved)
134             .map_err(|e| AlsaError::UnsupportedAccessType {
135                 device: dev_name.to_string(),
136                 e,
137             })?;
138 
139         hwp.set_format(alsa_format)
140             .map_err(|e| AlsaError::UnsupportedFormat {
141                 device: dev_name.to_string(),
142                 alsa_format,
143                 format,
144                 e,
145             })?;
146 
147         hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| {
148             AlsaError::UnsupportedSampleRate {
149                 device: dev_name.to_string(),
150                 samplerate: SAMPLE_RATE,
151                 e,
152             }
153         })?;
154 
155         hwp.set_channels(NUM_CHANNELS as u32)
156             .map_err(|e| AlsaError::UnsupportedChannelCount {
157                 device: dev_name.to_string(),
158                 channel_count: NUM_CHANNELS,
159                 e,
160             })?;
161 
162         hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest)
163             .map_err(AlsaError::HwParams)?;
164 
165         hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest)
166             .map_err(AlsaError::HwParams)?;
167 
168         pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?;
169 
170         let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?;
171 
172         // Don't assume we got what we wanted. Ask to make sure.
173         let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?;
174 
175         let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?;
176 
177         swp.set_start_threshold(frames_per_buffer - frames_per_period)
178             .map_err(AlsaError::SwParams)?;
179 
180         pcm.sw_params(&swp).map_err(AlsaError::Pcm)?;
181 
182         trace!("Frames per Buffer: {:?}", frames_per_buffer);
183         trace!("Frames per Period: {:?}", frames_per_period);
184 
185         // Let ALSA do the math for us.
186         pcm.frames_to_bytes(frames_per_period) as usize
187     };
188 
189     trace!("Period Buffer size in bytes: {:?}", bytes_per_period);
190 
191     Ok((pcm, bytes_per_period))
192 }
193 
194 impl Open for AlsaSink {
open(device: Option<String>, format: AudioFormat) -> Self195     fn open(device: Option<String>, format: AudioFormat) -> Self {
196         let name = match device.as_deref() {
197             Some("?") => match list_outputs() {
198                 Ok(_) => {
199                     exit(0);
200                 }
201                 Err(e) => {
202                     error!("{}", e);
203                     exit(1);
204                 }
205             },
206             Some(device) => device,
207             None => "default",
208         }
209         .to_string();
210 
211         info!("Using AlsaSink with format: {:?}", format);
212 
213         Self {
214             pcm: None,
215             format,
216             device: name,
217             period_buffer: vec![],
218         }
219     }
220 }
221 
222 impl Sink for AlsaSink {
start(&mut self) -> SinkResult<()>223     fn start(&mut self) -> SinkResult<()> {
224         if self.pcm.is_none() {
225             let (pcm, bytes_per_period) = open_device(&self.device, self.format)?;
226             self.pcm = Some(pcm);
227 
228             if self.period_buffer.capacity() != bytes_per_period {
229                 self.period_buffer = Vec::with_capacity(bytes_per_period);
230             }
231 
232             // Should always match the "Period Buffer size in bytes: " trace! message.
233             trace!(
234                 "Period Buffer capacity: {:?}",
235                 self.period_buffer.capacity()
236             );
237         }
238 
239         Ok(())
240     }
241 
stop(&mut self) -> SinkResult<()>242     fn stop(&mut self) -> SinkResult<()> {
243         // Zero fill the remainder of the period buffer and
244         // write any leftover data before draining the actual PCM buffer.
245         self.period_buffer.resize(self.period_buffer.capacity(), 0);
246         self.write_buf()?;
247 
248         let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?;
249 
250         pcm.drain().map_err(AlsaError::DrainFailure)?;
251 
252         Ok(())
253     }
254 
255     sink_as_bytes!();
256 }
257 
258 impl SinkAsBytes for AlsaSink {
write_bytes(&mut self, data: &[u8]) -> SinkResult<()>259     fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
260         let mut start_index = 0;
261         let data_len = data.len();
262         let capacity = self.period_buffer.capacity();
263 
264         loop {
265             let data_left = data_len - start_index;
266             let space_left = capacity - self.period_buffer.len();
267             let data_to_buffer = min(data_left, space_left);
268             let end_index = start_index + data_to_buffer;
269 
270             self.period_buffer
271                 .extend_from_slice(&data[start_index..end_index]);
272 
273             if self.period_buffer.len() == capacity {
274                 self.write_buf()?;
275             }
276 
277             if end_index == data_len {
278                 break Ok(());
279             }
280 
281             start_index = end_index;
282         }
283     }
284 }
285 
286 impl AlsaSink {
287     pub const NAME: &'static str = "alsa";
288 
write_buf(&mut self) -> SinkResult<()>289     fn write_buf(&mut self) -> SinkResult<()> {
290         let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?;
291 
292         if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) {
293             // Capture and log the original error as a warning, and then try to recover.
294             // If recovery fails then forward that error back to player.
295             warn!(
296                 "Error writing from AlsaSink buffer to PCM, trying to recover, {}",
297                 e
298             );
299 
300             pcm.try_recover(e, false).map_err(AlsaError::OnWrite)?
301         }
302 
303         self.period_buffer.clear();
304         Ok(())
305     }
306 }
307