vscode語音註釋, 讓資訊更豐富(中)

lulu_up發表於2022-02-21

vscode語音註釋, 讓資訊更豐富 (中)

前言

     上一篇我們做完了最基礎的功能"識別語音註釋", 本篇我們要一起開發語音'播放'等相關功能。

一、mac電腦獲取音訊檔案(後期有坑,到時會填)

     要開發音訊'播放'功能, 那我們首先需要一份音訊檔案, 上網找mp3檔案下載多數都需要註冊, 那索性直接使用電腦自帶的錄音功能生成mp3就好了,這種方式有bug後期我們再解決。

這裡演示mac電腦的錄音功能:

第一步: 找到軟體:

image.png

第二步: 將錄製好的音訊分享到某個app上

image.png

image.png

image.png

m4a檔案: (我們可以手動修改字尾名)

“m4a是MPEG-4音訊標準檔案的副檔名,與大家熟悉的mp3一樣,也是一種音訊格式檔案,蘋果公司用此命名來區分mpeg4視訊。”

二、播放音訊外掛的選擇

     這裡的播放指的是"滑鼠懸停"即可播放音訊, 那麼就不能是web意義上的播放, 因為我們無法利用audio標籤實現, vscode是基於Electron開發的, 所以其內的外掛也是處於node環境裡的, 那我們就可以利用node音訊流輸入到音訊輸出裝置從而達到播放的目的。

     在網上用node播放音訊的外掛不多, 在這裡推薦其中兩款:play.js&node-wav-player

  1. play.js: github地址
  2. node-wav-player: github地址

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.playpath 播放地址暫時寫死, 你會發現當前可以正常播放音訊, 如果此時你認為播放功能ok了那就大錯特錯了。

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

     node-wav-player的程式碼十分簡易, 遠比我想象的簡練, 下面都是我將程式碼化簡後的樣子, 是不是清爽很多:

初始化的play方法, 只負責整理資料, 真正播放是靠_play方法

image.png

_play方法

image.png
    node child_process.spawn 用來啟動一個新的'子程式', 這個就是用來啟動音訊播放的'子程式' 第一個引數是命令語句, 第二個引數是陣列的話就是執行命令的位置。

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

     比如說 afplay 音訊地址 就可以在mac上面播放聲音。

監聽報錯

image.png

如果不是code0亦或是this._called_stop === true人為手動呼叫停止的情況, 則報錯"播放失敗", 如果500毫秒內並未報錯, 則removeAllListeners("close")移除關閉的監聽。

如何終止播放

     在stop方法中直接kill掉'子程式'即可:
image.png

四、'何處'錄製音訊?

     我們做這個外掛的體驗宗旨就是方便快捷, 所以錄製音訊的"鏈路一定要短", 最好使用者一鍵就可以進行'錄製', 一鍵就可以生成音訊註釋。

     我最開始的想法是儘可能在vscode內部完成, 也就是不要新開一個 h5頁面, 讓使用者的不要越出vscode這個層級。

     錄製音訊就不能像播放音訊一樣, 因為錄製涉及到錄音結束後的重播, 音訊檔案的儲存, 以及開始+暫停+結束等等狀態的操作, 所以最好要有個操作介面而不是靠node單打獨鬥。

五、webview

建立webview

     vscode內部提供了webview的能力, 看到它的第一眼我就'心動'了, 我們使用下面的程式碼就可以增加一個webview頁。

  const panel = vscode.window.createWebviewPanel(
    "型別xxx",
    "標題xxx",
    vscode.ViewColumn.One,
    {}
  );

image.png

定義內容

     需要用到panel.webview.html屬性, 類似innerHTML:

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

image.png

侷限性

     閱讀了官方文件也檢視了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就是顯示在選單中的名稱。

image.png

開啟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
}
  1. src/html/index.html檔案是我們的錄音的h5介面檔案。
  2. 我們定義上傳為"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}` }))
    })
}
  1. 因為前端會使用formData的形式進行音訊檔案的傳遞, 所以需要這種接收方式。
  2. 將最後生成的這種// voice_annotation_20220220153713111音訊註釋字串返回給前端, 方便前端直接放入使用者剪下板。

end

     接下來是音訊的錄製與上傳(涉及到webRTC相關知識) , 以及如何定義儲存音訊檔案的路徑, 並且附加vscode外掛的釋出, 這次就是這樣, 希望與你一起進步。

相關文章