| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | use futures::executor::block_on; |
| 2 | use pulseaudio::protocol; |
| 3 | |
| 4 | mod stream; |
| 5 | |
| 6 | pub use stream::Stream; |
| 7 | |
| 8 | use crate::{ |
| 9 | traits::{DeviceTrait, HostTrait}, |
| 10 | BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, |
| 11 | DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, |
| 12 | DevicesError, FrameCount, HostId, HostUnavailable, InputCallbackInfo, OutputCallbackInfo, |
| 13 | SampleFormat, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, |
| 14 | SupportedStreamConfigRange, SupportedStreamConfigsError, |
| 15 | }; |
| 16 | |
| 17 | const PULSE_FORMATS: &[SampleFormat] = &[ |
| 18 | SampleFormat::U8, |
| 19 | SampleFormat::I16, |
| 20 | SampleFormat::I24, |
| 21 | SampleFormat::I32, |
| 22 | SampleFormat::F32, |
| 23 | ]; |
| 24 | |
| 25 | impl TryFrom<protocol::SampleFormat> for SampleFormat { |
| 26 | type Error = (); |
| 27 | |
| 28 | fn try_from(spec: protocol::SampleFormat) -> Result<Self, Self::Error> { |
| 29 | match spec { |
| 30 | protocol::SampleFormat::U8 => Ok(SampleFormat::U8), |
| 31 | protocol::SampleFormat::S16Le | protocol::SampleFormat::S16Be => Ok(SampleFormat::I16), |
| 32 | protocol::SampleFormat::S24Le | protocol::SampleFormat::S24Be => Ok(SampleFormat::I24), |
| 33 | protocol::SampleFormat::S32Le | protocol::SampleFormat::S32Be => Ok(SampleFormat::I32), |
| 34 | protocol::SampleFormat::Float32Le | protocol::SampleFormat::Float32Be => { |
| 35 | Ok(SampleFormat::F32) |
| 36 | } |
| 37 | _ => Err(()), |
| 38 | } |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | impl TryFrom<SampleFormat> for protocol::SampleFormat { |
| 43 | type Error = (); |
| 44 | |
| 45 | fn try_from(format: SampleFormat) -> Result<Self, Self::Error> { |
| 46 | match (format, cfg!(target_endian = "little")) { |
| 47 | (SampleFormat::U8, _) => Ok(protocol::SampleFormat::U8), |
| 48 | (SampleFormat::I16, true) => Ok(protocol::SampleFormat::S16Le), |
| 49 | (SampleFormat::I16, false) => Ok(protocol::SampleFormat::S16Be), |
| 50 | (SampleFormat::I24, true) => Ok(protocol::SampleFormat::S24Le), |
| 51 | (SampleFormat::I24, false) => Ok(protocol::SampleFormat::S24Be), |
| 52 | (SampleFormat::I32, true) => Ok(protocol::SampleFormat::S32Le), |
| 53 | (SampleFormat::I32, false) => Ok(protocol::SampleFormat::S32Be), |
| 54 | (SampleFormat::F32, true) => Ok(protocol::SampleFormat::Float32Le), |
| 55 | (SampleFormat::F32, false) => Ok(protocol::SampleFormat::Float32Be), |
| 56 | _ => Err(()), |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | impl From<pulseaudio::ClientError> for BackendSpecificError { |
| 62 | fn from(err: pulseaudio::ClientError) -> Self { |
| 63 | BackendSpecificError { |
| 64 | description: err.to_string(), |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | /// A Host for connecting to the popular PulseAudio and PipeWire (via |
| 70 | /// pipewire-pulse) audio servers on linux. |
| 71 | pub struct Host { |
| 72 | client: pulseaudio::Client, |
| 73 | } |
| 74 | |
| 75 | impl Host { |
| 76 | pub fn new() -> Result<Self, HostUnavailable> { |
| 77 | let client = |
| 78 | pulseaudio::Client::from_env(c"cpal-pulseaudio").map_err(|_| HostUnavailable)?; |
| 79 | |
| 80 | Ok(Self { client }) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | impl HostTrait for Host { |
| 85 | type Devices = std::vec::IntoIter<Device>; |
| 86 | type Device = Device; |
| 87 | |
| 88 | fn is_available() -> bool { |
| 89 | pulseaudio::socket_path_from_env().is_some() |
| 90 | } |
| 91 | |
| 92 | fn devices(&self) -> Result<Self::Devices, DevicesError> { |
| 93 | let sinks = block_on(self.client.list_sinks()).map_err(|err| BackendSpecificError { |
| 94 | description: format!("Failed to list sinks: {err}"), |
| 95 | })?; |
| 96 | |
| 97 | let sources = block_on(self.client.list_sources()).map_err(|err| BackendSpecificError { |
| 98 | description: format!("Failed to list sources: {err}"), |
| 99 | })?; |
| 100 | |
| 101 | Ok(sinks |
| 102 | .into_iter() |
| 103 | .map(|sink_info| Device::Sink { |
| 104 | client: self.client.clone(), |
| 105 | info: sink_info, |
| 106 | }) |
| 107 | .chain(sources.into_iter().map(|source_info| Device::Source { |
| 108 | client: self.client.clone(), |
| 109 | info: source_info, |
| 110 | })) |
| 111 | .collect::<Vec<_>>() |
| 112 | .into_iter()) |
| 113 | } |
| 114 | |
| 115 | fn default_input_device(&self) -> Option<Self::Device> { |
| 116 | let source_info = block_on( |
| 117 | self.client |
| 118 | .source_info_by_name(protocol::DEFAULT_SOURCE.to_owned()), |
| 119 | ) |
| 120 | .ok()?; |
| 121 | |
| 122 | Some(Device::Source { |
| 123 | client: self.client.clone(), |
| 124 | info: source_info, |
| 125 | }) |
| 126 | } |
| 127 | |
| 128 | fn default_output_device(&self) -> Option<Self::Device> { |
| 129 | let sink_info = block_on( |
| 130 | self.client |
| 131 | .sink_info_by_name(protocol::DEFAULT_SINK.to_owned()), |
| 132 | ) |
| 133 | .ok()?; |
| 134 | |
| 135 | Some(Device::Sink { |
| 136 | client: self.client.clone(), |
| 137 | info: sink_info, |
| 138 | }) |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | /// A PulseAudio sink or source. |
| 143 | #[derive(Debug, Clone)] |
| 144 | pub enum Device { |
| 145 | Sink { |
| 146 | client: pulseaudio::Client, |
| 147 | info: protocol::SinkInfo, |
| 148 | }, |
| 149 | Source { |
| 150 | client: pulseaudio::Client, |
| 151 | info: protocol::SourceInfo, |
| 152 | }, |
| 153 | } |
| 154 | |
| 155 | fn supported_config_ranges() -> Vec<SupportedStreamConfigRange> { |
| 156 | let mut ranges = vec![]; |
| 157 | for format in PULSE_FORMATS { |
| 158 | for channel_count in 1..protocol::sample_spec::MAX_CHANNELS { |
| 159 | let bytes_per_frame = channel_count as usize * format.sample_size(); |
| 160 | let max_frames = (protocol::MAX_MEMBLOCKQ_LENGTH / bytes_per_frame) as FrameCount; |
| 161 | ranges.push(SupportedStreamConfigRange { |
| 162 | channels: channel_count as _, |
| 163 | min_sample_rate: 1, |
| 164 | max_sample_rate: protocol::sample_spec::MAX_RATE, |
| 165 | buffer_size: SupportedBufferSize::Range { |
| 166 | min: 0, |
| 167 | max: max_frames, |
| 168 | }, |
| 169 | sample_format: *format, |
| 170 | }); |
| 171 | } |
| 172 | } |
| 173 | ranges |
| 174 | } |
| 175 | |
| 176 | fn default_config_from_spec( |
| 177 | sample_spec: &protocol::SampleSpec, |
| 178 | channel_map: &protocol::ChannelMap, |
| 179 | ) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 180 | let sample_format: SampleFormat = sample_spec |
| 181 | .format |
| 182 | .try_into() |
| 183 | .map_err(|_| DefaultStreamConfigError::StreamTypeNotSupported)?; |
| 184 | let bytes_per_frame = channel_map.num_channels() as usize * sample_format.sample_size(); |
| 185 | let max_frames = (protocol::MAX_MEMBLOCKQ_LENGTH / bytes_per_frame) as u32; |
| 186 | Ok(SupportedStreamConfig { |
| 187 | channels: channel_map.num_channels() as _, |
| 188 | sample_rate: sample_spec.sample_rate, |
| 189 | buffer_size: SupportedBufferSize::Range { |
| 190 | min: 0, |
| 191 | max: max_frames, |
| 192 | }, |
| 193 | sample_format, |
| 194 | }) |
| 195 | } |
| 196 | |
| 197 | impl DeviceTrait for Device { |
| 198 | type SupportedInputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>; |
| 199 | type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>; |
| 200 | type Stream = Stream; |
| 201 | |
| 202 | fn name(&self) -> Result<String, DeviceNameError> { |
| 203 | let name = match self { |
| 204 | Device::Sink { info, .. } => &info.name, |
| 205 | Device::Source { info, .. } => &info.name, |
| 206 | }; |
| 207 | |
| 208 | Ok(String::from_utf8_lossy(name.as_bytes()).into_owned()) |
| 209 | } |
| 210 | |
| 211 | fn supported_input_configs( |
| 212 | &self, |
| 213 | ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> { |
| 214 | let Device::Source { .. } = self else { |
| 215 | return Ok(vec![].into_iter()); |
| 216 | }; |
| 217 | Ok(supported_config_ranges().into_iter()) |
| 218 | } |
| 219 | |
| 220 | fn supported_output_configs( |
| 221 | &self, |
| 222 | ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 223 | let Device::Sink { .. } = self else { |
| 224 | return Ok(vec![].into_iter()); |
| 225 | }; |
| 226 | Ok(supported_config_ranges().into_iter()) |
| 227 | } |
| 228 | |
| 229 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 230 | let Device::Source { info, .. } = self else { |
| 231 | return Err(DefaultStreamConfigError::StreamTypeNotSupported); |
| 232 | }; |
| 233 | default_config_from_spec(&info.sample_spec, &info.channel_map) |
| 234 | } |
| 235 | |
| 236 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 237 | let Device::Sink { info, .. } = self else { |
| 238 | return Err(DefaultStreamConfigError::StreamTypeNotSupported); |
| 239 | }; |
| 240 | default_config_from_spec(&info.sample_spec, &info.channel_map) |
| 241 | } |
| 242 | |
| 243 | fn build_input_stream_raw<D, E>( |
| 244 | &self, |
| 245 | config: StreamConfig, |
| 246 | sample_format: SampleFormat, |
| 247 | data_callback: D, |
| 248 | error_callback: E, |
| 249 | _timeout: Option<std::time::Duration>, |
| 250 | ) -> Result<Self::Stream, BuildStreamError> |
| 251 | where |
| 252 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, |
| 253 | E: FnMut(StreamError) + Send + 'static, |
| 254 | { |
| 255 | let Device::Source { client, info } = self else { |
| 256 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 257 | }; |
| 258 | |
| 259 | let format: protocol::SampleFormat = sample_format |
| 260 | .try_into() |
| 261 | .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; |
| 262 | |
| 263 | let sample_spec = make_sample_spec(config, format); |
| 264 | let channel_map = make_channel_map(config); |
| 265 | let buffer_attr = make_record_buffer_attr(config, format); |
| 266 | let adjust_latency = matches!(config.buffer_size, crate::BufferSize::Fixed(_)); |
| 267 | |
| 268 | let params = protocol::RecordStreamParams { |
| 269 | sample_spec, |
| 270 | channel_map, |
| 271 | source_index: Some(info.index), |
| 272 | buffer_attr, |
| 273 | flags: protocol::stream::StreamFlags { |
| 274 | // Start the stream suspended. |
| 275 | start_corked: true, |
| 276 | // When a fixed buffer size is requested, ask PA to configure |
| 277 | // the source hardware to hit the requested latency end-to-end. |
| 278 | adjust_latency, |
| 279 | ..Default::default() |
| 280 | }, |
| 281 | ..Default::default() |
| 282 | }; |
| 283 | |
| 284 | stream::Stream::new_record(client.clone(), params, data_callback, error_callback) |
| 285 | } |
| 286 | |
| 287 | fn build_output_stream_raw<D, E>( |
| 288 | &self, |
| 289 | config: StreamConfig, |
| 290 | sample_format: SampleFormat, |
| 291 | data_callback: D, |
| 292 | error_callback: E, |
| 293 | _timeout: Option<std::time::Duration>, |
| 294 | ) -> Result<Self::Stream, BuildStreamError> |
| 295 | where |
| 296 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 297 | E: FnMut(StreamError) + Send + 'static, |
| 298 | { |
| 299 | let Device::Sink { client, info } = self else { |
| 300 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 301 | }; |
| 302 | |
| 303 | let format: protocol::SampleFormat = sample_format |
| 304 | .try_into() |
| 305 | .map_err(|_| BuildStreamError::StreamConfigNotSupported)?; |
| 306 | |
| 307 | let sample_spec = make_sample_spec(config, format); |
| 308 | let channel_map = make_channel_map(config); |
| 309 | let buffer_attr = make_playback_buffer_attr(config, format); |
| 310 | let adjust_latency = matches!(config.buffer_size, crate::BufferSize::Fixed(_)); |
| 311 | |
| 312 | let params = protocol::PlaybackStreamParams { |
| 313 | sink_index: Some(info.index), |
| 314 | sample_spec, |
| 315 | channel_map, |
| 316 | buffer_attr, |
| 317 | flags: protocol::stream::StreamFlags { |
| 318 | // Start the stream suspended. |
| 319 | start_corked: true, |
| 320 | // When a fixed buffer size is requested, ask PA to configure |
| 321 | // the sink hardware to hit the requested latency end-to-end. |
| 322 | adjust_latency, |
| 323 | ..Default::default() |
| 324 | }, |
| 325 | ..Default::default() |
| 326 | }; |
| 327 | |
| 328 | stream::Stream::new_playback(client.clone(), params, data_callback, error_callback) |
| 329 | } |
| 330 | |
| 331 | fn description(&self) -> Result<DeviceDescription, DeviceNameError> { |
| 332 | let (name, description, direction) = match self { |
| 333 | Device::Sink { info, .. } => (&info.name, &info.description, DeviceDirection::Output), |
| 334 | Device::Source { info, .. } => (&info.name, &info.description, DeviceDirection::Input), |
| 335 | }; |
| 336 | |
| 337 | let mut builder = DeviceDescriptionBuilder::new(String::from_utf8_lossy(name.as_bytes())) |
| 338 | .direction(direction); |
| 339 | if let Some(desc) = description { |
| 340 | builder = builder.add_extended_line(String::from_utf8_lossy(desc.as_bytes())); |
| 341 | } |
| 342 | |
| 343 | Ok(builder.build()) |
| 344 | } |
| 345 | |
| 346 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 347 | let id = match self { |
| 348 | Device::Sink { info, .. } => info.index, |
| 349 | Device::Source { info, .. } => info.index, |
| 350 | }; |
| 351 | |
| 352 | Ok(DeviceId(HostId::PulseAudio, id.to_string())) |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | fn make_sample_spec(config: StreamConfig, format: protocol::SampleFormat) -> protocol::SampleSpec { |
| 357 | protocol::SampleSpec { |
| 358 | format, |
| 359 | sample_rate: config.sample_rate, |
| 360 | channels: config.channels as _, |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | fn make_channel_map(config: StreamConfig) -> protocol::ChannelMap { |
| 365 | use protocol::ChannelPosition::*; |
| 366 | |
| 367 | // Standard channel layouts following the PulseAudio default channel map |
| 368 | // (PA_CHANNEL_MAP_DEFAULT) for 1-8 channels, and common Atmos height- |
| 369 | // channel conventions for 10 and 12 channels. Counts without a widely |
| 370 | // agreed layout (9, 11, >12) fall back to sequential Aux positions. |
| 371 | let standard: &[protocol::ChannelPosition] = match config.channels { |
| 372 | 1 => &[Mono], |
| 373 | 2 => &[FrontLeft, FrontRight], |
| 374 | 3 => &[FrontLeft, FrontRight, FrontCenter], |
| 375 | 4 => &[FrontLeft, FrontRight, RearLeft, RearRight], |
| 376 | 5 => &[FrontLeft, FrontRight, FrontCenter, RearLeft, RearRight], |
| 377 | 6 => &[FrontLeft, FrontRight, FrontCenter, Lfe, RearLeft, RearRight], |
| 378 | 7 => &[ |
| 379 | FrontLeft, |
| 380 | FrontRight, |
| 381 | FrontCenter, |
| 382 | Lfe, |
| 383 | RearLeft, |
| 384 | RearRight, |
| 385 | RearCenter, |
| 386 | ], |
| 387 | 8 => &[ |
| 388 | FrontLeft, |
| 389 | FrontRight, |
| 390 | FrontCenter, |
| 391 | Lfe, |
| 392 | RearLeft, |
| 393 | RearRight, |
| 394 | SideLeft, |
| 395 | SideRight, |
| 396 | ], |
| 397 | // 7.1.2 (Dolby Atmos): 7.1 + top-front L/R |
| 398 | 10 => &[ |
| 399 | FrontLeft, |
| 400 | FrontRight, |
| 401 | FrontCenter, |
| 402 | Lfe, |
| 403 | RearLeft, |
| 404 | RearRight, |
| 405 | SideLeft, |
| 406 | SideRight, |
| 407 | TopFrontLeft, |
| 408 | TopFrontRight, |
| 409 | ], |
| 410 | // 7.1.4 (Dolby Atmos): 7.1 + top-front L/R + top-rear L/R |
| 411 | 12 => &[ |
| 412 | FrontLeft, |
| 413 | FrontRight, |
| 414 | FrontCenter, |
| 415 | Lfe, |
| 416 | RearLeft, |
| 417 | RearRight, |
| 418 | SideLeft, |
| 419 | SideRight, |
| 420 | TopFrontLeft, |
| 421 | TopFrontRight, |
| 422 | TopRearLeft, |
| 423 | TopRearRight, |
| 424 | ], |
| 425 | _ => &[], |
| 426 | }; |
| 427 | |
| 428 | if !standard.is_empty() { |
| 429 | return protocol::ChannelMap::new(standard.iter().copied()); |
| 430 | } |
| 431 | |
| 432 | let aux = [ |
| 433 | Aux0, Aux1, Aux2, Aux3, Aux4, Aux5, Aux6, Aux7, Aux8, Aux9, Aux10, Aux11, Aux12, Aux13, |
| 434 | Aux14, Aux15, Aux16, Aux17, Aux18, Aux19, Aux20, Aux21, Aux22, Aux23, Aux24, Aux25, Aux26, |
| 435 | Aux27, Aux28, Aux29, Aux30, Aux31, |
| 436 | ]; |
| 437 | protocol::ChannelMap::new(aux.iter().copied().take(config.channels as usize)) |
| 438 | } |
| 439 | |
| 440 | fn make_playback_buffer_attr( |
| 441 | config: StreamConfig, |
| 442 | format: protocol::SampleFormat, |
| 443 | ) -> protocol::stream::BufferAttr { |
| 444 | match config.buffer_size { |
| 445 | crate::BufferSize::Default => Default::default(), |
| 446 | crate::BufferSize::Fixed(frame_count) => { |
| 447 | let len = frame_count * config.channels as u32 * format.bytes_per_sample() as u32; |
| 448 | protocol::stream::BufferAttr { |
| 449 | // Double-buffer: total buffer = 2 callback periods. With |
| 450 | // adjust_latency this becomes the end-to-end latency target, |
| 451 | // Minimum request = one callback period, ensuring the server |
| 452 | // always asks for exactly frame_count frames per call. |
| 453 | max_length: 2 * len, |
| 454 | target_length: 2 * len, |
| 455 | minimum_request_length: len, |
| 456 | ..Default::default() |
| 457 | } |
| 458 | } |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | fn make_record_buffer_attr( |
| 463 | config: StreamConfig, |
| 464 | format: protocol::SampleFormat, |
| 465 | ) -> protocol::stream::BufferAttr { |
| 466 | match config.buffer_size { |
| 467 | crate::BufferSize::Default => Default::default(), |
| 468 | crate::BufferSize::Fixed(frame_count) => { |
| 469 | let len = frame_count * config.channels as u32 * format.bytes_per_sample() as u32; |
| 470 | protocol::stream::BufferAttr { |
| 471 | // fragment_size controls the delivery chunk size for record |
| 472 | // streams; target_length is playback-only and is ignored here. |
| 473 | max_length: len, |
| 474 | fragment_size: len, |
| 475 | ..Default::default() |
| 476 | } |
| 477 | } |
| 478 | } |
| 479 | } |