我們可以在瀏覽器端,通過調用 JS 原生的 API,將語音轉換為文字,實現語音輸入的效果。思路是:
- 錄制一段音頻;
- 將音頻轉換為
URL格式的字符串(base64位編碼); - 調用訊飛開放接口,將
base64位編碼轉換為文本。
這篇文章實現前兩步,將音頻轉換為 URL 格式的字符串(base64 位編碼)。
這里將會用到於媒體錄制相關的諸多 API,先將其列出:
MediaDevices(MediaDevices使用方法)
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 內容的錄制數據,之后不再記錄
Blob(Blob使用方法)
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,該字符串作為 <audio> 元素的播放地址。
FileReader(FileReader使用方法)
FileReader()構造函數去創建一個新的FileReader對象。readAsDataURL()方法會讀取指定的Blob或File對象。- 讀取操作完成的時候,
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;
}
