VUE语音识别

1,551 阅读2分钟

引入websocket

const recoder = function (window) {
  // 兼容
  window.URL = window.URL || window.webkitURL
  navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia
  var HZRecorder = function (stream, config) {
    config = config || {}
    config.sampleBits = config.sampleBits || 16 // 采样数位 8, 16
    config.sampleRate = config.sampleRate || (16000 / 1) // 采样率(1/6
    // 44100)

    var context = new (window.webkitAudioContext || window.AudioContext)()
    // console.log(stream)
    var audioInput = context.createMediaStreamSource(stream)
    var createScript = context.createScriptProcessor || context.createJavaScriptNode
    var recorder = createScript.apply(context, [4096, 1, 1])

    var audioData = {
      size: 0, // 录音文件长度
      buffer: [], // 录音缓存
      inputSampleRate: context.sampleRate, // 输入采样率
      inputSampleBits: 16, // 输入采样数位 8, 16
      outputSampleRate: config.sampleRate, // 输出采样率
      oututSampleBits: config.sampleBits, // 输出采样数位 8, 16
      input: function (data) {
        this.buffer.push(new Float32Array(data))
        this.size += data.length
      },
      compress: function () { // 合并压缩
        // 合并
        var data = new Float32Array(this.size)
        var offset = 0
        for (var i = 0; i < this.buffer.length; i++) {
          data.set(this.buffer[i], offset)
          offset += this.buffer[i].length
        }
        // 压缩
        var compression = parseInt(this.inputSampleRate / this.outputSampleRate)
        var length = data.length / compression
        var result = new Float32Array(length)
        var index = 0; var j = 0
        while (index < length) {
          result[index] = data[j]
          j += compression
          index++
        }
        return result
      },
      clear: function () {
        this.size = 0
        this.buffer = []
      },
      encodeWAV: function () {
        var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate)

        // console.log(sampleRate);

        var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits)
        // console.log(sampleBits);

        var bytes = this.compress()
        var dataLength = bytes.length * (sampleBits / 8)
        var buffer = new ArrayBuffer(44 + dataLength)
        var data = new DataView(buffer)

        var channelCount = 1// 单声道
        var offset = 0

        var writeString = function (str) {
          for (var i = 0; i < str.length; i++) {
            data.setUint8(offset + i, str.charCodeAt(i))
          }
        }

        // 资源交换文件标识符
        writeString('RIFF'); offset += 4
        // 下个地址开始到文件尾总字节数,即文件大小-8
        data.setUint32(offset, 36 + dataLength, true); offset += 4
        // WAV文件标志
        writeString('WAVE'); offset += 4
        // 波形格式标志
        writeString('fmt '); offset += 4
        // 过滤字节,一般为 0x10 = 16
        data.setUint32(offset, 16, true); offset += 4
        // 格式类别 (PCM形式采样数据)
        data.setUint16(offset, 1, true); offset += 2
        // 通道数
        data.setUint16(offset, channelCount, true); offset += 2
        // 采样率,每秒样本数,表示每个通道的播放速度
        data.setUint32(offset, sampleRate, true); offset += 4
        // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
        data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4
        // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
        data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2
        // 每样本数据位数
        data.setUint16(offset, sampleBits, true); offset += 2
        // 数据标识符
        writeString('data'); offset += 4
        // 采样数据总数,即数据总大小-44
        data.setUint32(offset, dataLength * 100, true); offset += 4
        // 写入采样数据
        if (sampleBits === 8) {
          for (var i = 0; i < bytes.length; i++, offset++) {
            var s = Math.max(-1, Math.min(1, bytes[i]))
            var val = s < 0 ? s * 0x8000 : s * 0x7FFF
            val = parseInt(255 / (65535 / (val + 32768)))
            data.setInt8(offset, val, true)
          }
        } else {
          for (var i = 0; i < bytes.length; i++, offset += 2) {
            var s = Math.max(-1, Math.min(1, bytes[i]))
            data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
          }
        }

        return new Blob([data], { type: 'audio/wav' })
      }
    }

    // 开始录音
    this.start = function () {
      audioInput.connect(recorder)
      recorder.connect(context.destination)
    }

    // 停止
    this.stop = function () {
      recorder.disconnect()
    }

    // 获取音频文件
    this.getBlob = function () {
      this.stop()
      return audioData.encodeWAV()
    }

    // 回放
    this.play = function (audio) {
      audio.src = window.URL.createObjectURL(this.getBlob())
    }

    this.exportWAV = function (callback) {
      if (callback) {
        var blob = audioData.encodeWAV()
        audioData.clear()
        callback(blob)
      }
    }

    // 音频采集
    recorder.onaudioprocess = function (e) {
      audioData.input(e.inputBuffer.getChannelData(0))
      // record(e.inputBuffer.getChannelData(0));
    }
  }
  // 抛出异常
  HZRecorder.throwError = function (message) {
    alert(message)
    throw new function () { this.toString = function () { return message } }()
  }
  // 是否支持录音
  HZRecorder.canRecording = (navigator.getUserMedia != null)
  // 获取录音机
  HZRecorder.get = function (callback, config) {
    if (callback) {
      if (navigator.getUserMedia) {
        navigator.getUserMedia(
          { audio: true } // 只启用音频
          , function (stream) {
            var rec = new HZRecorder(stream, config)
            callback(rec)
          }
          , function (error) {
            switch (error.code || error.name) {
              case 'PERMISSION_DENIED':
              case 'PermissionDeniedError':
                HZRecorder.throwError('用户拒绝提供信息。')
                break
              case 'NOT_SUPPORTED_ERROR':
              case 'NotSupportedError':
                HZRecorder.throwError('浏览器不支持硬件设备。')
                break
              case 'MANDATORY_UNSATISFIED_ERROR':
              case 'MandatoryUnsatisfiedError':
                HZRecorder.throwError('无法发现指定的硬件设备。')
                break
              default:
                HZRecorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name))
                break
            }
          })
      } else {
        HZRecorder.throwErr('当前浏览器不支持录音功能。')
      }
    }
  }

  window.HZRecorder = HZRecorder
}

recoder(window)

var recorder
var intervalKey
var ws
export function startRecord(callback) {
  try {
    window.HZRecorder.get(rec => {
      if (rec.error)
        return callback.error(rec.error);
      recorder = rec;
      recorder.start();
      callback.success("Recording...");
      // console.log('Recording...')

    })
  } catch (error) {
    callback.error("Recordingfail" + error);
  }

}
export function stopRecord(callback) {
  try {
    let blobData = recorder.getBlob();
    callback.success(blobData);
  } catch (error) {
    callback.error("StopRecordingfail" + error);
  }
  // recorder.stop()
  // console.log('.........stoped...........')

}

export function startRecording(callback) {
  console.log(2222)

  console.log(callback)
  initWs(e => {
    window.HZRecorder.get(function (rec) {
      recorder = rec
      recorder.start()
      console.log('Recording...')
      intervalKey = setInterval(function () {
        recorder.exportWAV(function (blob) {
          ws.send(blob)
        })
      }, 50)
    })
    console.log('.........正在录音......')
  },
    callback
  )
}

export function stopRecording() {
  recorder.stop()
  console.log('.........stoped...........')
  clearInterval(intervalKey)
  // document.getElementById('ptime').innerHTML = "";
  ws.close()
}

window.start = startRecording
window.stop = stopRecording

let sessionId
function initWs(call, callback) {
  sessionId = 'ASTDEMO_' + _getRandomString(8)
  // var wsuri = "wss://124.114.129.219:2256/" + document.location.host + "/tuling/demo/ast/"+sessionId;  // 可以通
  var wsuri = 'ws://10.1.25.49:4567/tuling/ast/v2/' + sessionId + '?appId=10101&bizId=123&bizName=WebSocket&lan=chin&sr=16000&bps=16&fs=4096'

  // var wsuri = "ws://124.114.129.219:4567/tuling/ast/v2/" + sessionId; // 不能通
  console.log(wsuri)
  ws = new WebSocket(wsuri)
  ws.onopen = function () {
    console.log('Openened connection to websocket')
    call()
  }
  ws.onmessage = function (e) {
    console.log(`收到消息`)
    callback(e.data)
  }
  // console.log(callback)
};

// 获取长度为len的随机字符串
function _getRandomString(len) {
  len = len || 32
  var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' // 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1
  var maxPos = $chars.length
  var pwd = ''
  for (let i = 0; i < len; i++) {
    pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
  }
  return pwd
}

vue文件

<template>
    <div class="voice-page">
        <div class="handle">
            <div class="dis-flex">
                <div class="handle-btn" @click="getAudio" v-if="!isRecord">
                    <img class="handle-icon" src="@/assets/newversion/voice_icon.png" />
                    <div>开始</div>
                </div>
                <div class="handle-btn" @click="onEnd" v-show="isRecord">
                    <img src="@/assets/newversion/recording.png" class="handle-icon recording-icon" v-if="unStart" />
                    <img src="@/assets/newversion/recording.png" class="handle-icon" v-if="!unStart" />
                    <div>结束</div>
                </div>
                <div class="handle-btn" @click="onPause" v-if="unStart">
                    <img class="handle-icon" src="@/assets/newversion/audio-pause.png" />
                    <div>暂停</div>
                </div>
                <div class="handle-btn" @click="onStart" v-if="!unStart">
                    <img class="handle-icon" src="@/assets/newversion/audio-start.png" />
                    <div>继续</div>
                </div>
                <div class="handle-btn" @click="onSave">
                    <img class="handle-icon" src="@/assets/newversion/save.png" />
                    <div>保存</div>
                </div>
                <div class="handle-btn" @click="onDownload">
                    <img class="handle-icon" src="@/assets/newversion/download.png" />
                    <div>下载</div>
                </div>
                <div class="handle-btn" v-clipboard:copy="message" v-clipboard:success="onCopy" v-clipboard:error="onError">
                    <img class="handle-icon" src="@/assets/newversion/copy.png" />
                    <div>复制</div>
                </div>
                <div class="handle-btn reset-btn" @click="onReset">
                    <img class="handle-icon" src="@/assets/newversion/reset.png" />
                    <div>重置</div>
                </div>
            </div>
            <div class="dis-flex">
                <el-select v-model="fontSize" placeholder="请选择字号" class="font-select">
                    <el-option v-for="(item,index) in fonts" :key="index" :label="item.label" :value="item.value">
                    </el-option>
                </el-select>
                <img class="font-icon" src="@/assets/newversion/font-reduce.png" title="减小字号" @click="reduceFont" />
                <img class="font-icon" src="@/assets/newversion/font-add.png" title="增大字号" @click="addFont" />
            </div>
        </div>
        <div class="voice-content" :style="{fontSize: fontSize + 'rem'}">
			{{message}}
        </div>
        <div class="dialog-wrap" v-show="voiceVisible"></div>
        <div class="sound-dialog" v-show="voiceVisible"></div>
    </div>
</template>

<script>
import "../recoder";
var recorder;
let newRecorder;
import host from "../plugins/host"
export default {
    name: "VoiceComponent",
    data() {
        return {
            voiceVisible: false,
            isAudioClick: false, //开始识别是否被点击了
            isRecord: false, //识别中
            unStart: true, //是否开始录制
            audioTimer: 60, //定时时间
            timerObj: null, //定时器对象
            voiceTxt: "", //语音识别后的文字
            audioLength: 0, //音频时间
            ws: null,
            intervalKey: null,
            message: "",
            config: {
                sampleBits: 16,
                sampleRate: 16000
            },
            fontSize: '0.24',
            progressive: [],
            sentence: [],
            msgBox: [],
        };
    },
    mounted() {
        // 获取缓存
        let saved_msg = sessionStorage.getItem('saved_msg');
        if (saved_msg) {
            this.message = saved_msg;
        }
    },
    computed: {
        fonts: function () {
            return this.$store.state.fonts;
        },
    },
    methods: {
        onCopy: function (e) {
            // this.$message.success("复制成功");
            console.log('复制成功');
        },
        onError: function (e) {
            // this.$message.error("复制失败");
            console.log('复制失败');
        },

        // 字体减小
        reduceFont() {
            let fonts = this.fonts;
            let fontSize = this.fontSize;
            for (let i in fonts) {
                if (fonts[i].value == fontSize) {
                    if (i > 0) {
                        this.fontSize = fonts[Number(i) - 1].value;
                    }
                }
            }
        },

        // 字体加大
        addFont() {
            let fonts = this.fonts;
            let fontSize = this.fontSize;
            for (let i in fonts) {
                if (fonts[i].value == fontSize) {
                    if (i < fonts.length - 1) {
                        this.fontSize = fonts[Number(i) + 1].value;
                    }
                }
            }
        },

        // 暂停
        onPause() {
            if (!this.isRecord) return;
            let msgBox = sessionStorage.getItem('msgBox');
            if (msgBox) {
                msgBox = JSON.parse(msgBox);
                sessionStorage.setItem('msgBox', JSON.stringify(msgBox.concat(this.msgBox)));
            } else {
                sessionStorage.setItem('msgBox', JSON.stringify(this.msgBox));
            }
            this.msgBox = [];
            this.unStart = false;
            recorder.stop();
            this.ws.close();
            clearInterval(this.intervalKey);
            this.intervalKey = null;
        },

        // 继续
        onStart() {
            let config = this.config;
            let that = this;
            this.initWs(() => {
                this.unStart = true;
                HZRecorder.get(function (rec) {
                    recorder = rec;
                    recorder.start();
                    if (typeof recorder === "object") {
                        that.intervalKey = setInterval(function () {
                            recorder.exportWAV(function (blob) {
                                // console.log(blob); // 类型大小
                                if (that.ws.readyState === that.ws.OPEN) {
                                    // 若是ws开启状态
                                    that.ws.send(blob);
                                }
                            })
                        }, 50)
                    }
                }, config);
            })
        },

        // 结束
        onEnd() {
            this.isRecord = false;
            this.unStart = true;
            recorder.stop();
            this.ws.close();
            clearInterval(this.intervalKey);
            this.intervalKey = null;
        },

        // 重置
        onReset() {
            this.msgBox = [];
            sessionStorage.removeItem('msgBox');
            this.message = "";
            this.fontSize = '0.24';
        },

        // 保存
        onSave() {
            if (this.message === '') {
                // this.$message.error("请先录制语音");
                console.log('请先录制语音');
                return;
            }
            sessionStorage.setItem('saved_msg', this.message);
            // this.$message.success("保存成功");
            console.log('保存成功');
        },

        // 下载为txt
        onDownload() {
            if (this.message === '') {
                // this.$message.error("请先录制语音");
                console.log('请先录制语音');
                return;
            }
            // 没有服务器的情况下使用下方可获取连接
            let urlObj = window.URL || window.webkitURL || window;
            let export_blob = new Blob([this.message]);
            // 建立A标签
            let save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
            save_link.href = urlObj.createObjectURL(export_blob);
            save_link.download = this.formatDate() + ".txt";
            save_link.target = "_blank";
            let ev = document.createEvent("MouseEvents");
            ev.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
            save_link.dispatchEvent(ev);
        },
        formatDate() {
            let now = new Date();
            return (String(now.getFullYear()) + "-" + String(now.getMonth() + 1) + "-" + String(now.getDate()) + " " + String(now.getHours()) + "-" + String(now.getMinutes()) + "-" + String(now.getSeconds()));
        },

        stopRecoder() {
            newRecorder.stop()
        },

        // 新版获取音频流
        startRecorder() {
            // 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
            if (navigator.mediaDevices === undefined) {
                navigator.mediaDevices = {};
            }

            // 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia 
            // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
            if (navigator.mediaDevices.getUserMedia === undefined) {
                navigator.mediaDevices.getUserMedia = function (constraints) {

                    // 首先,如果有getUserMedia的话,就获得它
                    var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

                    // 一些浏览器根本没实现它 - 那么就返回一个error到promise的reject来保持一个统一的接口
                    if (!getUserMedia) {
                        return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
                    }

                    // 否则,为老的navigator.getUserMedia方法包裹一个Promise
                    return new Promise(function (resolve, reject) {
                        getUserMedia.call(navigator, constraints, resolve, reject);
                    });
                }
            }
            let that = this;
            navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
                that.initWs(function () {
                    newRecorder = new MediaRecorder(stream);
                    const recordedChunks = [];
                    newRecorder.ondataavailable = event => {
                        const blob = event.data;
                        let newBlob = new Blob([blob], { type: 'audio/wav' })
                        if (blob.size > 0) {
                            recordedChunks.push(newBlob);
                        }

                        console.log(newBlob)
                        that.ws.send(newBlob);
                    };
                    newRecorder.onstop = function () {
                        that.$refs.myAudio.src = URL.createObjectURL(new Blob(recordedChunks));
                    }
                    newRecorder.start(1000);
                })
            }).catch(function (err) {
                that.$message.error("未找到录音设备");
                console.log(err.name + ": " + err.message);
            })
        },

        // 开始识别
        getAudio(active) {
            console.log(active);
            let getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
            if (!getUserMedia) {
                // this.$message.error("当前浏览器不支持语音");
                console.log('当前浏览器不支持语音');
                return;
            }
            let that = this;
            if (this.voiceVisible) return; 
            this.initWs(() => {  
                this.voiceVisible = true;  //启动通话框
                setTimeout(() => {
                    this.voiceVisible = false;
                    this.isRecord = true; // 暂停识别中
                    this.message = "";
                    this.msgBox = [];
                    sessionStorage.removeItem('msgBox');
                    //开始采集音频 (这里的意思 开始 录音)
                    let config = this.config;
                    HZRecorder.get(function (rec) {
                        console.log(rec);
                        recorder = rec;
                        recorder.start();
                        if (typeof recorder === "object") {
                            that.intervalKey = setInterval(function () {
                                recorder.exportWAV(function (blob) {
                                    console.log(blob);
                                    if (that.ws.readyState === that.ws.OPEN) {
                                        // 若是ws开启状态
                                        that.ws.send(blob);
                                    }
                                });
                            }, 50);
                        }
                    }, config);
                }, 2000)
            })
        },
        // 初始化ws
        initWs(callback) {
            let loadIns = this.$loading({
                text: "正在连接服务器...",
                background: "rgba(0, 0, 0, 0.5)"
            });
            let that = this;
            let sessionId = "ASTDEMO_" + this.getRandomString(8); // 随机数
            let wsuri =	host.voiceUrl +sessionId +"?appId=10101&bizId=123&bizName=WebSocket&lan=chin&sr=16000&bps=16&fs=4096";
            this.ws = new WebSocket(wsuri); // 开启websocket
            this.ws.onopen = function () { //开启
                loadIns.close(); // 关闭正在连接服务
                callback();
            };
            // 接受的文字  
            this.ws.onmessage = function (e) {
                console.log(`收到消息:`);
                console.log(JSON.parse(e.data));
                let data = JSON.parse(e.data);
                that.handleMsg(data);
            };
            this.ws.onerror = function (err) {
                loadIns.close();
                that.$message.error("服务器连接失败,请重试");
            }
        },

        // 统一处理消息
        handleMsg(data) {
             // jquery
            const { finalResult, result } = data;
            if (!result) return;
            msgBox[msgBox.length ? msgBox.length - 1 : 0] = result
            if (finalResult) { msgBox.push('') }
            $('.voice-content').html(msgBox.join(''))
            //
        
            let msgBox = this.msgBox;
            if (msgBox.length == 0) {
                msgBox.push(data);
            } else {
                let status = false;
                for (let p in msgBox) {
                    // 数组当中已存在,替换
                    if (msgBox[p].segId == data.segId) {
                        msgBox.splice(p, 1, data);
                        status = true;
                    }
                }
                // 不存在,push一条新数据
                if (!status) {
                    this.msgBox.push(data);
                }
            }
            // 合并消息
            let list = [];
            let savedData = sessionStorage.getItem('msgBox');
            if (savedData) {
                savedData = JSON.parse(savedData);
                list = list.concat(savedData).concat(this.msgBox);
            } else {
                list = list.concat(this.msgBox);
            }
            // 渲染
            let str = '';
            for (let i in list) {
                let ws = list[i].ws;
                for (let j in ws) {
                    let cw = ws[j].cw;
                    for (let k in cw) {
                        let item = cw[k];
                        str += item.w;
                    }
                }
            }
            this.message = str;
        },
        // 处理未校验消息
        hanldeProcessive(data) {
            let progressive = this.progressive;
            if (progressive.length > 0) {
                let status = false;
                for (let p in progressive) {
                    // 数组当中已存在,替换
                    if (progressive[p].segId == data.segId) {
                        progressive.splice(p, 1, data);
                        status = true;
                    }
                }
                if (!status) {
                    this.progressive.push(data);
                }
            } else {
                this.progressive.push(data);
            }
            let list = [];
            let savedData = sessionStorage.getItem('progressive');
            if (savedData) {
                savedData = JSON.parse(savedData);
                list = list.concat(savedData).concat(this.progressive);
            } else {
                list = list.concat(this.progressive);
            }
            // 渲染
            let str = '';
            for (let i in list) {
                let ws = list[i].ws;
                for (let j in ws) {
                    let cw = ws[j].cw;
                    for (let k in cw) {
                        let item = cw[k];
                        str += item.w;
                    }
                }
            }
            this.message = str;
        },

        // 处理校验消息
        hanldeSentence(data) {
            this.sentence.push(data);
            let sentence = this.sentence;
            let progressive = this.progressive;
            if (progressive.length > 0) {
                let status = false;
                // 覆盖未校验消息
                for (let p in progressive) {
                    if (progressive[p].segId == data.segId) {
                        progressive.splice(p, 1, data);
                        status = true;
                    }
                }
                if (!status) {
                    this.progressive.push(data);
                }
            } else {
                // 直接返回的是最终校验消息
                this.progressive.push(data);
            }
            let list = [];
            let savedData = sessionStorage.getItem('sentence');
            if (savedData) {
                savedData = JSON.parse(savedData);
                list = list.concat(savedData).concat(this.sentence);
            } else {
                list = list.concat(this.sentence);
            }

            // 渲染
            let str = '';
            for (let i in list) {
                let ws = list[i].ws;
                for (let j in ws) {
                    let cw = ws[j].cw;
                    for (let k in cw) {
                        let item = cw[k];
                        str += item.w;
                    }
                }
            }
            this.message = str;
        },

        // 随机字符串
        getRandomString(len) {
            len = len || 32;
            let $chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"; // 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1
            let maxPos = $chars.length;
            let pwd = "";
            for (let i = 0; i < len; i++) {
                pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
            }
            return pwd;
        }
    },
    destroyed() {
        if (this.ws) {
            this.ws.close();
            sessionStorage.removeItem('msgBox');
        }
    }
};
</script>

<style scoped>
.dis-flex {
    display: flex;
    align-items: center;
}

.voice-page {
    width: 100%;
    height: 100%;
    padding: 0.2rem;
    background: url("../assets/newversion/cont-bg.png") no-repeat;
    background-size: 100% 100%;
}
.voice-page .handle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-bottom: 0.2rem;
    font-size: 0.16rem;
}
.voice-page .handle .handle-btn {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 1.2rem;
    height: 0.4rem;
    color: #fff;
    background-color: #378eef;
    border-radius: 5px;
    cursor: pointer;
}
.voice-page .handle .handle-btn.clear {
    background-color: #909399;
}
.voice-page .handle .handle-btn.reset {
    /* background-color: #f56c6c; */
    background-color: #909399;
}
.voice-page .handle .handle-btn .handle-icon {
    display: block;
    width: 0.2rem;
    margin-right: 0.1rem;
}
.voice-page .handle .handle-btn:not(:first-child) {
    margin-left: 0.15rem;
}

.line-box {
    display: inline-block;
    vertical-align: middle;
}

.line-box div {
    display: inline-block;
    vertical-align: middle;
    margin-right: 0.04rem;
    /* width: 0.02rem; */
    width: 2px;
    background-color: #fff;
    animation-play-state: running;
    -webkit-animation-play-state: running;
}

.line-box .line-1 {
    animation: line 0.6s infinite linear alternate;
}

.line-box .line-2 {
    animation: line 0.6s 0.2s infinite linear alternate;
}

.line-box .line-3 {
    animation: line 0.6s 0.4s infinite linear alternate;
}

.line-box .line-4 {
    animation: line 0.6s 0.6s infinite linear alternate;
}

.line-box div.paused {
    animation-play-state: paused;
    -webkit-animation-play-state: paused;
}

@keyframes line {
    0%,
    20% {
        height: 0.06rem;
    }
    100% {
        height: 0.2rem;
    }
}

.recording-icon {
    animation: recording 0.6s infinite ease-in alternate;
}

@keyframes recording {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}

.voice-page .voice-content {
    height: calc(100% - 0.6rem);
    color: #fff;
	padding: 0.2rem;
	word-break: break-all;
    background: url("../assets/newversion/cont-inner-new.png") no-repeat;
    background-size: 100% 100%;
	overflow-y: auto;
}

.recording-box {
    margin-top: 0.2rem;
    padding: 0.2rem;
    overflow-y: auto;
}

.finally-box,
.recording-box {
    height: calc((100% - 0.2rem) / 2);
    word-break: break-all;
    background: url("../assets/newversion/cont-inner-new.png") no-repeat;
    background-size: 100% 100%;
}

.finally-txt {
    margin: 0;
    padding: 0.2rem;
    width: 100%;
    height: 100%;
    resize: none;
    border: none;
    outline: none;
    color: #fff;
    background-color: transparent;
}

.voice-content::-webkit-scrollbar,
.recording-box::-webkit-scrollbar,
.finally-txt::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

.voice-content::-webkit-scrollbar-thumb,
.recording-box::-webkit-scrollbar-thumb,
.finally-txt::-webkit-scrollbar-thumb {
    border-radius: 5px;
    box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2);
    background-color: rgba(255, 255, 255, 0.75);
}

.voice-content::-webkit-scrollbar-track,
.recording-box::-webkit-scrollbar-track,
.finally-txt::-webkit-scrollbar-track {
    border-radius: 0;
    background: none;
}

.voice-page .dialog-wrap {
    z-index: 1000;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    overflow: auto;
    margin: 0;
    background: rgba(0, 0, 0, 0.4);
}

.voice-page .sound-dialog {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 389px;
    height: 280px;
    z-index: 1000;
    background: url("../assets/newversion/mkf.png") no-repeat 0 0;
    background-size: 100% 100%;
}
</style>