import React, { useCallback } from 'react'; import type { TFunction } from 'i18next'; import { PLAYBACK_PITCH_MAX, PLAYBACK_PITCH_MIN, PLAYBACK_PITCH_STEP, PLAYBACK_SPEED_MAX, PLAYBACK_SPEED_MIN, PLAYBACK_SPEED_PRESETS, PLAYBACK_SPEED_STEP, PLAYBACK_STRATEGIES, clampPlaybackPitch, clampPlaybackSpeed, derivedVarispeedSemitones, formatPitchLabel, formatSpeedLabel, isPlaybackRateApplied, type PlaybackStrategy, } from '../../../utils/audio/playbackRateHelpers'; import { usePlaybackRateStore } from '../../../store/playbackRateStore'; import { useOrbitStore } from '../../../store/orbitStore'; import { isOrbitPlaybackSyncActive } from '../../../utils/orbit'; interface Props { t: TFunction; /** When false, hide master enable (player popup). */ showEnable?: boolean; } export function PlaybackRateControls({ t, showEnable = true }: Props) { const compact = !showEnable; const enabled = usePlaybackRateStore(s => s.enabled); const strategy = usePlaybackRateStore(s => s.strategy); const speed = usePlaybackRateStore(s => s.speed); const pitchSemitones = usePlaybackRateStore(s => s.pitchSemitones); const { setEnabled, setStrategy, setSpeed, setPitchSemitones, applyPresetSpeed, } = usePlaybackRateStore(); const orbitRole = useOrbitStore(s => s.role); const orbitPhase = useOrbitStore(s => s.phase); const orbitActive = isOrbitPlaybackSyncActive(orbitRole, orbitPhase); const effectActive = isPlaybackRateApplied(enabled, strategy, speed, pitchSemitones, orbitActive); const derivedPitch = derivedVarispeedSemitones(speed); const strategyLabel = (s: PlaybackStrategy) => { switch (s) { case 'speed_corrected': return t('settings.playbackRateStrategySpeed'); case 'varispeed': return t('settings.playbackRateStrategyVarispeed'); case 'preserve_pitch': return t('settings.playbackRateStrategyPreserve'); } }; const handleWheelSpeed = useCallback((e: React.WheelEvent) => { if (!compact || !enabled) return; e.preventDefault(); e.stopPropagation(); const delta = e.deltaY > 0 ? -PLAYBACK_SPEED_STEP : PLAYBACK_SPEED_STEP; setSpeed(clampPlaybackSpeed(speed + delta)); }, [compact, enabled, speed, setSpeed]); const handleWheelPitch = useCallback((e: React.WheelEvent) => { if (!compact || !enabled || strategy !== 'preserve_pitch') return; e.preventDefault(); e.stopPropagation(); const delta = e.deltaY > 0 ? -PLAYBACK_PITCH_STEP : PLAYBACK_PITCH_STEP; setPitchSemitones(clampPlaybackPitch(pitchSemitones + delta)); }, [compact, enabled, strategy, pitchSemitones, setPitchSemitones]); return (
{showEnable && (
{t('settings.playbackRateEnabled')}
{t('settings.playbackRateEnabledDesc')}
)} {(!showEnable || enabled) && ( <> {showEnable &&
}
{!compact && ( {t('settings.playbackRateStrategy')} )}
{PLAYBACK_STRATEGIES.map(s => ( ))}
{!compact && ( {t('settings.playbackRateSpeed')} )} setSpeed(parseFloat(e.target.value))} className="playback-rate-slider" aria-label={t('settings.playbackRateSpeed')} /> {formatSpeedLabel(speed)}
{PLAYBACK_SPEED_PRESETS.map(preset => ( ))}
{strategy === 'varispeed' && !compact && (
{t('settings.playbackRateDerivedPitch', { value: formatPitchLabel(derivedPitch), })}
)} {strategy === 'speed_corrected' && !compact && (
{t('settings.playbackRateAutoPitch')}
)} {strategy === 'preserve_pitch' && (
{!compact && ( {t('settings.playbackRatePitch')} )} setPitchSemitones(parseFloat(e.target.value))} className="playback-rate-slider" aria-label={t('settings.playbackRatePitch')} /> {formatPitchLabel(pitchSemitones)}
)} {!compact && (

{t('settings.playbackRateHint')}

)} {orbitActive && enabled && (

{t(compact ? 'settings.playbackRateOrbitPausedShort' : 'settings.playbackRateOrbitPaused')}

)} {!compact && !effectActive && enabled && !orbitActive && (

{t('settings.playbackRateNeutral')}

)} )}
); } export function PlaybackRateBlock({ t }: { t: TFunction }) { return ; }