mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(audio): migrate Symphonia 0.5 -> 0.6 (#999)
* feat(audio): migrate Symphonia 0.5 -> 0.6 Port the audio + analysis pipeline to the Symphonia 0.6 API: - bump symphonia to 0.6 and symphonia-adapter-libopus to 0.3; drop rodio's symphonia-all feature to avoid a duplicate symphonia-core 0.5 - remove the local symphonia-format-isomp4 0.5 patch and rely on upstream 0.6 - switch codec registries to register_enabled_codecs / make_audio_decoder with AudioCodecParameters + AudioDecoderOptions - rework decode.rs SizedDecoder and analysis decode loops for the new AudioDecoder trait, GenericAudioBufferRef, Time/Timestamp newtypes, and next_packet() -> Result<Option<Packet>> Streaming regression fixes: - ProbeSeekGate hides seekability during probe for non-MP4 progressive streams so Symphonia 0.6's trailing-metadata scan no longer forces a full download before ranged FLAC/MP3/OGG playback can start - guard the streaming probe() with a 20s timeout on a worker thread so a stalled ranged source (e.g. right after a server switch) can no longer hang playback start until a player restart; add probe start/done diagnostics * docs(changelog): note Symphonia 0.6 migration and streaming fixes (#999) Add CHANGELOG entries (Changed + Fixed) and a CONTRIBUTORS line for the Symphonia 0.6 migration, ranged-stream start-latency fix, and probe timeout. * test(audio): cover streaming probe path and ProbeSeekGate Add unit tests for SizedDecoder::new_streaming (success + garbage) and ProbeSeekGate (seekability toggle, byte_len, read/seek passthrough) to restore decode.rs above the 70% hot-path coverage gate (67.3% -> 79.4%).
This commit is contained in:
@@ -17,8 +17,8 @@ futures-util = "0.3"
|
||||
ebur128 = "0.1"
|
||||
md5 = "0.8"
|
||||
rusqlite = { version = "0.40", features = ["bundled"] }
|
||||
symphonia = { version = "0.5", default-features = false, features = ["flac", "mp3", "pcm", "aac", "alac", "isomp4", "vorbis", "ogg", "wav", "adpcm"] }
|
||||
symphonia-adapter-libopus = "0.2.9"
|
||||
symphonia = { version = "0.6", default-features = false, features = ["flac", "mp3", "pcm", "aac", "alac", "isomp4", "vorbis", "ogg", "wav", "adpcm", "all-meta"] }
|
||||
symphonia-adapter-libopus = "0.3"
|
||||
oximedia-mir = { version = "0.1.7", default-features = false, features = ["tempo", "mood"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -2,13 +2,12 @@ use std::io::Cursor;
|
||||
use std::time::Instant;
|
||||
|
||||
use ebur128::{EbuR128, Mode as Ebur128Mode};
|
||||
use symphonia::core::audio::SampleBuffer;
|
||||
use symphonia::core::codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL};
|
||||
use symphonia::core::codecs::audio::{AudioDecoder, AudioDecoderOptions};
|
||||
use symphonia::core::errors::Error as SymphoniaError;
|
||||
use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo};
|
||||
use symphonia::core::formats::probe::Hint;
|
||||
use symphonia::core::formats::{FormatOptions, FormatReader, SeekMode, SeekTo, TrackType};
|
||||
use symphonia::core::io::MediaSourceStream;
|
||||
use symphonia::core::meta::MetadataOptions;
|
||||
use symphonia::core::probe::Hint;
|
||||
use symphonia::core::units::Time;
|
||||
use tauri::{Manager, Runtime};
|
||||
use psysonic_core::track_enrichment::TrackEnrichmentOutcome;
|
||||
@@ -299,7 +298,7 @@ fn analyze_loudness_and_waveform(
|
||||
/// when the container reports total track length.
|
||||
struct DecodeSession {
|
||||
format: Box<dyn FormatReader>,
|
||||
decoder: Box<dyn Decoder>,
|
||||
decoder: Box<dyn AudioDecoder>,
|
||||
track_id: u32,
|
||||
timeline_hint: Option<u64>,
|
||||
}
|
||||
@@ -334,25 +333,25 @@ fn open_decode_session(bytes: &[u8], format_hint: Option<&str>) -> Option<Decode
|
||||
if let Some(ext) = format_hint.or(sniffed.as_deref()) {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
let probed = symphonia::default::get_probe()
|
||||
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())
|
||||
let format = symphonia::default::get_probe()
|
||||
.probe(&hint, mss, FormatOptions::default(), MetadataOptions::default())
|
||||
.ok()?;
|
||||
let format = probed.format;
|
||||
// Prefer an audio track that reports both sample rate and channels; fall back to
|
||||
// the first audio track with a known codec (skips e.g. MJPEG cover-art tracks).
|
||||
let track = format
|
||||
.default_track()
|
||||
.filter(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.or_else(|| {
|
||||
format.tracks().iter().find(|t| {
|
||||
t.codec_params.codec != CODEC_TYPE_NULL
|
||||
&& t.codec_params.sample_rate.is_some()
|
||||
&& t.codec_params.channels.is_some()
|
||||
})
|
||||
.tracks()
|
||||
.iter()
|
||||
.find(|t| {
|
||||
t.codec_params
|
||||
.as_ref()
|
||||
.and_then(|c| c.audio())
|
||||
.is_some_and(|a| a.sample_rate.is_some() && a.channels.is_some())
|
||||
})
|
||||
.or_else(|| format.tracks().iter().find(|t| t.codec_params.codec != CODEC_TYPE_NULL))?;
|
||||
.or_else(|| format.first_track_known_codec(TrackType::Audio))?;
|
||||
let track_id = track.id;
|
||||
let timeline_hint = track.codec_params.n_frames.filter(|&n| n > 0);
|
||||
let codec_params = track.codec_params.clone();
|
||||
let decoder = match make_decoder(&codec_params, &DecoderOptions::default()) {
|
||||
let timeline_hint = track.num_frames.filter(|&n| n > 0);
|
||||
let audio_params = track.codec_params.as_ref()?.audio()?.clone();
|
||||
let decoder = match make_decoder(&audio_params, &AudioDecoderOptions::default().gapless(false)) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
crate::app_deprintln!("[analysis] decoder make failed: {}", e);
|
||||
@@ -372,8 +371,9 @@ fn count_mono_frames_from_audio_bytes(bytes: &[u8], format_hint: Option<&str>) -
|
||||
|
||||
let mut total: u64 = 0;
|
||||
let mut loop_i: u32 = 0;
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != track_id {
|
||||
let mut samples_buf: Vec<f32> = Vec::new();
|
||||
while let Ok(Some(packet)) = format.next_packet() {
|
||||
if packet.track_id != track_id {
|
||||
continue;
|
||||
}
|
||||
let decoded = match decoder.decode(&packet) {
|
||||
@@ -382,14 +382,12 @@ fn count_mono_frames_from_audio_bytes(bytes: &[u8], format_hint: Option<&str>) -
|
||||
Err(SymphoniaError::ResetRequired) => break,
|
||||
Err(_) => break,
|
||||
};
|
||||
let spec = *decoded.spec();
|
||||
let n_ch = spec.channels.count();
|
||||
let n_ch = decoded.spec().channels().count();
|
||||
if n_ch == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut samples = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
|
||||
samples.copy_interleaved_ref(decoded);
|
||||
let n = samples.samples().len();
|
||||
decoded.copy_to_vec_interleaved(&mut samples_buf);
|
||||
let n = samples_buf.len();
|
||||
if n < n_ch || !n.is_multiple_of(n_ch) {
|
||||
continue;
|
||||
}
|
||||
@@ -460,8 +458,9 @@ fn decode_scan_pcm(
|
||||
}
|
||||
let bin_grid_frames = decoded_frames.max(1);
|
||||
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != track_id {
|
||||
let mut samples_buf: Vec<f32> = Vec::new();
|
||||
while let Ok(Some(packet)) = format.next_packet() {
|
||||
if packet.track_id != track_id {
|
||||
continue;
|
||||
}
|
||||
let decoded = match decoder.decode(&packet) {
|
||||
@@ -471,15 +470,14 @@ fn decode_scan_pcm(
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let spec = *decoded.spec();
|
||||
let n_ch = spec.channels.count();
|
||||
let n_ch = decoded.spec().channels().count();
|
||||
if n_ch == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if loudness_target_lufs.is_some() && ebu.is_none() {
|
||||
let ch = spec.channels.count() as u32;
|
||||
let sr = spec.rate;
|
||||
let ch = decoded.spec().channels().count() as u32;
|
||||
let sr = decoded.spec().rate();
|
||||
match EbuR128::new(ch, sr, Ebur128Mode::I | Ebur128Mode::TRUE_PEAK) {
|
||||
Ok(v) => {
|
||||
ebu = Some(v);
|
||||
@@ -497,9 +495,8 @@ fn decode_scan_pcm(
|
||||
}
|
||||
}
|
||||
|
||||
let mut samples = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
|
||||
samples.copy_interleaved_ref(decoded);
|
||||
let slice = samples.samples();
|
||||
decoded.copy_to_vec_interleaved(&mut samples_buf);
|
||||
let slice = samples_buf.as_slice();
|
||||
if slice.len() < n_ch || !slice.len().is_multiple_of(n_ch) {
|
||||
continue;
|
||||
}
|
||||
@@ -531,7 +528,7 @@ fn decode_scan_pcm(
|
||||
|
||||
if loudness_target_lufs.is_some() {
|
||||
if let Some(e) = ebu.as_mut() {
|
||||
match e.add_frames_f32(samples.samples()) {
|
||||
match e.add_frames_f32(&samples_buf) {
|
||||
Ok(_) => fed_any_frames = true,
|
||||
Err(err) => {
|
||||
crate::app_deprintln!("[analysis] loudness add_frames failed: {}", err);
|
||||
@@ -634,9 +631,11 @@ pub fn audio_duration_from_bytes(bytes: &[u8]) -> Option<f64> {
|
||||
let session = open_decode_session(bytes, None)?;
|
||||
let sample_rate = session
|
||||
.format
|
||||
.default_track()
|
||||
.default_track(TrackType::Audio)
|
||||
.or_else(|| session.format.tracks().first())
|
||||
.and_then(|t| t.codec_params.sample_rate)
|
||||
.and_then(|t| t.codec_params.as_ref())
|
||||
.and_then(|c| c.audio())
|
||||
.and_then(|a| a.sample_rate)
|
||||
.filter(|&sr| sr > 0)?;
|
||||
let frames = session.timeline_hint?;
|
||||
Some(frames as f64 / sample_rate as f64)
|
||||
@@ -659,7 +658,8 @@ pub fn decode_mono_pcm_window(
|
||||
} = open_decode_session(bytes, None).ok_or_else(|| "failed to open audio decode session".to_string())?;
|
||||
|
||||
if start_sec.is_finite() && start_sec > 0.0 {
|
||||
let time: Time = start_sec.max(0.0).into();
|
||||
let time = Time::try_from_secs_f64(start_sec.max(0.0))
|
||||
.ok_or_else(|| "pcm window: invalid seek time".to_string())?;
|
||||
format
|
||||
.seek(
|
||||
SeekMode::Accurate,
|
||||
@@ -693,7 +693,7 @@ pub fn decode_mono_pcm_limited(
|
||||
|
||||
fn decode_mono_pcm_from_session(
|
||||
format: &mut Box<dyn FormatReader>,
|
||||
decoder: &mut Box<dyn Decoder>,
|
||||
decoder: &mut Box<dyn AudioDecoder>,
|
||||
track_id: u32,
|
||||
max_seconds: Option<f64>,
|
||||
) -> Result<(Vec<f32>, f32), String> {
|
||||
@@ -701,9 +701,10 @@ fn decode_mono_pcm_from_session(
|
||||
let mut sample_rate = 0_f32;
|
||||
let mut max_frames: Option<u64> = None;
|
||||
let mut loop_i: u32 = 0;
|
||||
let mut samples_buf: Vec<f32> = Vec::new();
|
||||
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != track_id {
|
||||
while let Ok(Some(packet)) = format.next_packet() {
|
||||
if packet.track_id != track_id {
|
||||
continue;
|
||||
}
|
||||
let decoded = match decoder.decode(&packet) {
|
||||
@@ -713,13 +714,12 @@ fn decode_mono_pcm_from_session(
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let spec = *decoded.spec();
|
||||
let n_ch = spec.channels.count();
|
||||
let n_ch = decoded.spec().channels().count();
|
||||
if n_ch == 0 {
|
||||
continue;
|
||||
}
|
||||
if sample_rate <= 0.0 {
|
||||
sample_rate = spec.rate as f32;
|
||||
sample_rate = decoded.spec().rate() as f32;
|
||||
if sample_rate <= 0.0 {
|
||||
return Err("invalid sample rate".to_string());
|
||||
}
|
||||
@@ -732,9 +732,8 @@ fn decode_mono_pcm_from_session(
|
||||
});
|
||||
}
|
||||
|
||||
let mut samples = SampleBuffer::<f32>::new(decoded.capacity() as u64, spec);
|
||||
samples.copy_interleaved_ref(decoded);
|
||||
let slice = samples.samples();
|
||||
decoded.copy_to_vec_interleaved(&mut samples_buf);
|
||||
let slice = samples_buf.as_slice();
|
||||
if slice.len() < n_ch || !slice.len().is_multiple_of(n_ch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
//! Symphonia codec registry — mirrors `psysonic-audio::codec` (Opus via libopus).
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use symphonia::core::codecs::{CodecRegistry, DecoderOptions};
|
||||
use symphonia::core::codecs::audio::{AudioCodecParameters, AudioDecoder, AudioDecoderOptions};
|
||||
use symphonia::core::codecs::registry::CodecRegistry;
|
||||
|
||||
pub(crate) fn psysonic_codec_registry() -> &'static CodecRegistry {
|
||||
static REGISTRY: OnceLock<CodecRegistry> = OnceLock::new();
|
||||
REGISTRY.get_or_init(|| {
|
||||
let mut registry = CodecRegistry::new();
|
||||
symphonia::default::register_enabled_codecs(&mut registry);
|
||||
registry.register_all::<symphonia_adapter_libopus::OpusDecoder>();
|
||||
registry.register_audio_decoder::<symphonia_adapter_libopus::OpusDecoder>();
|
||||
registry
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn make_decoder(
|
||||
params: &symphonia::core::codecs::CodecParameters,
|
||||
opts: &DecoderOptions,
|
||||
) -> Result<Box<dyn symphonia::core::codecs::Decoder>, symphonia::core::errors::Error> {
|
||||
psysonic_codec_registry().make(params, opts)
|
||||
params: &AudioCodecParameters,
|
||||
opts: &AudioDecoderOptions,
|
||||
) -> Result<Box<dyn AudioDecoder>, symphonia::core::errors::Error> {
|
||||
psysonic_codec_registry().make_audio_decoder(params, opts)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user