科大讯飞语音合成Vue版教程

本文档介绍如何在Vue.js项目中使用科大讯飞的文字转语音服务,并通过WebSocket进行实时音频数据处理。首先,从科大讯飞官网下载js版本的demo并放置于项目资源文件夹中。接着,展示了`transcode.worker.js`和`audio.js`两个关键文件,分别用于音频数据转换和WebSocket通信。在`audio.js`中,定义了一个`TTSRecorder`类,用于管理语音合成的整个流程,包括连接WebSocket、发送合成请求、接收和播放音频数据。最后,提供了Vue组件模板,展示如何调用`TTSRecorder`进行语音合成功能的启动和停止。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

项目需求: 把文字转语音

一. 去科大讯飞官网下载js版本demo:https://www.xfyun.cn/doc/tts/online_tts/API.html

放入src下面assets文件下: 

 transcode.worker.js:

/*
 * @Autor: lycheng
 * @Date: 2020-01-13 16:12:22
 */
(function(){
  let minSampleRate = 22050
  self.onmessage = function(e) {
    transcode.transToAudioData(e.data)
  }
  var transcode = {
    transToAudioData: function(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: function(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 * springFactor
        var before = Math.floor(tmp).toFixed()
        var after = Math.ceil(tmp).toFixed()
        var atPoint = tmp - before
        newData[i] = data[before] + (data[after] - data[before]) * atPoint
      }
      newData[fitCount - 1] = data[data.length - 1]
      return newData
    },
    transS16ToF32: function(input) {
      var tmpData = []
      for (let i = 0; i < input.length; i++) {
        var d = input[i] < 0 ? input[i] / 0x8000 : input[i] / 0x7fff
        tmpData.push(d)
      }
      return new Float32Array(tmpData)
    },
    base64ToS16: function(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)
    },
  }
})()

audio.js: 

/*
 * @Autor: lycheng
 * @Date: 2020-01-13 16:12:22
 */
/**
 * Created by iflytek on 2019/11/19.
 *
 * 在线语音合成调用demo
 * 此demo只是一个简单的调用示例,不适合用到实际生产环境中
 *
 * 在线语音合成 WebAPI 接口调用示例 接口文档(必看):https://www.xfyun.cn/doc/tts/online_tts/API.html
 * 错误码链接:
 * https://www.xfyun.cn/doc/tts/online_tts/API.html
 * https://www.xfyun.cn/document/error-code (code返回错误码时必看)
 *
 */

// 1. websocket连接:判断浏览器是否兼容,获取websocket url并连接,这里为了方便本地生成websocket url
// 2. 连接websocket,向websocket发送数据,实时接收websocket返回数据
// 3. 处理websocket返回数据为浏览器可以播放的音频数据
// 4. 播放音频数据
// ps: 该示例用到了es6中的一些语法,建议在chrome下运行
// import {downloadPCM, downloadWAV} from 'js/download.js'
import CryptoJS from 'crypto-js'
import Enc from 'enc'
import TransWorker from '@/assets/js/speechSynthesis/transcode.worker'
// import VConsole from 'vconsole'
import { Base64 } from 'js-base64'
// import './index.css'

let transWorker = new TransWorker()
//APPID,APISecret,APIKey在控制台-我的应用-语音合成(流式版)页面获取
const APPID = '你的APPID'
const API_SECRET = '你的API_SECRET'
const API_KEY = '你的API_KEY '

function getWebsocketUrl() {
  return new Promise((resolve, reject) => {
    var apiKey = API_KEY
    var apiSecret = API_SECRET
    var url = 'wss://tts-api.xfyun.cn/v2/tts'
    var host = location.host
    var 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)
  })
}
const TTSRecorder =class {
  constructor({
    speed = 30,
    voice = 50,
    pitch = 50,
    voiceName = 'xiaoyan',
    appId = APPID,
    text = '',
    tte = 'UTF8',
    defaultText = '请输入您要合成的文本',
  } = {}) {
    this.speed = speed
    this.voice = voice
    this.pitch = pitch
    this.voiceName = voiceName
    this.text = text
    this.tte = tte
    this.defaultText = defaultText
    this.appId = appId
    this.audioData = []
    this.rawAudioData = []
    this.audioDataOffset = 0
    this.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()
  }
  // 连接websocket
  connectWebSocket() {
    this.setStatus('ttsing')
    return getWebsocketUrl().then(url => {
      let ttsWS
      if ('WebSocket' in window) {
        ttsWS = new WebSocket(url)
      } else if ('MozWebSocket' in window) {
        ttsWS = new MozWebSocket(url)
      } else {
        alert('浏览器不支持WebSocket')
        return
      }
      this.ttsWS = ttsWS
      ttsWS.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',
        // sfl= 1,
        auf: 'audio/L16;rate=16000',
        vcn: this.voiceName,
        speed: this.speed,
        volume: this.voice,
        pitch: this.pitch,
        bgs: 0,
        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.byteLength
        for (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)
    
    if (jsonData.code === 0 && jsonData.data.status === 2) {
      this.ttsWS.close()
    }
  }
  // 重置音频数据
  resetAudio() {
    this.audioStop()
    this.setStatus('init')
    this.audioDataOffset = 0
    this.audioData = []
    this.rawAudioData = []
    this.ttsWS && this.ttsWS.close()
    clearTimeout(this.playTimeout)
  }
  // 音频初始化
  audioInit() {
    let AudioContext = window.AudioContext || window.webkitAudioContext
    if (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.length
    let 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 = audioBuffer
    bufferSource.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 = 0
    if (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()
  }
}
export default TTSRecorder
// ======================开始调用=============================
// var vConsole = new VConsole()
// let ttsRecorder = new TTSRecorder()
// ttsRecorder.onWillStatusChange = function(oldStatus, status) {
//   // 可以在这里进行页面中一些交互逻辑处理:按钮交互等
//   // 按钮中的文字
//   let btnState = {
//     init: '立即合成',
//     ttsing: '正在合成',
//     play: '停止播放',
//     endPlay: '重新播放',
//     errorTTS: '合成失败',
//   }
//   $('.audio-ctrl-btn')
//     .removeClass(oldStatus)
//     .addClass(status)
//     .text(btnState[status])
// }


// $('.audio-ctrl-btn').click(function() {
//   if (['init', 'endPlay', 'errorTTS'].indexOf(ttsRecorder.status) > -1) {
//     ttsRecorder.start()
//   } else {
//     ttsRecorder.stop()
//   }
// })


// $('#input_text').change(function(){
//   ttsRecorder.setParams({
//     text: this.value
//   })
// })

 如果导入transcode.worker.js文件报错问题类似于如下:

 

 那是因为js模块化后在vue里面导入不能直接使用new Worker(),所以audio.js里面new Worker()会报错,解决办法请到我的另一篇文章:

https://blog.csdn.net/qq_45963071/article/details/119838275?spm=1001.2014.3001.5501

二. 语音合成使用方法:

<template>
  <div class="contert">
    <button  @click="play">开始合成</button>
    <button @click="pause">停止播放</button>
  </div>
</template>
import TtsRecorder from "@/assets/js/speechSynthesis/audio";
const ttsRecorder = new TtsRecorder();
export default {
    data() {
    return {
      text: "你好,世界",
    };
  },
    methods: {
        play(){
           //要合成的文本
          ttsRecorder.setParams({
          // 文本内容
          text: this.text,
          // 角色
          //  voiceName: '',
          // 语速
          speed: 50,
          // 音量
          voice: 50,
          });
          ttsRecorder.start();
       },
        pause(){
          ttsRecorder.stop();
       }
    }
}

### 关于 TTs UniApp 的开发文档教程常见问题及解决方案 #### 一、UniApp 中集成 TTS 功能的方法 对于希望在 UniApp 应用程序中实现文字转语音(TTS)功能的开发者来说,通常有两种主要途径来达成这一目标: 1. **利用原生插件**:通过调用 Android 和 iOS 平台各自的 TTS API 来间接支持此特性。例如,在 Android 上可以通过 `TextToSpeech` 类来进行操作[^1]。 2. **第三方 SDK 集成**:引入像科大讯飞这样的专业服务商所提供的 SDK ,以获得更高质量的声音合成服务以及更多定制化选项[^3]。 为了确保最佳兼容性和性能表现,推荐优先考虑使用官方提供的工具和服务;而对于特定业务场景下的高级需求,则可以选择合适的第三方库作为补充。 #### 二、具体实施步骤概述 当决定采用上述任一种方式时,以下是通用的操作指南摘要: - 对于基于原生组件的方式: - 修改应用清单文件(`AndroidManifest.xml`)并加入必要的权限声明; - 编写 JavaScriptVue.js 组件代码片段用于初始化 TTS 引擎实例,并定义发音方法。 ```javascript // 初始化TTS引擎 (仅适用于Android) if (plus.os.name === 'Android') { const tts = plus.android.importClass('android.speech.tts.TextToSpeech'); } ``` - 如果选用的是第三方SDK的话, - 下载对应本的 SDK 文件包并将它们添加至项目依赖项列表内; - 参考供应商给出的具体说明完成环境配置工作,比如设置 AppKey 等参数信息; - 调试期间注意查看日志输出以便及时发现潜在错误源码位置。 #### 三、可能遇到的问题及其对策 在此类项目的实际推进过程中,可能会碰到如下挑战: - 当尝试绑定 TTS 引擎失败时(即出现 "not bound to TTS engine" 错误),应确认是否已在应用程序的 manifest 文件中正确定义了对 TTS_SERVICE 的访问请求。 - 若是遇到了与 Puppeteer 相关的技术难题,尽管这似乎偏离主题较远但仍值得提及——查阅最新 Puppeteer 文档中的变更记录可以帮助理解某些行为改变背后的原因,从而找到合理的应对措施[^2]。 - 接入复杂度较高的语音评测模块前需谨慎评估成本效益比率,因为这类任务往往伴随着较多不确定因素和技术风险。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JOJORiny

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值