如何基于SpeechSynthesis API实现语音淡出效果?
SpeechSynthesis 淡出效果不生效的修复方案
问题根源
当SpeechSynthesisUtterance实例通过speechSynthesis.speak()启动播放后,浏览器会锁定该实例的音量等参数,后续直接修改实例的volume属性不会影响当前正在播放的音频,只会作用于下一次播放的实例——这就是你看到控制台音量递减但听觉无变化,且下一段文本音量变为0的原因。
修复方案
核心思路是避免复用同一个SpeechSynthesisUtterance实例,每次播放都创建新实例,并通过useRef追踪当前播放实例,实现精准的音量控制。以下是修改后的完整代码:
import React, { useEffect, useState, useRef } from "react"; import VoiceMessages from "./messages"; const VoiceAssistantProvider = ({ children }) => { const [isUsable, setIsUsable] = useState(false); const [isSpeaking, setIsSpeaking] = useState(false); const [isActive, setIsActive] = useState(false); const [voice, setVoice] = useState(null); const fadeoutTime = 2000; // 用ref保存当前播放的utterance实例和淡出定时器 const currentUtteranceRef = useRef(null); const fadeTimeoutRef = useRef(null); const handleVoicesLoaded = () => { const voices = speechSynthesis.getVoices(); setVoice(voices[2]); }; useEffect(() => { if ("speechSynthesis" in window) { setIsUsable(true); window.speechSynthesis.onvoiceschanged = handleVoicesLoaded; } }, []); // 实现平滑淡出逻辑 const startFadeout = () => { if (!currentUtteranceRef.current) return; const initialVolume = currentUtteranceRef.current.volume; // 50ms间隔调整音量,让淡出更自然 const step = initialVolume / (fadeoutTime / 50); const fadeStep = () => { if (currentUtteranceRef.current.volume <= 0) { speechSynthesis.cancel(); setIsSpeaking(false); clearTimeout(fadeTimeoutRef.current); return; } currentUtteranceRef.current.volume -= step; fadeTimeoutRef.current = setTimeout(fadeStep, 50); }; fadeStep(); }; // 统一播放文本方法,每次创建新的utterance实例 const speakText = (text) => { // 清除之前的播放和定时器 speechSynthesis.cancel(); clearTimeout(fadeTimeoutRef.current); const utterance = new SpeechSynthesisUtterance(text); utterance.voice = voice; utterance.volume = 1; // 初始音量重置为1 currentUtteranceRef.current = utterance; utterance.addEventListener("end", () => { setIsSpeaking(false); currentUtteranceRef.current = null; }); speechSynthesis.speak(utterance); setIsSpeaking(true); }; // 处理isActive状态切换 useEffect(() => { if (!isUsable) return; if (isActive) { speakText(VoiceMessages.InitializeVoiceAssistant); } else { if (isSpeaking) { // 当前正在播放时,先执行淡出,结束后播放停止提示 startFadeout(); const fadeEndTimeout = setTimeout(() => { speakText(VoiceMessages.StopVoiceAssistant); }, fadeoutTime); return () => clearTimeout(fadeEndTimeout); } else { speakText(VoiceMessages.StopVoiceAssistant); } } }, [isActive, isUsable, isSpeaking]); const [voiceAI, setVoiceAI] = useState({ isUsable, isSpeaking, isActive, voice, setIsActive, }); useEffect(() => { setVoiceAI(prev => ({ ...prev, isUsable, isSpeaking, isActive, voice, setIsActive, })); }, [isUsable, isSpeaking, isActive, voice]); const wrappedChildren = React.Children.map(children, (child) => { return React.cloneElement(child, { voiceAI }); }); return <> {wrappedChildren} </>; }; export default VoiceAssistantProvider;
关键改动说明
- 使用
useRef追踪当前播放的utterance实例,避免复用导致的参数锁定问题 - 每次播放都创建新的
SpeechSynthesisUtterance实例,确保初始音量正确设置 - 调整淡出时间间隔为50ms,让音量变化更平滑,听觉效果更明显
- 重构播放逻辑为独立方法,统一管理播放状态和资源清理
- 优化状态切换逻辑:当前播放时先执行淡出,再启动下一段文本播放
内容的提问来源于stack exchange,提问作者Baba Dan Constantin
相关产品推荐
相关产品推荐

