Merge pull request #52 from thepycoder/feature/better-speed-control
This commit is contained in:
commit
08480f07ed
4 changed files with 141 additions and 36 deletions
|
|
@ -5,56 +5,106 @@ import { ChevronUpDownIcon } from '@/components/icons/Icons';
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const SpeedControl = ({ setSpeedAndRestart }: {
|
export const SpeedControl = ({
|
||||||
|
setSpeedAndRestart,
|
||||||
|
setAudioPlayerSpeedAndRestart
|
||||||
|
}: {
|
||||||
setSpeedAndRestart: (speed: number) => void;
|
setSpeedAndRestart: (speed: number) => void;
|
||||||
|
setAudioPlayerSpeedAndRestart: (speed: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { voiceSpeed } = useConfig();
|
const { voiceSpeed, audioPlayerSpeed } = useConfig();
|
||||||
const [localSpeed, setLocalSpeed] = useState(voiceSpeed);
|
const [localVoiceSpeed, setLocalVoiceSpeed] = useState(voiceSpeed);
|
||||||
|
const [localAudioSpeed, setLocalAudioSpeed] = useState(audioPlayerSpeed);
|
||||||
|
|
||||||
// Sync local speed with global state
|
// Sync local speeds with global state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalSpeed(voiceSpeed);
|
setLocalVoiceSpeed(voiceSpeed);
|
||||||
}, [voiceSpeed]);
|
}, [voiceSpeed]);
|
||||||
|
|
||||||
// Handler for slider change (updates local state only)
|
useEffect(() => {
|
||||||
const handleSpeedChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
setLocalAudioSpeed(audioPlayerSpeed);
|
||||||
setLocalSpeed(parseFloat(event.target.value));
|
}, [audioPlayerSpeed]);
|
||||||
|
|
||||||
|
// Handler for voice speed slider change (updates local state only)
|
||||||
|
const handleVoiceSpeedChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setLocalVoiceSpeed(parseFloat(event.target.value));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handler for slider release
|
// Handler for audio player speed slider change (updates local state only)
|
||||||
const handleSpeedChangeComplete = useCallback(() => {
|
const handleAudioSpeedChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (localSpeed !== voiceSpeed) {
|
setLocalAudioSpeed(parseFloat(event.target.value));
|
||||||
setSpeedAndRestart(localSpeed);
|
}, []);
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
<PopoverButton className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1">
|
<PopoverButton className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1">
|
||||||
<span>{Number.isInteger(localSpeed) ? localSpeed.toString() : localSpeed.toFixed(1)}x</span>
|
<span>{Number.isInteger(displaySpeed) ? displaySpeed.toString() : displaySpeed.toFixed(1)}x</span>
|
||||||
<ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
<ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
|
||||||
</PopoverButton>
|
</PopoverButton>
|
||||||
<PopoverPanel anchor="top" className="absolute z-50 bg-base p-3 rounded-md shadow-lg border border-offbase">
|
<PopoverPanel anchor="top" className="absolute z-50 bg-base p-3 rounded-md shadow-lg border border-offbase">
|
||||||
<div className="flex flex-col space-y-2">
|
<div className="flex flex-col space-y-4">
|
||||||
<div className="flex justify-between">
|
{/* Native Model Speed */}
|
||||||
<span className="text-xs">0.5x</span>
|
<div className="flex flex-col space-y-2">
|
||||||
<span className="text-xs font-bold">{Number.isInteger(localSpeed) ? localSpeed.toString() : localSpeed.toFixed(1)}x</span>
|
<div className="text-xs font-medium text-foreground">Native model speed</div>
|
||||||
<span className="text-xs">3x</span>
|
<div className="flex justify-between">
|
||||||
|
<span className="text-xs">0.5x</span>
|
||||||
|
<span className="text-xs font-bold">{Number.isInteger(localVoiceSpeed) ? localVoiceSpeed.toString() : localVoiceSpeed.toFixed(1)}x</span>
|
||||||
|
<span className="text-xs">3x</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
step="0.1"
|
||||||
|
value={localVoiceSpeed}
|
||||||
|
onChange={handleVoiceSpeedChange}
|
||||||
|
onMouseUp={handleVoiceSpeedChangeComplete}
|
||||||
|
onKeyUp={handleVoiceSpeedChangeComplete}
|
||||||
|
onTouchEnd={handleVoiceSpeedChangeComplete}
|
||||||
|
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Audio Player Speed */}
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<div className="text-xs font-medium text-foreground">Audio player speed</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-xs">0.5x</span>
|
||||||
|
<span className="text-xs font-bold">{Number.isInteger(localAudioSpeed) ? localAudioSpeed.toString() : localAudioSpeed.toFixed(1)}x</span>
|
||||||
|
<span className="text-xs">3x</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="3"
|
||||||
|
step="0.1"
|
||||||
|
value={localAudioSpeed}
|
||||||
|
onChange={handleAudioSpeedChange}
|
||||||
|
onMouseUp={handleAudioSpeedChangeComplete}
|
||||||
|
onKeyUp={handleAudioSpeedChangeComplete}
|
||||||
|
onTouchEnd={handleAudioSpeedChangeComplete}
|
||||||
|
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
|
||||||
type="range"
|
|
||||||
min="0.5"
|
|
||||||
max="3"
|
|
||||||
step="0.1"
|
|
||||||
value={localSpeed}
|
|
||||||
onChange={handleSpeedChange}
|
|
||||||
onMouseUp={handleSpeedChangeComplete}
|
|
||||||
onKeyUp={handleSpeedChangeComplete}
|
|
||||||
onTouchEnd={handleSpeedChangeComplete}
|
|
||||||
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</PopoverPanel>
|
</PopoverPanel>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
@ -24,6 +24,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
||||||
skipBackward,
|
skipBackward,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
|
setAudioPlayerSpeedAndRestart,
|
||||||
setVoiceAndRestart,
|
setVoiceAndRestart,
|
||||||
availableVoices,
|
availableVoices,
|
||||||
skipToLocation,
|
skipToLocation,
|
||||||
|
|
@ -33,7 +34,10 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
||||||
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-49 transition-opacity duration-300`}>
|
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 z-49 transition-opacity duration-300`}>
|
||||||
<div className="bg-base dark:bg-base rounded-full shadow-lg px-3 sm:px-4 py-0.5 sm:py-1 flex items-center space-x-0.5 sm:space-x-1 relative scale-90 sm:scale-100 border border-offbase">
|
<div className="bg-base dark:bg-base rounded-full shadow-lg px-3 sm:px-4 py-0.5 sm:py-1 flex items-center space-x-0.5 sm:space-x-1 relative scale-90 sm:scale-100 border border-offbase">
|
||||||
{/* Speed control */}
|
{/* Speed control */}
|
||||||
<SpeedControl setSpeedAndRestart={setSpeedAndRestart} />
|
<SpeedControl
|
||||||
|
setSpeedAndRestart={setSpeedAndRestart}
|
||||||
|
setAudioPlayerSpeedAndRestart={setAudioPlayerSpeedAndRestart}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Page Navigation */}
|
{/* Page Navigation */}
|
||||||
{currentPage && numPages && (
|
{currentPage && numPages && (
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type ConfigValues = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
viewType: ViewType;
|
viewType: ViewType;
|
||||||
voiceSpeed: number;
|
voiceSpeed: number;
|
||||||
|
audioPlayerSpeed: number;
|
||||||
voice: string;
|
voice: string;
|
||||||
skipBlank: boolean;
|
skipBlank: boolean;
|
||||||
epubTheme: boolean;
|
epubTheme: boolean;
|
||||||
|
|
@ -29,6 +30,7 @@ interface ConfigContextType {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
viewType: ViewType;
|
viewType: ViewType;
|
||||||
voiceSpeed: number;
|
voiceSpeed: number;
|
||||||
|
audioPlayerSpeed: number;
|
||||||
voice: string;
|
voice: string;
|
||||||
skipBlank: boolean;
|
skipBlank: boolean;
|
||||||
epubTheme: boolean;
|
epubTheme: boolean;
|
||||||
|
|
@ -58,6 +60,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||||
const [viewType, setViewType] = useState<ViewType>('single');
|
const [viewType, setViewType] = useState<ViewType>('single');
|
||||||
const [voiceSpeed, setVoiceSpeed] = useState<number>(1);
|
const [voiceSpeed, setVoiceSpeed] = useState<number>(1);
|
||||||
|
const [audioPlayerSpeed, setAudioPlayerSpeed] = useState<number>(1);
|
||||||
const [voice, setVoice] = useState<string>('af_sarah');
|
const [voice, setVoice] = useState<string>('af_sarah');
|
||||||
const [skipBlank, setSkipBlank] = useState<boolean>(true);
|
const [skipBlank, setSkipBlank] = useState<boolean>(true);
|
||||||
const [epubTheme, setEpubTheme] = useState<boolean>(false);
|
const [epubTheme, setEpubTheme] = useState<boolean>(false);
|
||||||
|
|
@ -83,6 +86,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
const cachedBaseUrl = await getItem('baseUrl');
|
const cachedBaseUrl = await getItem('baseUrl');
|
||||||
const cachedViewType = await getItem('viewType');
|
const cachedViewType = await getItem('viewType');
|
||||||
const cachedVoiceSpeed = await getItem('voiceSpeed');
|
const cachedVoiceSpeed = await getItem('voiceSpeed');
|
||||||
|
const cachedAudioPlayerSpeed = await getItem('audioPlayerSpeed');
|
||||||
const cachedVoice = await getItem('voice');
|
const cachedVoice = await getItem('voice');
|
||||||
const cachedSkipBlank = await getItem('skipBlank');
|
const cachedSkipBlank = await getItem('skipBlank');
|
||||||
const cachedEpubTheme = await getItem('epubTheme');
|
const cachedEpubTheme = await getItem('epubTheme');
|
||||||
|
|
@ -106,6 +110,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
// Set the other values with defaults
|
// Set the other values with defaults
|
||||||
setViewType((cachedViewType || 'single') as ViewType);
|
setViewType((cachedViewType || 'single') as ViewType);
|
||||||
setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1'));
|
setVoiceSpeed(parseFloat(cachedVoiceSpeed || '1'));
|
||||||
|
setAudioPlayerSpeed(parseFloat(cachedAudioPlayerSpeed || '1'));
|
||||||
setVoice(cachedVoice || 'af_sarah');
|
setVoice(cachedVoice || 'af_sarah');
|
||||||
setSkipBlank(cachedSkipBlank === 'false' ? false : true);
|
setSkipBlank(cachedSkipBlank === 'false' ? false : true);
|
||||||
setEpubTheme(cachedEpubTheme === 'true');
|
setEpubTheme(cachedEpubTheme === 'true');
|
||||||
|
|
@ -136,6 +141,12 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
if (cachedTTSInstructions === null) {
|
if (cachedTTSInstructions === null) {
|
||||||
await setItem('ttsInstructions', '');
|
await setItem('ttsInstructions', '');
|
||||||
}
|
}
|
||||||
|
if (!cachedVoiceSpeed) {
|
||||||
|
await setItem('voiceSpeed', '1');
|
||||||
|
}
|
||||||
|
if (!cachedAudioPlayerSpeed) {
|
||||||
|
await setItem('audioPlayerSpeed', '1');
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing:', error);
|
console.error('Error initializing:', error);
|
||||||
|
|
@ -151,7 +162,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
* Updates multiple configuration values simultaneously
|
* Updates multiple configuration values simultaneously
|
||||||
* Only saves API credentials if they are explicitly set
|
* 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 {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (newConfig.apiKey !== undefined || newConfig.apiKey !== '') {
|
if (newConfig.apiKey !== undefined || newConfig.apiKey !== '') {
|
||||||
|
|
@ -205,6 +216,9 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
case 'voiceSpeed':
|
case 'voiceSpeed':
|
||||||
setVoiceSpeed(value as number);
|
setVoiceSpeed(value as number);
|
||||||
break;
|
break;
|
||||||
|
case 'audioPlayerSpeed':
|
||||||
|
setAudioPlayerSpeed(value as number);
|
||||||
|
break;
|
||||||
case 'voice':
|
case 'voice':
|
||||||
setVoice(value as string);
|
setVoice(value as string);
|
||||||
break;
|
break;
|
||||||
|
|
@ -247,6 +261,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
viewType,
|
viewType,
|
||||||
voiceSpeed,
|
voiceSpeed,
|
||||||
|
audioPlayerSpeed,
|
||||||
voice,
|
voice,
|
||||||
skipBlank,
|
skipBlank,
|
||||||
epubTheme,
|
epubTheme,
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ interface TTSContextType {
|
||||||
setText: (text: string, shouldPause?: boolean) => void;
|
setText: (text: string, shouldPause?: boolean) => void;
|
||||||
setCurrDocPages: (num: number | undefined) => void;
|
setCurrDocPages: (num: number | undefined) => void;
|
||||||
setSpeedAndRestart: (speed: number) => void;
|
setSpeedAndRestart: (speed: number) => void;
|
||||||
|
setAudioPlayerSpeedAndRestart: (speed: number) => void;
|
||||||
setVoiceAndRestart: (voice: string) => void;
|
setVoiceAndRestart: (voice: string) => void;
|
||||||
skipToLocation: (location: string | number, shouldPause?: boolean) => void;
|
skipToLocation: (location: string | number, shouldPause?: boolean) => void;
|
||||||
registerLocationChangeHandler: (handler: (location: string | number) => void) => void; // EPUB-only: Handles chapter navigation
|
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,
|
baseUrl: openApiBaseUrl,
|
||||||
isLoading: configIsLoading,
|
isLoading: configIsLoading,
|
||||||
voiceSpeed,
|
voiceSpeed,
|
||||||
|
audioPlayerSpeed,
|
||||||
voice: configVoice,
|
voice: configVoice,
|
||||||
ttsModel: configTTSModel,
|
ttsModel: configTTSModel,
|
||||||
ttsInstructions: configTTSInstructions,
|
ttsInstructions: configTTSInstructions,
|
||||||
|
|
@ -141,6 +143,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
|
const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
|
||||||
const [speed, setSpeed] = useState(voiceSpeed);
|
const [speed, setSpeed] = useState(voiceSpeed);
|
||||||
|
const [audioSpeed, setAudioSpeed] = useState(audioPlayerSpeed);
|
||||||
const [voice, setVoice] = useState(configVoice);
|
const [voice, setVoice] = useState(configVoice);
|
||||||
const [ttsModel, setTTSModel] = useState(configTTSModel);
|
const [ttsModel, setTTSModel] = useState(configTTSModel);
|
||||||
const [ttsInstructions, setTTSInstructions] = useState(configTTSInstructions);
|
const [ttsInstructions, setTTSInstructions] = useState(configTTSInstructions);
|
||||||
|
|
@ -387,7 +390,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
const updateVoiceAndSpeed = useCallback(() => {
|
const updateVoiceAndSpeed = useCallback(() => {
|
||||||
setVoice(configVoice);
|
setVoice(configVoice);
|
||||||
setSpeed(voiceSpeed);
|
setSpeed(voiceSpeed);
|
||||||
}, [configVoice, voiceSpeed]);
|
setAudioSpeed(audioPlayerSpeed);
|
||||||
|
}, [configVoice, voiceSpeed, audioPlayerSpeed]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes configuration and fetches available voices
|
* Initializes configuration and fetches available voices
|
||||||
|
|
@ -570,6 +574,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
html5: true,
|
html5: true,
|
||||||
preload: true,
|
preload: true,
|
||||||
pool: 5,
|
pool: 5,
|
||||||
|
rate: audioSpeed,
|
||||||
onplay: () => {
|
onplay: () => {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
|
|
@ -666,7 +671,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
advance();
|
advance();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [isPlaying, advance, activeHowl, processSentence]);
|
}, [isPlaying, advance, activeHowl, processSentence, audioSpeed]);
|
||||||
|
|
||||||
const playAudio = useCallback(async () => {
|
const playAudio = useCallback(async () => {
|
||||||
const howl = await playSentenceWithHowl(sentences[currentIndex]);
|
const howl = await playSentenceWithHowl(sentences[currentIndex]);
|
||||||
|
|
@ -822,6 +827,35 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
});
|
});
|
||||||
}, [abortAudio, updateConfigKey, audioCache, isPlaying]);
|
}, [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
|
* Provides the TTS context value to child components
|
||||||
*/
|
*/
|
||||||
|
|
@ -843,6 +877,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
setText,
|
setText,
|
||||||
setCurrDocPages,
|
setCurrDocPages,
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
|
setAudioPlayerSpeedAndRestart,
|
||||||
setVoiceAndRestart,
|
setVoiceAndRestart,
|
||||||
skipToLocation,
|
skipToLocation,
|
||||||
registerLocationChangeHandler,
|
registerLocationChangeHandler,
|
||||||
|
|
@ -866,6 +901,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
setText,
|
setText,
|
||||||
setCurrDocPages,
|
setCurrDocPages,
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
|
setAudioPlayerSpeedAndRestart,
|
||||||
setVoiceAndRestart,
|
setVoiceAndRestart,
|
||||||
skipToLocation,
|
skipToLocation,
|
||||||
registerLocationChangeHandler,
|
registerLocationChangeHandler,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue