我们可以在浏览器端,通过调用 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