我们可以在浏览器端,通过调用 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;
}
欢迎写出你的看法,一起成长!