我们可以在浏览器端,通过调用 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;
}
来源:https://www.cnblogs.com/xinjie-just/p/12228039.html