浏览器原生 TTS(Text-to-Speech)工具类

1 使用

 单例模式,全局共享一个实例
 * - 自动检测浏览器语音合成 API
 * - 支持报警通知场景的优化参数
 * - 自动选择最佳音色

// 方式1:监听初始化完成后播放
 * tts.onReady(() => {
 *   tts.speakAlert('报警信息')
 * })
 * 
 * // 方式2:直接播放(音色未就绪时可能使用默认音色)
 * tts.speakAlert('报警信息')

2 BrowserTTS.ts类


/**
 * 浏览器原生 TTS(Text-to-Speech)工具类
 * 
 * 功能特点:
 * - 单例模式,全局共享一个实例
 * - 自动检测浏览器语音合成 API
 * - 支持报警通知场景的优化参数
 * - 自动选择最佳音色
 * 
 * 使用示例:
 * ```typescript
 * import BrowserTTS from '@/utils/BrowserTTS'
 * 
 * const tts = BrowserTTS.getInstance()
 * 
 * // 方式1:监听初始化完成后播放
 * tts.onReady(() => {
 *   tts.speakAlert('报警信息')
 * })
 * 
 * // 方式2:直接播放(音色未就绪时可能使用默认音色)
 * tts.speakAlert('报警信息')
 * ```
 */
class BrowserTTS {
    /** 单例实例 */
    private static instance: BrowserTTS;

    /** 可用音色列表 */
    private voices: SpeechSynthesisVoice[] = [];

    /**
     * 获取单例实例
     * @returns BrowserTTS 实例
     */
    public static getInstance(): BrowserTTS {
        if (!BrowserTTS.instance) {
            BrowserTTS.instance = new BrowserTTS();
        }
        return BrowserTTS.instance;
    }

    /**
     * 私有构造函数
     * 初始化时自动加载音色列表并监听变化
     */
    private constructor() {
        /**
         * 加载音色列表的内部方法
         * 会在构造时立即调用,并在 voiceschanged 事件触发时再次调用
         */
        const loadVoices = () => {
            this.voices = window.speechSynthesis.getVoices();

            // 只在 voices 首次加载完成且有回调时触发一次
            if (this.voices.length > 0) {
                this.isLoaded = true;

            }
        };

        // 立即尝试加载音色
        loadVoices();

        // 监听音色变化事件(某些浏览器需要等待事件触发才能获取完整列表)
        window.speechSynthesis.onvoiceschanged = loadVoices;
    }

    /**
     * 初始化完成回调(保证音色一定有值)
     * 
     * @param callback 回调函数,参数为可用音色列表
     * @note 回调只会执行一次,防止重复触发
     * 
     * 使用示例:
     * ```typescript
     * tts.onReady((voices) => {
     *   console.log('音色列表:', voices)
     *   tts.speakAlert('系统已就绪')
     * })
     * ```
     * 
     * 
     * 🔥 最安全的 onReady
     * 规则:
     * 1. 已加载 → 立即执行回调(1次)
     * 2. 未加载 → 加载完成执行(1次)
     * 3. 调用 N 次,也只执行 1 次
     */
    onReady(callback: (voices: SpeechSynthesisVoice[]) => void): void {
        // 已经有音色 → 立即执行
        if (this.voices.length > 0) {
            callback(this.voices);
            return;
        }

        // 否则监听一次,执行后自动销毁(不重复)
        const handler = () => {
            this.voices = window.speechSynthesis.getVoices();
            if (this.voices.length > 0) {
                window.speechSynthesis.removeEventListener('voiceschanged', handler);
                callback(this.voices);
            }
        };

        window.speechSynthesis.addEventListener('voiceschanged', handler);
    }

    /**
     * 自动选择最适合报警通知的音色
     * 
     * 优先级排序(按报警场景优化):
     * 1. 晓晓 (Xiaoxiao) - 女音,清晰正式
     * 2. 晓伊 (Xiaoyi) - 女音,温柔清晰
     * 3. 云扬 (Yunyang) - 男音,沉稳有力
     * 4. 云健 (Yunjian) - 男音,清晰
     * 5. 列表第一个音色(兜底)
     * 
     * @returns 最佳报警音色,若无则返回 undefined
     */
    getBestAlertVoice(): SpeechSynthesisVoice | undefined {
        const list = this.voices;

        // 报警首选:清晰、正式的女声
        const xiaoxiao = list.find(v => v.name.includes("Xiaoxiao") || v.name.includes("晓晓"));
        if (xiaoxiao) return xiaoxiao;

        const xiaoyi = list.find(v => v.name.includes("Xiaoyi") || v.name.includes("晓伊"));
        if (xiaoyi) return xiaoyi;

        // 备选:沉稳的男声
        const yunyang = list.find(v => v.name.includes("Yunyang") || v.name.includes("云扬"));
        if (yunyang) return yunyang;

        const yunjian = list.find(v => v.name.includes("Yunjian") || v.name.includes("云健"));
        if (yunjian) return yunjian;

        // 兜底:返回第一个可用音色
        return list[0];
    }

    /**
     * 播放报警语音(自动使用最佳音色和优化参数)
     * 
     * 报警场景优化参数:
     * - 语速:0.98(稍慢,更清晰)
     * - 音量:1.0(标准音量)
     * - 音调:1.0(标准音调)
     * 
     * @param text 要播放的报警内容
     * 
     * 使用示例:
     * ```typescript
     * tts.speakAlert('检测到异常情况,请及时处理')
     * ```
     */
    speakAlert(text: string): void {
        if (!window.speechSynthesis) return;
        const voice = this.getBestAlertVoice();
        if (!voice) return;

        // 停止当前正在播放的语音
        this.stop();

        // 创建语音合成实例
        const utter = new SpeechSynthesisUtterance(text);

        // 设置报警推荐参数
        utter.voice = voice;
        utter.rate = 0.98;      // 语速稍慢,更清晰
        utter.volume = 1.0;     // 标准音量
        utter.pitch = 1.0;      // 标准音调

        // 开始播放
        window.speechSynthesis.speak(utter);
    }

    /**
     * 普通朗读(手动指定参数)
     * 
     * @param text 要朗读的文本内容
     * @param voiceIndex 音色索引(默认 0,即第一个音色)
     * @param rate 语速(默认 1,范围:0.1-10)
     * @param volume 音量(默认 1,范围:0-1)
     * 
     * 使用示例:
     * ```typescript
     * // 使用默认参数朗读
     * tts.speak('你好世界')
     * 
     * // 指定音色和语速
     * tts.speak('慢速朗读', 0, 0.5)
     * 
     * // 指定音量
     * tts.speak('小声说话', 0, 1, 0.5)
     * ```
     */
    speak(
        text: string,
        voiceIndex: number = 0,
        rate: number = 1,
        volume: number = 1
    ): void {
        if (!window.speechSynthesis) return;
        // 停止当前正在播放的语音
        this.stop();

        // 创建语音合成实例
        const utter = new SpeechSynthesisUtterance(text);

        // 设置参数
        utter.rate = rate;
        utter.volume = volume;

        // 设置音色(如果指定的索引有效)
        if (this.voices[voiceIndex]) {
            utter.voice = this.voices[voiceIndex];
        }

        // 开始播放
        window.speechSynthesis.speak(utter);
    }

    /**
     * 暂停播放
     */
    pause(): void {
        window.speechSynthesis.pause();
    }

    /**
     * 恢复播放
     */
    resume(): void {
        window.speechSynthesis.resume();
    }

    /**
     * 停止播放(取消当前语音)
     */
    stop(): void {
        window.speechSynthesis.cancel();
    }

    /**
     * 检查是否正在播放
     * @returns 是否正在播放中
     */
    isSpeaking(): boolean {
        return window.speechSynthesis.speaking;
    }

    /**
     * 获取可用音色列表
     * @returns 音色列表
     */
    getVoices(): SpeechSynthesisVoice[] {
        return this.voices;
    }
    // 检查是否已加载完成
    isReady() { return this.voices.length > 0; }
}

export default BrowserTTS;
 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容