我們可以在瀏覽器端,通過調用 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;
}