最近准备一个技术分享,看到以前做的一个语音转文字的功能,放在slides上落灰了,索性整理到这里和大家分享下。

从技术选型,到方案设计,到实际落地,可以说把全链路都覆盖到了。

  • 语音转写流程图
  • PC端浏览器如何录音
  • 录音完毕后语音如何发送
  • 语音发送和实时转写
  • 通用录音组件
  • 总结

语音转写流程图

PC端浏览器如何录音

AudioContext,AudioNode是什么?
MediaDevice.getUserMedia()是什么?
为什么localhost能播放,预生产不能播放?
js中的数据类型TypedArray知多少?
js-audio-recorder源码分析
代码实现

AudioContext是什么?

AudioContext接口表示由链接在一起的音频模块构建的音频处理图形,每个模块由一个AudioNode表示。

一个audio context会控制所有节点的创建和音频处理解码的执行。所有事情都是在一个上下文中发生的。

ArrayBuffer:音频二进制文件
decodeAudioData:解码
AudioBufferSourceNode:
connect用于连接音频文件
start播放音频
AudioContext.destination:扬声器设备

AudioNode是什么?

  • AudioNode是用于音频处理的一个基类,包括context,numberOfInputs,channelCount,connect
  • 上文讲到的用于连接音频文件的AudioBufferSourceNode继承了AudioNode的connect和start方法
  • 用于设置音量的GainNode也继承于AudioNode
  • 用于连接麦克风设备的MediaElementAudioSourceNode也继承于AudioNode
  • 用于滤波的OscillationNode间接继承于AudioNode
  • 表示音频源信号在空间中的位置和行为的PannerNode也继承于AudioNode
  • AudioListener接口表示听音频场景的唯一的人的位置和方向,并用于音频空间化
  • 上述节点可以通过装饰者模式一层层connect,AudioBufferSourceCode可以先connect到GainNode,GainNode再connect到AudioContext.destination扬声器去调节音量

初见:MediaDevice.getUserMedia()是什么

MediaStream MediaStreamTrack audio track

demo演示:https://github.com/FrankKai/nodejs-rookie/issues/54

<button onclick="record()">开始录音</button>
<script>
function record () {navigator.mediaDevices.getUserMedia({audio: true}).then(mediaStream => {console.log(mediaStream);}).catch(err => {console.error(err);})  ;
}

相识:MediaDevice.getUserMedia()是什么

MediaStream MediaStreamTrack audio track

  • MediaStream接口代表media content stream
  • MediaStreamTrack接口代表的是在一个stream内部的单media track
  • track可以理解为音轨,所以audio track就是音频音轨的意思
  • 提醒用户”是否允许代码获得麦克风的权限“。若拒绝,会报错DOMException: Permission denied;若允许,返回一个由audio track组成的MediaStream,其中包含了audio音轨上的详细信息

为什么localhost能播放,预生产不能播放?

没招了,在stackOverflow提了一个问题

Why navigator.mediaDevice only works fine on localhost:9090?

网友说只能在HTTPS环境做测试。

嗯,生产是HTTPS,可以用。???但是我localhost哪里来的HTTPS环境???所以到底是什么原因?

终于从chromium官方更新记录中找到了答案
https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins

Chrome 47以后,getUserMedia API只能允许来自“安全可信”的客户端的视频音频请求,如HTTPS和本地的Localhost。如果页面的脚本从一个非安全源加载,navigator对象中则没有可用的mediaDevices对象,Chrome抛出错误。

语音功能预生产,预发需要以下配置:
地址栏输入chrome://flags
搜索:insecure origins treated as secure
配置:http://foo.test.gogo.com

生产的https://foo.gogo.com是完全OK的

js中的数据类型TypedArray知多少?

typed array基本知识: TypedArray Buffer ArrayBuffer View Unit8Array Unit16Array Float64Array
  • 用来处理未加工过的二进制数据
  • TypedArray分为buffers和views两种
  • buffer(通过ArrayBuffer类实现)指的是一个数据块对象;buffer没有固定的格式;buffer中的内容是不能访问到的。
  • buffer中内存的访问权限,需要用到view;view提供了一个上下文(包括数据类型,初始位置,元素数量),这个上下文将数据转换为typed array

https://github.com/FrankKai/FrankKai.github.io/issues/164

typed array使用例子
// 创建一个16字节定长的buffer
let buffer = new ArrayBuffer(16);

处理音频数据前置知识点

struct someStruct {unsigned long id; // long 32bitchar username[16];// char 8bitfloat amountDue;// float 32bit
};
let buffer = new ArrayBuffer(24);
// ... read the data into the buffer ...
let idView = new Uint32Array(buffer, 0, 1);
let usernameView = new Uint8Array(buffer, 4, 16);
let amountDueView = new Float32Array(buffer, 20, 1);

偏移量为什么是1,4,20?
因为32/8 = 4。0到3属于idView。8/8 =1。4到19属于usernameView。32/8 = 4。20到23属于amountView。

代码实现及源码分析

一、代码实现

流程图:1.初始化recorder 2.开始录音 3.停止录音

设计思路:录音器,录音器助手,语音构造器,语音转换器

二、尝试过的技术方案

1.人人网某前端开发

https://juejin.im/post/5b8bf7e3e51d4538c210c6b0

无法灵活指定采样位数,采样频率和声道数;不能输出多种格式的音频;因此弃用。

2.js-audio-recorder

可以灵活指定采样位数,采样频率和声道数;可输出多种格式的音频;提供多种易用的API。

github地址:https://github.com/2fps/recorder

没学过语音相关的知识,因此只能参考前辈的成果边学边做!

代码实现及源码分析

一、录音过程拆解

1.初始化录音实例

initRecorderInstance() {// 采样相关const sampleConfig = {sampleBits: 16, // 采样位数,讯飞实时语音转写 16bitssampleRate: 16000, // 采样率,讯飞实时语音转写 16000kHznumChannels: 1, // 声道,讯飞实时语音转写 单声道};this.recorderInstance = new Recorder(sampleConfig);
},

2.开始录音

startRecord() {try {this.recorderInstance.start();// 回调持续输出时长this.recorderInstance.onprocess = (duration) => {this.recorderHelper.duration = duration;};} catch (err) {this.$debug(err);}
},

3.停止录音

stopRecord() {this.recorderInstance.stop();this.recorder.blobObjMP3 = new Blob([this.recorderInstance.getWAV()], { type: 'audio/mp3' });this.recorder.blobObjPCM = this.recorderInstance.getPCMBlob();this.recorder.blobUrl = URL.createObjectURL(this.recorder.blobObjMP3);if (this.audioAutoTransfer) {this.$refs.audio.onloadedmetadata = () => {this.audioXFTransfer();};}
},
二、设计思路
  • 录音器实例recorderInstance

    • js-audio-recorder
  • 录音器助手RecorderHelper
    • blobUrl,blobObjPCM,blobObjMP3
    • hearing,tip,duration
  • 编辑器Editor
    • transfered,tip,loading
  • 语音器Audio
    • urlPC,urlMobile,size
  • 转换器Transfer
    • text
三、源码分析之初始化实例-constructor
/*** @param {Object} options 包含以下三个参数:* sampleBits,采样位数,一般8,16,默认16* sampleRate,采样率,一般 11025、16000、22050、24000、44100、48000,默认为浏览器自带的采样率* numChannels,声道,1或2*/
constructor(options: recorderConfig = {}) {// 临时audioContext,为了获取输入采样率的let context = new (window.AudioContext || window.webkitAudioContext)();this.inputSampleRate = context.sampleRate;     // 获取当前输入的采样率// 配置config,检查值是否有问题this.config = {// 采样数位 8, 16sampleBits: ~[8, 16].indexOf(options.sampleBits) ? options.sampleBits : 16,// 采样率sampleRate: ~[11025, 16000, 22050, 24000, 44100, 48000].indexOf(options.sampleRate) ? options.sampleRate : this.inputSampleRate,// 声道数,1或2numChannels: ~[1, 2].indexOf(options.numChannels) ? options.numChannels : 1,};// 设置采样的参数this.outputSampleRate = this.config.sampleRate;     // 输出采样率this.oututSampleBits = this.config.sampleBits;      // 输出采样数位 8, 16// 判断端字节序this.littleEdian = (function() {var buffer = new ArrayBuffer(2);new DataView(buffer).setInt16(0, 256, true);return new Int16Array(buffer)[0] === 256;})();
}

new DataView(buffer).setInt16(0, 256, true)怎么理解?

控制内存存储的大小端模式。
true是littleEndian,也就是小端模式,地位数据存储在低地址,Int16Array uses the platform’s endianness。

所谓大端模式,指的是低位数据高地址,0x12345678,12存buf[0],78(低位数据)存buf[3](高地址)。也就是常规的正序存储。
小端模式与大端模式相反。0x12345678,78存在buf[0],存在低地址。

三.源码分析之初始化实例-initRecorder
/** * 初始化录音实例*/
initRecorder(): void {if (this.context) {// 关闭先前的录音实例,因为前次的实例会缓存少量数据this.destroy();}this.context = new (window.AudioContext || window.webkitAudioContext)();this.analyser = this.context.createAnalyser();  // 录音分析节点this.analyser.fftSize = 2048;                   // 表示存储频域的大小// 第一个参数表示收集采样的大小,采集完这么多后会触发 onaudioprocess 接口一次,该值一般为1024,2048,4096等,一般就设置为4096// 第二,三个参数分别是输入的声道数和输出的声道数,保持一致即可。let createScript = this.context.createScriptProcessor || this.context.createJavaScriptNode;this.recorder = createScript.apply(this.context, [4096, this.config.numChannels, this.config.numChannels]);// 兼容 getUserMediathis.initUserMedia();// 音频采集this.recorder.onaudioprocess = e => {if (!this.isrecording || this.ispause) {// 不在录音时不需要处理,FF 在停止录音后,仍会触发 audioprocess 事件return;} // getChannelData返回Float32Array类型的pcm数据if (1 === this.config.numChannels) {let data = e.inputBuffer.getChannelData(0);// 单通道this.buffer.push(new Float32Array(data));this.size += data.length;} else {/** 双声道处理* e.inputBuffer.getChannelData(0)得到了左声道4096个样本数据,1是右声道的数据,* 此处需要组和成LRLRLR这种格式,才能正常播放,所以要处理下*/let lData = new Float32Array(e.inputBuffer.getChannelData(0)),rData = new Float32Array(e.inputBuffer.getChannelData(1)),// 新的数据为左声道和右声道数据量之和buffer = new ArrayBuffer(lData.byteLength + rData.byteLength),dData = new Float32Array(buffer),offset = 0;for (let i = 0; i < lData.byteLength; ++i) {dData[ offset ] = lData[i];offset++;dData[ offset ] = rData[i];offset++;}this.buffer.push(dData);this.size += offset;}// 统计录音时长this.duration += 4096 / this.inputSampleRate;// 录音时长回调this.onprocess && this.onprocess(this.duration);}
}
三.源码分析之开始录音-start
/*** 开始录音** @returns {void}* @memberof Recorder*/
start(): void {if (this.isrecording) {// 正在录音,则不允许return;}// 清空数据this.clear();this.initRecorder();this.isrecording = true;navigator.mediaDevices.getUserMedia({audio: true}).then(stream => {// audioInput表示音频源节点// stream是通过navigator.getUserMedia获取的外部(如麦克风)stream音频输出,对于这就是输入this.audioInput = this.context.createMediaStreamSource(stream);}, error => {// 抛出异常Recorder.throwError(error.name + " : " + error.message);}).then(() => {// audioInput 为声音源,连接到处理节点 recorderthis.audioInput.connect(this.analyser);this.analyser.connect(this.recorder);// 处理节点 recorder 连接到扬声器this.recorder.connect(this.context.destination);});
}
三.源码分析之停止录音及辅助函数
/*** 停止录音** @memberof Recorder*/
stop(): void {this.isrecording = false;this.audioInput && this.audioInput.disconnect();this.recorder.disconnect();
}// 录音时长回调
this.onprocess && this.onprocess(this.duration);
/*** 获取WAV编码的二进制数据(dataview)** @returns {dataview}  WAV编码的二进制数据* @memberof Recorder*/
private getWAV() {let pcmTemp = this.getPCM(),wavTemp = Recorder.encodeWAV(pcmTemp, this.inputSampleRate, this.outputSampleRate, this.config.numChannels, this.oututSampleBits, this.littleEdian);return wavTemp;
}
/*** 获取PCM格式的blob数据** @returns { blob }  PCM格式的blob数据* @memberof Recorder*/
getPCMBlob() {return new Blob([ this.getPCM() ]);
}
/*** 获取PCM编码的二进制数据(dataview)** @returns {dataview}  PCM二进制数据* @memberof Recorder*/
private getPCM() {// 二维转一维let data = this.flat();// 压缩或扩展data = Recorder.compress(data, this.inputSampleRate, this.outputSampleRate);// 按采样位数重新编码return Recorder.encodePCM(data, this.oututSampleBits, this.littleEdian);
}
四.源码分析之核心算法-encodeWAV
static encodeWAV(bytes: dataview, inputSampleRate: number, outputSampleRate: number, numChannels: number, oututSampleBits: number, littleEdian: boolean = true) {let sampleRate = Math.min(inputSampleRate, outputSampleRate),sampleBits = oututSampleBits,buffer = new ArrayBuffer(44 + bytes.byteLength),data = new DataView(buffer),channelCount = numChannels, // 声道offset = 0;// 资源交换文件标识符writeString(data, offset, 'RIFF'); offset += 4;// 下个地址开始到文件尾总字节数,即文件大小-8data.setUint32(offset, 36 + bytes.byteLength, littleEdian); offset += 4;// WAV文件标志writeString(data, offset, 'WAVE'); offset += 4;// 波形格式标志writeString(data, offset, 'fmt '); offset += 4;// 过滤字节,一般为 0x10 = 16data.setUint32(offset, 16, littleEdian); offset += 4;// 格式类别 (PCM形式采样数据)data.setUint16(offset, 1, littleEdian); offset += 2;// 声道数data.setUint16(offset, channelCount, littleEdian); offset += 2;// 采样率,每秒样本数,表示每个通道的播放速度data.setUint32(offset, sampleRate, littleEdian); offset += 4;// 波形数据传输率 (每秒平均字节数) 声道数 × 采样频率 × 采样位数 / 8data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), littleEdian); offset += 4;// 快数据调整数 采样一次占用字节数 声道数 × 采样位数 / 8data.setUint16(offset, channelCount * (sampleBits / 8), littleEdian); offset += 2;// 采样位数data.setUint16(offset, sampleBits, littleEdian); offset += 2;// 数据标识符writeString(data, offset, 'data'); offset += 4;// 采样数据总数,即数据总大小-44data.setUint32(offset, bytes.byteLength, littleEdian); offset += 4;// 给wav头增加pcm体for (let i = 0; i < bytes.byteLength;) {data.setUint8(offset, bytes.getUint8(i));offset++;i++;}return data;
}
四.源码分析之核心算法-encodePCM
/*** 转换到我们需要的对应格式的编码* * @static* @param {float32array} bytes      pcm二进制数据* @param {number}  sampleBits      采样位数* @param {boolean} littleEdian     是否是小端字节序* @returns {dataview}              pcm二进制数据* @memberof Recorder*/
static encodePCM(bytes, sampleBits: number, littleEdian: boolean = true)  {let offset = 0,dataLength = bytes.length * (sampleBits / 8),buffer = new ArrayBuffer(dataLength),data = new DataView(buffer);// 写入采样数据if (sampleBits === 8) {for (var i = 0; i < bytes.length; i++, offset++) {// 范围[-1, 1]var s = Math.max(-1, Math.min(1, bytes[i]));// 8位采样位划分成2^8=256份,它的范围是0-255; // 对于8位的话,负数*128,正数*127,然后整体向上平移128(+128),即可得到[0,255]范围的数据。var val = s < 0 ? s * 128 : s * 127;val = +val + 128;data.setInt8(offset, val);}} else {for (var i = 0; i < bytes.length; i++, offset += 2) {var s = Math.max(-1, Math.min(1, bytes[i]));// 16位的划分的是2^16=65536份,范围是-32768到32767// 因为我们收集的数据范围在[-1,1],那么你想转换成16位的话,只需要对负数*32768,对正数*32767,即可得到范围在[-32768,32767]的数据。data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, littleEdian);}}return data;
}

语音发送和实时转写

  • 音频文件存哪里?
  • Blob Url那些事儿
  • 实时语音转写服务服务端需要做什么?
  • 前端代码实现

音频文件存哪里?

语音录一次往阿里云OSS传一次吗?

这样做显示是浪费资源的。

编辑状态:存本地,当前浏览器端可访问即可

发送状态:存OSS,公网可访问

如何存本地?Blob Url的方式保存

如何存OSS?从cms获取token,上传到OSS的xxx-audio bucket,然后得到一个hash

Blob Url那些事儿

Blob Url长什么样?blob:http://localhost:9090/39b60422-26f4-4c67-8456-7ac3f29115ec

blob对象在前端开发中是非常常见的,下面我将列举几个应用场景:

canvas toDataURL后的base64格式属性,会超出标签属性值有最大长度的限制
<input type="file" />上传文件之后的File对象,最初只想在本地留存,时机合适再上传到服务器

创建BlobUrl:URL.createObjectURL(object)

释放BlobUrl:URL.revokeObjectURL(objectURL)

Blob Url那些事儿

URL的生命周期在vue组件中如何表现?

vue的单文件组件共有一个document,这也是它被称为单页应用的原因,因此可以在组件间直接通过blob URL进行通信。
在vue-router采用hash模式的情况下,页面间的路由跳转,不会重新加载整个页面,所以URL的生命周期非常强力,因此在跨页面(非新tab)的组件通信,也可以使用blob URL。
需要注意的是,在vue的hash mode模式下,需要更加注意通过URL.revokeObjectURL()进行的内存释放

<!--组件发出blob URL-->
<label for="background">上传背景</label>
<input type="file" style="display: none"id="background" name="background"accept="image/png, image/jpeg" multiple="false"@change="backgroundUpload"
>
backgroundUpload(event) {const fileBlobURL = window.URL.createObjectURL(event.target.files[0]);this.$emit('background-change', fileBlobURL);// this.$bus.$emit('background-change', fileBlobURL);
},<!--组件接收blob URL-->
<BackgroundUploader @background-change="backgroundChangeHandler"></BackgroundUploader>
// this.$bus.$on("background-change", backgroundChangeHandler);
backgroundChangeHandler(url) {// some code handle blob url...
},

URL的生命周期在vue组件中如何表现?

vue的单文件组件共有一个document,这也是它被称为单页应用的原因,因此可以在组件间直接通过blob URL进行通信。
在vue-router采用hash模式的情况下,页面间的路由跳转,不会重新加载整个页面,所以URL的生命周期非常强力,因此在跨页面(非新tab)的组件通信,也可以使用blob URL。
需要注意的是,在vue的hash mode模式下,需要更加注意通过URL.revokeObjectURL()进行的内存释放

https://github.com/FrankKai/FrankKai.github.io/issues/138

实时语音转写服务服务端需要做什么?

提供一个传递存储音频Blob对象的File实例返回文字的接口。

this.recorder.blobObjPCM = this.recorderInstance.getPCMBlob();transferAudioToText() {this.editor.loading = true;const formData = new FormData();const file = new File([this.recorder.blobObjPCM], `${+new Date()}`, { type: this.recorder.blobObjPCM.type });formData.append('file', file);apiXunFei.realTimeVoiceTransliterationByFile(formData).then((data) => {this.xunfeiTransfer.text = data;this.editor.tip = '发送文字';this.editor.transfered = !this.editor.transfered;this.editor.loading = false;}).catch(() => {this.editor.loading = false;this.$Message.error('转写语音失败');});
},
/**
* 获取PCM格式的blob数据
*
* @returns { blob }  PCM格式的blob数据
* @memberof Recorder
*/
getPCMBlob() {return new Blob([ this.getPCM() ]);
}

服务端需要如何实现呢?

1.鉴权

客户端在与服务端建立WebSocket链接的时候,需要使用Token进行鉴权

2.start and confirm

客户端发起请求,服务端确认请求有效

3.send and recognize

循环发送语音数据,持续接收识别结果

  1. stop and complete
    通知服务端语音数据发送完成,服务端识别结束后通知客户端识别完毕

阿里OSS提供了java,python,c++,ios,android等SDK

https://help.aliyun.com/document_detail/84428.html?spm=a2c4g.11186623.6.574.10d92318ApT1T6

前端代码实现

// 发送语音
async audioSender() {const audioValidated = await this.audioValidator();if (audioValidated) {this.audio.urlMobile = await this.transferMp3ToAmr(this.recorder.blobObjMP3);const audioBase64Str = await this.transferBlobFileToBase64(this.recorder.blobObjMP3);this.audio.urlPC = await this.uploadAudioToOSS(audioBase64Str);this.$emit('audio-sender', {audioPathMobile: this.audio.urlMobile,audioLength: parseInt(this.$refs.audio.duration * 1000),transferredText: this.xunfeiTransfer.text,audioPathPC: this.audio.urlPC,});this.closeSmartAudio();}
},// 生成移动端可以发送的amr格式音频
transferMp3ToAmr() {const formData = new FormData();const file = new File([this.recorder.blobObjMP3], `${+new Date()}`, { type: this.recorder.blobObjMP3.type });formData.append('file', file);return new Promise((resolve) => {apiXunFei.mp32amr(formData).then((data) => {resolve(data);}).catch(() => {this.$Message.error('mp3转换amr格式失败');});});
},// 转换Blob对象为Base64 string,以供上传OSS
async transferBlobFileToBase64(file) {return new Promise((resolve) => {const reader = new FileReader();reader.readAsDataURL(file);reader.onloadend = function onloaded() {const fileBase64 = reader.result;resolve(fileBase64);};});
},

通用录音组件

1.指定采样位数,采样频率,声道数2.指定音频格式3.指定音频计算单位Byte,KB,MB4.自定义开始和停止来自iView的icon,类型、大小5.返回音频blob,音频时长和大小6.指定最大音频时长和音频大小, 达到二者其一自动停止录制
通用组件代码分析
/** * 设计思路:* * 使用到的库:js-audio-recorder* * 功能:* * 1.指定采样位数,采样频率,声道数* * 2.指定音频格式* * 3.指定音频计算单位Byte,KB,MB* * 4.自定义开始和停止来自iView的icon,类型、大小* * 5.返回音频blob,音频时长和大小* * 6.指定最大音频时长和音频大小, 达到二者其一自动停止录制* * Author: 高凯* * Date: 2019.11.7*/<template><div class="audio-maker-container"><Icon :type="computedRecorderIcon" @click="recorderVoice" :size="iconSize" /></div>
</template><script>
import Recorder from 'js-audio-recorder';
/** js-audio-recorder实例* 在这里新建的原因在于无需对recorderInstance在当前vue组件上创建多余的watcher,避免性能浪费*/
let recorderInstance = null;
/*
* 录音器助手
* 做一些辅助录音的工作,例如记录录制状态,音频时长,音频大小等等
*/
const recorderHelperGenerator = () => ({hearing: false,duration: 0,size: 0,
});
export default {name: 'audio-maker',props: {sampleBits: {type: Number,default: 16,},sampleRate: {type: Number,},numChannels: {type: Number,default: 1,},audioType: {type: String,default: 'audio/wav',},startIcon: {type: String,default: 'md-arrow-dropright-circle',},stopIcon: {type: String,default: 'md-pause',},iconSize: {type: Number,default: 30,},sizeUnit: {type: String,default: 'MB',validator: (unit) => ['Byte', 'KB', 'MB'].includes(unit),},maxDuration: {type: Number,default: 10 * 60,},maxSize: {type: Number,default: 1,},},mounted() {this.initRecorderInstance();},beforeDestroy() {recorderInstance = null;},computed: {computedSampleRate() {const audioContext = new (window.AudioContext || window.webkitAudioContext)();const defaultSampleRate = audioContext.sampleRate;return this.sampleRate ? this.sampleRate : defaultSampleRate;},computedRecorderIcon() {return this.recorderHelper.hearing ? this.stopIcon : this.startIcon;},computedUnitDividend() {const sizeUnit = this.sizeUnit;let unitDividend = 1024 * 1024;switch (sizeUnit) {case 'Byte':unitDividend = 1;break;case 'KB':unitDividend = 1024;break;case 'MB':unitDividend = 1024 * 1024;break;default:unitDividend = 1024 * 1024;}return unitDividend;},computedMaxSize() {return this.maxSize * this.computedUnitDividend;},},data() {return {recorderHelper: recorderHelperGenerator(),};},watch: {'recorderHelper.duration': {handler(duration) {if (duration >= this.maxDuration) {this.stopRecord();}},},'recorderHelper.size': {handler(size) {if (size >= this.computedMaxSize) {this.stopRecord();}},},},methods: {initRecorderInstance() {// 采样相关const sampleConfig = {sampleBits: this.sampleBits, // 采样位数sampleRate: this.computedSampleRate, // 采样频率numChannels: this.numChannels, // 声道数};recorderInstance = new Recorder(sampleConfig);},recorderVoice() {if (!this.recorderHelper.hearing) {// 录音前重置录音状态this.reset();this.startRecord();} else {this.stopRecord();}this.recorderHelper.hearing = !this.recorderHelper.hearing;},startRecord() {try {recorderInstance.start();// 回调持续输出时长recorderInstance.onprogress = ({ duration }) => {this.recorderHelper.duration = duration;this.$emit('on-recorder-duration-change', parseFloat(this.recorderHelper.duration.toFixed(2)));};} catch (err) {this.$debug(err);}},stopRecord() {recorderInstance.stop();const audioBlob = new Blob([recorderInstance.getWAV()], { type: this.audioType });this.recorderHelper.size = (audioBlob.size / this.computedUnitDividend).toFixed(2);this.$emit('on-recorder-finish', { blob: audioBlob, size: parseFloat(this.recorderHelper.size), unit: this.sizeUnit });},reset() {this.recorderHelper = recorderHelperGenerator();},},
};
</script><style lang="scss" scoped>
.audio-maker-container {display: inline;i.ivu-icon {cursor: pointer;}
}
</style>

https://github.com/2fps/recorder/issues/21

通用组件使用
import AudioMaker from '@/components/audioMaker';
<AudioMakerv-if="!recorderAudio.blobUrl"@on-recorder-duration-change="durationChange"@on-recorder-finish="recorderFinish":maxDuration="audioMakerConfig.maxDuration":maxSize="audioMakerConfig.maxSize":sizeUnit="audioMakerConfig.sizeUnit"
></AudioMaker>durationChange(duration) {this.resetRecorderAudio();this.recorderAudio.duration = duration;
},
recorderFinish({ blob, size, unit }) {this.recorderAudio.blobUrl = window.URL.createObjectURL(blob);this.recorderAudio.size = size;this.recorderAudio.unit = unit;
},
releaseBlobMemory(blorUrl) {window.URL.revokeObjectURL(blorUrl);
},

总结

前端语音转文字实践总结相关推荐

  1. 前端js实现asr(语音转文字)

    <!--* @Author: xss 995550359@qq.com* @Date: 2022-09-23 16:40:16* @LastEditors: xss 995550359@qq.c ...

  2. 前端代码标准最佳实践:HTML篇

    Web前端代码中,HTML是根本,CSS和JavaScript也是围绕着既有的HTML结构来构建,所以良好的HTML代码结构,除了提高了HTML代码的可读性,可维护性和执行性能之外,也可以让相对应的C ...

  3. 前端技术演进(六):前端项目与技术实践

    这个来自之前做的培训,删减了一些业务相关的,参考了很多资料(参考资料列表),谢谢前辈们,么么哒 ? 任何五花八门的技术,最终还是要在实践中落地.现代的软件开发,大部分讲求的不是高难度高精尖,而是效率和 ...

  4. 语音自动识别文字软件

    广告关闭 腾讯云双11爆品提前享,精选热门产品助力上云,云服务器首年88元起,买的越多返的越多,最高满返5000元! 腾讯云语音识别(asr) 为开发者提供语音转文字服务的最佳体验. 语音识别服务具备 ...

  5. iOS即时语音聊天技术实践

    CMDN Club第十五期活动已于3月15日顺利举行,本次活动以"移动平台语音技术的应用与实践"主题,以语音技术开发为焦点,从语音基础服务.语音产品开发.语音技术实现等多个维度,探 ...

  6. 纯前端大数据处理技术:葡萄城纯前端开发工具应用实践

    SpreadJS 是一款基于 HTML5 的纯 JavaScript 电子表格和网格功能控件,满足多平台.跨平台的表格数据处理和类 Excel 的表格应用开发. WijmoJS 前端开发工具包由多款纯 ...

  7. 免费不限时长的语音转文字软件——Word365

    适用场景 想将语音转化成文字. 这里的语音可以是实时输入,也可以是已有音.视频转换成文字. 后者的操作比前者多一步操作. 1.实时语音转文字 可以直接打开Word365,[开始]选项卡中的[听写]功能 ...

  8. Buzz语音转文字安装使用(含Whisper模型下载)

    简介: Transcribe and translate audio offline on your personal computer. Powered by OpenAI's Whisper. 转 ...

  9. 课堂笔记如何高效完成!学会使用录音转文字助手,轻松语音转文字

    现在已经是9月6日了,相信大家已经进入了学习状态,但是想到记不完的笔记是不是已经很头疼的事情,今天小编给大家分享一个能快速记录课堂笔记的办法,那就是利用语音转文字的方法进行快速记录,下面小编就介绍下详 ...

  10. 面向前端的设计规范-文字初探-Part1

    在最初CSS还没有流行起来的时候,网页就是类似以 Word 的文本样式呈现的.去掉各种花里胡哨的样式,和装饰图片之后,留下的文字才是网页本来的样子,而这才是我们真正想要对用户说的话.文字几乎是网页的灵 ...

最新文章

  1. PL/SQL:使用pragma restrict_references限制包权限
  2. 编程软件python图片-python Plotly绘图工具的简单使用
  3. Screened Poisson Surface Reconstruction
  4. [BeiJing2010组队]次小生成树 Tree
  5. Ubuntu 配置 Go 语言开发环境(Sublime Text+GoSublime)
  6. gitlab基本工作原理
  7. 有望支撑半年时间!华为麒麟9000芯片库存约为1000万片
  8. 我选择的是一种生活态度
  9. java文件格式转换
  10. Codeforces Round #319 (Div. 2)B. Modulo Sum DP
  11. linux nvidia 361.run,Ubuntu 16.04安装nVidia驱动失败!
  12. 企业中常见的推荐系统架构(附交流视频和PPT下载链接)
  13. 驰骋工作流程引擎在流程设计发生变化后如何处理?
  14. mysql安装包5.7.17.0_mysql-5.7.17-winx64压缩版的安装包下载和安装配置
  15. 联想拯救者Y7000P的一些功能键
  16. 怎么把黑白照片还原成彩色?三个方法让你一键搞定黑白照片上色
  17. redis好用的界面管理工具分享
  18. 恶意访问、黑产猖獗,如何做好业务安全“守门人”?丨创新场景50
  19. java烟弹,java电子烟是啥牌子
  20. 【批处理DOS-CMD命令-汇总和小结】-上网和网络通信相关命令-用户账户管理-文件(夹)共享(net)

热门文章

  1. Raid5 Raid10 磁盘IOPS 计算方法
  2. 用java编写英寸到厘米的转换_像素、英寸、厘米的换算 - flyinglife - JavaEye技术网站...
  3. 背包问题(Knapsack Problem)—— 完全背包问题 —— (1)背包价值最大
  4. 计算机光驱故障分析,光驱故障示例解决
  5. Java中把word转换成图片
  6. 分数的表示、化简以及输出
  7. win7计算机相机,笔记本win7怎么拍照_win7电脑照相机如何打开
  8. 什么计算机有hdmi接口,hdmi接口是什么?hdmi是什么?
  9. python大神年薪_我程序员年薪 80 万被亲戚鄙视不如在二本教书的博士生?
  10. 为什么浏览器全面禁用三方 Cookie