浏览器端将语音转换为URL格式的字符串(base64 位编码)
我们可以在浏览器端,通过调用 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;
}