可能会遇到的坑
原文链接
自行了解 js webWorker线程
我的目录结构
TTS.js代码
// 科大讯飞 文字->语音
import {downloadPCM, downloadWAV} from '@/common/download.js'
import CryptoJS from 'crypto-js'
import { Base64 } from 'js-base64'
var transWorker = new Worker('../common/transcode.worker.js')
//测试完成后需要改成 var transWorker = new Worker('transcode.worker.js')
//APPID,APISecret,APIKey在控制台-我的应用-语音合成(流式版)页面获取
const APPID = ''
const API_SECRET = ''
const API_KEY = ''
function getWebsocketUrl() {return new Promise((resolve, reject) => {var apiKey = API_KEYvar apiSecret = API_SECRETvar url = 'wss://tts-api.xfyun.cn/v2/tts'var host = location.hostvar date = new Date().toGMTString()var algorithm = 'hmac-sha256'var headers = 'host date request-line'var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/tts HTTP/1.1`var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret)var signature = CryptoJS.enc.Base64.stringify(signatureSha)var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`var authorization = btoa(authorizationOrigin)url = `${url}?authorization=${authorization}&date=${date}&host=${host}`resolve(url)})
}
export default class TTSRecorder {constructor({speed = 20,voice = 50,pitch = 50,voiceName = 'xiaoyan',appId = APPID,text = '',tte = 'UTF8',defaultText = '请输入您要合成的文本',} = {}) {this.speed = speedthis.voice = voicethis.pitch = pitchthis.voiceName = voiceNamethis.text = textthis.tte = ttethis.defaultText = defaultTextthis.appId = appIdthis.audioData = []this.rawAudioData = []this.audioDataOffset = 0this.status = 'init'transWorker.onmessage = (e) => {this.audioData.push(...e.data.data)this.rawAudioData.push(...e.data.rawAudioData)}}// 修改录音听写状态setStatus(status) {this.onWillStatusChange && this.onWillStatusChange(this.status, status)this.status = status}// 设置合成相关参数setParams({ speed, voice, pitch, text, voiceName, tte }) {speed !== undefined && (this.speed = speed)voice !== undefined && (this.voice = voice)pitch !== undefined && (this.pitch = pitch)text && (this.text = text)tte && (this.tte = tte)voiceName && (this.voiceName = voiceName)this.resetAudio()}// 连接websocketconnectWebSocket() {this.setStatus('ttsing')return getWebsocketUrl().then(url => {let ttsWSif ('WebSocket' in window) {ttsWS = new WebSocket(url)} else if ('MozWebSocket' in window) {ttsWS = new MozWebSocket(url)} else {alert('浏览器不支持WebSocket')return}this.ttsWS = ttsWSttsWS.onopen = e => {this.webSocketSend()this.playTimeout = setTimeout(() => {this.audioPlay()}, 1000)}ttsWS.onmessage = e => {this.result(e.data)}ttsWS.onerror = e => {clearTimeout(this.playTimeout)this.setStatus('errorTTS')alert('WebSocket报错,请f12查看详情')console.error(`详情查看:${encodeURI(url.replace('wss:', 'https:'))}`)}ttsWS.onclose = e => {console.log(e)}})}// 处理音频数据transToAudioData(audioData) {}// websocket发送数据webSocketSend() {var params = {common: {app_id: this.appId, // APPID},business: {aue: 'raw',auf: 'audio/L16;rate=16000',vcn: this.voiceName,speed: this.speed,volume: this.voice,pitch: this.pitch,bgs: 1,tte: this.tte,},data: {status: 2,text: this.encodeText(this.text || this.defaultText,this.tte === 'unicode' ? 'base64&utf16le' : '')},}this.ttsWS.send(JSON.stringify(params))}encodeText (text, encoding) {switch (encoding) {case 'utf16le' : {let buf = new ArrayBuffer(text.length * 4)let bufView = new Uint16Array(buf)for (let i = 0, strlen = text.length; i < strlen; i++) {bufView[i] = text.charCodeAt(i)}return buf}case 'buffer2Base64': {let binary = ''let bytes = new Uint8Array(text)let len = bytes.byteLengthfor (let i = 0; i < len; i++) {binary += String.fromCharCode(bytes[i])}return window.btoa(binary)}case 'base64&utf16le' : {return this.encodeText(this.encodeText(text, 'utf16le'), 'buffer2Base64')}default : {return Base64.encode(text)}}}// websocket接收数据的处理result(resultData) {let jsonData = JSON.parse(resultData)// 合成失败if (jsonData.code !== 0) {alert(`合成失败: ${jsonData.code}:${jsonData.message}`)console.error(`${jsonData.code}:${jsonData.message}`)this.resetAudio()return}transWorker.postMessage(jsonData.data.audio)// window.postMessage(jsonData.data.audio)if (jsonData.code === 0 && jsonData.data.status === 2) {this.ttsWS.close()}}// 重置音频数据resetAudio() {this.audioStop()this.setStatus('init')this.audioDataOffset = 0this.audioData = []this.rawAudioData = []this.ttsWS && this.ttsWS.close()clearTimeout(this.playTimeout)}// 音频初始化audioInit() {let AudioContext = window.AudioContext || window.webkitAudioContextif (AudioContext) {this.audioContext = new AudioContext()this.audioContext.resume()this.audioDataOffset = 0} }// 音频播放audioPlay() {this.setStatus('play')let audioData = this.audioData.slice(this.audioDataOffset)this.audioDataOffset += audioData.lengthlet audioBuffer = this.audioContext.createBuffer(1, audioData.length, 22050)let nowBuffering = audioBuffer.getChannelData(0)if (audioBuffer.copyToChannel) {audioBuffer.copyToChannel(new Float32Array(audioData), 0, 0)} else {for (let i = 0; i < audioData.length; i++) {nowBuffering[i] = audioData[i]}}let bufferSource = this.bufferSource = this.audioContext.createBufferSource()bufferSource.buffer = audioBufferbufferSource.connect(this.audioContext.destination)bufferSource.start()bufferSource.onended = event => {if (this.status !== 'play') {return}if (this.audioDataOffset < this.audioData.length) {this.audioPlay()} else {this.audioStop()}}}// 音频播放结束audioStop() {this.setStatus('endPlay')clearTimeout(this.playTimeout)this.audioDataOffset = 0if (this.bufferSource) {try {this.bufferSource.stop()} catch (e) {console.log(e)}}}start() {if(this.audioData.length) {this.audioPlay()} else {if (!this.audioContext) {this.audioInit()}if (!this.audioContext) {alert('该浏览器不支持webAudioApi相关接口')return}this.connectWebSocket()}}stop() {this.audioStop()}
}
transcode.worker.js代码(科大讯飞demo里面的,但是稍作修改 语音合成(流式版)WebAPI 文档 | 讯飞开放平台文档中心)
/** @Autor: lycheng* @Date: 2020-01-13 16:12:22*/let minSampleRate = 22050self.onmessage = function(e) {transcode.transToAudioData(e.data)}var transcode = {transToAudioData(audioDataStr, fromRate = 16000, toRate = 22505) {let outputS16 = transcode.base64ToS16(audioDataStr)let output = transcode.transS16ToF32(outputS16)output = transcode.transSamplingRate(output, fromRate, toRate)output = Array.from(output)self.postMessage({data: output, rawAudioData: Array.from(outputS16)})},transSamplingRate(data, fromRate = 44100, toRate = 16000) {var fitCount = Math.round(data.length * (toRate / fromRate))var newData = new Float32Array(fitCount)var springFactor = (data.length - 1) / (fitCount - 1)newData[0] = data[0]for (let i = 1; i < fitCount - 1; i++) {var tmp = i * springFactorvar before = Math.floor(tmp).toFixed()var after = Math.ceil(tmp).toFixed()var atPoint = tmp - beforenewData[i] = data[before] + (data[after] - data[before]) * atPoint}newData[fitCount - 1] = data[data.length - 1]return newData},transS16ToF32(input) {var tmpData = []for (let i = 0; i < input.length; i++) {var d = input[i] < 0 ? input[i] / 0x8000 : input[i] / 0x7ffftmpData.push(d)}return new Float32Array(tmpData)},base64ToS16(base64AudioData) {base64AudioData = atob(base64AudioData)const outputArray = new Uint8Array(base64AudioData.length)for (let i = 0; i < base64AudioData.length; ++i) {outputArray[i] = base64AudioData.charCodeAt(i)}return new Int16Array(new DataView(outputArray.buffer).buffer)},}
index.vue代码
<template><view class="content"><view class="text-area"><text class="title">在线文字转语音</text></view><textarea type="text" v-model="txt" placeholder="请输入您要合成的文本"></textarea><button @click="startTrans">{{btnState[ttsStatus]}}</button><br/><view class="text-area"><text class="title">语音转文字</text></view><button>开始转写</button><button>结束转写</button></view>
</template><script>import TTSRecorder from "@/common/TTS.js"console.log(TTSRecorder,'TTSRecorder');// const transWorker = new Worker(new URL('../../common/transcode.worker.js', import.meta.url))let ttsRecorder = new TTSRecorder()export default {data() {return {title: 'Hello',txt: '',aTt: '',btnState: {init: '立即合成',ttsing: '正在合成',play: '停止播放',endPlay: '重新播放',errorTTS: '合成失败',},ttsStatus: 'init'}},onLoad() {const _this = thisttsRecorder.onWillStatusChange = function(oldStatus, status) {// 可以在这里进行页面中一些交互逻辑处理:按钮交互等_this.ttsStatus = status}},methods: {startTrans(){ttsRecorder.setParams({text: this.txt})console.log(ttsRecorder,'ttsRecorder');if (['init', 'endPlay', 'errorTTS'].indexOf(ttsRecorder.status) > -1) {console.log(111);ttsRecorder.start()} else {ttsRecorder.stop()}}}}
</script><style>.content {display: flex;flex-direction: column;align-items: center;justify-content: center;}.logo {height: 200rpx;width: 200rpx;margin-top: 200rpx;margin-left: auto;margin-right: auto;margin-bottom: 50rpx;}.text-area {display: flex;justify-content: center;}.title {font-size: 36rpx;color: #8f8f94;}
</style>
最后打包出来后,把transcode.worker.js放到根目录即可
关键点就是webWorker