Vue 浏览器录音、播放、上传服务端(PCM 8000采样率 16位)

需求:浏览器录音,发送到服务器触发语音搜索,代码中是把语音信息转成file,表单提交到后端

index.vue
<template>
  <div class="box">
    <div
      class="talk_search_btn_item"
      style="cursor: pointer"
      @click="handleVoice()"
      title="语音"
    >
      <img src="./icon2.png" v-show="!isOpenFlag" alt="" />
      <img src="./icon21.png" v-show="isOpenFlag" alt="" />
    </div>
    <div class="box_list">
      <div
        v-for="(item, index) in audioData"
        :key="index"
        class="box_list_item"
        @click="playAudio(item)"
      >
        <img
          src="./audio0.png"
          style="height: 100%; width: auto"
          alt=""
          v-show="!item.playAudioLoading"
        />
        <img
          src="./audio1.gif"
          style="height: 100%; width: auto"
          alt=""
          v-show="item.playAudioLoading"
        />
      </div>
    </div>
  </div>
</template>

<script>
// base64 转 ArrayBuffer
function _base64ToArrayBuffer(base64) {
  var binary_string = window.atob(base64); //解码使用base64编码的字符串
  var len = binary_string.length; //获取长度
  var bytes = new Uint8Array(len);
  for (var i = 0; i < len; i++) {
    bytes[i] = binary_string.charCodeAt(i);
  }
  // console.log(bytes); //打印解析出来的byte
  // return bytes;
  return bytes.buffer;
}
// 下载
function download(buff) {
  let url = window.URL.createObjectURL(
    new Blob([buff], { type: "arraybuffer" })
  );
  const link = document.createElement("a");
  link.style.display = "none";
  link.href = url;
  link.setAttribute("download", "out");
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
// 拼接 ArrayBuffer
function mergeArrayBuffers(arrayBuffers) {
  // 计算新的ArrayBuffer的总长度
  let totalLength = 0;
  for (const buffer of arrayBuffers) {
    totalLength += buffer.byteLength;
  }

  // 创建一个新的ArrayBuffer
  const mergedBuffer = new ArrayBuffer(totalLength);

  // 创建一个Uint8Array以便操作新的ArrayBuffer
  const uint8Array = new Uint8Array(mergedBuffer);

  let offset = 0;
  // 逐个复制ArrayBuffer到新的ArrayBuffer中
  for (const buffer of arrayBuffers) {
    const sourceArray = new Uint8Array(buffer);
    uint8Array.set(sourceArray, offset);
    offset += sourceArray.length;
  }

  return mergedBuffer;
}
// ArrayBuffer 转 Float32Array
function convertArrayBufferToFloat32Array(arrayBuffer) {
  const dataView = new DataView(arrayBuffer);
  // const float32Array = new Float32Array(arrayBuffer.byteLength / Float32Array.BYTES_PER_ELEMENT);
  const float32Array = new Float32Array(arrayBuffer.byteLength / 2);
  for (let i = 0; i < float32Array.length; i++) {
    const pcmValue = dataView.getInt16(i * 2, true);
    float32Array[i] = pcmValue / 32768.0;
  }
  return float32Array;
}
function blobToFile(blob, filename, type) {
  return new File([blob], filename, { type });
}
import AudioRecorder from "./AudioRecorder.js";
export default {
  data() {
    return {
      recorder: null, //录音对象
      isOpenFlag: false, //正在录音标识
      audioMessage: "正在进行语音对讲",
      loadingMessage: null,
      audioData: [],
    };
  },
  methods: {
    playAudio(data) {
      if (data.playAudioLoading) return;
      var arr = data.file_audio;
      const audioContext = new (window.AudioContext ||
        window.webkitAudioContext)();
      // 将 ArrayBuffer 格式的 PCM 数据转换为 Float32Array 格式
      const float32Array = convertArrayBufferToFloat32Array(arr);

      // 创建 AudioBufferSourceNode
      const audioBufferSource = audioContext.createBufferSource();

      // 创建 AudioBuffer
      const audioBuffer = audioContext.createBuffer(
        1,
        float32Array.length,
        8000
      );

      // 获取 AudioBuffer 的数据通道
      const channelData = audioBuffer.getChannelData(0);

      // 将 Float32Array 格式的 PCM 数据填充到 AudioBuffer 的数据通道
      channelData.set(float32Array);

      // 将 AudioBuffer 设置为 AudioBufferSourceNode 的音频数据
      audioBufferSource.buffer = audioBuffer;

      // 连接 AudioBufferSourceNode 到音频输出
      audioBufferSource.connect(audioContext.destination);

      // 播放音频
      audioBufferSource.start();
      data.playAudioLoading = true;
      audioBufferSource.onended = function () {
        // 播放结束后执行的操作
        console.log("音频播放结束");
        data.playAudioLoading = false;
      };
    },
    handleVoice() {
      var isOpenFlag = !this.isOpenFlag;
      if (isOpenFlag) {
        if (this.listLoading == true) {
          this.$message.success("正在查询中,请稍等");
          return;
        }
        this.onstart();
      } else {
        this.isOpenFlag = false;
        this.loadingMessage && this.loadingMessage.close(); // 关闭通知加载
      }
    },
    onstart() {
      let _this = this;
      if (_this.recorder) {
        _this.recorder.stop();
      }
      this.isOpenFlag = true;
      var all_ArrayBuffer = [];
      var num = -1;
      let config = {
        onAudioProcess: function (audioData) {
          let data = audioData.encodePCM();
          let reader = new FileReader();
          let base64data;
          reader.onloadend = function () {
            // console.log("发送语音", reader);
            base64data = reader.result;
            base64data = base64data.split(",")[1];
            // console.log(base64data);
            //   base64data = JSON.stringify({
            //     type: 1,
            //     content: base64data.split(",")[1],
            //   });
            var data2 = _base64ToArrayBuffer(base64data); // Uint8Array ArrayBuffer
            num++;
            // console.log(num);
            if (_this.isOpenFlag) {
              // num < 100
              // console.log(data2);
              all_ArrayBuffer.push(data2);
            } else if (_this.isOpenFlag == false) {
              // num == 100
              var arr = mergeArrayBuffers(all_ArrayBuffer);
              // console.log("arr", arr);
              _this.audioData.push({
                file_audio: arr,
                playAudioLoading: false,
              });

              // 播放------------------------------------------------------------------------

              // const audioContext = new (window.AudioContext ||
              //   window.webkitAudioContext)();
              // // 将 ArrayBuffer 格式的 PCM 数据转换为 Float32Array 格式
              // const float32Array = convertArrayBufferToFloat32Array(arr);

              // // 创建 AudioBufferSourceNode
              // const audioBufferSource = audioContext.createBufferSource();

              // // 创建 AudioBuffer
              // const audioBuffer = audioContext.createBuffer(
              //   1,
              //   float32Array.length,
              //   8000
              // );

              // // 获取 AudioBuffer 的数据通道
              // const channelData = audioBuffer.getChannelData(0);

              // // 将 Float32Array 格式的 PCM 数据填充到 AudioBuffer 的数据通道
              // channelData.set(float32Array);

              // // 将 AudioBuffer 设置为 AudioBufferSourceNode 的音频数据
              // audioBufferSource.buffer = audioBuffer;

              // // 连接 AudioBufferSourceNode 到音频输出
              // audioBufferSource.connect(audioContext.destination);

              // // 播放音频
              // audioBufferSource.start();
              // 播放------------------------------------------------------------------------

              // download(arr);//下载到本地 可用GoldWave播放
              var blob = new Blob([arr]); // ArrayBuffer 转 Blob 对象
              // var file = _this.blobToFile(blob, 'test', 'text/plain' )
              var file = blobToFile(
                blob,
                "audio" + _this.$moment().format("_YYYYMMDD_HHmmss"),
                "audio/mpeg"
              );
              // _this.onSearch(file); //上传到服务端

              if (_this.recorder) {
                _this.recorder.stop();
              }
              _this.loadingMessage && _this.loadingMessage.close(); // 关闭通知加载
            }
            audioData.clearInput();
          };
          reader.readAsDataURL(new Blob([data]));
        },
      };
      try {
        AudioRecorder.get(function (rec) {
          _this.recorder = rec;
          _this.recorder.start();
          _this.isOpenFlag = true;
          setTimeout(() => {
            _this.audioMessage = "正在录音";
            _this.loadingMessage && _this.loadingMessage.close(); // 关闭通知加载
            _this.loadingMessage = _this.$message({
              showClose: false,
              customClass: "evpMsgCls",
              dangerouslyUseHTMLString: true,
              message:
                '<div class="el-icon-loading"></div><a>' +
                // _this.videoName +
                // ": " +
                _this.audioMessage +
                "&nbsp;&nbsp;&nbsp;</a>",
              duration: 0,
            });
          }, 200);
        }, config);
      } catch (error) {
        _this.isOpenFlag = false;
      }
    },
  },
};
</script>
<style lang="scss" scoped>
.box {
  display: flex;
  flex-direction: column;
  img {
    width: 36px;
    height: 36px;
  }
  .box_list {
    height: 0;
    flex-grow: 1;
    .box_list_item {
      width: 120px;
      height: 36px;
      background-color: #3d6bf8;
      border-radius: 15px;
      margin-bottom: 10px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  }
}
</style>
AudioRecorder.js
// 兼容
/**
 * 注意:浏览器默认采样率为48000,国标采样率为8000,因此需要压缩音频文件到1/6
 *      input()、encodePCM() 录入任意时长的音频,最后调用encodePCM方法转成PCM格式
 *      inputCompress(),encodeCompressPCM两个方法:每触发一次inputCompress方法,压缩一次,
 *      最后将所有压缩文件合并生成一个PCM文件
 *      两种方式只能存在一种,根据要求修改代码
 *
 */
let lastTime = 0;
const AudioRecorder = function(stream, config) {
    config = config || {};
    config.sampleBits = config.sampleBits || 16; // 采样数位 8, 16
    config.sampleRate = config.sampleRate || 8000; // 采样率
    // console.log('采样数位',config.sampleBits, '采样率',config.sampleRate);
    // let context = new (window.AudioContext || window.webkitAudioContext)();
    // let audioInput = context.createMediaStreamSource(stream);
    // let recorder = context.createJavaScriptNode(4096, 1, 1);
    let context = new AudioContext();
    let audioInput = context.createMediaStreamSource(stream);
    // let recorder = context.createScriptProcessor(4096, 1, 1);// 缓冲区大小、指定输入node的声道的数量,默认值是2、指定输出node的声道的数量,默认2
    let recorder = context.createScriptProcessor(2048, 1, 1);// 缓冲区大小、指定输入node的声道的数量,默认值是2、指定输出node的声道的数量,默认2
    let audioData = {
        stream: stream,
        size: 0, // 录音文件长度
        buffer: [], // 录音缓存
        compressBuffer: [], // 录音缓存,压缩后
        compressBufferSize: 0,
        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;
            // this.inputCompress();
            if (lastTime) {
                // console.log("耗时:" + (new Date().getTime() - lastTime));
            }
            lastTime = new Date().getTime();
            // console.log("语音数据Size:" + this.size + "byte");
        },
        inputCompress: function (data) {
            this.compressBuffer.push(this.compress());
            this.compressBufferSize += this.compress().length;
            this.clearInput();
        },
        clearInput: function () {
            this.buffer = [];
            this.size = 0;
        },
        compress: function () { // 合并处理
            let data = new Float32Array(this.size);
            let offset = 0;
            for (let i = 0; i < this.buffer.length; i++) {
                data.set(this.buffer[i], offset);
                offset += this.buffer[i].length;
            }
            let compression = parseInt(this.inputSampleRate / this.outputSampleRate);
            let length = data.length / compression;
            let result = new Float32Array(length);
            let index = 0;
            let j = 0;
            while (index < length) {
                result[index] = data[j];
                j += compression;
                index++;
            }
            return result;
        },
        // 分次压缩后合并
        encodeCompressPCM() {
            let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
            let bytesArray = this.compressBuffer;
            let bytes = new Float32Array(this.compressBufferSize);
            // 合并分片
            let index = 0;
            for (let b = 0; b < bytesArray.length; b++) {
                for (let c = 0; c < bytesArray[b].length; c++) {
                    bytes[index] = bytesArray[b][c];
                    index++;
                }
            }
            let dataLength = this.compressBufferSize * (sampleBits / 8);
            let buffer = new ArrayBuffer(dataLength);
            let data = new DataView(buffer);
            let a = 0;
            if (sampleBits === 8) {
                for (let o = 0; o < bytes.length; o++, a++) {
                    let s = Math.max(-1, Math.min(1, bytes[o]));
                    let u = s < 0 ? 32768 * s : 32767 * s;
                    u = parseInt(255 / (65535 / (u + 32768)));
                    data.setInt8(a, u, !0);
                }
            } else {
                for (let o = 0; o < bytes.length; o++, a += 2) {
                    let s = Math.max(-1, Math.min(1, bytes[o]));
                    data.setInt16(a, s < 0 ? 32768 * s : 32767 * s, !0)
                }
            }
            return data;
        },
        // 压缩一次
        encodePCM: function() {
            let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
            let bytes = this.compress();
            let dataLength = bytes.length * (sampleBits / 8);
            let buffer = new ArrayBuffer(dataLength);
            let data = new DataView(buffer);
            let a = 0;
            if (sampleBits === 8) {
                for (let o = 0; o < bytes.length; o++, a++) {
                    let s = Math.max(-1, Math.min(1, bytes[o]));
                    let u = s < 0 ? 32768 * s : 32767 * s;
                    u = parseInt(255 / (65535 / (u + 32768)));
                    data.setInt8(a, u, !0);
                }
            } else {
                for (let o = 0; o < bytes.length; o++, a += 2) {
                    let s = Math.max(-1, Math.min(1, bytes[o]));
                    data.setInt16(a, s < 0 ? 32768 * s : 32767 * s, !0)
                }
            }
            return data;
        },
        encodeWAV: function () {
            let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
            let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
            let bytes = this.compress();
            let dataLength = bytes.length * (sampleBits / 8);
            let buffer = new ArrayBuffer(44 + dataLength);
            let data = new DataView(buffer);

            let channelCount = 1;// 单声道
            let offset = 0;

            let writeString = function (str) {
                for (let 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, true); offset += 4;
            // 写入采样数据
            if (sampleBits === 8) {
                for (let i = 0; i < bytes.length; i++, offset++) {
                    let s = Math.max(-1, Math.min(1, bytes[i]));
                    let val = s < 0 ? s * 0x8000 : s * 0x7FFF;
                    val = parseInt(255 / (65535 / (val + 32768)));
                    data.setInt8(offset, val, true);
                }
            } else {
                for (let i = 0; i < bytes.length; i++, offset += 2) {
                    let s = Math.max(-1, Math.min(1, bytes[i]));
                    data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                }
            }
            return data;
        },
        playSound: function () {
            context.decodeAudioData(this.encodeWAV().buffer, function(buffer) {
                let source = context.createBufferSource();
                source.buffer = buffer; // 设置数据
                source.connect(context.destination); // connect到扬声器
                source.start();
            }, function() {
                // console.log('error');
            });
        }
    };

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

    // 停止
    this.stop = function () {
        audioData.stream.getTracks()[0].stop();
        audioInput.disconnect();
        recorder.disconnect();
    }

    // 清空语音数据
    this.clearAudio = function () {
        audioData.clearInput();
    }

    // 获取音频文件
    this.getBlob_WAV = function () {
        this.stop();
        let data = audioData.encodeWAV();
        return new Blob([data], { type: 'audio/wav' });
    }

    // 获取音频文件
    this.getBlob_PCM = function () {
        this.stop();
        let data = audioData.encodePCM();
        return new Blob([data], { type: 'audio/pcm' });
    }
    // 获取音频文件
    this.getBlob_CompressPCM = function () {
        this.stop();
        let data = audioData.encodeCompressPCM();
        return new Blob([data], { type: 'audio/pcm' });
    }

    // 回放(WAV格式-使用audio标签)
    this.playWav = function (audio) {
        audio.src = window.URL.createObjectURL(this.getBlob_WAV());
    }

    // 播放语音(使用context)
    this.playSound = function () {
        audioData.playSound();
    }

    this.combineDateView = function(resultConstructor, ...arrays) {

    }

    // 音频采集
    recorder.onaudioprocess = function (e) {
        audioData.input(e.inputBuffer.getChannelData(0));
        // record(e.inputBuffer.getChannelData(0));
        // 回调函数
        if (config.onAudioProcess) {
            // console.log("--执行回调函数--");
            config.onAudioProcess(audioData);
        }
    }
}
// 获取
AudioRecorder.get = function (callback, config) {
    if (callback) {
        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ||
            (navigator.mediaDevices && (navigator.mediaDevices.getUserMedia || navigator.mediaDevices.webkitGetUserMedia || navigator.mediaDevices.mozGetUserMedia));
        // window.console.log("navigator.getUserMedia:" + navigator.getUserMedia);
        if (navigator.getUserMedia) {
            navigator.getUserMedia(
                { audio: true }, // 只启用音频
                function (stream) {
                    var rec = new AudioRecorder(stream, config);
                    callback(rec);
                },
                function (error) {
                    switch (error.code || error.name) {
                    case 'PERMISSION_DENIED':
                    case 'PermissionDeniedError':
                        alert('用户拒绝提供信息。');
                        break;
                    case 'NOT_SUPPORTED_ERROR':
                    case 'NotSupportedError':
                        alert('浏览器不支持硬件设备。');
                        break;
                    case 'MANDATORY_UNSATISFIED_ERROR':
                    case 'MandatoryUnsatisfiedError':
                        alert('无法发现指定的硬件设备。');
                        break;
                    default:
                        alert('无法打开麦克风。异常信息:' + (error.code || error.name));
                        break;
                    }
                });
        } else {
            alert('当前浏览器不支持录音功能。');
        }
    }
}
export default AudioRecorder;
保存到本地的文件,可用GoldWave进行播放,参数如下
相关推荐
想太多会累i7 分钟前
kkfileview代理配置,Vue对接kkfileview实现图片word、excel、pdf预览
vue.js·word·excel
计算机学姐7 分钟前
基于SpringBoot的校园求职招聘管理系统
java·前端·vue.js·spring boot·后端·mysql·intellij-idea
阿琳a_29 分钟前
解决uniapp中使用axios在真机和模拟器下请求报错问题
前端·javascript·uni-app
风清云淡_A2 小时前
【原生js案例】移动端如何实现页面的入场和出场动画
前端·javascript
一叶茶3 小时前
前端生成docx文档、excel表格、图片、pdf文件
前端·javascript·react
fxshy3 小时前
element table 表头header-cell-style设置的表头不生效
前端·javascript·vue.js
开心工作室_kaic3 小时前
springboot458家教管理系统(论文+源码)_kaic
运维·服务器·数据库·vue.js
万物得其道者成3 小时前
使用 js-cookie 和 localStorage/sessionStorage 构建高效的浏览器缓存管理
开发语言·javascript·缓存
一张假钞4 小时前
Hexo自动生成摘要
java·前端·javascript
Zh-jie5 小时前
flutter 快速实现侧边栏
前端·javascript·flutter