Skip to content

Commit 4c77854

Browse files
authored
Better errors alsa backend (#797)
Better error handling in Alsa backend * More consistent error messages * Bail on fatal errors in player * Capture and log the original error as a warning when trying to write to PCM before trying to recover
1 parent 51a6972 commit 4c77854

4 files changed

Lines changed: 187 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- [playback] `alsamixer`: complete rewrite (breaking)
2525
- [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise
2626
- [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise
27+
- [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking)
2728

2829
### Deprecated
2930
- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate
@@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4243
- [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected
4344
- [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness
4445
- [playback] `alsa`: revert buffer size to ~500 ms
46+
- [playback] `alsa`: better error handling
4547

4648
## [0.2.0] - 2021-05-04
4749

playback/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ rand = "0.8"
5151
rand_distr = "0.4"
5252

5353
[features]
54-
alsa-backend = ["alsa"]
54+
alsa-backend = ["alsa", "thiserror"]
5555
portaudio-backend = ["portaudio-rs"]
5656
pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"]
5757
jackaudio-backend = ["jack"]

playback/src/audio_backend/alsa.rs

Lines changed: 162 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,90 @@ use crate::decoder::AudioPacket;
55
use crate::{NUM_CHANNELS, SAMPLE_RATE};
66
use alsa::device_name::HintIter;
77
use alsa::pcm::{Access, Format, HwParams, PCM};
8-
use alsa::{Direction, Error, ValueOr};
8+
use alsa::{Direction, ValueOr};
99
use std::cmp::min;
10-
use std::ffi::CString;
1110
use std::io;
1211
use std::process::exit;
1312
use std::time::Duration;
13+
use thiserror::Error;
1414

1515
// 125 ms Period time * 4 periods = 0.5 sec buffer.
1616
const PERIOD_TIME: Duration = Duration::from_millis(125);
1717
const NUM_PERIODS: u32 = 4;
1818

19+
#[derive(Debug, Error)]
20+
enum AlsaError {
21+
#[error("AlsaSink, device {device} may be invalid or busy, {err}")]
22+
PCMSetUpError { device: String, err: alsa::Error },
23+
#[error("AlsaSink, device {device} unsupported access type RWInterleaved, {err}")]
24+
UnsupportedAccessTypeError { device: String, err: alsa::Error },
25+
#[error("AlsaSink, device {device} unsupported format {format:?}, {err}")]
26+
UnsupportedFormatError {
27+
device: String,
28+
format: AudioFormat,
29+
err: alsa::Error,
30+
},
31+
#[error("AlsaSink, device {device} unsupported sample rate {samplerate}, {err}")]
32+
UnsupportedSampleRateError {
33+
device: String,
34+
samplerate: u32,
35+
err: alsa::Error,
36+
},
37+
#[error("AlsaSink, device {device} unsupported channel count {channel_count}, {err}")]
38+
UnsupportedChannelCountError {
39+
device: String,
40+
channel_count: u8,
41+
err: alsa::Error,
42+
},
43+
#[error("AlsaSink Hardware Parameters Error, {0}")]
44+
HwParamsError(alsa::Error),
45+
#[error("AlsaSink Software Parameters Error, {0}")]
46+
SwParamsError(alsa::Error),
47+
#[error("AlsaSink PCM Error, {0}")]
48+
PCMError(alsa::Error),
49+
}
50+
1951
pub struct AlsaSink {
2052
pcm: Option<PCM>,
2153
format: AudioFormat,
2254
device: String,
2355
period_buffer: Vec<u8>,
2456
}
2557

26-
fn list_outputs() {
58+
fn list_outputs() -> io::Result<()> {
59+
println!("Listing available Alsa outputs:");
2760
for t in &["pcm", "ctl", "hwdep"] {
2861
println!("{} devices:", t);
29-
let i = HintIter::new(None, &*CString::new(*t).unwrap()).unwrap();
62+
let i = match HintIter::new_str(None, &t) {
63+
Ok(i) => i,
64+
Err(e) => {
65+
return Err(io::Error::new(io::ErrorKind::Other, e));
66+
}
67+
};
3068
for a in i {
3169
if let Some(Direction::Playback) = a.direction {
3270
// mimic aplay -L
33-
println!(
34-
"{}\n\t{}\n",
35-
a.name.unwrap(),
36-
a.desc.unwrap().replace("\n", "\n\t")
37-
);
71+
let name = a
72+
.name
73+
.ok_or(io::Error::new(io::ErrorKind::Other, "Could not parse name"))?;
74+
let desc = a
75+
.desc
76+
.ok_or(io::Error::new(io::ErrorKind::Other, "Could not parse desc"))?;
77+
println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t"));
3878
}
3979
}
4080
}
81+
82+
Ok(())
4183
}
4284

43-
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Box<Error>> {
44-
let pcm = PCM::new(dev_name, Direction::Playback, false)?;
85+
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> {
86+
let pcm =
87+
PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PCMSetUpError {
88+
device: dev_name.to_string(),
89+
err: e,
90+
})?;
91+
4592
let alsa_format = match format {
4693
AudioFormat::F64 => Format::float64(),
4794
AudioFormat::F32 => Format::float(),
@@ -56,23 +103,64 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Box<
56103
};
57104

58105
let bytes_per_period = {
59-
let hwp = HwParams::any(&pcm)?;
60-
hwp.set_access(Access::RWInterleaved)?;
61-
hwp.set_format(alsa_format)?;
62-
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?;
63-
hwp.set_channels(NUM_CHANNELS as u32)?;
106+
let hwp = HwParams::any(&pcm).map_err(|e| AlsaError::HwParamsError(e))?;
107+
hwp.set_access(Access::RWInterleaved).map_err(|e| {
108+
AlsaError::UnsupportedAccessTypeError {
109+
device: dev_name.to_string(),
110+
err: e,
111+
}
112+
})?;
113+
114+
hwp.set_format(alsa_format)
115+
.map_err(|e| AlsaError::UnsupportedFormatError {
116+
device: dev_name.to_string(),
117+
format: format,
118+
err: e,
119+
})?;
120+
121+
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| {
122+
AlsaError::UnsupportedSampleRateError {
123+
device: dev_name.to_string(),
124+
samplerate: SAMPLE_RATE,
125+
err: e,
126+
}
127+
})?;
128+
129+
hwp.set_channels(NUM_CHANNELS as u32).map_err(|e| {
130+
AlsaError::UnsupportedChannelCountError {
131+
device: dev_name.to_string(),
132+
channel_count: NUM_CHANNELS,
133+
err: e,
134+
}
135+
})?;
136+
64137
// Deal strictly in time and periods.
65-
hwp.set_periods(NUM_PERIODS, ValueOr::Nearest)?;
66-
hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest)?;
67-
pcm.hw_params(&hwp)?;
138+
hwp.set_periods(NUM_PERIODS, ValueOr::Nearest)
139+
.map_err(|e| AlsaError::HwParamsError(e))?;
140+
141+
hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest)
142+
.map_err(|e| AlsaError::HwParamsError(e))?;
143+
144+
pcm.hw_params(&hwp).map_err(|e| AlsaError::PCMError(e))?;
145+
146+
let swp = pcm
147+
.sw_params_current()
148+
.map_err(|e| AlsaError::PCMError(e))?;
68149

69-
let swp = pcm.sw_params_current()?;
70150
// Don't assume we got what we wanted.
71151
// Ask to make sure.
72-
let frames_per_period = hwp.get_period_size()?;
152+
let frames_per_period = hwp
153+
.get_period_size()
154+
.map_err(|e| AlsaError::HwParamsError(e))?;
155+
156+
let frames_per_buffer = hwp
157+
.get_buffer_size()
158+
.map_err(|e| AlsaError::HwParamsError(e))?;
159+
160+
swp.set_start_threshold(frames_per_buffer - frames_per_period)
161+
.map_err(|e| AlsaError::SwParamsError(e))?;
73162

74-
swp.set_start_threshold(hwp.get_buffer_size()? - frames_per_period)?;
75-
pcm.sw_params(&swp)?;
163+
pcm.sw_params(&swp).map_err(|e| AlsaError::PCMError(e))?;
76164

77165
// Let ALSA do the math for us.
78166
pcm.frames_to_bytes(frames_per_period) as usize
@@ -83,19 +171,23 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Box<
83171

84172
impl Open for AlsaSink {
85173
fn open(device: Option<String>, format: AudioFormat) -> Self {
86-
info!("Using Alsa sink with format: {:?}", format);
87-
88174
let name = match device.as_deref() {
89-
Some("?") => {
90-
println!("Listing available Alsa outputs:");
91-
list_outputs();
92-
exit(0)
93-
}
175+
Some("?") => match list_outputs() {
176+
Ok(_) => {
177+
exit(0);
178+
}
179+
Err(err) => {
180+
error!("Error listing Alsa outputs, {}", err);
181+
exit(1);
182+
}
183+
},
94184
Some(device) => device,
95185
None => "default",
96186
}
97187
.to_string();
98188

189+
info!("Using AlsaSink with format: {:?}", format);
190+
99191
Self {
100192
pcm: None,
101193
format,
@@ -108,18 +200,13 @@ impl Open for AlsaSink {
108200
impl Sink for AlsaSink {
109201
fn start(&mut self) -> io::Result<()> {
110202
if self.pcm.is_none() {
111-
let pcm = open_device(&self.device, self.format);
112-
match pcm {
113-
Ok((p, bytes_per_period)) => {
114-
self.pcm = Some(p);
203+
match open_device(&self.device, self.format) {
204+
Ok((pcm, bytes_per_period)) => {
205+
self.pcm = Some(pcm);
115206
self.period_buffer = Vec::with_capacity(bytes_per_period);
116207
}
117208
Err(e) => {
118-
error!("Alsa error PCM open {}", e);
119-
return Err(io::Error::new(
120-
io::ErrorKind::Other,
121-
"Alsa error: PCM open failed",
122-
));
209+
return Err(io::Error::new(io::ErrorKind::Other, e));
123210
}
124211
}
125212
}
@@ -131,9 +218,17 @@ impl Sink for AlsaSink {
131218
{
132219
// Write any leftover data in the period buffer
133220
// before draining the actual buffer
134-
self.write_bytes(&[]).expect("could not flush buffer");
135-
let pcm = self.pcm.as_mut().unwrap();
136-
pcm.drain().unwrap();
221+
self.write_bytes(&[])?;
222+
let pcm = self.pcm.as_mut().ok_or(io::Error::new(
223+
io::ErrorKind::Other,
224+
"Error stopping AlsaSink, PCM is None",
225+
))?;
226+
pcm.drain().map_err(|e| {
227+
io::Error::new(
228+
io::ErrorKind::Other,
229+
format!("Error stopping AlsaSink {}", e),
230+
)
231+
})?
137232
}
138233
self.pcm = None;
139234
Ok(())
@@ -154,7 +249,7 @@ impl SinkAsBytes for AlsaSink {
154249
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
155250
processed_data += data_to_buffer;
156251
if self.period_buffer.len() == self.period_buffer.capacity() {
157-
self.write_buf();
252+
self.write_buf()?;
158253
self.period_buffer.clear();
159254
}
160255
}
@@ -166,12 +261,30 @@ impl SinkAsBytes for AlsaSink {
166261
impl AlsaSink {
167262
pub const NAME: &'static str = "alsa";
168263

169-
fn write_buf(&mut self) {
170-
let pcm = self.pcm.as_mut().unwrap();
264+
fn write_buf(&mut self) -> io::Result<()> {
265+
let pcm = self.pcm.as_mut().ok_or(io::Error::new(
266+
io::ErrorKind::Other,
267+
"Error writing from AlsaSink buffer to PCM, PCM is None",
268+
))?;
171269
let io = pcm.io_bytes();
172-
match io.writei(&self.period_buffer) {
173-
Ok(_) => (),
174-
Err(err) => pcm.try_recover(err, false).unwrap(),
175-
};
270+
if let Err(err) = io.writei(&self.period_buffer) {
271+
// Capture and log the original error as a warning, and then try to recover.
272+
// If recovery fails then forward that error back to player.
273+
warn!(
274+
"Error writing from AlsaSink buffer to PCM, trying to recover {}",
275+
err
276+
);
277+
pcm.try_recover(err, false).map_err(|e| {
278+
io::Error::new(
279+
io::ErrorKind::Other,
280+
format!(
281+
"Error writing from AlsaSink buffer to PCM, recovery failed {}",
282+
e
283+
),
284+
)
285+
})?
286+
}
287+
288+
Ok(())
176289
}
177290
}

playback/src/player.rs

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::cmp::max;
22
use std::future::Future;
33
use std::io::{self, Read, Seek, SeekFrom};
44
use std::pin::Pin;
5+
use std::process::exit;
56
use std::task::{Context, Poll};
67
use std::time::{Duration, Instant};
78
use std::{mem, thread};
@@ -1057,7 +1058,10 @@ impl PlayerInternal {
10571058
}
10581059
match self.sink.start() {
10591060
Ok(()) => self.sink_status = SinkStatus::Running,
1060-
Err(err) => error!("Could not start audio: {}", err),
1061+
Err(err) => {
1062+
error!("Fatal error, could not start audio sink: {}", err);
1063+
exit(1);
1064+
}
10611065
}
10621066
}
10631067
}
@@ -1066,14 +1070,21 @@ impl PlayerInternal {
10661070
match self.sink_status {
10671071
SinkStatus::Running => {
10681072
trace!("== Stopping sink ==");
1069-
self.sink.stop().unwrap();
1070-
self.sink_status = if temporarily {
1071-
SinkStatus::TemporarilyClosed
1072-
} else {
1073-
SinkStatus::Closed
1074-
};
1075-
if let Some(callback) = &mut self.sink_event_callback {
1076-
callback(self.sink_status);
1073+
match self.sink.stop() {
1074+
Ok(()) => {
1075+
self.sink_status = if temporarily {
1076+
SinkStatus::TemporarilyClosed
1077+
} else {
1078+
SinkStatus::Closed
1079+
};
1080+
if let Some(callback) = &mut self.sink_event_callback {
1081+
callback(self.sink_status);
1082+
}
1083+
}
1084+
Err(err) => {
1085+
error!("Fatal error, could not stop audio sink: {}", err);
1086+
exit(1);
1087+
}
10771088
}
10781089
}
10791090
SinkStatus::TemporarilyClosed => {
@@ -1294,8 +1305,8 @@ impl PlayerInternal {
12941305
}
12951306

12961307
if let Err(err) = self.sink.write(&packet, &mut self.converter) {
1297-
error!("Could not write audio: {}", err);
1298-
self.ensure_sink_stopped(false);
1308+
error!("Fatal error, could not write audio to audio sink: {}", err);
1309+
exit(1);
12991310
}
13001311
}
13011312
}

0 commit comments

Comments
 (0)