mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
e8e41752a7
* 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>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
import { create } from 'zustand';
|
||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||
import { invoke } from '@tauri-apps/api/core';
|
||
import {
|
||
clampPlaybackPitch,
|
||
clampPlaybackSpeed,
|
||
DEFAULT_PLAYBACK_STRATEGY,
|
||
effectivePlaybackPitch,
|
||
type PlaybackStrategy,
|
||
} from '../utils/audio/playbackRateHelpers';
|
||
import {
|
||
restartPlaybackForRateChange,
|
||
shouldRestartPlaybackForRateChange,
|
||
type PlaybackRateSnapshot,
|
||
} from '../utils/audio/playbackRateRestart';
|
||
import { isOrbitPlaybackSyncActive } from '../utils/orbit';
|
||
|
||
interface PlaybackRateState extends PlaybackRateSnapshot {
|
||
|
||
setEnabled: (v: boolean) => void;
|
||
setStrategy: (s: PlaybackStrategy) => void;
|
||
setSpeed: (speed: number) => void;
|
||
setPitchSemitones: (semitones: number) => void;
|
||
applyPresetSpeed: (speed: number) => void;
|
||
syncToRust: () => void;
|
||
}
|
||
|
||
function syncPlaybackRate(state: PlaybackRateSnapshot, prev?: PlaybackRateSnapshot) {
|
||
// Orbit sync assumes 1.0× wall-clock playback; suppress DSP without mutating prefs.
|
||
const effectiveEnabled = state.enabled && !isOrbitPlaybackSyncActive();
|
||
invoke('audio_set_playback_rate', {
|
||
enabled: effectiveEnabled,
|
||
strategy: state.strategy,
|
||
speed: state.speed,
|
||
pitchSemitones: effectivePlaybackPitch(state.strategy, state.pitchSemitones),
|
||
}).catch(() => {});
|
||
|
||
if (prev && shouldRestartPlaybackForRateChange(prev, state)) {
|
||
restartPlaybackForRateChange();
|
||
}
|
||
}
|
||
|
||
export const usePlaybackRateStore = create<PlaybackRateState>()(
|
||
persist(
|
||
(set, get) => ({
|
||
enabled: false,
|
||
strategy: DEFAULT_PLAYBACK_STRATEGY,
|
||
speed: 1.0,
|
||
pitchSemitones: 0,
|
||
|
||
setEnabled: (v) => {
|
||
const prev = get();
|
||
set({ enabled: v });
|
||
syncPlaybackRate(get(), prev);
|
||
},
|
||
|
||
setStrategy: (strategy) => {
|
||
const prev = get();
|
||
set({ strategy });
|
||
syncPlaybackRate(get(), prev);
|
||
},
|
||
|
||
setSpeed: (speed) => {
|
||
const prev = get();
|
||
const clamped = clampPlaybackSpeed(speed);
|
||
set({ speed: clamped });
|
||
syncPlaybackRate(get(), prev);
|
||
},
|
||
|
||
setPitchSemitones: (semitones) => {
|
||
const prev = get();
|
||
const clamped = clampPlaybackPitch(semitones);
|
||
set({ pitchSemitones: clamped });
|
||
syncPlaybackRate(get(), prev);
|
||
},
|
||
|
||
applyPresetSpeed: (speed) => {
|
||
const prev = get();
|
||
const clamped = clampPlaybackSpeed(speed);
|
||
set({ speed: clamped });
|
||
syncPlaybackRate(get(), prev);
|
||
},
|
||
|
||
syncToRust: () => {
|
||
syncPlaybackRate(get());
|
||
},
|
||
}),
|
||
{
|
||
name: 'psysonic-playback-rate',
|
||
storage: createJSONStorage(() => localStorage),
|
||
partialize: (s) => ({
|
||
enabled: s.enabled,
|
||
strategy: s.strategy,
|
||
speed: s.speed,
|
||
pitchSemitones: s.pitchSemitones,
|
||
}),
|
||
},
|
||
),
|
||
);
|