H5语音合成功能(Web Speech API+科大讯飞)

H5语音合成效果图:

方案一:Web Speech API:

<template>
  <div class="contert">
    <h2 style="margin: 40px 0 20px 0">方案一:Web Speech API</h2>
    <button @click="speak">开始合成</button>
    <button class="stop" @click="pause">停止播放</button>
    <div class="result">
      <p>识别文字:</p>
      <div class="interim">{{ text }}</div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from "vue";

const text = ref(
  "岱宗夫如何,齐鲁青未了。造化钟神秀,阴阳割昏晓。汤胸生层云,决眦入归鸟,会当凌绝顶,一览众山小。"
);
const voices = ref([]);
const selectedVoice = ref(null);
const isSpeaking = ref(false);

// 语音参数
const rate = ref(1);
const pitch = ref(1);
const volume = ref(1);

let synthesis = null;
let utterance = null;

// 初始化语音合成
onMounted(() => {
  if ("speechSynthesis" in window) {
    synthesis = window.speechSynthesis;

    // 异步获取语音列表
    const loadVoices = () => {
      voices.value = synthesis.getVoices();
      // 默认选择第一个中文语音
      selectedVoice.value = voices.value.find(
        (voice) => voice.lang.startsWith("zh") || voice.lang.startsWith("cmn")
      );
    };

    synthesis.onvoiceschanged = loadVoices;
    loadVoices();
  } else {
    alert("您的浏览器不支持语音合成功能");
  }
});

// 创建语音实例
const createUtterance = () => {
  utterance = new SpeechSynthesisUtterance(text.value);
  utterance.voice = selectedVoice.value;
  utterance.rate = rate.value;
  utterance.pitch = pitch.value;
  utterance.volume = volume.value;

  // 事件监听
  utterance.onstart = () => (isSpeaking.value = true);
  utterance.onend = utterance.onerror = () => (isSpeaking.value = false);
};

// 控制方法
const speak = () => {
  if (!synthesis) return;

  synthesis.cancel(); // 停止当前播放
  createUtterance();
  synthesis.speak(utterance);
};

const pause = () => synthesis.pause();
</script>

<style scoped>
.contert {
  padding: 0 20px;
}
button {
  padding: 8px 15px;
  background: linear-gradient(135deg, #6253e1, #04befe);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.3s;
}

button:hover {
  background: #66b1ff;
}

button.stop {
  background: #f56c6c;
  margin-left: 20px;
}
.result {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
}

.interim {
  color: #666;
  margin-top: 10px;
  min-height: 20px;
}
</style>

方案二:科大讯飞

<template>
  <div class="contert">
    <h2 style="margin: 80px 0 20px 0">方案二:科大讯飞</h2>
    <button @click="play">开始合成</button>
    <button class="stop" @click="pause">停止播放</button>
    <div class="result">
      <p>识别文字:</p>
      <div class="interim">{{ text }}</div>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import TtsRecorder from "../utils/tts_xunfei/audio";

const text = ref(
  "孤鸿海上来,池潢不敢顾。侧见双翠鸟,巢在三珠树。矫矫珍木巅,得无金丸惧。美服患人指,高明逼神恶。今我游冥冥,弋者何所慕。"
);
const ttsRecorder = new TtsRecorder();

const play = () => {
  ttsRecorder.setParams({
    text: text.value,
    speed: 50,
    voice: 50,
  });
  ttsRecorder.start();
};

const pause = () => {
  ttsRecorder.stop();
};
</script>

<style scoped>
.contert {
  padding: 0 20px;
}
button {
  padding: 8px 15px;
  background: linear-gradient(135deg, #6253e1, #04befe);
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.3s;
}

button:hover {
  background: #66b1ff;
}

button.stop {
  background: #f56c6c;
  margin-left: 20px;
}
.result {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #eee;
  border-radius: 4px;
}

.interim {
  color: #666;
  margin-top: 10px;
  min-height: 20px;
}
</style>

/utils/tts_xunfei/audio.js:



// 在线语音合成 WebAPI 接口调用示例 接口文档(必看):
//https://www.xfyun.cn/doc/tts/online_tts/API.html

// 1. websocket连接:判断浏览器是否兼容,获取websocket url并连接,这里为了方便本地生成websocket url
// 2. 连接websocket,向websocket发送数据,实时接收websocket返回数据
// 3. 处理websocket返回数据为浏览器可以播放的音频数据
// 4. 播放音频数据
// ps: 该示例用到了es6中的一些语法,建议在chrome下运行
import CryptoJS from "crypto-js";


const transWorker = new Worker(
  new URL("./transcode.worker.js", import.meta.url)
);

import { Base64 } from "./base64js.js";


//APPID,APISecret,APIKey在控制台-我的应用-语音合成(流式版)页面获取
const APPID = "f70c6429";
const API_SECRET = "YjBiOTU2YTU4YzEzZWQ3MWZlNGNkM2I3";
const API_KEY = "beba0ca58e09b5a244858b5d0dab8b8b";

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;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值