Skip to content

Commit 2466e0b

Browse files
authored
Merge branch 'librespot-org:dev' into fix_pipe_backend
2 parents 0bece0d + 4c77854 commit 2466e0b

8 files changed

Lines changed: 206 additions & 65 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

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,6 @@ section = "sound"
9090
priority = "optional"
9191
assets = [
9292
["target/release/librespot", "usr/bin/", "755"],
93-
["contrib/librespot.service", "lib/systemd/system/", "644"]
93+
["contrib/librespot.service", "lib/systemd/system/", "644"],
94+
["contrib/librespot.user.service", "lib/systemd/user/", "644"]
9495
]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ The above command will create a receiver named ```Librespot```, with bitrate set
8989
A full list of runtime options are available [here](https://github.com/librespot-org/librespot/wiki/Options)
9090

9191
_Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._
92+
9293
## Contact
9394
Come and hang out on gitter if you need help or want to offer some.
9495
https://gitter.im/librespot-org/spotify-connect-resources

contrib/librespot.service

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[Unit]
2-
Description=Librespot
2+
Description=Librespot (an open source Spotify client)
3+
Documentation=https://github.com/librespot-org/librespot
4+
Documentation=https://github.com/librespot-org/librespot/wiki/Options
35
Requires=network-online.target
46
After=network-online.target
57

@@ -8,8 +10,7 @@ User=nobody
810
Group=audio
911
Restart=always
1012
RestartSec=10
11-
ExecStart=/usr/bin/librespot -n "%p on %H"
13+
ExecStart=/usr/bin/librespot --name "%p@%H"
1214

1315
[Install]
1416
WantedBy=multi-user.target
15-

contrib/librespot.user.service

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[Unit]
2+
Description=Librespot (an open source Spotify client)
3+
Documentation=https://github.com/librespot-org/librespot
4+
Documentation=https://github.com/librespot-org/librespot/wiki/Options
5+
6+
[Service]
7+
Restart=always
8+
RestartSec=10
9+
ExecStart=/usr/bin/librespot --name "%u@%H"
10+
11+
[Install]
12+
WantedBy=default.target

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
}

0 commit comments

Comments
 (0)