You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 08:05:01