import React, { useCallback } from 'react'; import type { TFunction } from 'i18next'; import { PLAYBACK_PITCH_MAX, PLAYBACK_PITCH_MIN, PLAYBACK_SPEED_MAX, PLAYBACK_SPEED_MIN, PLAYBACK_SPEED_PRESETS, PLAYBACK_STRATEGIES, clampPlaybackPitch, clampPlaybackSpeed, derivedVarispeedSemitones, formatPitchLabel, formatSpeedLabel, isPlaybackRateApplied, playbackPitchStep, playbackSpeedStep, varispeedSpeedFromSemitones, type PlaybackStrategy, } from '@/utils/audio/playbackRateHelpers'; import { usePlaybackRateStore } from '@/store/playbackRateStore'; import { useOrbitStore } from '@/features/orbit'; import { useAuthStore } from '@/store/authStore'; import { isOrbitPlaybackSyncActive } from '@/features/orbit'; import { SettingsToggle } from '@/features/settings/components/SettingsToggle'; import { SettingsSubCard } from '@/features/settings/components/SettingsSubCard'; 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 fineStep = usePlaybackRateStore(s => s.fineStep); const { setEnabled, setStrategy, setSpeed, setPitchSemitones, applyPresetSpeed, setFineStep, } = usePlaybackRateStore(); const orbitRole = useOrbitStore(s => s.role); const orbitPhase = useOrbitStore(s => s.phase); const advancedSettingsEnabled = useAuthStore(s => s.advancedSettingsEnabled); const orbitActive = isOrbitPlaybackSyncActive(orbitRole, orbitPhase); const effectActive = isPlaybackRateApplied(enabled, strategy, speed, pitchSemitones, orbitActive); const derivedPitch = derivedVarispeedSemitones(speed); const speedStep = playbackSpeedStep(fineStep); const pitchStep = playbackPitchStep(fineStep); const pitchDecimals = fineStep ? 2 : 1; const strategyLabel = (s: PlaybackStrategy) => { switch (s) { case 'speed_corrected': return t('settings.playbackRateStrategySpeed'); case 'varispeed': return t('settings.playbackRateStrategyVarispeed'); case 'varispeed_semitones': return t('settings.playbackRateStrategyVarispeedSemitones'); case 'preserve_pitch': return t('settings.playbackRateStrategyPreserve'); } }; const strategyTip = (s: PlaybackStrategy) => { switch (s) { case 'speed_corrected': return t('settings.playbackRateStrategySpeedTip'); case 'varispeed': return t('settings.playbackRateStrategyVarispeedTip'); case 'varispeed_semitones': return t('settings.playbackRateStrategyVarispeedSemitonesTip'); case 'preserve_pitch': return t('settings.playbackRateStrategyPreserveTip'); } }; const handleWheelSpeed = useCallback((e: React.WheelEvent) => { if (!compact || !enabled) return; e.preventDefault(); e.stopPropagation(); if (strategy === 'varispeed_semitones') { const step = e.deltaY > 0 ? -pitchStep : pitchStep; const st = clampPlaybackPitch(derivedVarispeedSemitones(speed) + step); setSpeed(clampPlaybackSpeed(varispeedSpeedFromSemitones(st))); return; } const delta = e.deltaY > 0 ? -speedStep : speedStep; setSpeed(clampPlaybackSpeed(speed + delta)); }, [compact, enabled, strategy, speed, speedStep, pitchStep, setSpeed]); const handleWheelPitch = useCallback((e: React.WheelEvent) => { if (!compact || !enabled || strategy !== 'preserve_pitch') return; e.preventDefault(); e.stopPropagation(); const delta = e.deltaY > 0 ? -pitchStep : pitchStep; setPitchSemitones(clampPlaybackPitch(pitchSemitones + delta)); }, [compact, enabled, strategy, pitchSemitones, pitchStep, setPitchSemitones]); return (
{showEnable && ( )} {(!showEnable || enabled) && (() => { const body = ( <>
{!compact && ( {t('settings.playbackRateStrategy')} )}
{PLAYBACK_STRATEGIES.map(s => ( ))}
{strategy !== 'varispeed_semitones' && (
{!compact && ( {t('settings.playbackRateSpeed')} )} setSpeed(parseFloat(e.target.value))} className="playback-rate-slider" aria-label={t('settings.playbackRateSpeed')} /> {formatSpeedLabel(speed)}
)} {strategy === 'varispeed_semitones' && (
{!compact && ( {t('settings.playbackRatePitch')} )} setSpeed(clampPlaybackSpeed(varispeedSpeedFromSemitones(parseFloat(e.target.value)))) } className="playback-rate-slider" aria-label={t('settings.playbackRatePitch')} /> {formatPitchLabel(derivedPitch, pitchDecimals)}
)}
{PLAYBACK_SPEED_PRESETS.map(preset => ( ))}
{strategy === 'varispeed' && !compact && (
{t('settings.playbackRateDerivedPitch', { value: formatPitchLabel(derivedPitch, pitchDecimals), })}
)} {strategy === 'varispeed_semitones' && !compact && (
{t('settings.playbackRateDerivedSpeed', { value: formatSpeedLabel(speed), })}
)} {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, pitchDecimals)}
)} {!compact && advancedSettingsEnabled && (
{t('settings.playbackRateFineStep')}
{t('settings.playbackRateFineStepDesc')}
)} {!compact && (

{t('settings.playbackRateHint')}

)} {orbitActive && enabled && (

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

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

{t('settings.playbackRateNeutral')}

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