浏览器端将语音转换为URL格式的字符串(base64 位编码)


我们可以在浏览器端,通过调用 JS 原生的 API,将语音转换为文字,实现语音输入的效果。思路是:

  1. 录制一段音频;
  2. 将音频转换为 URL 格式的字符串(base64 位编码);
  3. 调用讯飞开放接口,将 base64 位编码转换为文本。

这篇文章实现前两步,将音频转换为 URL 格式的字符串(base64 位编码)。

这里将会用到于媒体录制相关的诸多 API,先将其列出:

  • MediaDevicesMediaDevices 使用方法)
  • MediaDevices 接口提供访问连接媒体输入的设备,如照相机和麦克风,以及屏幕共享等。
  • MediaDevices.getUserMedia() 会提示用户给予使用媒体输入的许可。

我们将要访问浏览器的麦克风。若浏览器支持 getUserMedia,就可以访问麦克风权限。
MediaDevices.getUserMedia(),返回一个 Promise 对象,获得麦克风许可后,会 resolve 回调一个 MediaStream 对象。MediaStream 包含音频轨道的输入。

  • MediaRecorder MediaRecorder 使用方法)
  • MediaRecorder() 构造函数会创建一个对指定的 MediaStream 进行录制的 MediaRecorder 对象。
  • MediaStream 是将要录制的流. 它可以是来自于使用 navigator.mediaDevices.getUserMedia() 创建的流。
  • 实例化的 MediaRecorder 对象,提供媒体录制的接口

MediaRecorder() 构造函数接受 MediaDevices.getUserMedia() resolve 回调的 MediaStream, 作为将要录制的流。并且可以指定 MIMEType 类型和音频比特率。
实例化该构造函数后,可以读取录制对象的当前状态,并根据状态选择录取、暂停和停止。
MediaRecorder.stop() 方法会出发停止录制,同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据,之后不再记录

  • BlobBlob 使用方法)
  • Blob() 构造函数返回一个新的 Blob 对象。
  • Blob 对象表示一个不可变、原始数据的类文件对象。
  • File 接口基于Blob,接受 Blob 对象的API也被列在 File 文档中。

Blob() 构造函数接受 MediaRecorder.ondataavailable() 方法返回的 Blob 类型的录制数据,并指定音频格式。
实例化该构造函数后,新创建一个不可变、原始数据的类文件对象。

  • URL.createObjectURL() URL.createObjectURL() 使用方法)
  • URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。
  • 这个新的 URL 对象表示指定的 File 对象或 Blob 对象。

URL.createObjectURL() 接受一个 Blob 对象,创建一个 DomString,该字符串作为 元素的播放地址。

  • FileReaderFileReader 使用方法)
  • FileReader() 构造函数去创建一个新的 FileReader 对象。
  • readAsDataURL() 方法会读取指定的 BlobFile 对象。
  • 读取操作完成的时候,readyState 会变成已完成 DONE,并触发 loadend 事件,同时 result 属性将包含一个 data:URL 格式的字符串(base64 编码)以表示所读取文件的内容。

实例化 FileReader() 构造函数,新创建一个 FileReader 对象。
使用 readAsDataURL() 方法,接受一个 Blob 对象,读取完成后,触发 onload 方法,同时 result 属性将包含一个data:URL格式的字符串(base64 编码)

使用 Angular 将核心代码放置如下:

QaComponent

showVoice = false; // 录音动画显示隐藏

/**
 * 初始化完组件视图及其子视图之后,获取麦克风权限
 */
ngAfterViewInit(): void {
  this.mediaRecorder();
}

/**
 * 将语音文件转换为 base64 的字符串编码
 */
mediaRecorder() {
  const voiceIcon = document.getElementById('voiceIcon') as HTMLDivElement;
  // 在用户通过提示允许的情况下,打开系统上的麦克风
  if (navigator.mediaDevices.getUserMedia) {
    let chunks = [];
    const constraints = { audio: true }; // 指定请求的媒体类型
    navigator.mediaDevices.getUserMedia(constraints).then(
      stream => {
        // 成功后会resolve回调一个 MediaStream 对象,包含音频轨道的输入。
        console.log('授权成功!');

        const options = {
          audioBitsPerSecond: 22050, // 音频的比特率
        };
        // MediaRecorder 构造函数实例化的 mediaRecorder 对象是用于媒体录制的接口
        // @ts-ignore
        const mediaRecorder = new MediaRecorder(stream, options);

        voiceIcon.onclick = () => {
          // 录制对象 MediaRecorder  的当前状态(闲置中 inactive,录制中 recording 或者暂停 paused)
          if (mediaRecorder.state === 'recording') {
            // 停止录制. 同时触发dataavailable事件,之后不再记录
            mediaRecorder.stop();
            console.log('录音结束');
          } else {
            // 开始录制媒体
            mediaRecorder.start();
            console.log('录音中...');
          }
          console.log('录音器状态:', mediaRecorder.state);
        };

        mediaRecorder.ondataavailable = (e: { data: any }) => {
          // 返回一个存储Blob内容的录制数据,在事件的 data 属性中会提供一个可用的 Blob 对象
          chunks.push(e.data);
        };

        mediaRecorder.onstop = () => {
          // MIME类型 为 audio/wav
          // 实例化 Blob 构造函数,返回的 blob 对象表示一个不可变、原始数据的类文件对象
          const blob = new Blob(chunks, { type: 'audio/wav; codecs=opus' });
          chunks = [];

          // 如果作为音频播放,audioURL 是 

VoiceComponent

.voice-container {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 1;
  transform: translate(-50%, -50%);

  .icon-voice {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 4;
    display: block;
    color: #fff;
    font-size: 24px;
    transform: translate(-50%, -50%);
  }

  .audio {
    position: relative;
    top: 50%;
    left: 50%;
    z-index: 4;
    transform: translate(-50%, -50%);
  }

  .circle {
    position: absolute;
    top: 50%;
    left: 50%;
    z-index: 3;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    animation: gradient 1s infinite;
  }

  @keyframes gradient {
    from {
      width: 70px;
      height: 70px;
      background-color: rgb(24, 144, 255);
    }
    to {
      width: 160px;
      height: 160px;
      background-color: rgba(24, 144, 255, 0.3);
    }
  }
}
public _show: boolean;
@Input()
set show(val: boolean) {
  this._show = val;
}
get show() {
  return this._show;
}