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