vscode语音注释|vscode语音注释, 让信息更丰富(下)

vscode语音注释, 让信息更丰富 (下) 前言
???? 这个系列的最后一篇, 主要讲述录制音频&音频文件存储相关知识, 当时因为录音有bug搞得我一周没心情吃饭(voice-annotation)。
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

一、MP3 文件储存位置
"语音注释"使用场景

  1. 单个项目使用"语音注释"。
  2. 多个项目使用"语音注释"。
  3. "语音注释"生成的 mp3 文件都放在自己项目中。
  4. "语音注释"生成的 mp3 文件统一存放在全局的某处。
  5. "语音注释"生成的 mp3 一部分存在项目中一部分使用全局路径。
vscode 工作区 【vscode语音注释|vscode语音注释, 让信息更丰富(下)】???? 具体音频储存在哪里肯定要读取用户的配置, 但如果用户只在全局配置了一个路径, 那么这个路径无法满足每个项目存放音频文件的位置不同的场景, 这时候就引出了vscode 工作区的概念。
???? 假如我们每个工程的eslint规则各不相同, 此时我们只在全局配置eslint规则就无法满足这个场景了, 此时我们需要在项目中新建一个.vscode文件夹, 在其中建立settings.json文件, 在这个文件内编写的配置就是针对当前项目的个性化配置了。
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

配置工作区 (绝对路径 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项目中。
???? 上述问题没法准确的检验出用户的真实目标路径, 那我想到的办法是录制音频页面内预展示出将要保存到的路径, 让用户来做最后的守门人:
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

???? 当前插件简易用户配置:
{ "voiceAnnotation": { "dirPath": "../mp3" } }

vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

二、配置的定义
???? 如果用户不想把音频文件储存在项目内, 怕自己的项目变大起来, 那我们支持单独做一个音频存放的项目, 此时就需要在全局配置一个绝对路径, 因为全局的配置不会同步给其他开发者, 当我们获取不到用户在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" } } } },

具体每个属性的意义可以参考配置后的效果图:
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

三、获取音频文件夹位置的方法
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文件的配置, 并且也是获取全局配置的方法, 所以我们要做好区分才能去使用哪个, 所以这里我命名为dirPathglobalDirPath
3: 文件路径分割符 ???? /xxx/xx/x.js其中的 "/" 就是path.sep, 因为mac或者window等系统里面是有差异的, 这里使用path.sep是为了兼容其他系统的用户。
4: 报错 ???? 相对路径与绝对路径都获取不到就抛出报错:
vscode.window.showErrorMessage(错误信息)

vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

5: 使用 ???? 第一是用在server保存音频时, 第二是打开web页面时会传递给前端用户显示保存路径。
四、录音初始知识
???? 没使用过录音功能的同学你可能没见过navigator.mediaDevices这个方法, 返回一个MediaDevices对象,该对象可提供对相机和麦克风等媒体输入设备的连接访问,也包括屏幕共享。
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

???? 录制音频需要先获取用户的许可, navigator.mediaDevices.getUserMedia就是在获取用户许可成功并且设备可用时走成功回调。
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

navigator.mediaDevices.getUserMedia({audio:true}) .then((stream)=>{ // 因为我们输入的是{audio:true}, 则stream是音频的内容流 }) .carch((err)=>{})

vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

五、初始化录音设备与配置
下面展示的是定义播放标签以及环境的'初始化', 老样子先上代码, 然你后逐句解释:

let audioCtx = {} let processor; let userMediStream; navigator.mediaDevices.getUserMedia({ audio: true }) .then(function (stream) { userMediStream = stream; audio.srcObject = stream; audio.onloadedmetadata = https://www.it610.com/article/function (e) { audio.muted = true; }; }) .catch(function (err) { console.log(err); });

1: 发现有趣的事, 直接用id获取元素 vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

2: 保存音频的内容流 这里将媒体源保存在全局变量上, 方便后续重播声音:
userMediStream = stream;

srcObject属性指定标签关联的'媒体源':
audio.srcObject = stream;

3: 监听数据变化 当载入完成时设置 audio.muted = true; , 将设备静音处理, 录制音频为啥还要静音? 其实是因为录音的时候不需要同时播放我们的声音, 这会导致"回音"很重, 所以这里需要静音。
audio.onloadedmetadata = https://www.it610.com/article/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); } }

  1. oAudio.srcObject定义了播放标签的'媒体源'。
  2. oAudio.play(); 开始播放, 这里由于我们设置了muted = true静音, 所以这里就是开始录音。
  3. buffer是用来储存音频数据的, 每次录制需要清空一下上次的残留。
  4. new MediaRecorder 创建了一个对指定的 MediaStream 进行录制的 MediaRecorder 对象, 也就是说这个方法就是为了录制功能而存在的, 它的第二个参数可以输入指定的mimeType类型, 具体的类型我在MDN上查了一下。
    vscode语音注释|vscode语音注释, 让信息更丰富(下)
    文章图片
  5. mediaRecorder.ondataavailable定义了针对每段音频数据的具体处理逻辑。
  6. mediaRecorder.start(10); 对音频进行10毫秒一切片, 音频信息是储存在Blob里的, 这里的配置我理解是每10毫秒生成一个Blob对象。
???? 此时数组buffer里面就可以持续不断的收集到我们的音频信息了, 至此我们完成了录音功能, 接下来我们要丰富它的功能了。
七、结束, 重播, 重录
vscode语音注释|vscode语音注释, 让信息更丰富(下)
文章图片

1: 结束录音 ???? 录音当然要有个尽头了, 有同学提出是否需要限制音频的长短或大小? 但我感觉具体的限制规则还是每个团队自己来定制吧, 这一版我这边只提供核心功能。
const oEndBt = document.getElementById("endBt"); oEndBt.addEventListener("click", function () { oAudio.pause(); oAudio.srcObject = null; });

  1. 点击录制结束按钮, oAudio.pause()停止标签播放。
  2. 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 = https://www.it610.com/article/window.URL.createObjectURL(blob); oReplayAudio.play(); });

  1. Blob 一种数据的储存形式, 我们实现纯前端生成excel就是使用了blob, 可以简单理解为第一个参数是文件的数据, 第二个参数可以定义文件的类型。
  2. window.URL.createObjectURL参数是'资源数据', 此方法生成一串url, 通过url可以访问到传入的'资源数据', 需要注意生成的url是短暂的就会失效无法访问。
  3. oReplayAudio.src 为播放器指定播放地址, 由于不用录音所以就不用指定srcObject了。
  4. 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 = https://www.it610.com/article/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); });

  1. new AudioContext()音频处理的上下文, 对音频的操作基本都会在这个类型里面进行。
  2. audioCtx.createMediaStreamSource(stream) 创建音频接口有点抽象。
  3. audioCtx.createScriptProcessor(0, 1, 1) 这里创建了一个用于JavaScript直接处理音频的对象, 也就是创建了这个才能用js操作音频数据,三个参数分别为'缓冲区大小','输入声道数','输出声道数'。
  4. processor.onaudioprocess 监听新数据的处理方法。
  5. 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 = https://www.it610.com/article/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); // 新增 });

  1. source.connect(processor)别慌, source是上面说过的createMediaStreamSource返回的, processorcreateScriptProcessor返回的, 这里是把他们两个联系起来, 所以相当于开始使用js处理音频数据。
  2. audioCtx.destination 音频图形在特定情况下的最终输出地址, 通常是扬声器。
  3. processor.connect 形成链接, 也就是开始执行processor的监听。
相应的结束录音新增一些逻辑
oEndBt.addEventListener("click", function () { oAudio.pause(); oAudio.srcObject = null; mediaRecorder.stop(); // 新增 processor.disconnect(); // 新增 });

  1. mediaRecorder.stop 停止音频(用于回放录音)
  2. processor.disconnect()停止处理音频数据(转换成mp3后的)。
九、 录制好的音频文件发送给server
???? 弄好的数据要以FormData的形式传递给后端。
const oSubmitBt = document.getElementById("submitBt"); oSubmitBt.addEventListener("click", function () { var blob = new Blob(dataBuffer, { type: "audio/mp3" }); const formData = https://www.it610.com/article/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(); }); });

  1. 这里我们成功传递音频文件后就关闭当前页面了, 因为要录制的语音注释也确实不会很多。
十、未来展望
???? 在vscode插件商店也没有找到类似的插件, 并且github上也没找到类似的插件, 说明这个问题点并没有很痛, 但并不是说明这些问题就放任不管, 行动起来真的去做一些事来改善准没错。
???? 对于开发者这个"语音注释"插件可想而知, 只在文字无法描述清楚的情况下才会去使用, 所以平时录音功能的使用应该是很低频的, 正因如此音频文件也当然不会'多', 所以项目多出的体积可能也并不会造成很大的困扰。
???? 后续如果大家用起来了, 我计划是增加一个"一键删除未使用的注释", 随着项目的发展肯定有些注释会被淘汰, 手动清理肯定说不过去。
???? 播放的时候显示是谁的录音, 录制的具体时间的展示。
???? 除了语音注释, 用户也可以添加文字+图片, 也就是做一个以注释为核心的插件。
end
???? 这次就是这样, 希望与你一起进步。

    推荐阅读