mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(playback): global speed with three strategies (#852)
* feat(playback): global speed with three strategies Add Settings → Audio and player-bar controls for global playback speed (speed with auto pitch correction as default, varispeed, manual pitch shift). Time-stretch runs on a background worker; Orbit sessions force 1.0× passthrough. * fix(playback): align seekbar, seek, and progress on content timeline Unify UI timebase across varispeed and preserve strategies: full-track duration, speed-scaled progress for DSP paths, and content-timeline seeks without varispeed scaling. Reset the sample counter after seek so clicks land correctly; restart playback on strategy/enable changes instead of fragile hot-switching. * fix(ui): anchor playback speed popover like volume controls Replace the centered EQ-style modal with a player-bar popover (outside click, Escape, reposition on scroll). Show compact controls in the bar and overflow menu; keep strategy hints and labels in Settings only. * docs(release): CHANGELOG and credits for playback speed (PR #852) * docs(changelog): add playback speed entry for PR #852 * fix(clippy): simplify raw_counter_samples branch for CI Collapse duplicate if branches flagged by clippy::if-same-then-else. * fix(ui): wheel on pitch slider adjusts pitch in speed popover In compact player-bar controls, scroll over the pitch row changes pitch; elsewhere in the panel changes speed. Stop propagation so overflow menu wheel does not tweak volume. * fix(playback): address PR #852 review and drop ineffective dynamic imports Translate playback-rate strings for de/fr/es/zh/nb/nl/ro; restamp sample counter on live preserve-path speed changes; use neutral rate atomics for radio progress; static-import playerStore in playListenSession (move preview volume sync to previewPlayerVolumeSync side-effect module). * fix(i18n): translate playback-rate strategy labels in all locales Replace leftover English Varispeed/Pitch strings in ru and other non-en settings blocks so popover strategy buttons and hints read natively. * fix(i18n): refine German varispeed label to "Tonhöhe folgt dem Tempo" --------- Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,6 @@ const EQ_CHECK_INTERVAL: usize = 1024;
|
||||
|
||||
pub(crate) struct EqSource<S: Source<Item = f32>> {
|
||||
inner: S,
|
||||
sample_rate: rodio::SampleRate,
|
||||
channels: rodio::ChannelCount,
|
||||
gains: Arc<[AtomicU32; 10]>,
|
||||
enabled: Arc<AtomicBool>,
|
||||
@@ -47,7 +46,7 @@ impl<S: Source<Item = f32>> EqSource<S> {
|
||||
})
|
||||
});
|
||||
Self {
|
||||
inner, sample_rate, channels, gains, enabled, pre_gain,
|
||||
inner, channels, gains, enabled, pre_gain,
|
||||
filters,
|
||||
current_gains: [0.0; 10],
|
||||
sample_counter: 0,
|
||||
@@ -57,14 +56,15 @@ impl<S: Source<Item = f32>> EqSource<S> {
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
fn refresh_if_needed(&mut self) {
|
||||
let sample_rate = self.inner.sample_rate();
|
||||
for band in 0..10 {
|
||||
let gain_db = f32::from_bits(self.gains[band].load(Ordering::Relaxed));
|
||||
if (gain_db - self.current_gains[band]).abs() > 0.01 {
|
||||
self.current_gains[band] = gain_db;
|
||||
let freq = EQ_BANDS_HZ[band].clamp(20.0, (self.sample_rate.get() as f32 / 2.0) - 100.0);
|
||||
let freq = EQ_BANDS_HZ[band].clamp(20.0, (sample_rate.get() as f32 / 2.0) - 100.0);
|
||||
if let Ok(coeffs) = Coefficients::<f32>::from_params(
|
||||
FilterType::PeakingEQ(gain_db),
|
||||
(self.sample_rate.get() as f32).hz(),
|
||||
(sample_rate.get() as f32).hz(),
|
||||
freq.hz(),
|
||||
EQ_Q,
|
||||
) {
|
||||
@@ -109,19 +109,20 @@ impl<S: Source<Item = f32>> Iterator for EqSource<S> {
|
||||
impl<S: Source<Item = f32>> Source for EqSource<S> {
|
||||
fn current_span_len(&self) -> Option<usize> { self.inner.current_span_len() }
|
||||
fn channels(&self) -> rodio::ChannelCount { self.channels }
|
||||
fn sample_rate(&self) -> rodio::SampleRate { self.sample_rate }
|
||||
fn sample_rate(&self) -> rodio::SampleRate { self.inner.sample_rate() }
|
||||
fn total_duration(&self) -> Option<Duration> { self.inner.total_duration() }
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), rodio::source::SeekError> {
|
||||
let sample_rate = self.inner.sample_rate();
|
||||
// Reset biquad filter state to avoid glitches after seek.
|
||||
for band in 0..10 {
|
||||
let gain_db = f32::from_bits(self.gains[band].load(Ordering::Relaxed));
|
||||
self.current_gains[band] = gain_db;
|
||||
let freq = EQ_BANDS_HZ[band].clamp(20.0, (self.sample_rate.get() as f32 / 2.0) - 100.0);
|
||||
let freq = EQ_BANDS_HZ[band].clamp(20.0, (sample_rate.get() as f32 / 2.0) - 100.0);
|
||||
if let Ok(coeffs) = Coefficients::<f32>::from_params(
|
||||
FilterType::PeakingEQ(gain_db),
|
||||
(self.sample_rate.get() as f32).hz(),
|
||||
(sample_rate.get() as f32).hz(),
|
||||
freq.hz(),
|
||||
EQ_Q,
|
||||
) {
|
||||
@@ -144,14 +145,12 @@ impl<S: Source<Item = f32>> Source for EqSource<S> {
|
||||
pub(crate) struct DynSource {
|
||||
inner: Box<dyn Source<Item = f32> + Send>,
|
||||
channels: rodio::ChannelCount,
|
||||
sample_rate: rodio::SampleRate,
|
||||
}
|
||||
|
||||
impl DynSource {
|
||||
pub(crate) fn new(src: impl Source<Item = f32> + Send + 'static) -> Self {
|
||||
let channels = src.channels();
|
||||
let sample_rate = src.sample_rate();
|
||||
Self { inner: Box::new(src), channels, sample_rate }
|
||||
Self { inner: Box::new(src), channels }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +162,7 @@ impl Iterator for DynSource {
|
||||
impl Source for DynSource {
|
||||
fn current_span_len(&self) -> Option<usize> { self.inner.current_span_len() }
|
||||
fn channels(&self) -> rodio::ChannelCount { self.channels }
|
||||
fn sample_rate(&self) -> rodio::SampleRate { self.sample_rate }
|
||||
fn sample_rate(&self) -> rodio::SampleRate { 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> {
|
||||
self.inner.try_seek(pos)
|
||||
|
||||
Reference in New Issue
Block a user