refactor: migrate all code to rodio 0.22 API

- OutputStream/OutputStreamHandle -> DeviceSinkBuilder/MixerDeviceSink
- Sink -> Player (as RodioPlayer)
- current_frame_len -> current_span_len in all Source impls
- Source::channels() returns NonZero<u16>, sample_rate() returns NonZero<u32>
- SamplesBuffer no longer generic, constructor takes NonZero args
- SampleRate is now u32 type alias (no tuple struct)
- Decoder::new_mp4 no longer takes Mp4Type hint
- Applied across both crates/ workspace and src-tauri/ legacy code
This commit is contained in:
vicrodh committed 2026-02-28 07:48:03 -06:00
1 parent faf8844584
commit b5ec5de785
16 files changed
+230 -197

No files matched your search

+23 -11
View File
@@ -14,7 +14,7 @@ use rodio::{
traits::{DeviceTrait, HostTrait},
BufferSize, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig,
},
OutputStream, OutputStreamHandle,
DeviceSinkBuilder, MixerDeviceSink,
};
use std::collections::HashMap;
use std::fs;
@@ -45,7 +45,7 @@ fn get_supported_sample_rates(device: &rodio::cpal::Device) -> Option<Vec<u32>>
let mut supported = Vec::new();
for rate in COMMON_SAMPLE_RATES {
let sample_rate = rodio::cpal::SampleRate(*rate);
let sample_rate = *rate;
// Check if any config supports this rate
let is_supported = configs_vec.iter().any(|config| {
sample_rate >= config.min_sample_rate() && sample_rate <= config.max_sample_rate()
@@ -587,7 +587,7 @@ impl AudioBackend for AlsaBackend {
fn create_output_stream(
&self,
config: &BackendConfig,
) -> BackendResult<(OutputStream, OutputStreamHandle)> {
) -> BackendResult<MixerDeviceSink> {
log::info!(
"[ALSA Backend] Creating stream: {}Hz, {} channels, exclusive: {}, plugin: {:?}",
config.sample_rate,
@@ -622,7 +622,7 @@ impl AudioBackend for AlsaBackend {
// Create StreamConfig with requested sample rate
let stream_config = StreamConfig {
channels: config.channels,
sample_rate: SampleRate(config.sample_rate),
sample_rate: config.sample_rate,
buffer_size: if config.exclusive_mode {
// Smaller buffer for exclusive mode = lower latency
BufferSize::Fixed(512)
@@ -639,15 +639,15 @@ impl AudioBackend for AlsaBackend {
let mut found_matching = false;
for range in supported_configs {
if range.channels() == config.channels
&& config.sample_rate >= range.min_sample_rate().0
&& config.sample_rate <= range.max_sample_rate().0
&& config.sample_rate >= range.min_sample_rate()
&& config.sample_rate <= range.max_sample_rate()
{
found_matching = true;
log::info!(
"[ALSA Backend] Device supports {}Hz (range: {}-{}Hz)",
config.sample_rate,
range.min_sample_rate().0,
range.max_sample_rate().0
range.min_sample_rate(),
range.max_sample_rate()
);
break;
}
@@ -668,8 +668,20 @@ impl AudioBackend for AlsaBackend {
SampleFormat::F32,
);
// Create OutputStream with custom config
let stream = OutputStream::try_from_device_config(&device, supported_config)
// Create MixerDeviceSink with custom config
let mixer_sink = DeviceSinkBuilder::from_device(device)
.map_err(|e| {
if config.exclusive_mode {
format!(
"Failed to create exclusive ALSA stream at {}Hz: {}. Device may be in use by another application.",
config.sample_rate, e
)
} else {
format!("Failed to create ALSA device sink builder at {}Hz: {}", config.sample_rate, e)
}
})?
.with_supported_config(&supported_config)
.open_stream()
.map_err(|e| {
if config.exclusive_mode {
format!(
@@ -687,7 +699,7 @@ impl AudioBackend for AlsaBackend {
config.exclusive_mode
);
Ok(stream)
Ok(mixer_sink)
}
fn is_available(&self) -> bool {
+4 -4
View File
@@ -95,17 +95,17 @@ where
S: Source<Item = f32>,
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
self.inner.current_frame_len()
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
#[inline]
fn channels(&self) -> u16 {
fn channels(&self) -> std::num::NonZero<u16> {
self.inner.channels()
}
#[inline]
fn sample_rate(&self) -> u32 {
fn sample_rate(&self) -> std::num::NonZero<u32> {
self.inner.sample_rate()
}
+2 -2
View File
@@ -3,7 +3,7 @@
//! Provides a unified interface for different audio backends (PipeWire, ALSA, PulseAudio)
//! allowing users to choose their preferred audio stack.
use rodio::{OutputStream, OutputStreamHandle};
use rodio::MixerDeviceSink;
use serde::{Deserialize, Serialize};
/// Supported audio backends
@@ -220,7 +220,7 @@ pub trait AudioBackend: Send + Sync {
fn create_output_stream(
&self,
config: &BackendConfig,
) -> BackendResult<(OutputStream, OutputStreamHandle)>;
) -> BackendResult<MixerDeviceSink>;
/// Check if this backend is available on the current system
fn is_available(&self) -> bool;
+4 -4
View File
@@ -179,17 +179,17 @@ where
S: Source<Item = f32>,
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
self.inner.current_frame_len()
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
#[inline]
fn channels(&self) -> u16 {
fn channels(&self) -> std::num::NonZero<u16> {
self.inner.channels()
}
#[inline]
fn sample_rate(&self) -> u32 {
fn sample_rate(&self) -> std::num::NonZero<u32> {
self.inner.sample_rate()
}
+6 -6
View File
@@ -36,8 +36,8 @@ where
S: Source<Item = f32>,
{
pub fn new(source: S, gain_atomic: Arc<AtomicU32>, initial_gain: f32) -> Self {
let sample_rate = source.sample_rate();
let channels = source.channels() as u32;
let sample_rate = source.sample_rate().get();
let channels = source.channels().get() as u32;
// 50ms ramp in total samples (all channels)
let ramp_samples = (sample_rate * channels * 50) / 1000;
@@ -116,17 +116,17 @@ where
S: Source<Item = f32>,
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
self.inner.current_frame_len()
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
#[inline]
fn channels(&self) -> u16 {
fn channels(&self) -> std::num::NonZero<u16> {
self.inner.channels()
}
#[inline]
fn sample_rate(&self) -> u32 {
fn sample_rate(&self) -> std::num::NonZero<u32> {
self.inner.sample_rate()
}
+13 -10
View File
@@ -12,7 +12,7 @@ use rodio::{
traits::{DeviceTrait, HostTrait},
BufferSize, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig,
},
OutputStream, OutputStreamHandle,
DeviceSinkBuilder, MixerDeviceSink,
};
use std::process::Command;
@@ -168,7 +168,7 @@ impl AudioBackend for PipeWireBackend {
fn create_output_stream(
&self,
config: &BackendConfig,
) -> BackendResult<(OutputStream, OutputStreamHandle)> {
) -> BackendResult<MixerDeviceSink> {
let target_sink = config.device_id.clone();
// Temporarily set default sink to target (if specified)
@@ -274,7 +274,7 @@ impl AudioBackend for PipeWireBackend {
// Create StreamConfig with desired sample rate
let stream_config = StreamConfig {
channels: config.channels,
sample_rate: SampleRate(config.sample_rate),
sample_rate: config.sample_rate,
buffer_size: if config.exclusive_mode {
BufferSize::Fixed(512) // Lower latency for exclusive mode
} else {
@@ -290,15 +290,15 @@ impl AudioBackend for PipeWireBackend {
let mut found_matching = false;
for range in supported_configs {
if range.channels() == config.channels
&& config.sample_rate >= range.min_sample_rate().0
&& config.sample_rate <= range.max_sample_rate().0
&& config.sample_rate >= range.min_sample_rate()
&& config.sample_rate <= range.max_sample_rate()
{
found_matching = true;
log::info!(
"[PipeWire Backend] Device supports {}Hz (range: {}-{}Hz)",
config.sample_rate,
range.min_sample_rate().0,
range.max_sample_rate().0
range.min_sample_rate(),
range.max_sample_rate()
);
break;
}
@@ -319,13 +319,16 @@ impl AudioBackend for PipeWireBackend {
SampleFormat::F32,
);
// Create OutputStream with custom config
let stream = OutputStream::try_from_device_config(&device, supported_config)
// Create MixerDeviceSink with custom config
let mixer_sink = DeviceSinkBuilder::from_device(device)
.map_err(|e| format!("Failed to create device sink builder: {}", e))?
.with_supported_config(&supported_config)
.open_stream()
.map_err(|e| format!("Failed to create output stream at {}Hz: {}", config.sample_rate, e))?;
log::info!("[PipeWire Backend] Output stream created successfully at {}Hz", config.sample_rate);
Ok(stream)
Ok(mixer_sink)
}
fn is_available(&self) -> bool {
+2 -2
View File
@@ -5,7 +5,7 @@
use super::backend::{AudioBackend, AudioBackendType, AudioDevice, BackendConfig, BackendResult};
use super::pipewire_backend::PipeWireBackend;
use rodio::{OutputStream, OutputStreamHandle};
use rodio::MixerDeviceSink;
pub struct PulseBackend {
// Reuse PipeWire backend implementation (they're compatible)
@@ -33,7 +33,7 @@ impl AudioBackend for PulseBackend {
fn create_output_stream(
&self,
config: &BackendConfig,
) -> BackendResult<(OutputStream, OutputStreamHandle)> {
) -> BackendResult<MixerDeviceSink> {
// Delegate to PipeWire backend (same mechanism)
self.inner.create_output_stream(config)
}
@@ -68,17 +68,17 @@ where
S: Source<Item = f32>,
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
self.inner.current_frame_len()
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
#[inline]
fn channels(&self) -> u16 {
fn channels(&self) -> std::num::NonZero<u16> {
self.inner.channels()
}
#[inline]
fn sample_rate(&self) -> u32 {
fn sample_rate(&self) -> std::num::NonZero<u32> {
self.inner.sample_rate()
}
+69 -57
View File
@@ -22,9 +22,8 @@ use std::sync::mpsc::{self, Sender, SyncSender, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use rodio::{Decoder, OutputStream, Source};
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Source};
use rodio::buffer::SamplesBuffer;
use rodio::decoder::Mp4Type;
use rodio::cpal::traits::{DeviceTrait, HostTrait};
use rodio::cpal::{StreamConfig, SampleRate, BufferSize, SupportedStreamConfig, SupportedBufferSize, SampleFormat};
use symphonia::core::audio::SampleBuffer;
@@ -131,7 +130,7 @@ impl Seek for CursorMediaSource {
/// Audio specifications extracted from decoded audio
#[allow(dead_code)]
struct AudioSpecs {
samples: SamplesBuffer<f32>,
samples: SamplesBuffer,
sample_rate: u32,
channels: u16,
}
@@ -204,7 +203,11 @@ fn decode_with_symphonia(data: &[u8]) -> Result<AudioSpecs, String> {
}
Ok(AudioSpecs {
samples: SamplesBuffer::new(channels, sample_rate, samples),
samples: SamplesBuffer::new(
std::num::NonZero::new(channels).unwrap(),
std::num::NonZero::new(sample_rate).unwrap(),
samples,
),
sample_rate,
channels,
})
@@ -305,23 +308,22 @@ fn decode_with_fallback(
}
}
let mp4_attempts = [Mp4Type::M4a, Mp4Type::Mp4];
for hint in mp4_attempts {
let hint_label = format!("{:?}", hint);
// Try mp4 fallback (rodio 0.22 removed Mp4Type hint)
{
let attempt = panic::catch_unwind(AssertUnwindSafe(|| {
Decoder::new_mp4(BufReader::new(Cursor::new(data.to_vec())), hint)
Decoder::new_mp4(BufReader::new(Cursor::new(data.to_vec())))
}));
match attempt {
Ok(Ok(decoder)) => {
log::info!("Decoded audio using mp4 fallback ({})", hint_label);
log::info!("Decoded audio using mp4 fallback");
return Ok(Box::new(decoder));
}
Ok(Err(err)) => {
log::warn!("mp4 fallback ({}) failed: {}", hint_label, err);
log::warn!("mp4 fallback failed: {}", err);
}
Err(_) => {
log::warn!("mp4 fallback ({}) panicked", hint_label);
log::warn!("mp4 fallback panicked");
}
}
}
@@ -335,15 +337,15 @@ fn decode_with_fallback(
}
}
/// Create OutputStream with custom sample rate configuration
/// Create MixerDeviceSink with custom sample rate configuration
fn create_output_stream_with_config(
device: &rodio::cpal::Device,
device: rodio::cpal::Device,
sample_rate: u32,
channels: u16,
exclusive_mode: bool,
) -> Result<(OutputStream, rodio::OutputStreamHandle), String> {
) -> Result<MixerDeviceSink, String> {
log::info!(
"Creating OutputStream: {}Hz, {} channels, exclusive: {}",
"Creating MixerDeviceSink: {}Hz, {} channels, exclusive: {}",
sample_rate,
channels,
exclusive_mode
@@ -352,7 +354,7 @@ fn create_output_stream_with_config(
// Create StreamConfig with desired sample rate
let config = StreamConfig {
channels,
sample_rate: SampleRate(sample_rate),
sample_rate,
buffer_size: if exclusive_mode {
BufferSize::Fixed(512) // Lower latency for exclusive mode
} else {
@@ -368,15 +370,15 @@ fn create_output_stream_with_config(
let mut found_matching = false;
for range in supported_configs {
if range.channels() == channels
&& sample_rate >= range.min_sample_rate().0
&& sample_rate <= range.max_sample_rate().0
&& sample_rate >= range.min_sample_rate()
&& sample_rate <= range.max_sample_rate()
{
found_matching = true;
log::info!(
"Device supports {}Hz (range: {}-{}Hz)",
sample_rate,
range.min_sample_rate().0,
range.max_sample_rate().0
range.min_sample_rate(),
range.max_sample_rate()
);
break;
}
@@ -397,14 +399,22 @@ fn create_output_stream_with_config(
SampleFormat::F32,
);
// Create OutputStream with custom config
match OutputStream::try_from_device_config(device, supported_config) {
Ok((stream, handle)) => {
log::info!("OutputStream created successfully at {}Hz", sample_rate);
Ok((stream, handle))
// Create MixerDeviceSink with custom config
match DeviceSinkBuilder::from_device(device) {
Ok(builder) => {
match builder.with_supported_config(&supported_config).open_stream() {
Ok(mixer_sink) => {
log::info!("MixerDeviceSink created successfully at {}Hz", sample_rate);
Ok(mixer_sink)
}
Err(e) => {
log::error!("Failed to open stream at {}Hz: {}", sample_rate, e);
Err(format!("Failed to create output stream: {}", e))
}
}
}
Err(e) => {
log::error!("❌ Failed to create OutputStream at {}Hz: {}", sample_rate, e);
log::error!("Failed to create device sink builder: {}", e);
Err(format!("Failed to create output stream: {}", e))
}
}
@@ -412,7 +422,7 @@ fn create_output_stream_with_config(
/// Output stream type - either rodio or ALSA Direct
enum StreamType {
Rodio(OutputStream, rodio::OutputStreamHandle),
Rodio(MixerDeviceSink),
#[cfg(target_os = "linux")]
AlsaDirect(Arc<qbz_audio::AlsaDirectStream>),
}
@@ -486,12 +496,12 @@ fn try_init_stream_with_backend(
// Fallback to regular rodio stream (PipeWire, Pulse, ALSA via CPAL)
match backend.create_output_stream(&config) {
Ok(stream) => {
Ok(mixer_sink) => {
log::info!("Stream created via {:?} backend at {}Hz", backend_type, sample_rate);
Some(Ok(StreamType::Rodio(stream.0, stream.1)))
Some(Ok(StreamType::Rodio(mixer_sink)))
}
Err(e) => {
log::error!("❌ Backend stream creation failed: {}", e);
log::error!("Backend stream creation failed: {}", e);
Some(Err(e))
}
}
@@ -881,17 +891,19 @@ impl Player {
}
};
match OutputStream::try_from_device(&device) {
Ok((stream, handle)) => {
match DeviceSinkBuilder::from_device(device)
.and_then(|b| b.open_sink_or_fallback())
{
Ok(mixer_sink) => {
log::info!("Audio output initialized successfully");
Some(StreamType::Rodio(stream, handle))
Some(StreamType::Rodio(mixer_sink))
}
Err(e) => {
log::error!("Failed to create audio output on device: {}. Trying default...", e);
match OutputStream::try_default() {
Ok((stream, handle)) => {
match DeviceSinkBuilder::open_default_sink() {
Ok(mixer_sink) => {
log::info!("Fallback to default audio output succeeded");
Some(StreamType::Rodio(stream, handle))
Some(StreamType::Rodio(mixer_sink))
}
Err(e2) => {
log::error!("Failed to create default audio output: {}", e2);
@@ -996,7 +1008,7 @@ impl Player {
if (dac_passthrough || using_alsa_direct) && format_changed {
let mode = if using_alsa_direct { "ALSA Direct" } else { "DAC passthrough" };
log::info!(
"Sample rate/channels changed from {:?}Hz/{:?}ch to {}Hz/{}ch - recreating OutputStream ({})",
"Sample rate/channels changed from {:?}Hz/{:?}ch to {}Hz/{}ch - recreating audio stream ({})",
*current_sample_rate,
*current_channels,
sample_rate,
@@ -1004,7 +1016,7 @@ impl Player {
mode
);
} else {
log::info!("Creating initial OutputStream");
log::info!("Creating initial audio stream");
}
// Drop old stream
drop(stream_opt.take());
@@ -1072,11 +1084,11 @@ impl Player {
}
create_output_stream_with_config(
&device,
device,
sample_rate,
channels,
dac_passthrough,
).map(|(stream, handle)| StreamType::Rodio(stream, handle))
).map(StreamType::Rodio)
}
}
} else {
@@ -1108,11 +1120,11 @@ impl Player {
}
create_output_stream_with_config(
&device,
device,
sample_rate,
channels,
dac_passthrough,
).map(|(stream, handle)| StreamType::Rodio(stream, handle))
).map(StreamType::Rodio)
};
// Handle stream creation result
@@ -1151,7 +1163,7 @@ impl Player {
} else if format_changed {
// Format changed but DAC passthrough is disabled - reuse existing stream
log::info!(
"Audio format changed from {:?}Hz/{:?}ch to {}Hz/{}ch - reusing OutputStream (DAC passthrough disabled, gapless enabled)",
"Audio format changed from {:?}Hz/{:?}ch to {}Hz/{}ch - reusing audio stream (DAC passthrough disabled, gapless enabled)",
*current_sample_rate,
*current_channels,
sample_rate,
@@ -1168,7 +1180,7 @@ impl Player {
if let Some(engine) = current_engine.take() {
engine.stop();
// Small delay to allow the audio sink to fully release its
// reference to the OutputStreamHandle before creating a new sink.
// reference to the mixer before creating a new player.
// This prevents "resource busy" errors on rapid track switches.
std::thread::sleep(Duration::from_millis(50));
}
@@ -1178,8 +1190,8 @@ impl Player {
// Create PlaybackEngine from StreamType
let mut engine = match stream {
StreamType::Rodio(_stream, handle) => {
match PlaybackEngine::new_rodio(handle) {
StreamType::Rodio(ref mixer_sink) => {
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
Ok(e) => {
*consecutive_sink_failures = 0;
thread_state.set_stream_error(false);
@@ -1353,7 +1365,7 @@ impl Player {
if (dac_passthrough || using_alsa_direct) && format_changed {
let mode = if using_alsa_direct { "ALSA Direct" } else { "DAC passthrough" };
log::info!(
"Streaming: Sample rate/channels changed to {}Hz/{}ch - recreating OutputStream ({})",
"Streaming: Sample rate/channels changed to {}Hz/{}ch - recreating audio stream ({})",
sample_rate,
channels,
mode
@@ -1398,11 +1410,11 @@ impl Player {
}
create_output_stream_with_config(
&device,
device,
sample_rate,
channels,
dac_passthrough,
).map(|(stream, handle)| StreamType::Rodio(stream, handle))
).map(StreamType::Rodio)
}
}
} else {
@@ -1412,8 +1424,8 @@ impl Player {
thread_state.set_stream_error(true);
return;
};
create_output_stream_with_config(&device, sample_rate, channels, dac_passthrough)
.map(|(stream, handle)| StreamType::Rodio(stream, handle))
create_output_stream_with_config(device, sample_rate, channels, dac_passthrough)
.map(StreamType::Rodio)
};
match stream_result {
@@ -1446,8 +1458,8 @@ impl Player {
// Create PlaybackEngine
let mut engine = match stream {
StreamType::Rodio(_stream, handle) => {
match PlaybackEngine::new_rodio(handle) {
StreamType::Rodio(ref mixer_sink) => {
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
Ok(e) => {
*consecutive_sink_failures = 0;
thread_state.set_stream_error(false);
@@ -1639,8 +1651,8 @@ impl Player {
};
let mut engine = match stream {
StreamType::Rodio(_stream, handle) => {
match PlaybackEngine::new_rodio(handle) {
StreamType::Rodio(ref mixer_sink) => {
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
Ok(e) => e,
Err(e) => {
log::error!("Failed to create engine for resume: {}", e);
@@ -1760,8 +1772,8 @@ impl Player {
}
let mut engine = match stream {
StreamType::Rodio(_stream, handle) => {
match PlaybackEngine::new_rodio(handle) {
StreamType::Rodio(ref mixer_sink) => {
match PlaybackEngine::new_rodio(&mixer_sink.mixer()) {
Ok(e) => e,
Err(e) => {
log::error!("Failed to create rodio engine for seek: {}", e);
@@ -7,7 +7,7 @@
//! This abstraction allows the player to work with both approaches transparently.
use qbz_audio::AlsaDirectStream;
use rodio::{OutputStreamHandle, Sink, Source};
use rodio::{mixer::Mixer, Player as RodioPlayer, Source};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
@@ -17,7 +17,7 @@ use std::time::Duration;
pub enum PlaybackEngine {
/// Rodio-based (PipeWire, Pulse, ALSA via CPAL)
Rodio {
sink: Sink,
sink: RodioPlayer,
},
/// Direct ALSA (hw: devices, bit-perfect)
AlsaDirect {
@@ -33,9 +33,8 @@ pub enum PlaybackEngine {
impl PlaybackEngine {
/// Create Rodio engine
pub fn new_rodio(stream_handle: &OutputStreamHandle) -> Result<Self, String> {
let sink = Sink::try_new(stream_handle)
.map_err(|e| format!("Failed to create Sink: {}", e))?;
pub fn new_rodio(mixer: &Mixer) -> Result<Self, String> {
let sink = RodioPlayer::connect_new(mixer);
Ok(Self::Rodio { sink })
}
@@ -658,17 +658,17 @@ impl IncrementalStreamingSource {
}
impl Source for IncrementalStreamingSource {
fn current_frame_len(&self) -> Option<usize> {
fn current_span_len(&self) -> Option<usize> {
// We don't know frame boundaries in the queue
None
}
fn channels(&self) -> u16 {
self.channels
fn channels(&self) -> std::num::NonZero<u16> {
std::num::NonZero::new(self.channels).unwrap()
}
fn sample_rate(&self) -> u32 {
self.sample_rate
fn sample_rate(&self) -> std::num::NonZero<u32> {
std::num::NonZero::new(self.sample_rate).unwrap()
}
fn total_duration(&self) -> Option<Duration> {