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:
16 files changed
+230
-197
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in new issue
Block a user