当前位置:   article > 正文

项目:对话系统。TTS和ASR流前端工程问题总结_vue asr+tts

vue asr+tts


前言

demo模拟线上客户通话,实现tts播报和asr语音转文本,最终将tts和asr播放的文本进行存储或下载。

相关知识:

        (1)网页媒体权限授权

        (2)ArrayBuffer、TypeBuffer 截断、拼接

        (3)音频流式文件播放方式

        (4)ASR、TTS

工程相关:vue2

一、网页授权媒体

这个之前有写过

  1. // 方法就一个
  2. openMedia() {
  3. const that = this;
  4. const gotMediaStream = () => {
  5. that.getMedia = true;
  6. //这里是开始websocket的方法
  7. that.useWebSocket();
  8. };
  9. const handleError = (err) => {
  10. // console.log("navigator catch error", err);
  11. that.$message.error("未获录音权限");
  12. };
  13. if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
  14. // console.log("getusermedia is not supported!");
  15. that.$message.error("无录音权限");
  16. } else {
  17. const constrants = {
  18. audio: {
  19. volume: { min: 0.0, max: 1.0 },
  20. noiseSuppression: false,
  21. echoCancellation: false,
  22. },
  23. };
  24. navigator.mediaDevices
  25. .getUserMedia(constrants)
  26. .then(gotMediaStream)
  27. .catch(handleError);
  28. }
  29. },
  30. // 初始化的websocket
  31. useWebSocket() {
  32. const that = this;
  33. asrWs = new WebSocket(process.env.VUE_APP_WS_URL);
  34. asrWs.binaryType = "arraybuffer"; // 传输的是 ArrayBuffer 类型的数据
  35. asrWs.onopen = function () {
  36. if (asrWs.readyState === 1) {
  37. // 开启体验和心跳 但是未开始录音
  38. // that.experienceStatus = true;
  39. that.heartStart();
  40. }
  41. };
  42. asrWs.onmessage = function (msg) {
  43. //这里就是相关的业务逻辑了
  44. that.heartStart();
  45. };
  46. asrWs.onclose = function (err) {
  47. console.log(err);
  48. };
  49. asrWs.onerror = function (err) {
  50. that.heartStart();
  51. };
  52. },

 修改了一些配置,这个配置解决录音音量较小的问题。

  1. navigator.mediaDevices
  2. .getUserMedia({audio:true })
  3. .then(gotMediaStream)
  4. .catch(handleError);

二、ArrayBuffer、TypeBuffer 截断、拼接

首先,这个 ArrayBuffer 类型化数组,类型化数组是JavaScript操作二进制数据的一个接口。最初为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式的背景下诞生的。

ArrayBuffer

  1. var bf = new ArrayBuffer(40); // 生成了字节长度为40的内存区域
  2. //通过提供的 byteLength 属性返回分配字节的长度
  3. console.log(bf.byteLength); // 40
  4. /*
  5. 值得注意的是如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。
  6. */

ArrayBuffer对象有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。 

  1. const bf = new ArrayBuffer(40);
  2. const newBf = bf.slice(0, 10); //0 - 9 不包括 10

上面代码拷贝buffer对象的前10个字节,生成一个新的ArrayBuffer对象。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。
slice方法接受两个参数,第一个参数表示拷贝开始的字节序号,第二个参数表示拷贝截止的字节序号。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。
除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。

TypeBuffer  

Int8Array:8位有符号整数,长度1个字节。
Uint8Array:8位无符号整数,长度1个字节。
Int16Array:16位有符号整数,长度2个字节。
Uint16Array:16位无符号整数,长度2个字节。
Int32Array:32位有符号整数,长度4个字节。
Uint32Array:32位无符号整数,长度4个字节。
Float32Array:32位浮点数,长度4个字节。
Float64Array:64位浮点数,长度8个字节。
 

下面这个文本有相关方法 

JavaScript 之 ArrayBuffer_短暂又灿烂的的博客-CSDN博客

我的需求是将多个ArrayBuffer完成拼接

二、ASR/TTS是什么?

ASR:语音转文本,将语音转成对应的文本,然后做其他处理。

有http方式和websocket方式。

http方式

传送还是file,file属于blob 使用FormData

  1. export function updataBlob(data) {
  2. let formData = new FormData();
  3. formData.append("file", data);
  4. formData.append("chatId", store.getters.chatId);
  5. return request({
  6. url: "model/audio/upload",
  7. method: "post",
  8. data: formData,
  9. headers: {
  10. "Content-Type": "multipart/form-data",
  11. },
  12. });
  13. }

 这种方式需要考虑是否为PCM和还是WAV格式的问题,后端解析需不需要加头

websocket这种方式

这种方式实时传送音频,对音频进行了切片。之前文章中也有代码。
https://mp.csdn.net/mp_blog/creation/editor/124618257

TTS是什么

tts是将文本转成语音。
涉及语音就涉及播放。

(1)如果是一次性返回pcm格式文件,需要播放就需要加上头,wav格式。播放TTS返回的文件

添加依赖

    "wav-headers": "^1.0.1"

 var getFileHeaders = require("wav-headers");

 添加wav请求头

  1. generateWav(buffer) {
  2. var options = {
  3. channels: 1,
  4. sampleRate: 16000,
  5. bitDepth: 16,
  6. dataLength: buffer.length,
  7. };
  8. var headersBuffer = getFileHeaders(options);
  9. var temp = new Uint8Array(buffer.byteLength + headersBuffer.byteLength);
  10. temp.set(new Uint8Array(headersBuffer), 0);
  11. temp.set(new Uint8Array(buffer), headersBuffer.byteLength);
  12. return temp;
  13. },

 上传文件 new Blob([this.PCMList], { type: "audio/wav" });  记住有个[ ]  本来this.PCMList 就是个TypeArray。

下面的生成wav文件下载。

  1. upAudioOne() {
  2. store.commit("audio/SET_AUDIO_CUT_SIZE", -1);
  3. this.PCMList = this.generateWav(this.PCMList);
  4. const blob = new Blob([this.PCMList], { type: "audio/wav" });
  5. updataBlob(blob);
  6. // let blobUrl = window.URL.createObjectURL(blob);
  7. // let link = document.createElement('a')
  8. // link.style.display = 'none'
  9. // link.href = blobUrl
  10. // link.download = 'test' + '.wav'
  11. // document.body.appendChild(link)
  12. // link.click()
  13. },

 这个是生成wav文件 直接下载或播放

  1. if (Object.prototype.toString.call(data) == "[object Object]") {
  2. this.$message({
  3. message: data.message,
  4. type: "warning",
  5. duration: 3 * 1000,
  6. });
  7. } else {
  8. if (type) {
  9. this.cunrentAudioUrl = URL.createObjectURL(data);
  10. this.$refs.audio.volume = 0.1;
  11. } else {
  12. const reader = new FileReader();
  13. reader.readAsDataURL(data);
  14. reader.onload = (e) => {
  15. const a = document.createElement("a");
  16. a.download = item.id + ".wav";
  17. a.href = e.target.result;
  18. document.body.appendChild(a);
  19. a.click();
  20. document.body.removeChild(a);
  21. };
  22. }
  23. }

 (2) 如果返回的是流式文件就需要将文件合成拼接在一起 记录TTS返回音频

  1. concatenate(resultConstructor, ...arrays) {
  2. let totalLength = 0;
  3. let startIndx = 0;
  4. if (!arrays[0].length) {
  5. this.PCMListAllIndex = [];
  6. } else {
  7. startIndx = arrays[0].length;
  8. }
  9. for (let arr of arrays) {
  10. totalLength += arr.length;
  11. }
  12. let result = new resultConstructor(totalLength);
  13. let offset = 0;
  14. for (let arr of arrays) {
  15. result.set(arr, offset);
  16. offset += arr.length;
  17. }
  18. this.PCMListAllIndex.push({ start: startIndx, end: totalLength });
  19. return result;
  20. },

 使用方法合成

     var dataAudio = new Uint8Array(msg.data);

          if (that.ttsSart) {

            that.PCMList = that.concatenate(

              Uint8Array,

              that.PCMList,

              dataAudio

            );

  }

 TTS流式播放

返回是ArrayBuffer ,这个写了看了一个第三方的做了修改

  1. var bufferSource = null;
  2. class PCMPlayer {
  3. constructor(option) {
  4. this.init(option);
  5. }
  6. // 初始化
  7. init(option) {
  8. // 默认参数
  9. const defaultOption = {
  10. inputCodec: "Int16", // 传入的数据是采用多少位编码,默认16
  11. channels: 1, // 声道数
  12. sampleRate: 8000, // 采样率 单位Hz
  13. flushTime: 3000, // 缓存时间 单位 ms
  14. };
  15. this.option = Object.assign({}, defaultOption, option); // 实例最终配置参数
  16. this.samples = new Float32Array(); // 样本存放区域
  17. this.audioCtx = null;
  18. this.convertValue = this.getConvertValue();
  19. // 获取类型
  20. this.typedArray = this.getTypedArray();
  21. // 初始下音频上线文本
  22. this.initAudioContext();
  23. // 需要播放资源统计
  24. this.sourceCount = 0;
  25. // pcm二进制文件播放次数
  26. this.bufferSourceEndCount = 0;
  27. // 一次文本二进制文件全部返回会 与tts ws中的 speech_end 相关 设置成false
  28. this.bufferFeed = false;
  29. }
  30. // 设置 tts 传入状态, true: 传送中, false :未开始|传送结束
  31. setBufferFeedState(type = false) {
  32. this.bufferFeed = type;
  33. }
  34. // 创建浮点型数据数组
  35. // 初始某人值
  36. sourceLenInit() {
  37. this.samples = new Float32Array();
  38. // console.log("初始化=》被调用");
  39. this.sourceCount = 0;
  40. this.setBufferFeedState(true);
  41. this.bufferSourceEndCount = 0;
  42. }
  43. // 播放暂停 初始化参数值
  44. ttsClose() {
  45. this.sourceCount = 0;
  46. this.setBufferFeedState();
  47. this.bufferSourceEndCount = 0;
  48. if (bufferSource) {
  49. bufferSource.stop(0); //立即停止
  50. }
  51. this.destroy();
  52. }
  53. // 获取不同TypeArray类型的转化
  54. getConvertValue() {
  55. const inputCodecs = {
  56. Int8: 128,
  57. Int16: 32768,
  58. Int32: 2147483648,
  59. Float32: 1,
  60. };
  61. if (!inputCodecs[this.option.inputCodec])
  62. throw new Error(
  63. "wrong codec.please input one of these codecs:Int8,Int16,Int32,Float32"
  64. );
  65. return inputCodecs[this.option.inputCodec];
  66. }
  67. //获取TypeArray
  68. getTypedArray() {
  69. // 根据传入的目标编码位数
  70. // 选定前端的所需要的保存的二进制数据格式
  71. // 完整TypedArray请看文档
  72. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
  73. const typedArrays = {
  74. Int8: Int8Array,
  75. Int16: Int16Array,
  76. Int32: Int32Array,
  77. Float32: Float32Array,
  78. };
  79. if (!typedArrays[this.option.inputCodec])
  80. throw new Error(
  81. "wrong codec.please input one of these codecs:Int8,Int16,Int32,Float32"
  82. );
  83. return typedArrays[this.option.inputCodec];
  84. }
  85. //初始化音频上下文
  86. initAudioContext() {
  87. this.sourceCount = 0;
  88. this.bufferSourceEndCount = 0;
  89. this.setBufferFeedState();
  90. // 初始化音频上下文的东西
  91. this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  92. // 控制音量的 GainNode
  93. // https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createGain
  94. //创建音量控制节点
  95. this.gainNode = this.audioCtx.createGain();
  96. this.gainNode.gain.value = 0.1;
  97. this.gainNode.connect(this.audioCtx.destination);
  98. this.startTime = this.audioCtx.currentTime;
  99. }
  100. // 检测 是否为TypeArray
  101. static isTypedArray(data) {
  102. // 检测输入的数据是否为 TypedArray 类型或 ArrayBuffer 类型
  103. return (
  104. (data.byteLength &&
  105. data.buffer &&
  106. data.buffer.constructor == ArrayBuffer) ||
  107. data.constructor == ArrayBuffer
  108. );
  109. }
  110. // 是否为 ArrayBuffer 或者 TypedArray
  111. isSupported(data) {
  112. // 数据类型是否支持
  113. // 目前支持 ArrayBuffer 或者 TypedArray
  114. if (!PCMPlayer.isTypedArray(data))
  115. throw new Error("请传入ArrayBuffer或者任意TypedArray");
  116. return true;
  117. }
  118. // pcm二进制文件输入(一句话多个二进制文件输入)
  119. feed(data) {
  120. if (!this.audioCtx) {
  121. return;
  122. }
  123. this.isSupported(data);
  124. // 获取格式化后的buffer
  125. data = this.getFormatedValue(data);
  126. // 开始拷贝buffer数据
  127. // 新建一个Float32Array的空间
  128. const tmp = new Float32Array(this.samples.length + data.length);
  129. // console.log(data, this.samples, this.samples.length)
  130. // 复制当前的实例的buffer值(历史buff)
  131. // 从头(0)开始复制
  132. tmp.set(this.samples, 0);
  133. // 复制传入的新数据
  134. // 从历史buff位置开始
  135. tmp.set(data, this.samples.length);
  136. // 将新的完整buff数据赋值给samples
  137. // interval定时器也会从samples里面播放数据
  138. this.samples = tmp;
  139. // 播放和回调的相关操作
  140. this.flush();
  141. }
  142. // 获取 对应类型数据的方法
  143. getFormatedValue(data) {
  144. if (data.constructor == ArrayBuffer) {
  145. data = new this.typedArray(data);
  146. } else {
  147. data = new this.typedArray(data.buffer);
  148. }
  149. let float32 = new Float32Array(data.length);
  150. for (let i = 0; i < data.length; i++) {
  151. // buffer 缓冲区的数据,需要是IEEE75432位的线性PCM,范围从-1+1
  152. // 所以对数据进行除法
  153. // 除以对应的位数范围,得到-1+1的数据
  154. // float32[i] = data[i] / 0x8000;
  155. float32[i] = data[i] / this.convertValue;
  156. }
  157. return float32;
  158. }
  159. // 音量控制
  160. volume(volume) {
  161. this.gainNode.gain.value = volume;
  162. }
  163. // 关闭上线文
  164. destroy() {
  165. this.samples = null;
  166. this.audioCtx.close();
  167. this.audioCtx = null;
  168. }
  169. // 每个音频文件
  170. flush() {
  171. const self = this;
  172. if (this.samples.length) {
  173. this.sourceCount += 1;
  174. }
  175. if (!this.samples.length) {
  176. return;
  177. }
  178. if (!this.audioCtx) {
  179. return;
  180. }
  181. bufferSource = this.audioCtx.createBufferSource();
  182. // 两个方法的回调
  183. if (
  184. typeof this.option.onended === "function" &&
  185. typeof this.option.allPlayed === "function"
  186. ) {
  187. bufferSource.onended = function (event) {
  188. self.bufferSourceEndCount += 1;
  189. self.option.allPlayed(
  190. self,
  191. self.bufferSourceEndCount == self.sourceCount && !self.bufferFeed
  192. );
  193. self.option.onended(this, event);
  194. };
  195. }
  196. const length = this.samples.length / this.option.channels;
  197. const audioBuffer = this.audioCtx.createBuffer(
  198. // 声道数
  199. this.option.channels,
  200. // 长度
  201. length,
  202. // 采样率
  203. this.option.sampleRate
  204. );
  205. // 多声道处理
  206. for (let channel = 0; channel < this.option.channels; channel++) {
  207. const audioData = audioBuffer.getChannelData(channel);
  208. let offset = channel;
  209. let decrement = 50;
  210. for (let i = 0; i < length; i++) {
  211. audioData[i] = this.samples[offset];
  212. /* fadein */
  213. if (i < 50) {
  214. audioData[i] = (audioData[i] * i) / 50;
  215. }
  216. /* fadeout*/
  217. if (i >= length - 51) {
  218. audioData[i] = (audioData[i] * decrement--) / 50;
  219. }
  220. offset += this.option.channels;
  221. }
  222. }
  223. if (this.startTime < this.audioCtx.currentTime) {
  224. this.startTime = this.audioCtx.currentTime;
  225. }
  226. // console.log(
  227. // "start vs current " +
  228. // this.startTime +
  229. // " vs " +
  230. // this.audioCtx.currentTime +
  231. // " duration: " +
  232. // audioBuffer.duration
  233. // );
  234. bufferSource.buffer = audioBuffer;
  235. bufferSource.connect(this.gainNode);
  236. bufferSource.start(this.startTime);
  237. // 添加开始播放的回调函数
  238. if (this.sourceCount == 1) {
  239. if (typeof this.option.firstPlay === "function") {
  240. self.option.firstPlay(this, Date.now());
  241. }
  242. }
  243. this.startTime += audioBuffer.duration;
  244. this.samples = new Float32Array();
  245. }
  246. // 暂停
  247. async pause() {
  248. await this.audioCtx.suspend();
  249. }
  250. // 继续播放
  251. async continue() {
  252. await this.audioCtx.resume();
  253. }
  254. // 绑定音频上线文的的状态 - 未使用
  255. bindAudioContextEvent() {
  256. const self = this;
  257. if (typeof self.option.onstatechange === "function") {
  258. this.audioCtx.onstatechange = function (event) {
  259. self.option.onstatechange(this, event, self.audioCtx.state);
  260. };
  261. }
  262. }
  263. // 获取音频上下文的状态
  264. getSate() {
  265. const self = this;
  266. return self.audioCtx.state;
  267. }
  268. }
  269. export default PCMPlayer;

使用页面

typeBuffer 加个wav 头用于播放 和上产后台

npm install wav-headers --save

 

  1. var ws = null; // 实现WebSocket
  2. import PCMPlayer from "./pcm-player.js";
  3. var getFileHeaders = require("wav-headers");
  4. export default {
  5. data() {
  6. return {
  7. timeoutTTs: 6000, // 6s发一次心跳,比server端设置的连接时间稍微小一点,在接近断开的情况下以通信的方式去重置连接时间。
  8. ttsTimeoutObj: null, // ws定时器
  9. ttsSart: false,
  10. ttsPlayStatus: false,
  11. player: null,
  12. PCMList: new Uint8Array(0),
  13. };
  14. },
  15. created() {
  16. // console.log('获取摄像头和麦克风权限')
  17. },
  18. mounted() {
  19. //初始化 PCMPlayer
  20. this.play();
  21. },
  22. beforeDestroy() {
  23. this.closeServe();
  24. this.ttsStop();
  25. },
  26. methods: {
  27. // 设置wav 头
  28. generateWav(buffer) {
  29. var options = {
  30. channels: 1,
  31. sampleRate: 16000,
  32. bitDepth: 16,
  33. dataLength: buffer.length,
  34. };
  35. var headersBuffer = getFileHeaders(options);
  36. var temp = new Uint8Array(buffer.byteLength + headersBuffer.byteLength);
  37. temp.set(new Uint8Array(headersBuffer), 0);
  38. temp.set(new Uint8Array(buffer), headersBuffer.byteLength);
  39. return temp;
  40. },
  41. // 拼接
  42. concatenate(resultConstructor, ...arrays) {
  43. let totalLength = 0;
  44. for (let arr of arrays) {
  45. totalLength += arr.length;
  46. }
  47. let result = new resultConstructor(totalLength);
  48. let offset = 0;
  49. for (let arr of arrays) {
  50. result.set(arr, offset);
  51. offset += arr.length;
  52. }
  53. return result;
  54. },
  55. // 初始化 PCMPlayer
  56. play() {
  57. const that = this;
  58. if (this.player) return;
  59. this.player = new PCMPlayer({
  60. encoding: "16bitInt",
  61. channels: 1,
  62. sampleRate: 16000,
  63. // 所有cpm 播放结束
  64. allPlayed: function (r, data) {
  65. console.log("allPlayed", data);
  66. },
  67. // 每个pcm 播放结束
  68. onended: function (r) {
  69. },
  70. // 音频上线状态
  71. onstatechange: function (r) {
  72. },
  73. });
  74. if (!ws) this.useTTSSocket();
  75. },
  76. // 播放暂停 用于打断使用
  77. ttsPlayEnd() {
  78. this.player.ttsClose();
  79. this.ttsPlayStatus = false;
  80. },
  81. // 摧毁状态
  82. ttsStop() {
  83. this.player.destroy();
  84. this.ttsPlayStatus = false;
  85. this.player = null;
  86. },
  87. // 关闭tts服务
  88. closeServe() {
  89. if (ws) {
  90. ws.close(); // 离开路由之后断开websocket连接
  91. ws = null;
  92. }
  93. // 2、通过关闭计时器和倒计时关闭心跳监测
  94. this.heartTTSReset();
  95. },
  96. // 心跳初重置
  97. heartTTSReset: function () {
  98. clearInterval(this.ttsTimeoutObj);
  99. },
  100. // 心跳初始化
  101. heartTTSStart() {
  102. const that = this;
  103. this.ttsTimeoutObj && clearInterval(this.ttsTimeoutObj);
  104. this.ttsTimeoutObj = setInterval(function () {
  105. if (ws.readyState === 1) {
  106. // console.log('连接状态,发送消息保持连接' + new Date())
  107. ws.send(JSON.stringify({ signal: "heart_beat" }));
  108. that.heartTTSStart();
  109. } else {
  110. that.useTTSSocket();
  111. }
  112. }, that.timeoutTTs);
  113. },
  114. // 初始化的websocket
  115. useTTSSocket() {
  116. const that = this;
  117. ws = new WebSocket("ws:/xx.xx.xx.xx:8000");
  118. // 传输的是 ArrayBuffer 类型的数据
  119. ws.binaryType = "arraybuffer";
  120. ws.onopen = function () {
  121. if (ws.readyState === 1) {
  122. that.heartTTSStart();
  123. }
  124. };
  125. ws.onmessage = function (msg) {
  126. if (!that.isArrayBuffer(msg.data)) {
  127. const backMsg = JSON.parse(msg.data);
  128. if (backMsg.message === "speech_start") {
  129. that.PCMList = [];
  130. that.player && that.player.sourceLenInit();
  131. that.ttsSart = true;
  132. }
  133. if (backMsg.message === "timestamp") {
  134. if (that.ttsSart) {
  135. that.setTxtProgress(backMsg.syllable_times);
  136. }
  137. // console.log("播放中");
  138. }
  139. if (backMsg.message === "speech_end") {
  140. that.ttsSart = false;
  141. that.player.bufferFeed = true;
  142. that.player && that.player.setBufferFeedState(false);
  143. }
  144. } else {
  145. const dataAudio = new Uint8Array(msg.data);
  146. if (that.ttsSart) {
  147. that.ttsPlayStatus = true;
  148. that.PCMList = that.concatenate(
  149. Uint8Array,
  150. that.PCMList,
  151. dataAudio
  152. );
  153. that.player.feed(dataAudio);
  154. }
  155. }
  156. that.heartTTSStart();
  157. };
  158. ws.onerror = function (err) {
  159. that.heartTTSStart();
  160. };
  161. },
  162. // 判断返回是否为 ArrayBuffer
  163. isArrayBuffer(data) {
  164. const strType = Object.prototype.toString.call(data);
  165. return strType.includes("ArrayBuffer");
  166. },
  167. // 开始发送文本
  168. startTTS(text) {
  169. const that = this;
  170. this.player.initAudioContext();
  171. setTimeout(() => {
  172. ws.send(
  173. JSON.stringify({
  174. text: text,
  175. speaker: "xiaoqian",
  176. pitch: 70,
  177. volume: 80,
  178. })
  179. );
  180. }, 0);
  181. },
  182. },
  183. };

audioBuffer转wav格式的方式

npm install audiobuffer-to-wav --save

截断获取

  1. watch: {
  2. //播放暂停状态
  3. ttsPlayStatus: {
  4. handler(val) {
  5. if (val) {
  6. this.playLongTime = Date.now();
  7. } else {
  8. //获取播放时间
  9. this.playLongTime = Date.now() - this.playLongTime;
  10. //这里很重要 decodeAudioData 需要的arrayBuffer是需要 有头的wav MP3都是可以
  11. //上一个 调用实现方法中 有PCMList 获取方式
  12. this.PCMList = this.generateWav(this.PCMList);
  13. // 调用截断的方法
  14. this.audioSilce(this.PCMList, this.playLongTime / 1000);
  15. }
  16. },
  17. },
  18. },
  19. //音频截断
  20. audioSilce(arrBuffer, time) {
  21. //兼容写法
  22. // window.AudioContext =
  23. // window.AudioContext ||
  24. // window.webkitAudioContext ||
  25. // window.mozAudioContext ||
  26. // window.msAudioContext;
  27. //创建音频上下文
  28. var audioCtx = new window.AudioContext();
  29. // var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  30. //异步解析 ARRaybuffer
  31. audioCtx.decodeAudioData(arrBuffer.buffer, function (audioBuffer) {
  32. const channels = audioBuffer.numberOfChannels;
  33. // 声道数量和采样率
  34. // var channels = audioBuffer.numberOfChannels;
  35. const rate = audioBuffer.sampleRate;
  36. // 获取截断的音频
  37. const startOffset = 0;
  38. const endOffset = rate * time;
  39. // 对应的帧数
  40. const frameCount = endOffset - startOffset;
  41. // 创建同样采用率、同样声道数量,长度是前3秒的空的AudioBuffer
  42. const newAudioBuffer = new AudioContext().createBuffer(
  43. channels,
  44. endOffset - startOffset,
  45. rate
  46. );
  47. // 创建临时的Array存放复制的buffer数据
  48. const anotherArray = new Float32Array(frameCount);
  49. // 声道的数据的复制和写入
  50. const offset = 0;
  51. for (let channel = 0; channel < channels; channel++) {
  52. audioBuffer.copyFromChannel(anotherArray, channel, startOffset);
  53. newAudioBuffer.copyToChannel(anotherArray, channel, offset);
  54. }
  55. //播放的方法 方便验证
  56. // setTimeout(() => {
  57. // var source = audioCtx.createBufferSource();
  58. // source.buffer = newAudioBuffer;
  59. // source.connect(gainNode);
  60. // source.start(0);
  61. // source.onended = function (event) {
  62. // console.log("播放结束");
  63. // };
  64. // }, 1000);
  65. const wav = toWav(newAudioBuffer);
  66. const blob = new Blob([wav], { type: "audio/wav" });
  67. const url = URL.createObjectURL(blob);
  68. const a = document.createElement("a");
  69. a.download = "测试.wav";
  70. a.href = url;
  71. document.body.appendChild(a);
  72. a.click();
  73. document.body.removeChild(a);
  74. });
  75. },

 

引入js

import PCMPlayer from "./pcm-player.js";

 方法中调用

this.player.sourcePlayLen 和 playLen  是所有 流式的ArrayBuffer播放完成的判断。

原理:

    使用音频上下文及createBufferSource() 实时播放二进制流

    this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();

    bufferSource = this.audioCtx.createBufferSource();   见pcm-player.js源码

      

        每个ArrayBuffer都会播放,每次播放播放会调用 onended方法,记录 每次feed方法的调用的次数(就是sourcePlayLen)和个文件播放完成  onended 次数做个判断。相等就播放完成。

        firstPlay这个回调是 sourcePlayLen= 1的的回调,判断音频播放中状态的回调方法。这些和业务相关,自己可以补充回调方法。

  1. var dataAudio = new Uint8Array(msg.data);
  2. if (that.ttsSart) {
  3. that.PCMList = that.concatenate(
  4. Uint8Array,
  5. that.PCMList,
  6. dataAudio
  7. );
  8. that.txtProgreeArr.push(that.sorcePushConut);
  9. // that.ttsPlayStatus = true;
  10. that.player && that.player.feed(dataAudio);
  11. that.sorcePushConut += 1;
  12. }
  1. play() {
  2. const that = this;
  3. if (this.player) return;
  4. this.player = new PCMPlayer({
  5. encoding: "16bitInt",
  6. channels: 1,
  7. sampleRate: 16000,
  8. flushTime: 1000,
  9. firstPlay: function (r) {
  10. that.ttsPlayStatus = true;
  11. },
  12. onended: function (r) {
  13. that.playLen += 1;
  14. if (that.playLen === that.player.sourcePlayLen) {
  15. that.playLen = 0;
  16. that.player.sourceLenInit();
  17. // 正常结束
  18. that.ttsIsInterrupt = true;
  19. that.ttsPlayStatus = false;
  20. that.ttsOnend();
  21. if (that.chatSate == 4) {
  22. that.ttsStop();
  23. }
  24. }
  25. },
  26. })
  27. },

 主要的几个方法

创建:this.player = new PCMPlayer({})

暂停:this.player.ttsClose();

摧毁:this.player.destroy();

总结

 以上是在项目中,是使用到TTS、ASR不同协议对音频的上传、下载、播放、等功能,用得到请点赞谢谢。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/1010493
推荐阅读
相关标签
  

闽ICP备14008679号