mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
feat: v1.29.0 — Radio fast-start, seek fix, OGG, error toasts, contributors
Incorporates PRs #38 (nisarg-78), #42 #43 #44 (JulianNymark) with fixes: Audio / Playback - Artist Radio starts immediately from fast getTopSongs (local library); getSimilarSongs2 (Last.fm) enriches queue in background — no more wait - Fix seek audio glitch: EqualPowerFadeIn only resets to zero-gain on seeks to track start (<100 ms); all other seeks resume at unity gain - OGG/Vorbis container support via symphonia-format-ogg (PR #42) - Human-readable audio error messages in SizedDecoder (PR #44) - Audio playback errors shown as themed toast notifications (PR #43) Queue / Radio - Infinite Queue: proactive load of 5 tracks (was 25) when ≤2 remain - Radio: proactive reload at ≤2 remaining tracks, independent of Infinite Queue setting — radio no longer stops at last track - Fix: clicking Start Radio multiple times no longer stacks duplicates - Fix: Start Radio on artist keeps current song playing - Manual tracks always appear before Radio, Radio before Auto-added - Queue separators: "— Radio —" and "— Auto —" dividers - Fix: radio proactive load now works even when songs lack artistId (uses currentRadioArtistId module var as fallback) UI / UX - Click synced lyrics lines to seek (PR #38) - Volume scroll wheel on volume slider (PR #38) - Lyrics: active / completed / upcoming visual states (PR #38) - Shared toast utility (src/utils/toast.ts) replaces inline toast fn - Auto-updater: relaunch_after_update Rust command exits first to release single-instance lock before spawning new process About / Credits - nisarg-78 and JulianNymark added to contributors (since v1.29.0) - netherguy4 added as Special Thanks for feature ideas and feedback - i18n: aboutSpecialThanksLabel in all 5 languages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-9
@@ -229,9 +229,15 @@ impl<S: Source<Item = f32>> Source for EqualPowerFadeIn<S> {
|
||||
fn sample_rate(&self) -> u32 { self.inner.sample_rate() }
|
||||
fn total_duration(&self) -> Option<Duration> { self.inner.total_duration() }
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), rodio::source::SeekError> {
|
||||
// Restart the fade envelope after seeking (avoids a mid-song click if
|
||||
// the user seeks to the very beginning while a fade was in progress).
|
||||
self.sample_count = 0;
|
||||
// For mid-track seeks: skip straight to unity gain so the new position
|
||||
// plays at full volume immediately — no audible fade-in glitch.
|
||||
// For seeks to the very start (< 100 ms): keep the micro-fade to
|
||||
// suppress any DC-offset click from the fresh decode.
|
||||
if pos.as_millis() < 100 {
|
||||
self.sample_count = 0;
|
||||
} else {
|
||||
self.sample_count = self.fade_samples;
|
||||
}
|
||||
self.inner.try_seek(pos)
|
||||
}
|
||||
}
|
||||
@@ -478,13 +484,20 @@ impl SizedDecoder {
|
||||
|
||||
let probed = symphonia::default::get_probe()
|
||||
.format(&hint, mss, &format_opts, &MetadataOptions::default())
|
||||
.map_err(|e| format!("probe failed: {e}"))?;
|
||||
.map_err(|e| {
|
||||
let hint_str = format_hint.unwrap_or("unknown");
|
||||
if e.to_string().to_lowercase().contains("unsupported") {
|
||||
format!("unsupported format: .{hint_str} files cannot be played (no demuxer)")
|
||||
} else {
|
||||
format!("could not open audio stream (.{hint_str}): {e}")
|
||||
}
|
||||
})?;
|
||||
|
||||
let track = probed.format
|
||||
.tracks()
|
||||
.iter()
|
||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.ok_or_else(|| "no supported audio track".to_string())?;
|
||||
.ok_or_else(|| "no playable audio track found in file".to_string())?;
|
||||
|
||||
let track_id = track.id;
|
||||
let total_duration = track.codec_params.time_base
|
||||
@@ -493,7 +506,13 @@ impl SizedDecoder {
|
||||
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make(&track.codec_params, &DecoderOptions::default())
|
||||
.map_err(|e| format!("codec init failed: {e}"))?;
|
||||
.map_err(|e| {
|
||||
if e.to_string().to_lowercase().contains("unsupported") {
|
||||
"unsupported codec: no decoder available for this audio format".to_string()
|
||||
} else {
|
||||
format!("failed to initialise audio decoder: {e}")
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut format = probed.format;
|
||||
|
||||
@@ -505,7 +524,7 @@ impl SizedDecoder {
|
||||
Err(symphonia::core::errors::Error::IoError(_)) => {
|
||||
break decoder.last_decoded();
|
||||
}
|
||||
Err(e) => return Err(format!("first packet: {e}")),
|
||||
Err(e) => return Err(format!("could not read audio data: {e}")),
|
||||
};
|
||||
if packet.track_id() != track_id { continue; }
|
||||
match decoder.decode(&packet) {
|
||||
@@ -513,10 +532,10 @@ impl SizedDecoder {
|
||||
Err(symphonia::core::errors::Error::DecodeError(_)) => {
|
||||
decode_errors += 1;
|
||||
if decode_errors > DECODE_MAX_RETRIES {
|
||||
return Err("too many decode errors".into());
|
||||
return Err("too many decode errors — file may be corrupt".into());
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(format!("decode: {e}")),
|
||||
Err(e) => return Err(format!("audio decode error: {e}")),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user