diff --git a/src/components/player/SpeedControl.tsx b/src/components/player/SpeedControl.tsx index af9e2e6..0cd0aa1 100644 --- a/src/components/player/SpeedControl.tsx +++ b/src/components/player/SpeedControl.tsx @@ -5,56 +5,106 @@ import { ChevronUpDownIcon } from '@/components/icons/Icons'; import { useConfig } from '@/contexts/ConfigContext'; import { useCallback, useEffect, useState } from 'react'; -export const SpeedControl = ({ setSpeedAndRestart }: { +export const SpeedControl = ({ + setSpeedAndRestart, + setAudioPlayerSpeedAndRestart +}: { setSpeedAndRestart: (speed: number) => void; + setAudioPlayerSpeedAndRestart: (speed: number) => void; }) => { - const { voiceSpeed } = useConfig(); - const [localSpeed, setLocalSpeed] = useState(voiceSpeed); + const { voiceSpeed, audioPlayerSpeed } = useConfig(); + const [localVoiceSpeed, setLocalVoiceSpeed] = useState(voiceSpeed); + const [localAudioSpeed, setLocalAudioSpeed] = useState(audioPlayerSpeed); - // Sync local speed with global state + // Sync local speeds with global state useEffect(() => { - setLocalSpeed(voiceSpeed); + setLocalVoiceSpeed(voiceSpeed); }, [voiceSpeed]); - // Handler for slider change (updates local state only) - const handleSpeedChange = useCallback((event: React.ChangeEvent) => { - setLocalSpeed(parseFloat(event.target.value)); + useEffect(() => { + setLocalAudioSpeed(audioPlayerSpeed); + }, [audioPlayerSpeed]); + + // Handler for voice speed slider change (updates local state only) + const handleVoiceSpeedChange = useCallback((event: React.ChangeEvent) => { + setLocalVoiceSpeed(parseFloat(event.target.value)); }, []); - // Handler for slider release - const handleSpeedChangeComplete = useCallback(() => { - if (localSpeed !== voiceSpeed) { - setSpeedAndRestart(localSpeed); + // Handler for audio player speed slider change (updates local state only) + const handleAudioSpeedChange = useCallback((event: React.ChangeEvent) => { + setLocalAudioSpeed(parseFloat(event.target.value)); + }, []); + + // Handler for voice speed slider release + const handleVoiceSpeedChangeComplete = useCallback(() => { + if (localVoiceSpeed !== voiceSpeed) { + setSpeedAndRestart(localVoiceSpeed); } - }, [localSpeed, voiceSpeed, setSpeedAndRestart]); + }, [localVoiceSpeed, voiceSpeed, setSpeedAndRestart]); + + // Handler for audio player speed slider release + const handleAudioSpeedChangeComplete = useCallback(() => { + if (localAudioSpeed !== audioPlayerSpeed) { + setAudioPlayerSpeedAndRestart(localAudioSpeed); + } + }, [localAudioSpeed, audioPlayerSpeed, setAudioPlayerSpeedAndRestart]); + + // Display the currently active speed (prioritize audio player speed if different from 1.0) + const displaySpeed = localAudioSpeed !== 1.0 ? localAudioSpeed : localVoiceSpeed; return ( - {Number.isInteger(localSpeed) ? localSpeed.toString() : localSpeed.toFixed(1)}x + {Number.isInteger(displaySpeed) ? displaySpeed.toString() : displaySpeed.toFixed(1)}x -
-
- 0.5x - {Number.isInteger(localSpeed) ? localSpeed.toString() : localSpeed.toFixed(1)}x - 3x +
+ {/* Native Model Speed */} +
+
Native model speed
+
+ 0.5x + {Number.isInteger(localVoiceSpeed) ? localVoiceSpeed.toString() : localVoiceSpeed.toFixed(1)}x + 3x +
+ +
+ + {/* Audio Player Speed */} +
+
Audio player speed
+
+ 0.5x + {Number.isInteger(localAudioSpeed) ? localAudioSpeed.toString() : localAudioSpeed.toFixed(1)}x + 3x +
+
-
); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/player/TTSPlayer.tsx b/src/components/player/TTSPlayer.tsx index e60fb88..07bf938 100644 --- a/src/components/player/TTSPlayer.tsx +++ b/src/components/player/TTSPlayer.tsx @@ -24,6 +24,7 @@ export default function TTSPlayer({ currentPage, numPages }: { skipBackward, isProcessing, setSpeedAndRestart, + setAudioPlayerSpeedAndRestart, setVoiceAndRestart, availableVoices, skipToLocation, @@ -33,7 +34,10 @@ export default function TTSPlayer({ currentPage, numPages }: {
{/* Speed control */} - + {/* Page Navigation */} {currentPage && numPages && ( diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx index 324c885..f82ab01 100644 --- a/src/contexts/ConfigContext.tsx +++ b/src/contexts/ConfigContext.tsx @@ -12,6 +12,7 @@ type ConfigValues = { baseUrl: string; viewType: ViewType; voiceSpeed: number; + audioPlayerSpeed: number; voice: string; skipBlank: boolean; epubTheme: boolean; @@ -29,6 +30,7 @@ interface ConfigContextType { baseUrl: string; viewType: ViewType; voiceSpeed: number; + audioPlayerSpeed: number; voice: string; skipBlank: boolean; epubTheme: boolean; @@ -58,6 +60,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { const [baseUrl, setBaseUrl] = useState(''); const [viewType, setViewType] = useState('single'); const [voiceSpeed, setVoiceSpeed] = useState(1); + const [audioPlayerSpeed, setAudioPlayerSpeed] = useState(1); const [voice, setVoice] = useState('af_sarah'); const [skipBlank, setSkipBlank] = useState(true); const [epubTheme, setEpubTheme] = useState(false); @@ -83,6 +86,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { const cachedBaseUrl = await getItem('baseUrl'); const cachedViewType = await getItem('viewType'); const cachedVoiceSpeed = await getItem('voiceSpeed'); + const cachedAudioPlayerSpeed = await getItem('audioPlayerSpeed'); const cachedVoice = await getItem('voice'); const cachedSkipBlank = await getItem('skipBlank'); const cachedEpubTheme = await getItem('epubTheme'); @@ -106,6 +110,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { // Set the other values with defaults setViewType((cachedViewType || 'single') as ViewType); setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1')); + setAudioPlayerSpeed(parseFloat(cachedAudioPlayerSpeed || '1')); setVoice(cachedVoice || 'af_sarah'); setSkipBlank(cachedSkipBlank === 'false' ? false : true); setEpubTheme(cachedEpubTheme === 'true'); @@ -136,6 +141,12 @@ export function ConfigProvider({ children }: { children: ReactNode }) { if (cachedTTSInstructions === null) { await setItem('ttsInstructions', ''); } + if (!cachedVoiceSpeed) { + await setItem('voiceSpeed', '1'); + } + if (!cachedAudioPlayerSpeed) { + await setItem('audioPlayerSpeed', '1'); + } } catch (error) { console.error('Error initializing:', error); @@ -151,7 +162,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { * Updates multiple configuration values simultaneously * Only saves API credentials if they are explicitly set */ - const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => { + const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => { try { setIsLoading(true); if (newConfig.apiKey !== undefined || newConfig.apiKey !== '') { @@ -205,6 +216,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) { case 'voiceSpeed': setVoiceSpeed(value as number); break; + case 'audioPlayerSpeed': + setAudioPlayerSpeed(value as number); + break; case 'voice': setVoice(value as string); break; @@ -247,6 +261,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) { baseUrl, viewType, voiceSpeed, + audioPlayerSpeed, voice, skipBlank, epubTheme, diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index 07eb6e2..28ef4ef 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -74,6 +74,7 @@ interface TTSContextType { setText: (text: string, shouldPause?: boolean) => void; setCurrDocPages: (num: number | undefined) => void; setSpeedAndRestart: (speed: number) => void; + setAudioPlayerSpeedAndRestart: (speed: number) => void; setVoiceAndRestart: (voice: string) => void; skipToLocation: (location: string | number, shouldPause?: boolean) => void; registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation @@ -98,6 +99,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement baseUrl: openApiBaseUrl, isLoading: configIsLoading, voiceSpeed, + audioPlayerSpeed, voice: configVoice, ttsModel: configTTSModel, ttsInstructions: configTTSInstructions, @@ -141,6 +143,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement const [currentIndex, setCurrentIndex] = useState(0); const [activeHowl, setActiveHowl] = useState(null); const [speed, setSpeed] = useState(voiceSpeed); + const [audioSpeed, setAudioSpeed] = useState(audioPlayerSpeed); const [voice, setVoice] = useState(configVoice); const [ttsModel, setTTSModel] = useState(configTTSModel); const [ttsInstructions, setTTSInstructions] = useState(configTTSInstructions); @@ -387,7 +390,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement const updateVoiceAndSpeed = useCallback(() => { setVoice(configVoice); setSpeed(voiceSpeed); - }, [configVoice, voiceSpeed]); + setAudioSpeed(audioPlayerSpeed); + }, [configVoice, voiceSpeed, audioPlayerSpeed]); /** * Initializes configuration and fetches available voices @@ -570,6 +574,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement html5: true, preload: true, pool: 5, + rate: audioSpeed, onplay: () => { setIsProcessing(false); if ('mediaSession' in navigator) { @@ -666,7 +671,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement advance(); return null; } - }, [isPlaying, advance, activeHowl, processSentence]); + }, [isPlaying, advance, activeHowl, processSentence, audioSpeed]); const playAudio = useCallback(async () => { const howl = await playSentenceWithHowl(sentences[currentIndex]); @@ -822,6 +827,35 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement }); }, [abortAudio, updateConfigKey, audioCache, isPlaying]); + /** + * Sets the audio player speed and restarts the playback + * + * @param {number} newSpeed - The new audio player speed to set + */ + const setAudioPlayerSpeedAndRestart = useCallback((newSpeed: number) => { + const wasPlaying = isPlaying; + + // Set a flag to prevent double audio requests during config update + setIsProcessing(true); + + // First stop any current playback + setIsPlaying(false); + abortAudio(true); // Clear pending requests since speed changed + setActiveHowl(null); + + // Update audio speed and config + setAudioSpeed(newSpeed); + + // Update config after state changes + updateConfigKey('audioPlayerSpeed', newSpeed).then(() => { + setIsProcessing(false); + // Resume playback if it was playing before + if (wasPlaying) { + setIsPlaying(true); + } + }); + }, [abortAudio, updateConfigKey, isPlaying]); + /** * Provides the TTS context value to child components */ @@ -843,6 +877,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement setText, setCurrDocPages, setSpeedAndRestart, + setAudioPlayerSpeedAndRestart, setVoiceAndRestart, skipToLocation, registerLocationChangeHandler, @@ -866,6 +901,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement setText, setCurrDocPages, setSpeedAndRestart, + setAudioPlayerSpeedAndRestart, setVoiceAndRestart, skipToLocation, registerLocationChangeHandler,