mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(analysis): decode Opus in waveform and loudness pipeline (#883)
* fix(analysis): decode Opus in waveform and loudness pipeline Playback already registered symphonia-adapter-libopus; the analysis crate used the default Symphonia codec registry without Opus, so .opus tracks failed at decoder creation. Mirror the audio codec registry, pass format hints from file suffix and OggS sniffing, and thread hints through the CPU seed queue. * docs: CHANGELOG and credits for PR #883 (Opus analysis decode)
This commit is contained in:
Generated
+1
@@ -4099,6 +4099,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"symphonia",
|
||||
"symphonia-adapter-libopus",
|
||||
"tauri",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -18,6 +18,7 @@ ebur128 = "0.1"
|
||||
md5 = "0.8"
|
||||
rusqlite = { version = "0.39", 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"
|
||||
oximedia-mir = { version = "0.1.7", default-features = false, features = ["tempo", "mood"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -14,6 +14,7 @@ use tauri::{Manager, Runtime};
|
||||
use psysonic_core::track_enrichment::TrackEnrichmentOutcome;
|
||||
|
||||
use crate::analysis_perf::AnalysisSeedTimings;
|
||||
use crate::codec::make_decoder;
|
||||
|
||||
use super::store::{now_unix_ts, AnalysisCache, LoudnessEntry, TrackKey, WaveformEntry};
|
||||
|
||||
@@ -47,6 +48,7 @@ pub fn seed_from_bytes_execute<R: Runtime>(
|
||||
server_id: &str,
|
||||
track_id: &str,
|
||||
bytes: &[u8],
|
||||
format_hint: Option<&str>,
|
||||
notify_ui: bool,
|
||||
) -> Result<(SeedFromBytesOutcome, AnalysisSeedTimings), String> {
|
||||
let seed_started = Instant::now();
|
||||
@@ -61,7 +63,8 @@ pub fn seed_from_bytes_execute<R: Runtime>(
|
||||
AnalysisSeedTimings::default(),
|
||||
));
|
||||
};
|
||||
let (outcome, md5_16kb) = seed_from_bytes_into_cache(&cache, server_id, track_id, bytes)?;
|
||||
let (outcome, md5_16kb) =
|
||||
seed_from_bytes_into_cache(&cache, server_id, track_id, bytes, format_hint)?;
|
||||
let seed_ms = seed_started.elapsed().as_millis() as u64;
|
||||
// E2 bridge (analysis → library content_hash): once the playback-derived
|
||||
// md5_16kb is known — whether freshly written or already cached — record it
|
||||
@@ -129,6 +132,7 @@ pub fn seed_from_bytes_into_cache(
|
||||
server_id: &str,
|
||||
track_id: &str,
|
||||
bytes: &[u8],
|
||||
format_hint: Option<&str>,
|
||||
) -> Result<(SeedFromBytesOutcome, String), String> {
|
||||
let started = Instant::now();
|
||||
// Write under the playback server's scope.
|
||||
@@ -168,7 +172,8 @@ pub fn seed_from_bytes_into_cache(
|
||||
let build = (|| -> Result<(bool, usize), String> {
|
||||
cache.touch_track_status(&key, "queued")?;
|
||||
|
||||
let (wf_bins, loudness_opt, used_pcm_decode) = match analyze_loudness_and_waveform(bytes, -16.0, 500) {
|
||||
let (wf_bins, loudness_opt, used_pcm_decode) =
|
||||
match analyze_loudness_and_waveform(bytes, -16.0, 500, format_hint) {
|
||||
Some((integrated_lufs, true_peak, recommended_gain_db, target_lufs, bins)) => {
|
||||
(
|
||||
bins,
|
||||
@@ -275,15 +280,16 @@ fn analyze_loudness_and_waveform(
|
||||
bytes: &[u8],
|
||||
target_lufs: f64,
|
||||
bin_count: usize,
|
||||
format_hint: Option<&str>,
|
||||
) -> Option<(f64, f64, f64, f64, Vec<u8>)> {
|
||||
if bytes.is_empty() || bin_count == 0 {
|
||||
return None;
|
||||
}
|
||||
let (decoded_frames, timeline_hint) = count_mono_frames_from_audio_bytes(bytes)?;
|
||||
let (decoded_frames, timeline_hint) = count_mono_frames_from_audio_bytes(bytes, format_hint)?;
|
||||
if decoded_frames == 0 {
|
||||
return None;
|
||||
}
|
||||
let scanned = decode_scan_pcm(bytes, bin_count, decoded_frames, timeline_hint, Some(target_lufs))?;
|
||||
let scanned = decode_scan_pcm(bytes, bin_count, decoded_frames, timeline_hint, Some(target_lufs), format_hint)?;
|
||||
let (i, t, r, tgt) = scanned.loudness?;
|
||||
Some((i, t, r, tgt, scanned.bins))
|
||||
}
|
||||
@@ -298,10 +304,36 @@ struct DecodeSession {
|
||||
timeline_hint: Option<u64>,
|
||||
}
|
||||
|
||||
fn open_decode_session(bytes: &[u8]) -> Option<DecodeSession> {
|
||||
fn format_hint_from_bytes(bytes: &[u8]) -> Option<String> {
|
||||
if bytes.len() < 4 {
|
||||
return None;
|
||||
}
|
||||
if bytes[0..4] == *b"OggS" {
|
||||
return Some("ogg".into());
|
||||
}
|
||||
if bytes.len() >= 4 && bytes[0..4] == *b"fLaC" {
|
||||
return Some("flac".into());
|
||||
}
|
||||
if bytes.len() >= 12 && bytes[0..4] == *b"RIFF" && bytes[8..12] == *b"WAVE" {
|
||||
return Some("wav".into());
|
||||
}
|
||||
let scan = bytes.len().min(4096).saturating_sub(4);
|
||||
for i in 0..=scan {
|
||||
if bytes[i..i + 4] == *b"ftyp" {
|
||||
return Some("m4a".into());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn open_decode_session(bytes: &[u8], format_hint: Option<&str>) -> Option<DecodeSession> {
|
||||
let source = Box::new(Cursor::new(bytes.to_vec()));
|
||||
let mss = MediaSourceStream::new(source, Default::default());
|
||||
let hint = Hint::new();
|
||||
let sniffed = format_hint_from_bytes(bytes);
|
||||
let mut hint = Hint::new();
|
||||
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())
|
||||
.ok()?;
|
||||
@@ -320,7 +352,7 @@ fn open_decode_session(bytes: &[u8]) -> Option<DecodeSession> {
|
||||
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 symphonia::default::get_codecs().make(&codec_params, &DecoderOptions::default()) {
|
||||
let decoder = match make_decoder(&codec_params, &DecoderOptions::default()) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
crate::app_deprintln!("[analysis] decoder make failed: {}", e);
|
||||
@@ -334,9 +366,9 @@ fn open_decode_session(bytes: &[u8]) -> Option<DecodeSession> {
|
||||
/// `codec_params.n_frames` when the container reports total track length — used
|
||||
/// as a **fixed** waveform time axis so partial decodes do not remap every bin
|
||||
/// when the buffer grows.
|
||||
fn count_mono_frames_from_audio_bytes(bytes: &[u8]) -> Option<(u64, Option<u64>)> {
|
||||
fn count_mono_frames_from_audio_bytes(bytes: &[u8], format_hint: Option<&str>) -> Option<(u64, Option<u64>)> {
|
||||
let DecodeSession { mut format, mut decoder, track_id, timeline_hint } =
|
||||
open_decode_session(bytes)?;
|
||||
open_decode_session(bytes, format_hint)?;
|
||||
|
||||
let mut total: u64 = 0;
|
||||
let mut loop_i: u32 = 0;
|
||||
@@ -399,8 +431,9 @@ fn decode_scan_pcm(
|
||||
decoded_frames: u64,
|
||||
timeline_hint: Option<u64>,
|
||||
loudness_target_lufs: Option<f64>,
|
||||
format_hint: Option<&str>,
|
||||
) -> Option<PcmScanResult> {
|
||||
let DecodeSession { mut format, mut decoder, track_id, .. } = open_decode_session(bytes)?;
|
||||
let DecodeSession { mut format, mut decoder, track_id, .. } = open_decode_session(bytes, format_hint)?;
|
||||
|
||||
let mut bin_max = vec![0.0f32; bin_count];
|
||||
let mut bin_sum = vec![0.0f32; bin_count];
|
||||
@@ -598,7 +631,7 @@ pub fn analysis_pcm_window(total_duration_sec: f64, window_sec: f64) -> PcmAnaly
|
||||
|
||||
/// Best-effort container duration from codec metadata (seconds).
|
||||
pub fn audio_duration_from_bytes(bytes: &[u8]) -> Option<f64> {
|
||||
let session = open_decode_session(bytes)?;
|
||||
let session = open_decode_session(bytes, None)?;
|
||||
let sample_rate = session
|
||||
.format
|
||||
.default_track()
|
||||
@@ -623,7 +656,7 @@ pub fn decode_mono_pcm_window(
|
||||
mut decoder,
|
||||
track_id,
|
||||
..
|
||||
} = open_decode_session(bytes).ok_or_else(|| "failed to open audio decode session".to_string())?;
|
||||
} = 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();
|
||||
@@ -654,7 +687,7 @@ pub fn decode_mono_pcm_limited(
|
||||
mut decoder,
|
||||
track_id,
|
||||
..
|
||||
} = open_decode_session(bytes).ok_or_else(|| "failed to open audio decode session".to_string())?;
|
||||
} = open_decode_session(bytes, None).ok_or_else(|| "failed to open audio decode session".to_string())?;
|
||||
decode_mono_pcm_from_session(&mut format, &mut decoder, track_id, max_seconds)
|
||||
}
|
||||
|
||||
@@ -927,7 +960,7 @@ mod tests {
|
||||
#[test]
|
||||
fn count_mono_frames_returns_decoded_length_for_synthetic_wav() {
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.0), 44_100);
|
||||
let (frames, _hint) = count_mono_frames_from_audio_bytes(&wav)
|
||||
let (frames, _hint) = count_mono_frames_from_audio_bytes(&wav, None)
|
||||
.expect("WAV decode must succeed");
|
||||
// 1 second × 44.1 kHz mono = 44 100 frames; allow ±1 packet tolerance.
|
||||
assert!(
|
||||
@@ -938,18 +971,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn count_mono_frames_returns_none_for_garbage_bytes() {
|
||||
assert!(count_mono_frames_from_audio_bytes(b"not an audio file").is_none());
|
||||
assert!(count_mono_frames_from_audio_bytes(b"not an audio file", None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_mono_frames_returns_none_for_empty_bytes() {
|
||||
assert!(count_mono_frames_from_audio_bytes(&[]).is_none());
|
||||
assert!(count_mono_frames_from_audio_bytes(&[], None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_loudness_and_waveform_returns_loudness_for_synthetic_sine() {
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.5), 44_100);
|
||||
let result = analyze_loudness_and_waveform(&wav, -14.0, 100)
|
||||
let result = analyze_loudness_and_waveform(&wav, -14.0, 100, None)
|
||||
.expect("WAV decode must succeed");
|
||||
let (integrated_lufs, true_peak, recommended_gain_db, target_lufs, bins) = result;
|
||||
assert_eq!(bins.len(), 200, "bins layout is peak_u8 + mean_u8 = 2 * bin_count");
|
||||
@@ -974,19 +1007,19 @@ mod tests {
|
||||
#[test]
|
||||
fn analyze_loudness_returns_none_for_zero_bin_count() {
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 0.5), 44_100);
|
||||
assert!(analyze_loudness_and_waveform(&wav, -14.0, 0).is_none());
|
||||
assert!(analyze_loudness_and_waveform(&wav, -14.0, 0, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_loudness_returns_none_for_empty_bytes() {
|
||||
assert!(analyze_loudness_and_waveform(&[], -14.0, 100).is_none());
|
||||
assert!(analyze_loudness_and_waveform(&[], -14.0, 100, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_from_bytes_into_cache_upserts_waveform_and_loudness_for_wav() {
|
||||
let cache = AnalysisCache::open_in_memory();
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.5), 44_100);
|
||||
let (outcome, md5) = seed_from_bytes_into_cache(&cache, "server-a", "wav-track", &wav).unwrap();
|
||||
let (outcome, md5) = seed_from_bytes_into_cache(&cache, "server-a", "wav-track", &wav, None).unwrap();
|
||||
assert_eq!(outcome, SeedFromBytesOutcome::Upserted);
|
||||
assert_eq!(md5, md5_first_16kb(&wav), "outcome carries the content fingerprint");
|
||||
|
||||
@@ -1007,7 +1040,7 @@ mod tests {
|
||||
fn seed_from_bytes_into_cache_writes_under_the_given_server_scope() {
|
||||
let cache = AnalysisCache::open_in_memory();
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.5), 44_100);
|
||||
seed_from_bytes_into_cache(&cache, "server-x", "scoped-track", &wav).unwrap();
|
||||
seed_from_bytes_into_cache(&cache, "server-x", "scoped-track", &wav, None).unwrap();
|
||||
|
||||
let md5 = md5_first_16kb(&wav);
|
||||
let scoped = TrackKey {
|
||||
@@ -1028,9 +1061,9 @@ mod tests {
|
||||
fn seed_from_bytes_into_cache_returns_skipped_on_second_call() {
|
||||
let cache = AnalysisCache::open_in_memory();
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.0), 44_100);
|
||||
let (first, _) = seed_from_bytes_into_cache(&cache, "server-a", "wav-track-2", &wav).unwrap();
|
||||
let (first, _) = seed_from_bytes_into_cache(&cache, "server-a", "wav-track-2", &wav, None).unwrap();
|
||||
assert_eq!(first, SeedFromBytesOutcome::Upserted);
|
||||
let (second, _) = seed_from_bytes_into_cache(&cache, "server-a", "wav-track-2", &wav).unwrap();
|
||||
let (second, _) = seed_from_bytes_into_cache(&cache, "server-a", "wav-track-2", &wav, None).unwrap();
|
||||
assert_eq!(
|
||||
second,
|
||||
SeedFromBytesOutcome::SkippedWaveformCacheHit,
|
||||
@@ -1044,7 +1077,7 @@ mod tests {
|
||||
// Garbage bytes — Symphonia probe fails, the pipeline falls back to
|
||||
// `derive_waveform_bins` (no loudness row gets cached).
|
||||
let bytes = vec![0xAAu8; 8 * 1024];
|
||||
let (outcome, _) = seed_from_bytes_into_cache(&cache, "server-a", "garbage", &bytes).unwrap();
|
||||
let (outcome, _) = seed_from_bytes_into_cache(&cache, "server-a", "garbage", &bytes, None).unwrap();
|
||||
assert_eq!(outcome, SeedFromBytesOutcome::Upserted);
|
||||
|
||||
let key = TrackKey {
|
||||
@@ -1126,8 +1159,8 @@ mod tests {
|
||||
#[test]
|
||||
fn decode_scan_pcm_supports_waveform_only_mode_without_loudness() {
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.0), 44_100);
|
||||
let (frames, hint) = count_mono_frames_from_audio_bytes(&wav).expect("frame counting");
|
||||
let scanned = decode_scan_pcm(&wav, 64, frames, hint, None).expect("scan must succeed");
|
||||
let (frames, hint) = count_mono_frames_from_audio_bytes(&wav, None).expect("frame counting");
|
||||
let scanned = decode_scan_pcm(&wav, 64, frames, hint, None, None).expect("scan must succeed");
|
||||
assert_eq!(scanned.bins.len(), 128);
|
||||
assert!(scanned.loudness.is_none());
|
||||
}
|
||||
@@ -1135,8 +1168,8 @@ mod tests {
|
||||
#[test]
|
||||
fn decode_scan_pcm_with_loudness_target_returns_loudness_tuple() {
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.0), 44_100);
|
||||
let (frames, hint) = count_mono_frames_from_audio_bytes(&wav).expect("frame counting");
|
||||
let scanned = decode_scan_pcm(&wav, 64, frames, hint, Some(-14.0)).expect("scan must succeed");
|
||||
let (frames, hint) = count_mono_frames_from_audio_bytes(&wav, None).expect("frame counting");
|
||||
let scanned = decode_scan_pcm(&wav, 64, frames, hint, Some(-14.0), None).expect("scan must succeed");
|
||||
assert_eq!(scanned.bins.len(), 128);
|
||||
let (integrated_lufs, true_peak, recommended_gain_db, target_lufs) =
|
||||
scanned.loudness.expect("loudness tuple must be present");
|
||||
@@ -1148,7 +1181,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decode_scan_pcm_returns_none_for_non_audio_input() {
|
||||
assert!(decode_scan_pcm(b"nope", 32, 10, None, Some(-14.0)).is_none());
|
||||
assert!(decode_scan_pcm(b"nope", 32, 10, None, Some(-14.0), None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1178,7 +1211,7 @@ mod tests {
|
||||
assert!(!cache.loudness_row_exists_for_key(&key).unwrap());
|
||||
|
||||
let (outcome, _) =
|
||||
seed_from_bytes_into_cache(&cache, "server-a", "track-reseed", &wav).unwrap();
|
||||
seed_from_bytes_into_cache(&cache, "server-a", "track-reseed", &wav, None).unwrap();
|
||||
assert_eq!(outcome, SeedFromBytesOutcome::Upserted);
|
||||
assert!(cache.loudness_row_exists_for_key(&key).unwrap());
|
||||
}
|
||||
@@ -1219,14 +1252,14 @@ mod tests {
|
||||
#[test]
|
||||
fn decode_scan_pcm_returns_none_when_no_frames_decoded() {
|
||||
let wav = build_mono_pcm16_wav(&[], 44_100);
|
||||
assert!(analyze_loudness_and_waveform(&wav, -14.0, 64).is_none());
|
||||
assert!(analyze_loudness_and_waveform(&wav, -14.0, 64, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_scan_pcm_ignores_oversized_timeline_hint() {
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 1.0), 44_100);
|
||||
let (frames, _hint) = count_mono_frames_from_audio_bytes(&wav).expect("frame counting");
|
||||
let scanned = decode_scan_pcm(&wav, 64, frames, Some(frames * 10), None).unwrap();
|
||||
let (frames, _hint) = count_mono_frames_from_audio_bytes(&wav, None).expect("frame counting");
|
||||
let scanned = decode_scan_pcm(&wav, 64, frames, Some(frames * 10), None, None).unwrap();
|
||||
assert_eq!(scanned.bins.len(), 128);
|
||||
}
|
||||
|
||||
@@ -1235,7 +1268,7 @@ mod tests {
|
||||
let app = tauri::test::mock_app();
|
||||
let wav = build_mono_pcm16_wav(&sine_440_at_minus_6db(44_100, 0.25), 44_100);
|
||||
let handle = app.handle().clone();
|
||||
let (outcome, timings) = seed_from_bytes_execute(&handle, "s", "t", &wav, true)
|
||||
let (outcome, timings) = seed_from_bytes_execute(&handle, "s", "t", &wav, None, true)
|
||||
.expect("seed execute should return a graceful skip");
|
||||
assert_eq!(outcome, SeedFromBytesOutcome::SkippedNoAnalysisCache);
|
||||
assert_eq!(timings.seed_ms, 0);
|
||||
@@ -1250,12 +1283,12 @@ mod tests {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
let (first, timings_first) =
|
||||
seed_from_bytes_execute(&handle, "server-a", "track-exec", &wav, true).unwrap();
|
||||
seed_from_bytes_execute(&handle, "server-a", "track-exec", &wav, None, true).unwrap();
|
||||
assert_eq!(first, SeedFromBytesOutcome::Upserted);
|
||||
assert!(timings_first.seed_ms <= 30_000);
|
||||
|
||||
let (second, timings_second) =
|
||||
seed_from_bytes_execute(&handle, "server-a", "track-exec", &wav, true).unwrap();
|
||||
seed_from_bytes_execute(&handle, "server-a", "track-exec", &wav, None, true).unwrap();
|
||||
assert_eq!(second, SeedFromBytesOutcome::SkippedWaveformCacheHit);
|
||||
assert!(timings_second.seed_ms <= 30_000);
|
||||
}
|
||||
|
||||
@@ -384,9 +384,10 @@ pub async fn enqueue_track_analysis(
|
||||
server_id: &str,
|
||||
track_id: &str,
|
||||
bytes: &[u8],
|
||||
format_hint: Option<&str>,
|
||||
priority: AnalysisBackfillPriority,
|
||||
) -> Result<EnqueueTrackAnalysisOutcome, String> {
|
||||
enqueue_track_analysis_with_fetch(app, server_id, track_id, bytes, priority, 0).await
|
||||
enqueue_track_analysis_with_fetch(app, server_id, track_id, bytes, format_hint, priority, 0).await
|
||||
}
|
||||
|
||||
async fn enqueue_track_analysis_with_fetch(
|
||||
@@ -394,6 +395,7 @@ async fn enqueue_track_analysis_with_fetch(
|
||||
server_id: &str,
|
||||
track_id: &str,
|
||||
bytes: &[u8],
|
||||
format_hint: Option<&str>,
|
||||
priority: AnalysisBackfillPriority,
|
||||
fetch_ms: u64,
|
||||
) -> Result<EnqueueTrackAnalysisOutcome, String> {
|
||||
@@ -424,6 +426,7 @@ async fn enqueue_track_analysis_with_fetch(
|
||||
server_id.to_string(),
|
||||
track_id.to_string(),
|
||||
bytes.to_vec(),
|
||||
format_hint.map(str::to_string),
|
||||
priority,
|
||||
fetch_ms,
|
||||
)
|
||||
@@ -505,7 +508,12 @@ pub async fn enqueue_track_analysis_from_file(
|
||||
if bytes.is_empty() {
|
||||
return Ok(EnqueueTrackAnalysisOutcome::Complete);
|
||||
}
|
||||
enqueue_track_analysis(app, server_id, track_id, &bytes, priority).await
|
||||
let format_hint = file_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.to_ascii_lowercase())
|
||||
.filter(|e| !e.is_empty());
|
||||
enqueue_track_analysis(app, server_id, track_id, &bytes, format_hint.as_deref(), priority).await
|
||||
}
|
||||
|
||||
/// Decode `bytes` for `track_id` via the cpu-seed queue. Prefer [`enqueue_track_analysis`].
|
||||
@@ -516,7 +524,7 @@ pub async fn enqueue_analysis_seed(
|
||||
bytes: &[u8],
|
||||
) -> Result<bool, String> {
|
||||
let priority = analysis_backfill_resolve_priority(app, server_id, track_id, None);
|
||||
let outcome = enqueue_track_analysis(app, server_id, track_id, bytes, priority).await?;
|
||||
let outcome = enqueue_track_analysis(app, server_id, track_id, bytes, None, priority).await?;
|
||||
Ok(!matches!(outcome, EnqueueTrackAnalysisOutcome::Complete))
|
||||
}
|
||||
|
||||
@@ -659,6 +667,7 @@ async fn spawn_backfill_slots(app: &tauri::AppHandle, shared: &Arc<AnalysisBackf
|
||||
&server_id,
|
||||
&track_id,
|
||||
&bytes,
|
||||
None,
|
||||
priority,
|
||||
fetch_ms,
|
||||
)
|
||||
@@ -941,6 +950,7 @@ struct AnalysisCpuSeedJob {
|
||||
server_id: String,
|
||||
track_id: String,
|
||||
bytes: Vec<u8>,
|
||||
format_hint: Option<String>,
|
||||
waiters: Vec<SeedDoneSender>,
|
||||
/// HTTP download time when this job came from the backfill worker.
|
||||
fetch_ms: u64,
|
||||
@@ -1028,6 +1038,7 @@ impl AnalysisCpuSeedQueueState {
|
||||
server_id: String,
|
||||
track_id: String,
|
||||
bytes: Vec<u8>,
|
||||
format_hint: Option<String>,
|
||||
priority: AnalysisBackfillPriority,
|
||||
fetch_ms: u64,
|
||||
) -> (
|
||||
@@ -1049,6 +1060,7 @@ impl AnalysisCpuSeedQueueState {
|
||||
let mut job = self.tier_deque_mut(existing_tier).remove(pos).unwrap();
|
||||
job.server_id = server_id;
|
||||
job.bytes = bytes;
|
||||
job.format_hint = format_hint;
|
||||
job.fetch_ms = fetch_ms;
|
||||
job.waiters.push(done_tx);
|
||||
if priority > existing_tier {
|
||||
@@ -1065,6 +1077,7 @@ impl AnalysisCpuSeedQueueState {
|
||||
server_id,
|
||||
track_id: track_id.clone(),
|
||||
bytes,
|
||||
format_hint,
|
||||
waiters: vec![done_tx],
|
||||
fetch_ms,
|
||||
priority,
|
||||
@@ -1257,12 +1270,14 @@ async fn spawn_cpu_seed_slots(app: &tauri::AppHandle, shared: &Arc<AnalysisCpuSe
|
||||
let sid = job.server_id.clone();
|
||||
let tid = job.track_id.clone();
|
||||
let bytes = job.bytes;
|
||||
let format_hint = job.format_hint;
|
||||
let seed_result = tokio::task::spawn_blocking(move || {
|
||||
analysis_cache::seed_from_bytes_execute(
|
||||
&app_for_decode,
|
||||
&sid,
|
||||
&tid,
|
||||
&bytes,
|
||||
format_hint.as_deref(),
|
||||
notify_ui,
|
||||
)
|
||||
})
|
||||
@@ -1378,13 +1393,14 @@ pub async fn submit_analysis_cpu_seed(
|
||||
server_id: String,
|
||||
track_id: String,
|
||||
bytes: Vec<u8>,
|
||||
format_hint: Option<String>,
|
||||
priority: AnalysisBackfillPriority,
|
||||
fetch_ms: u64,
|
||||
) -> Result<analysis_cache::SeedFromBytesOutcome, String> {
|
||||
let shared = analysis_cpu_seed_shared(&app);
|
||||
let rx = {
|
||||
let mut st = shared.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (kind, rx) = st.enqueue(server_id, track_id.clone(), bytes, priority, fetch_ms);
|
||||
let (kind, rx) = st.enqueue(server_id, track_id.clone(), bytes, format_hint, priority, fetch_ms);
|
||||
crate::app_deprintln!("[analysis] cpu-seed submit: kind={kind:?} priority={priority:?}");
|
||||
drop(st);
|
||||
shared.ping_worker();
|
||||
@@ -1587,6 +1603,7 @@ mod tests {
|
||||
String::new(),
|
||||
"a".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1601,6 +1618,7 @@ mod tests {
|
||||
String::new(),
|
||||
"first".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1608,6 +1626,7 @@ mod tests {
|
||||
String::new(),
|
||||
"hot".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::High,
|
||||
0,
|
||||
);
|
||||
@@ -1622,6 +1641,7 @@ mod tests {
|
||||
"server-a".into(),
|
||||
"dup".into(),
|
||||
vec![1, 2, 3],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1629,6 +1649,7 @@ mod tests {
|
||||
"server-b".into(),
|
||||
"dup".into(),
|
||||
vec![4, 5, 6],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1647,6 +1668,7 @@ mod tests {
|
||||
String::new(),
|
||||
"first".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1654,6 +1676,7 @@ mod tests {
|
||||
String::new(),
|
||||
"dup".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1661,6 +1684,7 @@ mod tests {
|
||||
String::new(),
|
||||
"dup".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::High,
|
||||
0,
|
||||
);
|
||||
@@ -1677,6 +1701,7 @@ mod tests {
|
||||
String::new(),
|
||||
"active".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1692,6 +1717,7 @@ mod tests {
|
||||
String::new(),
|
||||
"a".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1699,6 +1725,7 @@ mod tests {
|
||||
String::new(),
|
||||
"b".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1706,6 +1733,7 @@ mod tests {
|
||||
String::new(),
|
||||
"a".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1713,6 +1741,7 @@ mod tests {
|
||||
String::new(),
|
||||
"c".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
@@ -1731,6 +1760,7 @@ mod tests {
|
||||
String::new(),
|
||||
"doomed".into(),
|
||||
vec![],
|
||||
None,
|
||||
AnalysisBackfillPriority::Low,
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Symphonia codec registry — mirrors `psysonic-audio::codec` (Opus via libopus).
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use symphonia::core::codecs::{CodecRegistry, DecoderOptions};
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
pub mod analysis_cache;
|
||||
pub mod analysis_perf;
|
||||
pub mod analysis_runtime;
|
||||
mod codec;
|
||||
pub mod commands;
|
||||
pub mod track_analysis_plan;
|
||||
pub mod track_enrichment;
|
||||
|
||||
@@ -200,6 +200,7 @@ pub(crate) async fn dispatch_track_analysis_bytes(
|
||||
server_id,
|
||||
track_id,
|
||||
&bytes,
|
||||
None,
|
||||
priority,
|
||||
)
|
||||
.await
|
||||
|
||||
+10
-1
@@ -174,7 +174,16 @@ pub async fn promote_stream_cache_to_hot_cache(
|
||||
&track_id,
|
||||
None,
|
||||
);
|
||||
let _ = enqueue_track_analysis(&app, &server_id, &track_id, &bytes, priority).await;
|
||||
let format_hint = Some(suffix.to_ascii_lowercase());
|
||||
let _ = enqueue_track_analysis(
|
||||
&app,
|
||||
&server_id,
|
||||
&track_id,
|
||||
&bytes,
|
||||
format_hint.as_deref(),
|
||||
priority,
|
||||
)
|
||||
.await;
|
||||
|
||||
let size = tokio::fs::metadata(&file_path)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user