fix(player): decode alac via symphonia

This commit is contained in:
vicrodh committed 2026-01-12 13:04:45 -06:00
1 parent 0c456d4ae3
commit 3f13212978
1 file changed
+138 -5
+138 -5
View File
@@ -9,7 +9,7 @@
//!
//! Uses a dedicated audio thread since rodio's OutputStream is not Send.
use std::io::{BufReader, Cursor};
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
use std::panic::{self, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{self, Sender, RecvTimeoutError};
@@ -17,8 +17,17 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;
use rodio::{Decoder, OutputStream, Sink, Source};
use rodio::buffer::SamplesBuffer;
use rodio::decoder::Mp4Type;
use rodio::cpal::traits::{DeviceTrait, HostTrait};
use symphonia::core::audio::SampleBuffer;
use symphonia::core::codecs::DecoderOptions;
use symphonia::core::errors::Error as SymphoniaError;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::{MediaSource, MediaSourceStream};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::Hint;
use symphonia::default::{get_codecs, get_probe};
use crate::api::{client::QobuzClient, models::Quality};
@@ -40,15 +49,133 @@ enum AudioCommand {
ReinitDevice { device_name: Option<String> },
}
struct CursorMediaSource {
inner: Cursor<Vec<u8>>,
len: u64,
}
impl CursorMediaSource {
fn new(data: Vec<u8>) -> Self {
let len = data.len() as u64;
Self { inner: Cursor::new(data), len }
}
}
impl MediaSource for CursorMediaSource {
fn is_seekable(&self) -> bool {
true
}
fn byte_len(&self) -> Option<u64> {
Some(self.len)
}
}
impl Read for CursorMediaSource {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl Seek for CursorMediaSource {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
fn decode_with_symphonia(data: &[u8]) -> Result<SamplesBuffer<i16>, String> {
let source = Box::new(CursorMediaSource::new(data.to_vec())) as Box<dyn MediaSource>;
let mss = MediaSourceStream::new(source, Default::default());
let mut hint = Hint::new();
hint.with_extension("m4a");
let format_opts = FormatOptions {
enable_gapless: true,
..Default::default()
};
let metadata_opts: MetadataOptions = Default::default();
let mut probed = get_probe()
.format(&hint, mss, &format_opts, &metadata_opts)
.map_err(|err| format!("Symphonia probe failed: {}", err))?;
let track = probed
.format
.default_track()
.ok_or_else(|| "Symphonia: no supported audio tracks".to_string())?;
let mut decoder = get_codecs()
.make(&track.codec_params, &DecoderOptions::default())
.map_err(|err| format!("Symphonia decoder init failed: {}", err))?;
let mut sample_rate = 0;
let mut channels = 0u16;
let mut samples: Vec<i16> = Vec::new();
loop {
let packet = match probed.format.next_packet() {
Ok(packet) => packet,
Err(SymphoniaError::IoError(_)) => break,
Err(err) => return Err(format!("Symphonia read error: {}", err)),
};
if packet.track_id() != track.id {
continue;
}
match decoder.decode(&packet) {
Ok(audio_buf) => {
let spec = *audio_buf.spec();
if sample_rate == 0 {
sample_rate = spec.rate;
channels = spec.channels.count() as u16;
}
let mut sample_buf = SampleBuffer::<i16>::new(audio_buf.frames() as u64, spec);
sample_buf.copy_interleaved_ref(audio_buf);
samples.extend_from_slice(sample_buf.samples());
}
Err(SymphoniaError::DecodeError(_)) => continue,
Err(SymphoniaError::ResetRequired) => {
decoder.reset();
continue;
}
Err(err) => return Err(format!("Symphonia decode error: {}", err)),
}
}
if samples.is_empty() || sample_rate == 0 || channels == 0 {
return Err("Symphonia decode produced no audio".to_string());
}
Ok(SamplesBuffer::new(channels, sample_rate, samples))
}
fn is_isomp4(data: &[u8]) -> bool {
if data.len() < 12 {
return false;
}
&data[4..8] == b"ftyp"
}
fn decode_with_fallback(
data: &[u8],
) -> Result<Decoder<BufReader<Cursor<Vec<u8>>>>, rodio::decoder::DecoderError> {
) -> Result<Box<dyn Source<Item = i16> + Send>, String> {
if is_isomp4(data) {
return decode_with_symphonia(data)
.map(|buffer| {
log::info!("Decoded audio using symphonia fallback (isomp4)");
Box::new(buffer) as Box<dyn Source<Item = i16> + Send>
});
}
let primary = panic::catch_unwind(AssertUnwindSafe(|| {
Decoder::new(BufReader::new(Cursor::new(data.to_vec())))
}));
match primary {
Ok(Ok(decoder)) => return Ok(decoder),
Ok(Ok(decoder)) => return Ok(Box::new(decoder)),
Ok(Err(err)) => {
log::warn!("Primary decode failed, attempting mp4 fallback: {}", err);
}
@@ -67,7 +194,7 @@ fn decode_with_fallback(
match attempt {
Ok(Ok(decoder)) => {
log::info!("Decoded audio using mp4 fallback ({})", hint_label);
return Ok(decoder);
return Ok(Box::new(decoder));
}
Ok(Err(err)) => {
log::warn!("mp4 fallback ({}) failed: {}", hint_label, err);
@@ -78,7 +205,13 @@ fn decode_with_fallback(
}
}
Err(rodio::decoder::DecoderError::UnrecognizedFormat)
match decode_with_symphonia(data) {
Ok(buffer) => {
log::info!("Decoded audio using symphonia fallback");
Ok(Box::new(buffer))
}
Err(err) => Err(err),
}
}
/// Event payload for playback state updates