Browse Source

讯飞语音sdk接入

刘洋 6 tháng trước cách đây
mục cha
commit
45e232c874

+ 1 - 1
.eslintrc.js

@@ -17,5 +17,5 @@ module.exports = {
     "vue/no-v-html": "off",
     "vue/multi-word-component-names": "off",
   },
-  ignorePatterns: ["public/"],
+  ignorePatterns: ["public/", "src/plugins/tts/"],
 };

+ 1 - 0
public/processor.worker.js

@@ -0,0 +1 @@
+!function(){"use strict";self.onmessage=function(a){var t=a.data,e=t.type,r=t.data,o=r;if("init"===e){var n=r.fromSampleRate,l=r.toSampleRate;return self.fromSampleRate=n,void(self.toSampleRate=l)}if("base64"===e&&(o=atob(r)),"string"==typeof o){for(var f=new Uint8Array(o.length+o.length%2),i=0;i<o.length;++i)f[i]=o.charCodeAt(i);o=new Int16Array(f.buffer)}var s=o;"Float32Array"!==e&&(o=function(a){for(var t=[],e=0;e<a.length;e++){var r=a[e]<0?a[e]/32768:a[e]/32767;t.push(r)}return new Float32Array(t)}(o)),o=function(a,t,e){var r=Math.round(a.length*(e/t)),o=new Float32Array(r),n=(a.length-1)/(r-1);o[0]=a[0];for(var l=1;l<r-1;l++){var f=l*n,i=Math.floor(f).toFixed(),s=Math.ceil(f).toFixed(),u=f-i;o[l]=a[i]+(a[s]-a[i])*u}return o[r-1]=a[a.length-1],o}(o,self.fromSampleRate,self.toSampleRate),self.postMessage({audioData:o,pcmAudioData:s})}}();

+ 100 - 0
src/components/vEditor/components/TtsDialog.vue

@@ -0,0 +1,100 @@
+<template>
+  <el-dialog
+    class="tts-dialog"
+    :visible.sync="modalIsShow"
+    title="语音合成"
+    top="10px"
+    width="600px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @opened="initData"
+    @closed="closed"
+  >
+    <el-input
+      type="textarea"
+      :rows="8"
+      v-model="text"
+      maxlength="2000"
+      show-word-limit
+    ></el-input>
+    <div class="btns">
+      <el-button
+        :type="status === 'PLAY' ? 'danger' : 'primary'"
+        @click="action"
+        :disabled="!text"
+        >{{ statusStr }}</el-button
+      >
+    </div>
+    <div slot="footer">
+      <el-button type="primary" :disabled="loading || !blob" @click="submit"
+        >保存音频</el-button
+      >
+      <el-button type="danger" plain @click="cancel">取消</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import TtsVoice from "@/plugins/tts";
+
+export default {
+  name: "TtsDialog",
+  props: {},
+  data() {
+    return {
+      loading: false,
+      modalIsShow: false,
+      ttsVoice: null,
+      text: ``,
+      status: "UNDEFINED",
+      statusStr: "立即合成",
+      blob: null,
+    };
+  },
+  created() {
+    this.ttsVoice = new TtsVoice({
+      appId: "5cc0340f",
+      apiSecret: "fb1702602169809fef3dc60a16d60493",
+      apiKey: "e5b67c040d614b7bfb9aa852731947eb",
+      cb: this.watchStatus,
+      blobDoneCb: this.getBlob,
+    });
+  },
+  beforeDestroy() {
+    this.ttsVoice.ttsWS?.close();
+  },
+  methods: {
+    action() {
+      this.ttsVoice?.action(this.text);
+    },
+    watchStatus(obj) {
+      this.status = obj.status;
+      this.statusStr = obj.statusStr;
+    },
+    initData() {},
+    closed() {
+      this.ttsVoice.ttsWS?.close();
+    },
+    cancel() {
+      this.ttsVoice.ttsWS?.close();
+      this.modalIsShow = false;
+    },
+    open() {
+      this.modalIsShow = true;
+    },
+    getBlob(blob) {
+      this.blob = blob;
+      console.log("done blob", blob);
+    },
+    async submit() {
+      this.$emit("confirm", this.blob);
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.btns {
+  padding-top: 10px;
+}
+</style>

+ 22 - 1
src/components/vEditor/components/VMenu.vue

@@ -94,6 +94,14 @@
         @mousedown="addFormula"
       />
     </div>
+    <div class="edit-menu-item">
+      <img
+        class="intLink"
+        title="文字合成语音"
+        src="../../img/text_audio.png"
+        @mousedown="openVoiceDialog"
+      />
+    </div>
 
     <!-- <span class="intLink" title="显示源码" @click="setDocMode('HTML')">
       &lt;/&gt;
@@ -112,6 +120,7 @@
       ref="SpecharDialog"
       @confirm="specharConfirm"
     ></spechar-dialog>
+    <TtsDialog ref="TtsDialog" @confirm="voiceConfirm"></TtsDialog>
   </div>
 </template>
 
@@ -122,16 +131,28 @@ import { imageHandle } from "./image";
 import { answerPointHandle } from "./answerPoint";
 import FormulaDialog from "./FormulaDialog";
 import SpecharDialog from "./SpecharDialog";
+import TtsDialog from "./TtsDialog.vue";
 
 export default {
   name: "VMenu",
-  components: { FormulaDialog, SpecharDialog },
+  components: { FormulaDialog, SpecharDialog, TtsDialog },
   data() {
     return {
       curEditFocusRange: null,
+      ttsVoice: null,
     };
   },
+
   methods: {
+    voiceConfirm(blob) {
+      const file = new File([blob], `${new Date().getTime()}.wav`, {
+        type: "audio/wav",
+      });
+      console.log("file", file);
+    },
+    openVoiceDialog() {
+      this.$refs.TtsDialog.open();
+    },
     setDocMode(type) {
       setDocMode(type, this.$parent.$refs.editor);
     },

+ 194 - 0
src/plugins/tts/audioPlayer.esm.js

@@ -0,0 +1,194 @@
+function t(t, a, i) {
+  for (var o = 0; o < i.length; o++) t.setUint8(a + o, i.charCodeAt(o));
+}
+function a(a, i, o) {
+  var e = (function (a, i, o, e, s) {
+    var u = a.reduce(function (t, a) {
+        return t + a.byteLength;
+      }, 0),
+      n = new ArrayBuffer(44 + u),
+      r = new DataView(n),
+      h = o,
+      d = 0;
+    return (
+      t(r, d, "RIFF"),
+      (d += 4),
+      r.setUint32(d, 36 + u, !0),
+      t(r, (d += 4), "WAVE"),
+      t(r, (d += 4), "fmt "),
+      (d += 4),
+      r.setUint32(d, 16, !0),
+      (d += 4),
+      r.setUint16(d, 1, !0),
+      (d += 2),
+      r.setUint16(d, h, !0),
+      (d += 2),
+      r.setUint32(d, i, !0),
+      (d += 4),
+      r.setUint32(d, h * i * (e / 8), !0),
+      (d += 4),
+      r.setUint16(d, h * (e / 8), !0),
+      (d += 2),
+      r.setUint16(d, e, !0),
+      t(r, (d += 2), "data"),
+      (d += 4),
+      r.setUint32(d, u, !0),
+      (d += 4),
+      a.forEach(function (t) {
+        for (var a = new DataView(t.buffer), i = 0; i < t.byteLength; )
+          r.setUint8(d, a.getUint8(i)), d++, i++;
+      }),
+      r
+    );
+  })(a, i || 16e3, 1, o || 16);
+  return new Blob([e], { type: "audio/wav" });
+}
+var i = (function () {
+  function t(t) {
+    (this.toSampleRate = 22050),
+      (this.resumePlayDuration = 1e3),
+      (this.fromSampleRate = 16e3),
+      (this.isAudioDataEnded = !1),
+      (this.status = "uninit"),
+      (this.audioDatas = []),
+      (this.pcmAudioDatas = []),
+      (this.audioDataOffset = 0),
+      // (this.processor = new Worker("".concat(t, "/processor.worker.js")));
+      (this.processor = new Worker("/admin/processor.worker.js"));
+  }
+  return (
+    (t.prototype.postMessage = function (t) {
+      var a = t.type,
+        i = t.data,
+        o = t.isLastData;
+      "uninit" !== this.status &&
+        (this.processor.postMessage({ type: a, data: i }),
+        (this.isAudioDataEnded = o));
+    }),
+    (t.prototype.playAudio = function () {
+      var t = this;
+      if ((clearTimeout(this.playAudioTime), this.audioContext)) {
+        for (
+          var a = 0, i = this.audioDataOffset;
+          i < this.audioDatas.length;
+          i++
+        )
+          a += this.audioDatas[i].length;
+        if (!a)
+          return void (
+            "play" === this.status &&
+            (this.isAudioDataEnded || this.resumePlayDuration <= 0
+              ? this.stop()
+              : (this.playAudioTime = setTimeout(function () {
+                  t.playAudio();
+                }, this.resumePlayDuration)))
+          );
+        for (
+          var o = this.audioContext.createBuffer(1, a, this.toSampleRate),
+            e = o.getChannelData(0),
+            s = this.audioDatas[this.audioDataOffset],
+            u = 0;
+          s;
+
+        ) {
+          if (((this.audioDataOffset += 1), o.copyToChannel))
+            o.copyToChannel(s, 0, u), (u += s.length);
+          else for (i = 0; i < s.length; i++) e[i] = s[i];
+          s = this.audioDatas[this.audioDataOffset];
+        }
+        var n = this.audioContext.createBufferSource();
+        (this.bufferSource = n),
+          (n.buffer = o),
+          n.connect(this.audioContext.destination),
+          n.start(),
+          (n.onended = function (a) {
+            "play" === t.status &&
+              (t.audioDatas.length
+                ? t.playAudio()
+                : t.isAudioDataEnded || t.resumePlayDuration <= 0
+                ? t.stop()
+                : (t.playAudioTime = setTimeout(function () {
+                    t.playAudio();
+                  }, t.resumePlayDuration)));
+          });
+      }
+    }),
+    (t.prototype.reset = function () {
+      var t;
+      (this.processor.onmessage = null),
+        (this.audioDataOffset = 0),
+        (this.audioDatas = []),
+        (this.pcmAudioDatas = []),
+        (this.status = "uninit"),
+        (this.isAudioDataEnded = !1),
+        clearTimeout(this.playAudioTime);
+      try {
+        null === (t = this.bufferSource) || void 0 === t || t.stop();
+      } catch (t) {
+        console.log(t);
+      }
+    }),
+    (t.prototype.start = function (t) {
+      var a = this,
+        i = void 0 === t ? {} : t,
+        o = i.autoPlay,
+        e = void 0 === o || o,
+        s = i.sampleRate,
+        u = void 0 === s ? 16e3 : s,
+        n = i.resumePlayDuration,
+        r = void 0 === n ? 1e3 : n;
+      this.reset(), (this.status = "init"), (this.resumePlayDuration = r);
+      var h = u,
+        d = Math.max(h, 22050);
+      (d = Math.min(d, 96e3)),
+        (this.fromSampleRate = h),
+        (this.toSampleRate = d),
+        this.processor.postMessage({
+          type: "init",
+          data: { fromSampleRate: h, toSampleRate: d },
+        }),
+        (this.processor.onmessage = function (t) {
+          var i = t.data,
+            o = i.audioData,
+            s = i.pcmAudioData;
+          a.audioDatas.push(o),
+            a.pcmAudioDatas.push(s),
+            1 === a.audioDatas.length && e && "init" === a.status && a.play();
+        });
+    }),
+    (t.prototype.play = function () {
+      var t;
+      this.audioContext ||
+        ((this.audioContext = new (window.AudioContext ||
+          window.webkitAudioContext)()),
+        this.audioContext.resume()),
+        this.audioContext &&
+          ((this.status = "play"),
+          null === (t = this.onPlay) || void 0 === t || t.call(this),
+          this.playAudio());
+    }),
+    (t.prototype.stop = function () {
+      var t, a;
+      (this.audioDataOffset = 0),
+        (this.status = "stop"),
+        clearTimeout(this.playAudioTime);
+      try {
+        null === (t = this.bufferSource) || void 0 === t || t.stop(),
+          null === (a = this.onStop) ||
+            void 0 === a ||
+            a.call(this, this.audioDatas);
+      } catch (t) {
+        console.log(t);
+      }
+    }),
+    (t.prototype.getAudioDataBlob = function (t) {
+      var i, o;
+      if (null === (i = this.pcmAudioDatas) || void 0 === i ? void 0 : i.length)
+        return "wav" === t
+          ? a(this.pcmAudioDatas, this.fromSampleRate, 16)
+          : ((o = this.pcmAudioDatas), new Blob(o, { type: "audio/pcm" }));
+    }),
+    t
+  );
+})();
+export { i as default };

+ 436 - 0
src/plugins/tts/base64.js

@@ -0,0 +1,436 @@
+//
+// THIS FILE IS AUTOMATICALLY GENERATED! DO NOT EDIT BY HAND!
+//
+(function (global, factory) {
+  typeof exports === "object" && typeof module !== "undefined"
+    ? (module.exports = factory())
+    : typeof define === "function" && define.amd
+    ? define(factory)
+    : // cf. https://github.com/dankogai/js-base64/issues/119
+      (function () {
+        // existing version for noConflict()
+        var _Base64 = global.Base64;
+        var gBase64 = factory();
+        gBase64.noConflict = function () {
+          global.Base64 = _Base64;
+          return gBase64;
+        };
+        if (global.Meteor) {
+          // Meteor.js
+          Base64 = gBase64;
+        }
+        global.Base64 = gBase64;
+      })();
+})(
+  typeof self !== "undefined"
+    ? self
+    : typeof window !== "undefined"
+    ? window
+    : typeof global !== "undefined"
+    ? global
+    : this,
+  function () {
+    "use strict";
+    /**
+     *  base64.ts
+     *
+     *  Licensed under the BSD 3-Clause License.
+     *    http://opensource.org/licenses/BSD-3-Clause
+     *
+     *  References:
+     *    http://en.wikipedia.org/wiki/Base64
+     *
+     * @author Dan Kogai (https://github.com/dankogai)
+     */
+    var version = "3.7.5";
+    /**
+     * @deprecated use lowercase `version`.
+     */
+    var VERSION = version;
+    var _hasatob = typeof atob === "function";
+    var _hasbtoa = typeof btoa === "function";
+    var _hasBuffer = typeof Buffer === "function";
+    var _TD = typeof TextDecoder === "function" ? new TextDecoder() : undefined;
+    var _TE = typeof TextEncoder === "function" ? new TextEncoder() : undefined;
+    var b64ch =
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+    var b64chs = Array.prototype.slice.call(b64ch);
+    var b64tab = (function (a) {
+      var tab = {};
+      a.forEach(function (c, i) {
+        return (tab[c] = i);
+      });
+      return tab;
+    })(b64chs);
+    var b64re =
+      /^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/;
+    var _fromCC = String.fromCharCode.bind(String);
+    var _U8Afrom =
+      typeof Uint8Array.from === "function"
+        ? Uint8Array.from.bind(Uint8Array)
+        : function (it) {
+            return new Uint8Array(Array.prototype.slice.call(it, 0));
+          };
+    var _mkUriSafe = function (src) {
+      return src.replace(/=/g, "").replace(/[+\/]/g, function (m0) {
+        return m0 == "+" ? "-" : "_";
+      });
+    };
+    var _tidyB64 = function (s) {
+      return s.replace(/[^A-Za-z0-9\+\/]/g, "");
+    };
+    /**
+     * polyfill version of `btoa`
+     */
+    var btoaPolyfill = function (bin) {
+      // console.log('polyfilled');
+      var u32,
+        c0,
+        c1,
+        c2,
+        asc = "";
+      var pad = bin.length % 3;
+      for (var i = 0; i < bin.length; ) {
+        if (
+          (c0 = bin.charCodeAt(i++)) > 255 ||
+          (c1 = bin.charCodeAt(i++)) > 255 ||
+          (c2 = bin.charCodeAt(i++)) > 255
+        )
+          throw new TypeError("invalid character found");
+        u32 = (c0 << 16) | (c1 << 8) | c2;
+        asc +=
+          b64chs[(u32 >> 18) & 63] +
+          b64chs[(u32 >> 12) & 63] +
+          b64chs[(u32 >> 6) & 63] +
+          b64chs[u32 & 63];
+      }
+      return pad ? asc.slice(0, pad - 3) + "===".substring(pad) : asc;
+    };
+    /**
+     * does what `window.btoa` of web browsers do.
+     * @param {String} bin binary string
+     * @returns {string} Base64-encoded string
+     */
+    var _btoa = _hasbtoa
+      ? function (bin) {
+          return btoa(bin);
+        }
+      : _hasBuffer
+      ? function (bin) {
+          return Buffer.from(bin, "binary").toString("base64");
+        }
+      : btoaPolyfill;
+    var _fromUint8Array = _hasBuffer
+      ? function (u8a) {
+          return Buffer.from(u8a).toString("base64");
+        }
+      : function (u8a) {
+          // cf. https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string/12713326#12713326
+          var maxargs = 0x1000;
+          var strs = [];
+          for (var i = 0, l = u8a.length; i < l; i += maxargs) {
+            strs.push(_fromCC.apply(null, u8a.subarray(i, i + maxargs)));
+          }
+          return _btoa(strs.join(""));
+        };
+    /**
+     * converts a Uint8Array to a Base64 string.
+     * @param {boolean} [urlsafe] URL-and-filename-safe a la RFC4648 §5
+     * @returns {string} Base64 string
+     */
+    var fromUint8Array = function (u8a, urlsafe) {
+      if (urlsafe === void 0) {
+        urlsafe = false;
+      }
+      return urlsafe ? _mkUriSafe(_fromUint8Array(u8a)) : _fromUint8Array(u8a);
+    };
+    // This trick is found broken https://github.com/dankogai/js-base64/issues/130
+    // const utob = (src: string) => unescape(encodeURIComponent(src));
+    // reverting good old fationed regexp
+    var cb_utob = function (c) {
+      if (c.length < 2) {
+        var cc = c.charCodeAt(0);
+        return cc < 0x80
+          ? c
+          : cc < 0x800
+          ? _fromCC(0xc0 | (cc >>> 6)) + _fromCC(0x80 | (cc & 0x3f))
+          : _fromCC(0xe0 | ((cc >>> 12) & 0x0f)) +
+            _fromCC(0x80 | ((cc >>> 6) & 0x3f)) +
+            _fromCC(0x80 | (cc & 0x3f));
+      } else {
+        var cc =
+          0x10000 +
+          (c.charCodeAt(0) - 0xd800) * 0x400 +
+          (c.charCodeAt(1) - 0xdc00);
+        return (
+          _fromCC(0xf0 | ((cc >>> 18) & 0x07)) +
+          _fromCC(0x80 | ((cc >>> 12) & 0x3f)) +
+          _fromCC(0x80 | ((cc >>> 6) & 0x3f)) +
+          _fromCC(0x80 | (cc & 0x3f))
+        );
+      }
+    };
+    var re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
+    /**
+     * @deprecated should have been internal use only.
+     * @param {string} src UTF-8 string
+     * @returns {string} UTF-16 string
+     */
+    var utob = function (u) {
+      return u.replace(re_utob, cb_utob);
+    };
+    //
+    var _encode = _hasBuffer
+      ? function (s) {
+          return Buffer.from(s, "utf8").toString("base64");
+        }
+      : _TE
+      ? function (s) {
+          return _fromUint8Array(_TE.encode(s));
+        }
+      : function (s) {
+          return _btoa(utob(s));
+        };
+    /**
+     * converts a UTF-8-encoded string to a Base64 string.
+     * @param {boolean} [urlsafe] if `true` make the result URL-safe
+     * @returns {string} Base64 string
+     */
+    var encode = function (src, urlsafe) {
+      if (urlsafe === void 0) {
+        urlsafe = false;
+      }
+      return urlsafe ? _mkUriSafe(_encode(src)) : _encode(src);
+    };
+    /**
+     * converts a UTF-8-encoded string to URL-safe Base64 RFC4648 §5.
+     * @returns {string} Base64 string
+     */
+    var encodeURI = function (src) {
+      return encode(src, true);
+    };
+    // This trick is found broken https://github.com/dankogai/js-base64/issues/130
+    // const btou = (src: string) => decodeURIComponent(escape(src));
+    // reverting good old fationed regexp
+    var re_btou =
+      /[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g;
+    var cb_btou = function (cccc) {
+      switch (cccc.length) {
+        case 4:
+          var cp =
+              ((0x07 & cccc.charCodeAt(0)) << 18) |
+              ((0x3f & cccc.charCodeAt(1)) << 12) |
+              ((0x3f & cccc.charCodeAt(2)) << 6) |
+              (0x3f & cccc.charCodeAt(3)),
+            offset = cp - 0x10000;
+          return (
+            _fromCC((offset >>> 10) + 0xd800) +
+            _fromCC((offset & 0x3ff) + 0xdc00)
+          );
+        case 3:
+          return _fromCC(
+            ((0x0f & cccc.charCodeAt(0)) << 12) |
+              ((0x3f & cccc.charCodeAt(1)) << 6) |
+              (0x3f & cccc.charCodeAt(2))
+          );
+        default:
+          return _fromCC(
+            ((0x1f & cccc.charCodeAt(0)) << 6) | (0x3f & cccc.charCodeAt(1))
+          );
+      }
+    };
+    /**
+     * @deprecated should have been internal use only.
+     * @param {string} src UTF-16 string
+     * @returns {string} UTF-8 string
+     */
+    var btou = function (b) {
+      return b.replace(re_btou, cb_btou);
+    };
+    /**
+     * polyfill version of `atob`
+     */
+    var atobPolyfill = function (asc) {
+      // console.log('polyfilled');
+      asc = asc.replace(/\s+/g, "");
+      if (!b64re.test(asc)) throw new TypeError("malformed base64.");
+      asc += "==".slice(2 - (asc.length & 3));
+      var u24,
+        bin = "",
+        r1,
+        r2;
+      for (var i = 0; i < asc.length; ) {
+        u24 =
+          (b64tab[asc.charAt(i++)] << 18) |
+          (b64tab[asc.charAt(i++)] << 12) |
+          ((r1 = b64tab[asc.charAt(i++)]) << 6) |
+          (r2 = b64tab[asc.charAt(i++)]);
+        bin +=
+          r1 === 64
+            ? _fromCC((u24 >> 16) & 255)
+            : r2 === 64
+            ? _fromCC((u24 >> 16) & 255, (u24 >> 8) & 255)
+            : _fromCC((u24 >> 16) & 255, (u24 >> 8) & 255, u24 & 255);
+      }
+      return bin;
+    };
+    /**
+     * does what `window.atob` of web browsers do.
+     * @param {String} asc Base64-encoded string
+     * @returns {string} binary string
+     */
+    var _atob = _hasatob
+      ? function (asc) {
+          return atob(_tidyB64(asc));
+        }
+      : _hasBuffer
+      ? function (asc) {
+          return Buffer.from(asc, "base64").toString("binary");
+        }
+      : atobPolyfill;
+    //
+    var _toUint8Array = _hasBuffer
+      ? function (a) {
+          return _U8Afrom(Buffer.from(a, "base64"));
+        }
+      : function (a) {
+          return _U8Afrom(
+            _atob(a)
+              .split("")
+              .map(function (c) {
+                return c.charCodeAt(0);
+              })
+          );
+        };
+    /**
+     * converts a Base64 string to a Uint8Array.
+     */
+    var toUint8Array = function (a) {
+      return _toUint8Array(_unURI(a));
+    };
+    //
+    var _decode = _hasBuffer
+      ? function (a) {
+          return Buffer.from(a, "base64").toString("utf8");
+        }
+      : _TD
+      ? function (a) {
+          return _TD.decode(_toUint8Array(a));
+        }
+      : function (a) {
+          return btou(_atob(a));
+        };
+    var _unURI = function (a) {
+      return _tidyB64(
+        a.replace(/[-_]/g, function (m0) {
+          return m0 == "-" ? "+" : "/";
+        })
+      );
+    };
+    /**
+     * converts a Base64 string to a UTF-8 string.
+     * @param {String} src Base64 string.  Both normal and URL-safe are supported
+     * @returns {string} UTF-8 string
+     */
+    var decode = function (src) {
+      return _decode(_unURI(src));
+    };
+    /**
+     * check if a value is a valid Base64 string
+     * @param {String} src a value to check
+     */
+    var isValid = function (src) {
+      if (typeof src !== "string") return false;
+      var s = src.replace(/\s+/g, "").replace(/={0,2}$/, "");
+      return !/[^\s0-9a-zA-Z\+/]/.test(s) || !/[^\s0-9a-zA-Z\-_]/.test(s);
+    };
+    //
+    var _noEnum = function (v) {
+      return {
+        value: v,
+        enumerable: false,
+        writable: true,
+        configurable: true,
+      };
+    };
+    /**
+     * extend String.prototype with relevant methods
+     */
+    var extendString = function () {
+      var _add = function (name, body) {
+        return Object.defineProperty(String.prototype, name, _noEnum(body));
+      };
+      _add("fromBase64", function () {
+        return decode(this);
+      });
+      _add("toBase64", function (urlsafe) {
+        return encode(this, urlsafe);
+      });
+      _add("toBase64URI", function () {
+        return encode(this, true);
+      });
+      _add("toBase64URL", function () {
+        return encode(this, true);
+      });
+      _add("toUint8Array", function () {
+        return toUint8Array(this);
+      });
+    };
+    /**
+     * extend Uint8Array.prototype with relevant methods
+     */
+    var extendUint8Array = function () {
+      var _add = function (name, body) {
+        return Object.defineProperty(Uint8Array.prototype, name, _noEnum(body));
+      };
+      _add("toBase64", function (urlsafe) {
+        return fromUint8Array(this, urlsafe);
+      });
+      _add("toBase64URI", function () {
+        return fromUint8Array(this, true);
+      });
+      _add("toBase64URL", function () {
+        return fromUint8Array(this, true);
+      });
+    };
+    /**
+     * extend Builtin prototypes with relevant methods
+     */
+    var extendBuiltins = function () {
+      extendString();
+      extendUint8Array();
+    };
+    var gBase64 = {
+      version: version,
+      VERSION: VERSION,
+      atob: _atob,
+      atobPolyfill: atobPolyfill,
+      btoa: _btoa,
+      btoaPolyfill: btoaPolyfill,
+      fromBase64: decode,
+      toBase64: encode,
+      encode: encode,
+      encodeURI: encodeURI,
+      encodeURL: encodeURI,
+      utob: utob,
+      btou: btou,
+      decode: decode,
+      isValid: isValid,
+      fromUint8Array: fromUint8Array,
+      toUint8Array: toUint8Array,
+      extendString: extendString,
+      extendUint8Array: extendUint8Array,
+      extendBuiltins: extendBuiltins,
+    };
+    //
+    // export Base64 to the namespace
+    //
+    // ES5 is yet to have Object.assign() that may make transpilers unhappy.
+    // gBase64.Base64 = Object.assign({}, gBase64);
+    gBase64.Base64 = {};
+    Object.keys(gBase64).forEach(function (k) {
+      return (gBase64.Base64[k] = gBase64[k]);
+    });
+    return gBase64;
+  }
+);

+ 205 - 0
src/plugins/tts/index.js

@@ -0,0 +1,205 @@
+// import Base64 from "crypto-js/enc-base64";
+import CryptoJS from "crypto-js";
+import AudioPlayer from "./audioPlayer.esm";
+import Base64 from "./base64";
+
+export default class TtsVoice {
+  appId = "";
+  apiSecret = "";
+  apiKey = "";
+  ttsWS = null;
+  text = "";
+  status = "UNDEFINED";
+  statusStr = "";
+  audioPlayer = null;
+  text = "";
+  blob = "";
+  cb = () => {};
+  blobDoneCb = () => {};
+  constructor(params) {
+    this.appId = params.appId;
+    this.apiSecret = params.apiSecret;
+    this.apiKey = params.apiKey;
+    if (params.cb) {
+      this.cb = params.cb;
+    }
+    if (params.blobDoneCb) {
+      this.blobDoneCb = params.blobDoneCb;
+    }
+    this.audioPlayer = new AudioPlayer();
+    this.audioPlayer.onPlay = () => {
+      this.changeBtnStatus("PLAY");
+    };
+    this.audioPlayer.onStop = (audioDatas) => {
+      console.log(audioDatas);
+      this.status === "PLAY" && this.changeBtnStatus("STOP");
+    };
+  }
+  setText(text) {
+    this.text = text;
+  }
+  changeBtnStatus(status) {
+    this.status = status;
+    if (status === "UNDEFINED") {
+      this.statusStr = "立即合成";
+    } else if (status === "CONNECTING") {
+      this.statusStr = "正在合成";
+    } else if (status === "PLAY") {
+      this.statusStr = "停止播放";
+    } else if (status === "STOP") {
+      this.statusStr = "重新播放";
+    }
+    const _this = this;
+    this.cb({ status, statusStr: _this.statusStr });
+  }
+  encodeText(text, type) {
+    if (type === "unicode") {
+      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);
+      }
+      let binary = "";
+      let bytes = new Uint8Array(buf);
+      let len = bytes.byteLength;
+      for (let i = 0; i < len; i++) {
+        binary += String.fromCharCode(bytes[i]);
+      }
+      return window.btoa(binary);
+    } else {
+      return Base64.encode(text);
+    }
+  }
+  getWebSocketUrl() {
+    let apiKey = this.apiKey;
+    let apiSecret = this.apiSecret;
+    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}`;
+    return url;
+  }
+  stopConnect() {
+    this.changeBtnStatus("UNDEFINED");
+    this.ttsWS?.close();
+    this.audioPlayer.reset();
+  }
+  action(text) {
+    if (text && typeof text === "string") {
+      if (text !== this.text) {
+        this.changeBtnStatus("UNDEFINED");
+      }
+      this.setText(text);
+    }
+    if (this.status === "UNDEFINED") {
+      // 开始合成
+
+      this.connect();
+    } else if (this.status === "CONNECTING") {
+      // 停止合成
+      this.changeBtnStatus("UNDEFINED");
+      this.ttsWS?.close();
+      this.audioPlayer.reset();
+      return;
+    } else if (this.status === "PLAY") {
+      this.audioPlayer.stop();
+    } else if (this.status === "STOP") {
+      this.audioPlayer.play();
+    }
+  }
+  connect() {
+    this.ttsWS?.close();
+    this.blob = "";
+    this.blobDoneCb && this.blobDoneCb(this.blob);
+    const text = this.text || "请输入文字!";
+    const appId = this.appId;
+    const apiKey = this.apiKey;
+    const apiSecret = this.apiSecret;
+    const url = this.getWebSocketUrl(apiKey, apiSecret);
+    if ("WebSocket" in window) {
+      this.ttsWS = new WebSocket(url);
+    } else if ("MozWebSocket" in window) {
+      this.ttsWS = new window.MozWebSocket(url);
+    } else {
+      alert("浏览器不支持WebSocket");
+      return;
+    }
+    this.changeBtnStatus("CONNECTING");
+    const _this = this;
+    this.ttsWS.onopen = () => {
+      _this.audioPlayer.start({
+        autoPlay: true,
+        sampleRate: 16000,
+        resumePlayDuration: 1000,
+      });
+      _this.changeBtnStatus("PLAY");
+      var tte = "UTF8";
+      var params = {
+        common: {
+          app_id: appId,
+        },
+        business: {
+          aue: "raw",
+          auf: "audio/L16;rate=16000",
+          vcn: "50",
+          speed: 50,
+          volume: 50,
+          pitch: 50,
+          bgs: 1,
+          tte,
+        },
+        data: {
+          status: 2,
+          text: _this.encodeText(text, tte),
+        },
+      };
+      _this.ttsWS.send(JSON.stringify(params));
+    };
+    this.ttsWS.onmessage = (e) => {
+      let jsonData = JSON.parse(e.data);
+      // 合成失败
+      if (jsonData.code !== 0) {
+        console.error(jsonData);
+        _this.changeBtnStatus("UNDEFINED");
+        return;
+      }
+      _this.audioPlayer.postMessage({
+        type: "base64",
+        data: jsonData.data.audio,
+        isLastData: jsonData.data.status === 2,
+      });
+      if (jsonData.code === 0 && jsonData.data.status === 2) {
+        _this.ttsWS.close();
+        setTimeout(() => {
+          _this.blob = this.audioPlayer.getAudioDataBlob("wav");
+          _this.blobDoneCb && _this.blobDoneCb(_this.blob);
+        });
+      }
+    };
+    _this.ttsWS.onerror = (e) => {
+      console.error(e);
+    };
+    _this.ttsWS.onclose = (e) => {
+      console.log(e);
+    };
+  }
+  download() {
+    const blob = this.audioPlayer.getAudioDataBlob("wav");
+    if (!blob) {
+      return;
+    }
+    let defaultName = new Date().getTime();
+    let node = document.createElement("a");
+    node.href = window.URL.createObjectURL(blob);
+    node.download = `${defaultName}.wav`;
+    node.click();
+    node.remove();
+  }
+}