瀏覽器端將語音轉換為URL格式的字符串(base64 位編碼)


我們可以在瀏覽器端,通過調用 JS 原生的 API,將語音轉換為文字,實現語音輸入的效果。思路是:

  1. 錄制一段音頻;
  2. 將音頻轉換為 URL 格式的字符串(base64 位編碼);
  3. 調用訊飛開放接口,將 base64 位編碼轉換為文本。

這篇文章實現前兩步,將音頻轉換為 URL 格式的字符串(base64 位編碼)。

這里將會用到於媒體錄制相關的諸多 API,先將其列出:

  • MediaDevices 接口提供訪問連接媒體輸入的設備,如照相機和麥克風,以及屏幕共享等。
  • MediaDevices.getUserMedia() 會提示用戶給予使用媒體輸入的許可。

我們將要訪問瀏覽器的麥克風。若瀏覽器支持 getUserMedia,就可以訪問麥克風權限。
MediaDevices.getUserMedia(),返回一個 Promise 對象,獲得麥克風許可后,會 resolve 回調一個 MediaStream 對象。MediaStream 包含音頻軌道的輸入。

  • MediaRecorder() 構造函數會創建一個對指定的 MediaStream 進行錄制的 MediaRecorder 對象。
  • MediaStream 是將要錄制的流. 它可以是來自於使用 navigator.mediaDevices.getUserMedia() 創建的流。
  • 實例化的 MediaRecorder 對象,提供媒體錄制的接口

MediaRecorder() 構造函數接受 MediaDevices.getUserMedia() resolve 回調的 MediaStream, 作為將要錄制的流。並且可以指定 MIMEType 類型和音頻比特率。
實例化該構造函數后,可以讀取錄制對象的當前狀態,並根據狀態選擇錄取、暫停和停止。
MediaRecorder.stop() 方法會出發停止錄制,同時觸發 dataavailable 事件,返回一個存儲 Blob 內容的錄制數據,之后不再記錄

  • Blob() 構造函數返回一個新的 Blob 對象。
  • Blob 對象表示一個不可變、原始數據的類文件對象。
  • File 接口基於Blob,接受 Blob 對象的API也被列在 File 文檔中。

Blob() 構造函數接受 MediaRecorder.ondataavailable() 方法返回的 Blob 類型的錄制數據,並指定音頻格式。
實例化該構造函數后,新創建一個不可變、原始數據的類文件對象。

  • URL.createObjectURL() 靜態方法會創建一個 DOMString,其中包含一個表示參數中給出的對象的URL。
  • 這個新的 URL 對象表示指定的 File 對象或 Blob 對象。

URL.createObjectURL() 接受一個 Blob 對象,創建一個 DomString,該字符串作為 <audio> 元素的播放地址。

  • FileReader() 構造函數去創建一個新的 FileReader 對象。
  • readAsDataURL() 方法會讀取指定的 BlobFile 對象。
  • 讀取操作完成的時候,readyState 會變成已完成 DONE,並觸發 loadend 事件,同時 result 屬性將包含一個 data:URL 格式的字符串(base64 編碼)以表示所讀取文件的內容。

實例化 FileReader() 構造函數,新創建一個 FileReader 對象。
使用 readAsDataURL() 方法,接受一個 Blob 對象,讀取完成后,觸發 onload 方法,同時 result 屬性將包含一個data:URL格式的字符串(base64 編碼)

使用 Angular 將核心代碼放置如下:

QaComponent

<div id="voiceIcon" class="iconfont icon-voice"  (click)="showVoice = !showVoice"  [title]="showVoice ? '停止' : '錄制'"></div>

<!-- 語音錄制動畫 -->
<app-voice [show]="showVoice"></app-voice>
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 是 <audio>元素的地址
          const audioURL = window.URL.createObjectURL(blob);

          const reader = new FileReader();
          // 取指定的 Blob 或 File 對象,讀取操作完成的時候,readyState 會變成已完成DONE
          reader.readAsDataURL(blob);
          reader.onload = () => {
            // result 屬性將包含一個data:URL格式的字符串(base64編碼)以表示所讀取文件的內容
            console.log(reader.result); // reader.result 為 base64 字符串編碼
          };
        };
      },
      () => {
        console.error('授權失敗!');
      },
    );
  } else {
    console.error('瀏覽器不支持 getUserMedia');
  }
}

VoiceComponent

<div class="voice-container" *ngIf="_show">
 <i class="iconfont icon-voice"></i>
 <div class="circle"></div>
</div>
.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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM