Skip to content

Commit 9ff3398

Browse files
authored
Better errors in PulseAudio backend (#801)
* More meaningful error messages * Use F32 if a user requests F64 (F64 is not supported by PulseAudio) * Move all code that can fail to `start` where errors can be returned to prevent panics * Use drain in `stop`
1 parent 751ccf6 commit 9ff3398

3 files changed

Lines changed: 81 additions & 51 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4444
- [playback] `alsamixer`: make `--volume-ctrl {linear|log}` work as expected
4545
- [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness
4646
- [playback] `alsa`: revert buffer size to ~500 ms
47-
- [playback] `alsa`, `pipe`: better error handling
47+
- [playback] `alsa`, `pipe`, `pulseaudio`: better error handling
4848

4949
## [0.2.0] - 2021-05-04
5050

playback/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ rand_distr = "0.4"
5353
[features]
5454
alsa-backend = ["alsa", "thiserror"]
5555
portaudio-backend = ["portaudio-rs"]
56-
pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"]
56+
pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding", "thiserror"]
5757
jackaudio-backend = ["jack"]
5858
rodio-backend = ["rodio", "cpal", "thiserror"]
5959
rodiojack-backend = ["rodio", "cpal/jack", "thiserror"]

playback/src/audio_backend/pulseaudio.rs

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,117 @@ use crate::config::AudioFormat;
33
use crate::convert::Converter;
44
use crate::decoder::AudioPacket;
55
use crate::{NUM_CHANNELS, SAMPLE_RATE};
6-
use libpulse_binding::{self as pulse, stream::Direction};
6+
use libpulse_binding::{self as pulse, error::PAErr, stream::Direction};
77
use libpulse_simple_binding::Simple;
88
use std::io;
9+
use thiserror::Error;
910

1011
const APP_NAME: &str = "librespot";
1112
const STREAM_NAME: &str = "Spotify endpoint";
1213

14+
#[derive(Debug, Error)]
15+
enum PulseError {
16+
#[error("Error starting PulseAudioSink, invalid PulseAudio sample spec")]
17+
InvalidSampleSpec,
18+
#[error("Error starting PulseAudioSink, could not connect to PulseAudio server, {0}")]
19+
ConnectionRefused(PAErr),
20+
#[error("Error stopping PulseAudioSink, failed to drain PulseAudio server buffer, {0}")]
21+
DrainFailure(PAErr),
22+
#[error("Error in PulseAudioSink, Not connected to PulseAudio server")]
23+
NotConnected,
24+
#[error("Error writing from PulseAudioSink to PulseAudio server, {0}")]
25+
OnWrite(PAErr),
26+
}
27+
1328
pub struct PulseAudioSink {
1429
s: Option<Simple>,
15-
ss: pulse::sample::Spec,
1630
device: Option<String>,
1731
format: AudioFormat,
1832
}
1933

2034
impl Open for PulseAudioSink {
2135
fn open(device: Option<String>, format: AudioFormat) -> Self {
22-
info!("Using PulseAudio sink with format: {:?}", format);
36+
let mut actual_format = format;
37+
38+
if actual_format == AudioFormat::F64 {
39+
warn!("PulseAudio currently does not support F64 output");
40+
actual_format = AudioFormat::F32;
41+
}
42+
43+
info!("Using PulseAudioSink with format: {:?}", actual_format);
44+
45+
Self {
46+
s: None,
47+
device,
48+
format: actual_format,
49+
}
50+
}
51+
}
52+
53+
impl Sink for PulseAudioSink {
54+
fn start(&mut self) -> io::Result<()> {
55+
if self.s.is_some() {
56+
return Ok(());
57+
}
2358

2459
// PulseAudio calls S24 and S24_3 different from the rest of the world
25-
let pulse_format = match format {
60+
let pulse_format = match self.format {
2661
AudioFormat::F32 => pulse::sample::Format::FLOAT32NE,
2762
AudioFormat::S32 => pulse::sample::Format::S32NE,
2863
AudioFormat::S24 => pulse::sample::Format::S24_32NE,
2964
AudioFormat::S24_3 => pulse::sample::Format::S24NE,
3065
AudioFormat::S16 => pulse::sample::Format::S16NE,
31-
_ => {
32-
unimplemented!("PulseAudio currently does not support {:?} output", format)
33-
}
66+
_ => unreachable!(),
3467
};
3568

3669
let ss = pulse::sample::Spec {
3770
format: pulse_format,
3871
channels: NUM_CHANNELS,
3972
rate: SAMPLE_RATE,
4073
};
41-
debug_assert!(ss.is_valid());
4274

43-
Self {
44-
s: None,
45-
ss,
46-
device,
47-
format,
48-
}
49-
}
50-
}
51-
52-
impl Sink for PulseAudioSink {
53-
fn start(&mut self) -> io::Result<()> {
54-
if self.s.is_some() {
55-
return Ok(());
75+
if !ss.is_valid() {
76+
return Err(io::Error::new(
77+
io::ErrorKind::Other,
78+
PulseError::InvalidSampleSpec,
79+
));
5680
}
5781

58-
let device = self.device.as_deref();
5982
let result = Simple::new(
60-
None, // Use the default server.
61-
APP_NAME, // Our application's name.
62-
Direction::Playback, // Direction.
63-
device, // Our device (sink) name.
64-
STREAM_NAME, // Description of our stream.
65-
&self.ss, // Our sample format.
66-
None, // Use default channel map.
67-
None, // Use default buffering attributes.
83+
None, // Use the default server.
84+
APP_NAME, // Our application's name.
85+
Direction::Playback, // Direction.
86+
self.device.as_deref(), // Our device (sink) name.
87+
STREAM_NAME, // Description of our stream.
88+
&ss, // Our sample format.
89+
None, // Use default channel map.
90+
None, // Use default buffering attributes.
6891
);
92+
6993
match result {
7094
Ok(s) => {
7195
self.s = Some(s);
72-
Ok(())
7396
}
74-
Err(e) => Err(io::Error::new(
75-
io::ErrorKind::ConnectionRefused,
76-
e.to_string().unwrap(),
77-
)),
97+
Err(e) => {
98+
return Err(io::Error::new(
99+
io::ErrorKind::ConnectionRefused,
100+
PulseError::ConnectionRefused(e),
101+
));
102+
}
78103
}
104+
105+
Ok(())
79106
}
80107

81108
fn stop(&mut self) -> io::Result<()> {
109+
let s = self
110+
.s
111+
.as_mut()
112+
.ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?;
113+
114+
s.drain()
115+
.map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::DrainFailure(e)))?;
116+
82117
self.s = None;
83118
Ok(())
84119
}
@@ -88,20 +123,15 @@ impl Sink for PulseAudioSink {
88123

89124
impl SinkAsBytes for PulseAudioSink {
90125
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
91-
if let Some(s) = &self.s {
92-
match s.write(data) {
93-
Ok(_) => Ok(()),
94-
Err(e) => Err(io::Error::new(
95-
io::ErrorKind::BrokenPipe,
96-
e.to_string().unwrap(),
97-
)),
98-
}
99-
} else {
100-
Err(io::Error::new(
101-
io::ErrorKind::NotConnected,
102-
"Not connected to PulseAudio",
103-
))
104-
}
126+
let s = self
127+
.s
128+
.as_mut()
129+
.ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?;
130+
131+
s.write(data)
132+
.map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::OnWrite(e)))?;
133+
134+
Ok(())
105135
}
106136
}
107137

0 commit comments

Comments
 (0)