Files
Psychotoxical-psysonic/src/store/playbackRateStore.ts
T
cucadmuh e8e41752a7 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>
2026-05-22 18:59:28 +02:00

100 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}),
},
),
);