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

SegmentFault

共 11711字,需浏览 24分钟

 ·

2022-03-04 01:35

作者:lulu_up

来源:SegmentFault  思否社区 


前言



上一篇我们做完了最基础的功能"识别语音注释", 本篇我们要一起开发语音'播放'等相关功能。


一、mac电脑获取音频文件(后期有坑,到时会填)



要开发音频'播放'功能, 那我们首先需要一份音频文件, 上网找mp3文件下载多数都需要注册, 那索性直接使用电脑自带的录音功能生成mp3就好了,这种方式有bug后期我们再解决。


这里演示mac电脑的录音功能:


第一步: 找到软件:



第二步: 将录制好的音频分享到某个app上





m4a文件: (我们可以手动修改后缀名)


“m4a是MPEG-4音频标准文件的扩展名,与大家熟悉的mp3一样,也是一种音频格式文件,苹果公司用此命名来区分mpeg4视频。”


二、播放音频插件的选择



这里的播放指的是"鼠标悬停"即可播放音频, 那么就不能是web意义上的播放, 因为我们无法利用audio标签实现, vscode是基于Electron开发的, 所以其内的插件也是处于node环境里的, 那我们就可以利用node将音频流输入到音频输出设备从而达到播放的目的。


在网上用node播放音频的插件不多, 在这里推荐其中两款:play.js&node-wav-player

  • play.js: github地址

https://github.com/Marak/play.js
  • node-wav-player: github地址

https://github.com/futomi/node-wav-player


play.js有个瑕疵, 就是无法暂停播放这个问题可能是开发者无法忍受的, 所以我最终选择了node-wav-player, 别看它叫wav播放器, mp3也是能播的。

安装走起:

yarn add

使用播放:(这里暂时使用绝对地址)

在`hover.ts文件内添加:

import * as vscode from 'vscode';
import * as player from 'node-wav-player';
import { getVoiceAnnotationDirPath, targetName, testTargetReg } from './util'
let stopFn: () => void;

function playVoice(id: string) {
    const voiceAnnotationDirPath = getVoiceAnnotationDirPath()
    if (voiceAnnotationDirPath) {
        player.play({
            path: `/xxx/xxxx/xx.mp3`
        }).catch(() => {
            vscode.window.showErrorMessage('播放失败')
        })
        stopFn = () => {
            player.stop()
        }
    }
}

export default vscode.languages.registerHoverProvider("*", {
    provideHover(documennt: vscode.TextDocument, position: vscode.Position) {
        stopFn?.()
        const word = documennt.getText(documennt.getWordRangeAtPosition(position));
        const testTargetRes = testTargetReg.exec(word);
        if (testTargetRes) {
            playVoice(testTargetRes[1])
            return new vscode.Hover('播放中 ...')
        }
    }
})

player.play path 播放地址暂时写死, 你会发现当前可以正常播放音频, 如果此时你认为播放功能ok了那就大错特错了。

三、node-wav-player的核心原理



node-wav-player的代码十分简易, 远比我想象的简练, 下面都是我将代码化简后的样子, 是不是清爽很多:

初始化的play方法, 只负责整理数据, 真正播放是靠_play方法


_play方法



node child_process.spawn 用来启动一个新的'子进程', 这个就是用来启动音频播放的'子进程' 第一个参数是命令语句, 第二个参数是数组的话就是执行命令的位置。

spawn的使用方法我演示一下:


比如说 afplay 音频地址 就可以在mac上面播放声音。

监听报错


如果不是code为 0亦或是this._called_stop === true人为手动调用停止的情况, 则报错"播放失败", 如果500毫秒内并未报错, 则removeAllListeners("close")移除关闭的监听。
如何终止播放

stop方法中直接kill掉'子进程'即可:


四、'何处'录制音频?



我们做这个插件的体验宗旨就是方便快捷, 所以录制音频的"链路一定要短", 最好用户一键就可以进行'录制', 一键就可以生成音频注释。


我最开始的想法是尽可能在vscode内部完成, 也就是不要新开一个 h5页面, 让用户的不要越出vscode这个层级。

录制音频就不能像播放音频一样, 因为录制涉及到录音结束后的重播, 音频文件的保存, 以及开始+暂停+结束等等状态的操作, 所以最好要有个操作界面而不是靠node单打独斗。

五、webview



创建webview

vscode内部提供了webview的能力, 看到它的第一眼我就'心动'了, 我们使用下面的代码就可以增加一个webview页。

const panel = vscode.window.createWebviewPanel(
    "类型xxx",
    "标题xxx",
    vscode.ViewColumn.One,
    {}
  );


定义内容

需要用到panel.webview.html属性, 类似innerHTML:

const panel = vscode.window.createWebviewPanel(
    "类型xxx",
    "标题xxx",
    vscode.ViewColumn.One,
    {}
  );
  panel.webview.html = `<div>123</div>`;




局限性

阅读了官方文档也查看了ts类型文件, 但是遗憾没能发现为音频授权的方法, 所以导致无法使用audio标签来采集到用户的音频信息, 只能选择换种方式实现。


六、右键录音



我参考了一些音乐播放软件, 发现大家播放功能几乎都是通过打开h5页面实现的, 那咱们的录音功能也可以尝试这种方式, 原理当然是利用node启动一个web服务, 然后帮助用户打开类似http://localhost:8830/这种地址, 这个地址返回给用户一段html, 这里就是录音的地方。


定义右键导航

package.json文件内增加
  

"contributes"
: {
    "menus": {
      "editor/context": [
        {
          "when""editorFocus",
          "command""vn.recording",
          "group""navigation"
        }
      ]
    },
    "commands": [
      {
        "command""vn.recording",
        "title""此工程内录制语音注释"
      }
    ]
}

  1. editor/context里面定义了右键呼出的菜单栏的内容。

  2. when在什么生命周期激活这个功能定义, 这里选择了当获得编辑焦点时。

  3. command定义了命令名称。

  4. title就是显示在菜单中的名称。



打开h5页面

extension.ts新增navigation模块:

import * as vscode from 'vscode';
import hover from './hover';
import initVoiceAnnotationStyle from './initVoiceAnnotationStyle';
import navigation from './navigation' // 新增

export function activate(context: vscode.ExtensionContext) {
    initVoiceAnnotationStyle()
    context.subscriptions.push(hover);
    context.subscriptions.push(navigation); // 新增
    context.subscriptions.push(
        vscode.window.onDidChangeActiveTextEditor(() => {
            initVoiceAnnotationStyle()
        })
    )
}

export function deactivate() { }

navigation,ts文件, 负责启动服务并且打开浏览器跳到对应页面:

yarn add open

import * as vscode from 'vscode';
import * as open from 'open';
import server from './server';
import { serverProt } from './util';
import { Server } from 'http';

let serverObj: Server;
export default vscode.commands.registerCommand("vn.recording"function () {
    const voiceAnnotationDirPath = getVoiceAnnotationDirPath()
    if (voiceAnnotationDirPath) {
        if (!serverObj) {
            serverObj = server()
        }
        open(`http://127.0.0.1:${serverProt()}`);
    }
})

启动server

因为咱们的插件要尽可能的小, 这里当然不使用任何框架, 手撸原生即可:

新建server.ts文件:

import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import * as url from 'url';
import { targetName, getVoiceID } from './util';

export default function () {
    const server = http.createServer(function (
      req: http.IncomingMessage, res: http.ServerResponse) {
            res.write(123)
            res.end()
    }).listen(8830)

    return server
}

七、返回页面, 定义api



server光启动不行, 现在开始定义接口能力, 在server.ts内:


import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
import * as url from 'url';
import { serverProt, targetName, getVoiceID } from './util';

const temp = fs.readFileSync(
    path.join(__dirname, "./index.html")
)

export default function () {
    const server = http.createServer(function (req: http.IncomingMessage, res: http.ServerResponse) {
        if (req.method === "POST" && req.url === "/create_voice") {
            createVoice(req, res)
        }else {
            res.writeHead(200, {
                "content-type"'text/html;charset="fs.unwatchFile-8"'
            })
            res.write(temp)
            res.end()
        }
    }).listen(serverProt())

    return server
}

  • src/html/index.html文件是我们的录音的h5界面文件。

  • 我们定义上传为"POST"请求, 并且请求地址为/create_voice


createVoice方法

此方法用于接收音频文件, 并将音频文件保存在用户指定的位置:

function createVoice(req: http.IncomingMessage, res: http.ServerResponse) {
    let data: Uint8Array[] = [];
    req.on("data", (chunck: Uint8Array) => {
        data.push(chunck)
    })
    req.on("end", () => {
        let buffer = Buffer.concat(data);
        const voiceId = getVoiceID()
        try {
            fs.writeFileSync(`保存音频的位置`,
                buffer,
            )
        } catch (error) {
            res.writeHead(200)
            res.end()
        }
        res.writeHead(200)
        res.end(JSON.stringify({ voiceId: `// ${targetName}_${voiceId}` }))
    })
}

  • 因为前端会使用formData的形式进行音频文件的传递, 所以需要这种接收方式。

  • 将最后生成的这种// voice_annotation_20220220153713111音频注释字符串返回给前端, 方便前端直接放入用户剪切板。


End



接下来是音频的录制与上传(涉及到webRTC相关知识) , 以及如何定义储存音频文件的路径, 并且附加vscode插件的发布, 这次就是这样, 希望与你一起进步。



点击左下角阅读原文,到 SegmentFault 思否社区 和文章作者展开更多互动和交流,扫描下方”二维码“或在“公众号后台回复“ 入群 ”即可加入我们的技术交流群,收获更多的技术文章~

- END -


浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报