录音功能实战

242 阅读4分钟

实现思路

前端把 音频文件以流的形式给后端接口,接收接口解析完成之后返回的内容,需要以下步骤:

  1. 获取手机录音权限

  2. 开启录音

  3. 结束录音,将 录音 Blob 转为 音频文件

  4. 将音频文件给到后端

封装音频类

// mediaDevices.js

// import FFmpeg, { createFFmpeg } from "@ffmpeg/ffmpeg";
// import ffmpegCore from "@ffmpeg/core/dist/ffmpeg-core";

const ERROR_CODE_MESSAGE = {
  PERMISSION_DENIED: "用户拒绝提供信息。",
  PermissionDeniedError: "用户拒绝提供信息。",
  NOT_SUPPORTED_ERROR: "浏览器不支持硬件设备。",
  NotSupportedError: "浏览器不支持硬件设备。",
  MANDATORY_UNSATISFIED_ERROR: "无法发现指定的硬件设备。",
  MandatoryUnsatisfiedError: "无法发现指定的硬件设备。"
};

// 获取录音权限
export function getUserMedia() {
  //  获取录音权限异常
  function throwError(err) {
    const errCode = err.code;
    const errName = err.name;
    const errorMessage = ERROR_CODE_MESSAGE[errCode] || ERROR_CODE_MESSAGE[errName];
    return errorMessage || `无法打开麦克风。异常信息: ${errCode || errName}`;
  }
  return new Promise((resolve, reject) => {
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
      // 授权音频
      navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then(stream => {
          /* 使用音频流 */
          console.log("授权成功", stream);
          resolve(stream);
        })
        .catch(err => {
          console.error("发生错误: " + err);
          reject(throwError(err));
        });
    } else {
      reject("抱歉,你的浏览器暂不支持录音功能");
    }
  });
}

export default class MediaDevices {
  constructor(config = {}, stream) {
    // 配置
    config = config || {};
    config.sampleBits = 16; //采样数位 8, 16
    config.sampleRate = 16000; //采样率(1/6 44100)

    // 创建音频上下文
    var context = new (window.webkitAudioContext || window.AudioContext)();
    // 创建音频源
    var mediaStreamSource = context.createMediaStreamSource(stream);
    var createScript = context.createScriptProcessor || context.createJavaScriptNode;
    var recorder = createScript.apply(context, [4096, 1, 1]);

    this.context = context;
    this.mediaStreamSource = mediaStreamSource;
    this.recorder = recorder;

    // 创建录音对象
    var recognition = this.createRecognition(config);
    // 音频采集
    recorder.onaudioprocess = function(e) {
      recognition.input(e.inputBuffer.getChannelData(0));
    };
    this.recognition = recognition;
  }
  // 创建语音识别对象
  createRecognition(config) {
    return {
      size: 0, //录音文件长度
      buffer: [], //录音缓存
      inputSampleRate: this.context.sampleRate, //输入采样率
      inputSampleBits: 16, //输入采样数位 8, 16
      outputSampleRate: config.sampleRate, //输出采样率
      oututSampleBits: config.sampleBits, //输出采样数位 8, 16
      init: function() {
        this.size = 0;
        this.buffer = [];
      },
      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,
          j = 0;
        while (index < length) {
          result[index] = data[j];
          j += compression;
          index++;
        }
        return result;
      },
      encodeWAV: function() {
        var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
        var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
        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, 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: config.audioType || "audio/wav" });
      }
    };
  }
  // 开始录音
  start() {
    // 初始化
    this.recognition.init();
    this.mediaStreamSource.connect(this.recorder);
    this.recorder.connect(this.context.destination);
  }
  // 停止录音
  stop() {
    this.recorder.disconnect();
  }
  // 清除数据
  clear() {
    this.recognition.init();
  }
  // 上传录音
  upload(url, callback) {
    var fd = new FormData();
    fd.append("audioData", this.getBlob());
    var xhr = new XMLHttpRequest();
    if (callback) {
      xhr.upload.addEventListener(
        "progress",
        function(e) {
          callback("uploading", e);
        },
        false
      );
      xhr.addEventListener(
        "load",
        function(e) {
          callback("ok", e);
        },
        false
      );
      xhr.addEventListener(
        "error",
        function(e) {
          callback("error", e);
        },
        false
      );
      xhr.addEventListener(
        "abort",
        function(e) {
          callback("cancel", e);
        },
        false
      );
    }
    xhr.open("POST", url);
    xhr.send(fd);
  }
  // 获取音频文件
  getBlob() {
    this.stop();
    return this.recognition.encodeWAV();
  }
  // 转为pcm格式音频文件
  async toPcmFile(blob, config = {}) {
    if (!blob) {
      blob = this.getBlob();
    }
    // FFmpeg.load().then(() => {
    //   console.log("加载完成");
    // });
    // console.log("....", FFmpeg);
    // const ffmpeg = createFFmpeg({log: true, corePath: ffmpegCore, ...config});
    // 异步加载 ffmpeg
    // await ffmpeg.load();
    // return new Promise((resolve, reject) => {
    //   ffmpeg
    //     .run("-i", blob, "-f", "s16le", "-acodec", "pcm_s16le", "output.pcm")
    //     .then(res => {
    //       resolve(res);
    //     })
    //     .catch(err => {
    //       reject(err);
    //     });
    // });
    // FFmpeg({
    //   arguments: ["-i", audioBlob, "-f", "s16le", "-acodec", "pcm_s16le", "output.pcm"],
    //   files: [audioBlob]
    // }).then(() => {
    //   // PCM数据现在可用
    // });
  }
}

离线音频示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>H5 录音功能</title>
</head>
<body>

<button id="startRecord">开始录音</button>
<button id="stopRecord" disabled>停止录音</button>
<button id="playAudio" disabled>播放录音</button>
<script>
import MediaDevices, { getUserMedia } from "./mediaDevices.js";
    let mediaRecorder;
    let recordedChunks = [];
    
    // 获取音频权限
    getUserMedia().then(stream => {
        // 创建音频对象
        mediaRecorder = new MediaDevices({}, stream);
    })
    

    document.getElementById('startRecord').addEventListener('click', () => {
        if (mediaRecorder) {
            mediaRecorder.start();
            document.getElementById('startRecord').disabled = true;
            document.getElementById('stopRecord').disabled = false;
            recordedChunks = []; // 清空之前的录音数据
        }
    });

    document.getElementById('stopRecord').addEventListener('click', () => {
        if (mediaRecorder) {
            mediaRecorder.stop();
            document.getElementById('startRecord').disabled = false;
            document.getElementById('stopRecord').disabled = true;
            document.getElementById('playAudio').disabled = false;
        }
    });

    document.getElementById('playAudio').addEventListener('click', () => {
        const audioBlob = new Blob(recordedChunks, { type: 'audio/wav' });
        const audioUrl = URL.createObjectURL(audioBlob);
        const audio = new Audio(audioUrl);
        audio.play();
    });
</script>

</body>
</html>