feat: convert wav by ffmpeg

This commit is contained in:
linyuchen 2024-02-25 12:46:37 +08:00
parent d9d7e9e830
commit 730294236c
6 changed files with 863 additions and 28 deletions

View File

@ -17,7 +17,7 @@ jobs:
node-version: 18 node-version: 18
- name: install dependenies - name: install dependenies
run: npm install run: export ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install
- name: build - name: build
run: npm run build run: npm run build

776
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@
"main": "dist/main.js", "main": "dist/main.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"postinstall": "cross-env ELECTRON_SKIP_BINARY_DOWNLOAD=1 && npm install electron --no-save",
"build": "npm run build-main && npm run build-preload && npm run build-renderer", "build": "npm run build-main && npm run build-preload && npm run build-renderer",
"build-main": "webpack --config webpack.main.config.js", "build-main": "webpack --config webpack.main.config.js",
"build-preload": "webpack --config webpack.preload.config.js", "build-preload": "webpack --config webpack.preload.config.js",
@ -19,6 +18,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"fluent-ffmpeg": "^2.1.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"music-metadata": "^8.1.4", "music-metadata": "^8.1.4",
"silk-wasm": "^3.2.3", "silk-wasm": "^3.2.3",
@ -28,12 +28,14 @@
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.23.2", "@babel/preset-env": "^7.23.2",
"@types/express": "^4.17.20", "@types/express": "^4.17.20",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^20.11.19", "@types/node": "^20.11.19",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2", "copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"electron": "^29.0.1",
"ts-loader": "^9.5.0", "ts-loader": "^9.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.89.0", "webpack": "^5.89.0",

View File

@ -5,6 +5,8 @@ import util from "util";
import {encode, getDuration} from "silk-wasm"; import {encode, getDuration} from "silk-wasm";
import fs from 'fs'; import fs from 'fs';
import {v4 as uuidv4} from "uuid"; import {v4 as uuidv4} from "uuid";
import {exec} from "node:child_process";
import ffmpeg from "fluent-ffmpeg"
export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; export const CONFIG_DIR = global.LiteLoader.plugins["LLOneBot"].path.data;
@ -136,7 +138,22 @@ export function mergeNewProperties(newObj: any, oldObj: any) {
}); });
} }
export function checkFFMPEG(): boolean {
let ffmpegExist: boolean;
exec('ffmpeg -version', (error, stdout, stderr) => {
if (error) {
log('ffmpeg is not installed or not found in PATH:', error);
ffmpegExist = false
}
log('ffmpeg is installed. Version info:', stdout);
ffmpegExist = true;
});
return ffmpegExist;
}
export async function encodeSilk(filePath: string) { export async function encodeSilk(filePath: string) {
const fsp = require("fs").promises
function getFileHeader(filePath: string) { function getFileHeader(filePath: string) {
// 定义要读取的字节数 // 定义要读取的字节数
const bytesToRead = 7; const bytesToRead = 7;
@ -154,6 +171,35 @@ export async function encodeSilk(filePath: string) {
} }
} }
function isWavFile(filePath: string) {
return new Promise((resolve, reject) => {
fs.open(filePath, 'r', (err, fd) => {
if (err) {
reject(err);
return;
}
// 读取前12个字节
const buffer = Buffer.alloc(12);
fs.read(fd, buffer, 0, 12, 0, (err, bytesRead, buffer) => {
if (err) {
reject(err);
return;
}
fs.close(fd, (err) => {
if (err) {
reject(err);
return;
}
// 检查RIFF头和WAVE格式标识
const isRIFF = buffer.toString('utf8', 0, 4) === 'RIFF';
const isWAVE = buffer.toString('utf8', 8, 12) === 'WAVE';
resolve(isRIFF && isWAVE);
});
});
});
});
}
async function getAudioSampleRate(filePath: string) { async function getAudioSampleRate(filePath: string) {
try { try {
const mm = await import('music-metadata'); const mm = await import('music-metadata');
@ -172,10 +218,25 @@ export async function encodeSilk(filePath: string) {
const pttPath = path.join(CONFIG_DIR, uuidv4()); const pttPath = path.join(CONFIG_DIR, uuidv4());
if (getFileHeader(filePath) !== "02232153494c4b") { if (getFileHeader(filePath) !== "02232153494c4b") {
log(`语音文件${filePath}需要转换`) log(`语音文件${filePath}需要转换`)
const isWav = await isWavFile(filePath);
if (!isWav) {
log(`语音文件${filePath}正在转换成wav`)
let voiceData = await fsp.readFile(filePath)
const wavPath = pttPath + ".wav"
await new Promise((resolve, reject) => {
ffmpeg(filePath).toFormat("wav").on('end', function () {
// console.log('转换完成');
filePath = wavPath
})
.on('error', function (err) {
// console.log('转换出错: ' + err.message);
})
.save(wavPath);
})
const sampleRate = await getAudioSampleRate(filePath) || 44100; const sampleRate = await getAudioSampleRate(filePath) || 44100;
const silk = await encode(pcm, sampleRate); const silk = await encode(pcm, sampleRate);
fs.writeFileSync(pttPath, silk.data); fs.writeFileSync(pttPath, silk.data);
log(`语音文件${filePath}转换成功!`) log(`语音文件${filePath}转换成功!`, pttPath)
return { return {
converted: true, converted: true,
path: pttPath, path: pttPath,
@ -189,6 +250,7 @@ export async function encodeSilk(filePath: string) {
duration: duration, duration: duration,
}; };
} }
}
} catch (error) { } catch (error) {
log("convert silk failed", error.stack); log("convert silk failed", error.stack);
return {}; return {};

View File

@ -8,8 +8,7 @@ import {
SendTextElement SendTextElement
} from "./types"; } from "./types";
import {NTQQApi} from "./ntcall"; import {NTQQApi} from "./ntcall";
import {encodeSilk, log} from "../common/utils"; import {encodeSilk} from "../common/utils";
import fs from "fs";
export class SendMsgElementConstructor { export class SendMsgElementConstructor {
@ -83,9 +82,9 @@ export class SendMsgElementConstructor {
static async ptt(pttPath: string): Promise<SendPttElement> { static async ptt(pttPath: string): Promise<SendPttElement> {
const {converted, path: silkPath, duration} = await encodeSilk(pttPath); const {converted, path: silkPath, duration} = await encodeSilk(pttPath);
// log("生成语音", silkPath, duration); // log("生成语音", silkPath, duration);
const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath); const {md5, fileName, path, fileSize} = await NTQQApi.uploadFile(silkPath, ElementType.PTT);
if (converted){ if (converted){
fs.unlink(silkPath, ()=>{}); // fs.unlink(silkPath, ()=>{});
} }
return { return {
elementType: ElementType.PTT, elementType: ElementType.PTT,

View File

@ -3,7 +3,9 @@ import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} fr
import {log} from "../common/utils"; import {log} from "../common/utils";
import { import {
ChatType, ChatType,
Friend, FriendRequest, ElementType,
Friend,
FriendRequest,
Group, Group,
GroupMember, GroupMember,
GroupNotifies, GroupNotifies,
@ -329,7 +331,7 @@ export class NTQQApi {
} }
// 上传文件到QQ的文件夹 // 上传文件到QQ的文件夹
static async uploadFile(filePath: string) { static async uploadFile(filePath: string, elementType: ElementType=ElementType.PIC) {
const md5 = await NTQQApi.getFileMd5(filePath); const md5 = await NTQQApi.getFileMd5(filePath);
let ext = (await NTQQApi.getFileType(filePath))?.ext let ext = (await NTQQApi.getFileType(filePath))?.ext
if (ext) { if (ext) {
@ -344,7 +346,7 @@ export class NTQQApi {
path_info: { path_info: {
md5HexStr: md5, md5HexStr: md5,
fileName: fileName, fileName: fileName,
elementType: 2, elementType: elementType,
elementSubType: 0, elementSubType: 0,
thumbSize: 0, thumbSize: 0,
needCreate: true, needCreate: true,