1.功能场景
有时候需要在网页上面播报一段语音,而这段语音是动态的。例如收银时播报请出示付款吗,收钱成功后播报某某某为您收到金额XX元。
2.思路
第一种思路是前端不需要怎么动手写代码的也是最容易实现的,调用语音合成api。但是api的局限性就在于免费的没有语音包,收钱的就有点贵了,不适用于重复调用(我们系统目前规模不大,但是每天也能产生1-2万条成功的交易订单)。
第二种思路是调用windows本地的tts语音合成服务,这是能免费使用且可以支持每次根据不同的内容来合成不同的语音的一个功能。
第三种思路使用video元素直接组装一些零散的文字来形成一段完整的音频。
这里就讲一下第二种跟第三种思路
3.实现
3.1windows本地的tts语音合成服务
这里使用的是SpeechSynthesisUtterance这个html5新的api,这个对象主要用来构建语音合成实例,具体的属性如下。
text
– 要合成的文字内容:string。lang
– 使用的语言:string, 例如:"zh-cn"
voiceURI
– 指定希望使用的声音和服务:string。volume
– 声音的音量:number,范围是0-
1
,默认11
。rate
– 语速:number,范围是0.1-
10
,默认1。
pitch
– 表示说话的音高:number,范围是0
-2
。默认为1
。
当然,这个实例对象也包括一些方法
onstart
– 合成开始的回调。onpause
– 合成暂停的回调。onresume
– 合成重新开始的回调。onend
– 合成结束时的回调。
这是mdn上面对SpeechSynthesisUtterance这个对象的说明:SpeechSynthesisUtterance - Web API 接口参考 | MDN
然后还有一个跟SpeechSynthesisUtterance搭配使用的SpeechSynthesis对象。该接口是语音服务的控制接口,它可以用于获取设备上关于可用的合成声音的信息,开始、暂停语音,或除此之外的其他命令。SpeechSynthesisUtterance - Web API 接口参考 | MDN
speak()
– 只能接收SpeechSynthesisUtterance
作为唯一的参数,作用是读合成的话语。
stop()
– 立即终止合成过程。
pause()
– 暂停合成过程。
resume()
– 重新开始合成过程。
getVoices
– 此方法不接受任何参数,用来返回浏览器支持的语音包列表,是个数组。谷歌浏览器
getVoices获取的声音列表,国内能使用的应该就前三个
let synth;let msg;const initSpeak = () => {synth = window.speechSynthesismsg = new SpeechSynthesisUtterance()msg.text = '收到新的订单'msg.lang = 'zh-CN'msg.pitch = 1.1msg.rate = 1.8msg.volume = 10//getVoices() 是一个异步的方法,需要使用一个定时器来保证每次都能获取到值setTimeout(()=>{synth.getVoices().find(i=>i.lang=='zh-CN'&&i.localService==true)},100)}initSpeak()// 调用这个方法传入一个你需要合成的文字的话就能开始使用浏览器来播报语音了const handleSpeak = (message) => {msg.text = messagesynth.speak(msg)}
到这里就完成了语音合成了,不过由于我们项目中有的客户电脑使用的阉割版的windows或者没有去安装语音合成引擎,这个版本上线一周就被pass掉了,所以使用这个方案的前提是你能保证客户机上面是安装了语音合成引擎。
3.2 组装一些零散的mp3片段来形成一段完整的音频(伪语音播报)
结合到实际的应用场景项目中最终选择了这种方式来实现语音合成,虽然方法很笨,但是至少所有客户机都能满足需求且可以做到客户自定义语音包。
实现:
1.需要一个将数字转换成中文读数的方法(网上找的)
2.再将这个中文读数构造成一个数组(urlList)
3.调用组件进行播报
数字转换成中文读数的方法
/*** 数字转成汉字* @params num === 要转换的数字* @return 汉字* @eg 例如,输入0.41, 返回"零点四一元"* */const numToChines = (tranvalue) => {if (typeof tranvalue == "number") {tranvalue = tranvalue + ''}//拆分整数与小数let splits = function (tranvalue) {var value = new Array('', '');temp = tranvalue.split(".");for (var i = 0; i < temp.length; i++) {value[i] = temp[i];}return value;}try {var i = 1;var dw2 = new Array("", "万", "亿");//大单位var dw1 = new Array("十", "百", "千");//小单位var dw = new Array("零", "一", "二", "三", "四", "五", "六", "七", "八", "九");//整数部分用//以下是小写转换成大写显示在合计大写的文本框中//分离整数与小数var source = splits(tranvalue);var num = source[0];var dig = source[1];//转换整数部分var k1 = 0;//计小单位var k2 = 0;//计大单位var sum = 0;var str = "";var len = source[0].length;//整数的长度for (i = 1; i <= len; i++) {var n = source[0].charAt(len - i);//取得某个位数上的数字var bn = 0;if (len - i - 1 >= 0) {bn = source[0].charAt(len - i - 1);//取得某个位数前一位上的数字}sum = sum + Number(n);if (sum != 0) {str = dw[Number(n)].concat(str);//取得该数字对应的大写数字,并插入到str字符串的前面if (n == '0') sum = 0;}if (len - i - 1 >= 0) {//在数字范围内if (k1 != 3) {//加小单位if (bn != 0) {str = dw1[k1].concat(str);}k1++;} else {//不加小单位,加大单位k1 = 0;var temp = str.charAt(0);if (temp == "万" || temp == "亿")//若大单位前没有数字则舍去大单位str = str.substr(1, str.length - 1);str = dw2[k2].concat(str);sum = 0;}}if (k1 == 3)//小单位到千则大单位进一{ k2++; }}//转换小数部分var strdig = "";if (dig != "") {var n = dig.charAt(0)var nn = dig.charAt(1)if (nn !== "") {strdig = "点" + dw[Number(n)] + dw[Number(nn)]} else {strdig = "点" + dw[Number(n)]}}if (str) {str += strdig + "元";} else {str = "零" + strdig + "元"}} catch (e) {return "零元";}return str;}
构建待播报的数组
//将中文枚举成对应的英文路径const voiceEnum = {"万": "ten_thousand","十": "ten","百": "hundred","千": "thousand","零": "0","一": "1","二": "2","三": "3","四": "4","五": "5","六": "6","七": "7","八": "8","九": "9","点": "point","元": "element"}const speak = () => {let list = []let chineseNum = numToChines(0.41)for (let i = 0; i < chineseNum.length; i++) {const item = chineseNum[i];//这里的地址拼接成你自己存放零散mp3片段的地址,voicePacket是语音包的配置,需要自己去定义urlList.push(`/voice/${voicePacket.value}/amount/${voiceEnum[item]}.mp3`)}//调用一个组件来播报这个数组形式的urlaudioLoopRef.value && audioLoopRef.value.start(list)}
附上audioLoop这个组件代码,构建好urlList之后通过ref调用组件内的方法即可
<template><div v-if="audioUrlList.length > 0"><audio @ended="voiceEnded" id="voice"><source :src="audioUrlList[currentIndex]" type="audio/mpeg">您的浏览器不支持 audio 元素。</audio></div></template><script setup>import { ref, nextTick } from "vue"// 用来保存传过来的需要播放的urlList:[]const audioUrlList = ref([])// 默认从头开始播报const currentIndex = ref(0)// 开始播报const start = (urlList) => {audioUrlList.value = urlListnextTick(() => {let dom = document.getElementById("voice")dom.play()})}// 停止播报const stop = () => {let dom = document.getElementById("voice")nextTick(() => {dom.pause()dom.setAttribute("src", "xxxx")currentIndex.value = 0audioUrlList.value = []})}// 播放完一个就继续播放下一个 const voiceEnded = () => {if (currentIndex.value == (audioUrlList.value.length - 1)) {audioUrlList.value = []currentIndex.value = 0} else {currentIndex.value++let dom = document.getElementById("voice")dom.setAttribute("src", audioUrlList.value[currentIndex.value])dom.play()}}defineExpose({start, stop})</script>
语音包的实现其实就是将相同的文字多录制几个语音包放在不同的oss目录下面,到时候通过前端的配置动态生成urlList时去对应到不同的语音包。
到这里就完成了自己手动合成一段语音并播报了。