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辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。