vscode语音正文, 让信息更丰盛 (下)
前言
这个系列的最初一篇, 次要讲述录制音频&音频文件存储相干常识, 过后因为录音有bug搞得我一周没情绪吃饭(voice-annotation)。
一、MP3
文件贮存地位
"语音正文"应用场景
- 单个我的项目应用"语音正文"。
- 多个我的项目应用"语音正文"。
- "语音正文"生成的
mp3
文件都放在本人我的项目中。 - "语音正文"生成的
mp3
文件对立寄存在全局的某处。 - "语音正文"生成的
mp3
一部分存在我的项目中一部分应用全局门路。
vscode 工作区
具体音频贮存在哪里必定要读取用户的配置, 但如果用户只在全局配置了一个门路, 那么这个门路无奈满足每个我的项目寄存音频文件的地位不同的场景, 这时候就引出了vscode 工作区
的概念。
如果咱们每个工程的eslint
规定各不相同, 此时咱们只在全局配置eslint
规定就无奈满足这个场景了, 此时咱们须要在我的项目中新建一个.vscode
文件夹, 在其中建设settings.json
文件, 在这个文件内编写的配置就是针对以后我的项目的个性化配置了。
配置工作区 (绝对路径 or
相对路径)
尽管懂了工作区的概念, 然而还不能解决实际上的问题, 比方咱们在工作区配置音频文件的绝对路径
, 那么.vscode > settings.json
文件是要上传到代码仓库的, 所以配置会被所有人拉到, 每个开发者的电脑系统可能不一样, 寄存我的项目的文件夹地位也不一样, 所以在工作区定义绝对路径
不能解决团队合作问题。
假若用户配置了相对路径
, 并且这个门路是绝对于以后的settings.json
文件本身的, 那么问题变成了如何晓得settings.json
文件到底在哪? vscode插件
外部尽管能够读取到工作区的配置信息, 然而读不到settings.json
文件的地位。
settings.json
文件寻踪
我最开始想过每次录音完结后, 让用户手动抉择一个寄存音频文件的地位, 但显然这个形式在操作上不够简洁, 在一次跑步的时候我忽然想到, 其实用户想要录制音频的时候必定要点击某处触发录音性能, vscode
内提供了办法去获取用户触发命令时所在文件的地位。
那我就以用户触发命令的文件地位为启点, 进行逐级的搜查.vscode
文件, 比方获取到用户在/xxx1/xxx2/xxx3.js
文件外部点击了录制音频正文
, 则我就先判断/xxx1/xxx2/.vscode
是否为文件夹, 如果不是则判断/xxx1/.vscode
是否为文件夹, 顺次类推直到找到.vscode
文件夹的地位, 如果没找到则报错。
音频文件夹门路的校验
应用settings.json
文件的地位加上用户配置的相对路径
, 则可得出真正的音频贮存地位, 此时也不能松散须要测验一下失去的文件夹门路是否真的有文件夹, 这里并不会被动为用户创立文件夹。
此时还有可能出问题, 如果以后有个a我的项目
外部套了个b我的项目
, 然而想要在b我的项目
里录制音频, 可是b我的项目
内未设置.vscode 工作区文件夹
, 然而a我的项目里有.vscode > settings.json
, 那么此时会导致将b我的项目
的录音文件贮存到a我的项目
中。
上述问题没法精确的测验出用户的实在指标门路, 那我想到的方法是录制音频页面内预展现出将要保留到的门路, 让用户来做最初的守门人:
以后插件繁难用户配置:
{ "voiceAnnotation": { "dirPath": "../mp3" }}
二、配置的定义
如果用户不想把音频文件贮存在我的项目内, 怕本人的我的项目变大起来, 那咱们反对独自做一个音频寄存的我的项目, 此时就须要在全局配置一个绝对路径
, 因为全局的配置不会同步给其余开发者, 当咱们获取不到用户在vscode工作区
定义的音频门路时, 咱们就取全局门路的值, 上面咱们就一起配置一下全局的属性:
package.json
新增全局配置设定:
"contributes": "configuration": { "type": "object", "title": "语音正文配置", "properties": { "voiceAnnotation.globalDirPath": { "type": "string", "default": "", "description": "语音正文文件的'绝对路径' (优先级低于工作空间的voiceAnnotation.dirPath)。" }, "voiceAnnotation.serverProt": { "type": "number", "default": 8830, "description": "默认值为8830" } } } },
具体每个属性的意义能够参考配置后的效果图:
三、获取音频文件夹地位的办法
util/index.ts
(上面有具体的办法解析):
export function getVoiceAnnotationDirPath() { const activeFilePath: string = vscode.window.activeTextEditor?.document?.fileName ?? ""; const voiceAnnotationDirPath: string = vscode.workspace.getConfiguration().get("voiceAnnotation.dirPath") || ""; const workspaceFilePathArr = activeFilePath.split(path.sep) let targetPath = ""; for (let i = workspaceFilePathArr.length - 1; i > 0; i--) { try { const itemPath = `${path.sep}${workspaceFilePathArr.slice(1, i).join(path.sep)}${path.sep}.vscode`; fs.statSync(itemPath).isDirectory(); targetPath = itemPath; break } catch (_) { } } if (voiceAnnotationDirPath && targetPath) { return path.resolve(targetPath, voiceAnnotationDirPath) } else { const globalDirPath = vscode.workspace .getConfiguration() .get("voiceAnnotation.globalDirPath"); if (globalDirPath) { return globalDirPath as string } else { getVoiceAnnotationDirPathErr() } }}function getVoiceAnnotationDirPathErr() { vscode.window.showErrorMessage(`请于 .vscode/setting.json 内设置 "voiceAnnotation": { "dirPath": "音频文件夹的相对路径" }`)}
逐句解析
1: 获取激活地位
vscode.window.activeTextEditor?.document?.fileName
上述办法能够获取到你以后触发命令所在的文件地位, 例如你在a.js
外部点击右键, 在菜单中点击了某个选项, 此时应用上述办法就会获取到a.js
文件的绝对路径
, 当然不只是操作菜单, 所有命令包含hover
某段文字都能够调用这个办法获取文件地位。
2: 获取配置项
vscode.workspace.getConfiguration().get("voiceAnnotation.dirPath") || ""; vscode.workspace.getConfiguration().get("voiceAnnotation.globalDirPath");
上述办法不仅能够获取我的项目中.vscode > settings.json
文件的配置, 并且也是获取全局配置的办法, 所以咱们要做好辨别能力去应用哪个, 所以这里我命名为dirPath
与globalDirPath
。
3: 文件门路宰割符
/xxx/xx/x.js
其中的 "/" 就是path.sep
, 因为mac或者window等零碎外面是有差别的, 这里应用path.sep
是为了兼容其余零碎的用户。
4: 报错
相对路径与绝对路径都获取不到就抛出报错:
vscode.window.showErrorMessage(错误信息)
5: 应用
第一是用在server保留音频时, 第二是关上web页面时会传递给前端用户显示保留门路。
四、录音初始常识
没应用过录音性能的同学你可能没见过navigator.mediaDevices
这个办法, 返回一个MediaDevices
对象,该对象可提供对相机和麦克风等媒体输出设施的连贯拜访,也包含屏幕共享。
录制音频须要先获取用户的许可, navigator.mediaDevices.getUserMedia
就是在获取用户许可胜利并且设施可用时走胜利回调。
navigator.mediaDevices.getUserMedia({audio:true}).then((stream)=>{ // 因为咱们输出的是{audio:true}, 则stream是音频的内容流}).carch((err)=>{})
五、初始化录音设施与配置
上面展现的是定义播放标签以及环境的'初始化', 老样子先上代码, 然你后逐句解释:
<header> <audio id="audio" controls></audio> <audio id="replayAudio" controls></audio> </header>
let audioCtx = {} let processor; let userMediStream; navigator.mediaDevices.getUserMedia({ audio: true }) .then(function (stream) { userMediStream = stream; audio.srcObject = stream; audio.onloadedmetadata = function (e) { audio.muted = true; }; }) .catch(function (err) { console.log(err); });
1: 发现乏味的事, 间接用id获取元素
2: 保留音频的内容流
这里将媒体源保留在全局变量上, 不便后续重播声音:
userMediStream = stream;
srcObject
属性指定<audio>标签
关联的'媒体源':
audio.srcObject = stream;
3: 监听数据变动
当载入实现时设置 audio.muted = true;
, 将设施静音解决, 录制音频为啥还要静音? 其实是因为录音的时候不须要同时播放咱们的声音, 这会导致"回音"很重, 所以这里须要静音。
audio.onloadedmetadata = function (e) { audio.muted = true;};
六、开始录音
先为'开始录制'按钮增加点击事件:
const oAudio = document.getElementById("audio"); let buffer = []; oStartBt.addEventListener("click", function () { oAudio.srcObject = userMediStream; oAudio.play(); buffer = []; const options = { mimeType: "audio/webm" }; mediaRecorder = new MediaRecorder(userMediStream, options); mediaRecorder.ondataavailable = handleDataAvailable; mediaRecorder.start(10); });
解决获取到的音频数据
function handleDataAvailable(e) { if (e && e.data && e.data.size > 0) { buffer.push(e.data); } }
oAudio.srcObject
定义了播放标签的'媒体源'。oAudio.play();
开始播放, 这里因为咱们设置了muted = true
静音, 所以这里就是开始录音。buffer
是用来贮存音频数据的, 每次录制须要清空一下上次的残留。new MediaRecorder
创立了一个对指定的 MediaStream 进行录制的 MediaRecorder 对象, 也就是说这个办法就是为了录制性能而存在的, 它的第二个参数能够输出指定的mimeType
类型, 具体的类型我在MDN上查了一下。mediaRecorder.ondataavailable
定义了针对每段音频数据的具体解决逻辑。mediaRecorder.start(10);
对音频进行10毫秒所有片, 音频信息是贮存在Blob里的, 这里的配置我了解是每10毫秒生成一个Blob对象。
此时数组buffer
外面就能够继续一直的收集到咱们的音频信息了, 至此咱们实现了录音性能, 接下来咱们要丰盛它的性能了。
七、完结, 重播, 重录
1: 完结录音
录音当然要有个止境了, 有同学提出是否须要限度音频的长短或大小? 但我感觉具体的限度规定还是每个团队本人来定制吧, 这一版我这边只提供外围性能。
const oEndBt = document.getElementById("endBt"); oEndBt.addEventListener("click", function () { oAudio.pause(); oAudio.srcObject = null; });
- 点击
录制完结
按钮,oAudio.pause()
进行标签播放。 oAudio.srcObject = null;
切断媒体源, 这样这个标签无奈持续取得音频数据了。
2: 重播录音
每次用完牙线都可能会忍不住闻一下(不堪回首), 录好的音频当然也要会听一遍成果才行啦:
const oReplayBt = document.getElementById("replayBt"); const oReplayAudio = document.getElementById("replayAudio"); oReplayBt.addEventListener("click", function () { let blob = new Blob(buffer, { type: "audio/webm" }); oReplayAudio.src = window.URL.createObjectURL(blob); oReplayAudio.play(); });
Blob
一种数据的贮存模式, 咱们实现纯前端生成excel
就是应用了blob
, 能够简略了解为第一个参数是文件的数据, 第二个参数能够定义文件的类型。window.URL.createObjectURL
参数是'资源数据', 此办法生成一串url
, 通过url
能够拜访到传入的'资源数据', 须要留神生成的url
是短暂的就会生效无法访问。oReplayAudio.src
为播放器指定播放地址, 因为不必录音所以就不必指定srcObject
了。oReplayAudio.play();
开始播放。
3: 从新录制音频
录制的不好当然要从新录制了, 最早我还想兼容暂停与续录, 然而感觉这些能力有些片离外围, 预计应该很少呈现很长的语音正文, 这里就间接暴力刷页面了。
const oResetBt = document.getElementById("resetBt"); oResetBt.addEventListener("click", function () { location.reload(); });
八、转换格局
获取到的音频文件间接应用node
进行播放可能是播放失败的, 尽管这种单纯的音频数据流文件能够被浏览器辨认, 为了打消不同浏览器与不同操作系统的差别,保险起见咱们须要将其转换成规范的mp3音频格式。
MP3是一种有损音乐格式,而WAV则是一种无损音乐格式。其实两者的区别非常明显,前者是以就义音乐的品质来换取更小的文件体积,后者却是尽最大限度保障音乐的品质。这也就导致两者的用处不同,MP3个别是用于咱们普通用户听歌,而WAV文件通常用于录音室录音和业余音频我的项目。
这里我抉择的是lamejs
这款插件, 插件的 github地址在这里。
lamejs是一个用JS重写的mp3编码器, 简略了解就是它能够产出规范的mp3
编码格局。
在初始化逻辑外面新增一些初始逻辑:
let audioCtx = {}; let processor; let source; let userMediStream; navigator.mediaDevices .getUserMedia({ audio: true }) .then(function (stream) { userMediStream = stream; audio.srcObject = stream; audio.onloadedmetadata = function (e) { audio.muted = true; }; audioCtx = new AudioContext(); // 新增 source = audioCtx.createMediaStreamSource(stream); // 新增 processor = audioCtx.createScriptProcessor(0, 1, 1); // 新增 processor.onaudioprocess = function (e) { // 新增 const array = e.inputBuffer.getChannelData(0); encode(array); }; }) .catch(function (err) { console.log(err); });
new AudioContext()
音频解决的上下文, 对音频的操作根本都会在这个类型外面进行。audioCtx.createMediaStreamSource(stream)
创立音频接口有点形象。audioCtx.createScriptProcessor(0, 1, 1)
这里创立了一个用于JavaScript间接解决音频的对象, 也就是创立了这个能力用js操作音频数据,三个参数别离为'缓冲区大小','输出声道数','输入声道数'。processor.onaudioprocess
监听新数据的解决办法。encode
解决音频并返回一个float32Array
数组。
上面代码是参考网上其他人的代码, 具体成果就是实现了lamejs
的转换工作:
let mp3Encoder, maxSamples = 1152, samplesMono, lame, config, dataBuffer; const clearBuffer = function () { dataBuffer = []; }; const appendToBuffer = function (mp3Buf) { dataBuffer.push(new Int8Array(mp3Buf)); }; const init = function (prefConfig) { config = prefConfig || {}; lame = new lamejs(); mp3Encoder = new lame.Mp3Encoder( 1, config.sampleRate || 44100, config.bitRate || 128 ); clearBuffer(); }; init(); const floatTo16BitPCM = function (input, output) { for (let i = 0; i < input.length; i++) { let s = Math.max(-1, Math.min(1, input[i])); output[i] = s < 0 ? s * 0x8000 : s * 0x7fff; } }; const convertBuffer = function (arrayBuffer) { let data = new Float32Array(arrayBuffer); let out = new Int16Array(arrayBuffer.length); floatTo16BitPCM(data, out); return out; }; const encode = function (arrayBuffer) { samplesMono = convertBuffer(arrayBuffer); let remaining = samplesMono.length; for (let i = 0; remaining >= 0; i += maxSamples) { let left = samplesMono.subarray(i, i + maxSamples); let mp3buf = mp3Encoder.encodeBuffer(left); appendToBuffer(mp3buf); remaining -= maxSamples; } };
相应的开始录音要新增一些逻辑
oStartBt.addEventListener("click", function () { clearBuffer(); oAudio.srcObject = userMediStream; oAudio.play(); buffer = []; const options = { mimeType: "audio/webm", }; mediaRecorder = new MediaRecorder(userMediStream, options); mediaRecorder.ondataavailable = handleDataAvailable; mediaRecorder.start(10); source.connect(processor); // 新增 processor.connect(audioCtx.destination); // 新增 });
source.connect(processor)
别慌,source
是下面说过的createMediaStreamSource
返回的,processor
是createScriptProcessor
返回的, 这里是把他们两个分割起来, 所以相当于开始应用js
解决音频数据。audioCtx.destination
音频图形在特定状况下的最终输入地址, 通常是扬声器。processor.connect
造成链接, 也就是开始执行processor
的监听。
相应的完结录音新增一些逻辑
oEndBt.addEventListener("click", function () { oAudio.pause(); oAudio.srcObject = null; mediaRecorder.stop(); // 新增 processor.disconnect(); // 新增 });
mediaRecorder.stop
进行音频(用于回放录音)processor.disconnect()
进行解决音频数据(转换成mp3后的)。
九、 录制好的音频文件发送给server
弄好的数据要以FormData
的模式传递给后端。
const oSubmitBt = document.getElementById("submitBt"); oSubmitBt.addEventListener("click", function () { var blob = new Blob(dataBuffer, { type: "audio/mp3" }); const formData = new FormData(); formData.append("file", blob); fetch("/create_voice", { method: "POST", body: formData, }) .then((res) => res.json()) .catch((err) => console.log(err)) .then((res) => { copy(res.voiceId); alert(`已保到剪切板: ${res.voiceId}`); window.opener = null; window.open("", "_self"); window.close(); }); });
- 这里咱们胜利传递音频文件后就敞开以后页面了, 因为要录制的语音正文也的确不会很多。
十、将来瞻望
在vscode
插件商店也没有找到相似的插件, 并且github
上也没找到相似的插件, 阐明这个问题点并没有很痛, 但并不是阐明这些问题就放任不管, 口头起来真的去做一些事来改善准没错。
对于开发者这个"语音正文"插件可想而知, 只在文字无奈形容分明的状况下才会去应用, 所以平时录音性能的应用应该是很低频的, 正因如此音频文件也当然不会'多', 所以我的项目多出的体积可能也并不会造成很大的困扰。
后续如果大家用起来了, 我打算是减少一个"一键删除未应用的正文", 随着我的项目的倒退必定有些正文会被淘汰, 手动清理必定说不过去。
播放的时候显示是谁的录音, 录制的具体工夫的展现。
除了语音正文, 用户也能够增加文字+图片, 也就是做一个以正文为外围的插件。
end
这次就是这样, 心愿与你一起提高。