mirror of
https://github.com/LLOneBot/LLOneBot.git
synced 2024-11-22 01:56:33 +00:00
refactor: audio.ts
This commit is contained in:
parent
5a0dbdb5ce
commit
36f7f1b026
134
src/common/utils/audio.ts
Normal file
134
src/common/utils/audio.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import {encode, getDuration, getWavFileInfo, isWav} from "silk-wasm";
|
||||||
|
import fsPromise from "fs/promises";
|
||||||
|
import {log} from "./log";
|
||||||
|
import path from "node:path";
|
||||||
|
import {DATA_DIR} from "./index";
|
||||||
|
import {v4 as uuidv4} from "uuid";
|
||||||
|
import {getConfigUtil} from "../config";
|
||||||
|
import ffmpeg from "fluent-ffmpeg";
|
||||||
|
|
||||||
|
export async function encodeSilk(filePath: string) {
|
||||||
|
function getFileHeader(filePath: string) {
|
||||||
|
// 定义要读取的字节数
|
||||||
|
const bytesToRead = 7;
|
||||||
|
try {
|
||||||
|
const buffer = fs.readFileSync(filePath, {
|
||||||
|
encoding: null,
|
||||||
|
flag: "r",
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileHeader = buffer.toString("hex", 0, bytesToRead);
|
||||||
|
return fileHeader;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("读取文件错误:", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isWavFile(filePath: string) {
|
||||||
|
return isWav(fs.readFileSync(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function guessDuration(pttPath: string) {
|
||||||
|
const pttFileInfo = await fsPromise.stat(pttPath)
|
||||||
|
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
|
||||||
|
duration = Math.floor(duration)
|
||||||
|
duration = Math.max(1, duration)
|
||||||
|
log(`通过文件大小估算语音的时长:`, duration)
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// function verifyDuration(oriDuration: number, guessDuration: number) {
|
||||||
|
// // 单位都是秒
|
||||||
|
// if (oriDuration - guessDuration > 10) {
|
||||||
|
// return guessDuration
|
||||||
|
// }
|
||||||
|
// oriDuration = Math.max(1, oriDuration)
|
||||||
|
// return oriDuration
|
||||||
|
// }
|
||||||
|
// async function getAudioSampleRate(filePath: string) {
|
||||||
|
// try {
|
||||||
|
// const mm = await import('music-metadata');
|
||||||
|
// const metadata = await mm.parseFile(filePath);
|
||||||
|
// log(`${filePath}采样率`, metadata.format.sampleRate);
|
||||||
|
// return metadata.format.sampleRate;
|
||||||
|
// } catch (error) {
|
||||||
|
// log(`${filePath}采样率获取失败`, error.stack);
|
||||||
|
// // console.error(error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pttPath = path.join(DATA_DIR, uuidv4());
|
||||||
|
if (getFileHeader(filePath) !== "02232153494c4b") {
|
||||||
|
log(`语音文件${filePath}需要转换成silk`)
|
||||||
|
const _isWav = await isWavFile(filePath);
|
||||||
|
const wavPath = pttPath + ".wav"
|
||||||
|
const convert = async () => {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
|
||||||
|
if (ffmpegPath) {
|
||||||
|
ffmpeg.setFfmpegPath(ffmpegPath);
|
||||||
|
}
|
||||||
|
ffmpeg(filePath).toFormat("wav").audioChannels(1).audioFrequency(24000).on('end', function () {
|
||||||
|
log('wav转换完成');
|
||||||
|
})
|
||||||
|
.on('error', function (err) {
|
||||||
|
log(`wav转换出错: `, err.message,);
|
||||||
|
reject(err);
|
||||||
|
})
|
||||||
|
.save(wavPath)
|
||||||
|
.on("end", () => {
|
||||||
|
filePath = wavPath
|
||||||
|
resolve(wavPath);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let wav: Buffer
|
||||||
|
if (!_isWav) {
|
||||||
|
log(`语音文件${filePath}正在转换成wav`)
|
||||||
|
await convert()
|
||||||
|
} else {
|
||||||
|
wav = fs.readFileSync(filePath)
|
||||||
|
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
|
||||||
|
const {fmt} = getWavFileInfo(wav)
|
||||||
|
if (!allowSampleRate.includes(fmt.sampleRate)) {
|
||||||
|
wav = undefined
|
||||||
|
await convert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wav ||= fs.readFileSync(filePath);
|
||||||
|
const silk = await encode(wav, 0);
|
||||||
|
fs.writeFileSync(pttPath, silk.data);
|
||||||
|
fs.unlink(wavPath, (err) => {
|
||||||
|
});
|
||||||
|
const gDuration = await guessDuration(pttPath)
|
||||||
|
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
|
||||||
|
return {
|
||||||
|
converted: true,
|
||||||
|
path: pttPath,
|
||||||
|
duration: silk.duration / 1000
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const silk = fs.readFileSync(filePath);
|
||||||
|
let duration = 0;
|
||||||
|
const gDuration = await guessDuration(filePath)
|
||||||
|
try {
|
||||||
|
duration = getDuration(silk) / 1000
|
||||||
|
} catch (e) {
|
||||||
|
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
|
||||||
|
duration = gDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
converted: false,
|
||||||
|
path: filePath,
|
||||||
|
duration: duration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log("convert silk failed", error.stack);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,10 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import fsPromise from "fs/promises";
|
import fsPromise from "fs/promises";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import ffmpeg from "fluent-ffmpeg";
|
|
||||||
import util from "util";
|
import util from "util";
|
||||||
import {encode, getDuration, isWav, getWavFileInfo} from "silk-wasm";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {checkFfmpeg, DATA_DIR, log, TEMP_DIR} from "./index";
|
import {log, TEMP_DIR} from "./index";
|
||||||
import {getConfigUtil} from "../config";
|
|
||||||
import {dbUtil} from "../db";
|
import {dbUtil} from "../db";
|
||||||
import * as fileType from "file-type";
|
import * as fileType from "file-type";
|
||||||
import {net} from "electron";
|
import {net} from "electron";
|
||||||
@ -66,133 +63,6 @@ export async function file2base64(path: string) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encodeSilk(filePath: string) {
|
|
||||||
const fsp = require("fs").promises
|
|
||||||
|
|
||||||
function getFileHeader(filePath: string) {
|
|
||||||
// 定义要读取的字节数
|
|
||||||
const bytesToRead = 7;
|
|
||||||
try {
|
|
||||||
const buffer = fs.readFileSync(filePath, {
|
|
||||||
encoding: null,
|
|
||||||
flag: "r",
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileHeader = buffer.toString("hex", 0, bytesToRead);
|
|
||||||
return fileHeader;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("读取文件错误:", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isWavFile(filePath: string) {
|
|
||||||
return isWav(fs.readFileSync(filePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function guessDuration(pttPath: string) {
|
|
||||||
const pttFileInfo = await fsPromise.stat(pttPath)
|
|
||||||
let duration = pttFileInfo.size / 1024 / 3 // 3kb/s
|
|
||||||
duration = Math.floor(duration)
|
|
||||||
duration = Math.max(1, duration)
|
|
||||||
log(`通过文件大小估算语音的时长:`, duration)
|
|
||||||
return duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// function verifyDuration(oriDuration: number, guessDuration: number) {
|
|
||||||
// // 单位都是秒
|
|
||||||
// if (oriDuration - guessDuration > 10) {
|
|
||||||
// return guessDuration
|
|
||||||
// }
|
|
||||||
// oriDuration = Math.max(1, oriDuration)
|
|
||||||
// return oriDuration
|
|
||||||
// }
|
|
||||||
// async function getAudioSampleRate(filePath: string) {
|
|
||||||
// try {
|
|
||||||
// const mm = await import('music-metadata');
|
|
||||||
// const metadata = await mm.parseFile(filePath);
|
|
||||||
// log(`${filePath}采样率`, metadata.format.sampleRate);
|
|
||||||
// return metadata.format.sampleRate;
|
|
||||||
// } catch (error) {
|
|
||||||
// log(`${filePath}采样率获取失败`, error.stack);
|
|
||||||
// // console.error(error);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pttPath = path.join(DATA_DIR, uuidv4());
|
|
||||||
if (getFileHeader(filePath) !== "02232153494c4b") {
|
|
||||||
log(`语音文件${filePath}需要转换成silk`)
|
|
||||||
const _isWav = await isWavFile(filePath);
|
|
||||||
const wavPath = pttPath + ".wav"
|
|
||||||
const convert = async () => {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const ffmpegPath = getConfigUtil().getConfig().ffmpeg;
|
|
||||||
if (ffmpegPath) {
|
|
||||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
|
||||||
}
|
|
||||||
ffmpeg(filePath).toFormat("wav").audioChannels(1).audioFrequency(24000).on('end', function () {
|
|
||||||
log('wav转换完成');
|
|
||||||
})
|
|
||||||
.on('error', function (err) {
|
|
||||||
log(`wav转换出错: `, err.message,);
|
|
||||||
reject(err);
|
|
||||||
})
|
|
||||||
.save(wavPath)
|
|
||||||
.on("end", () => {
|
|
||||||
filePath = wavPath
|
|
||||||
resolve(wavPath);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let wav: Buffer
|
|
||||||
if (!_isWav) {
|
|
||||||
log(`语音文件${filePath}正在转换成wav`)
|
|
||||||
await convert()
|
|
||||||
} else {
|
|
||||||
wav = fs.readFileSync(filePath)
|
|
||||||
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000]
|
|
||||||
const { fmt } = getWavFileInfo(wav)
|
|
||||||
if (!allowSampleRate.includes(fmt.sampleRate)) {
|
|
||||||
wav = undefined
|
|
||||||
await convert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wav ||= fs.readFileSync(filePath);
|
|
||||||
const silk = await encode(wav, 0);
|
|
||||||
fs.writeFileSync(pttPath, silk.data);
|
|
||||||
fs.unlink(wavPath, (err) => {
|
|
||||||
});
|
|
||||||
const gDuration = await guessDuration(pttPath)
|
|
||||||
log(`语音文件${filePath}转换成功!`, pttPath, `时长:`, silk.duration)
|
|
||||||
return {
|
|
||||||
converted: true,
|
|
||||||
path: pttPath,
|
|
||||||
duration: silk.duration / 1000
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const silk = fs.readFileSync(filePath);
|
|
||||||
let duration = 0;
|
|
||||||
const gDuration = await guessDuration(filePath)
|
|
||||||
try {
|
|
||||||
duration = getDuration(silk) / 1000
|
|
||||||
} catch (e) {
|
|
||||||
log("获取语音文件时长失败, 使用文件大小推测时长", filePath, e.stack)
|
|
||||||
duration = gDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
converted: false,
|
|
||||||
path: filePath,
|
|
||||||
duration: duration,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log("convert silk failed", error.stack);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function calculateFileMD5(filePath: string): Promise<string> {
|
export function calculateFileMD5(filePath: string): Promise<string> {
|
||||||
|
@ -15,3 +15,4 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|||||||
}
|
}
|
||||||
export {getVideoInfo} from "./video";
|
export {getVideoInfo} from "./video";
|
||||||
export {checkFfmpeg} from "./video";
|
export {checkFfmpeg} from "./video";
|
||||||
|
export {encodeSilk} from "./audio";
|
@ -14,9 +14,10 @@ import {
|
|||||||
import {promises as fs} from "node:fs";
|
import {promises as fs} from "node:fs";
|
||||||
import ffmpeg from "fluent-ffmpeg"
|
import ffmpeg from "fluent-ffmpeg"
|
||||||
import {NTQQFileApi} from "./api/file";
|
import {NTQQFileApi} from "./api/file";
|
||||||
import {calculateFileMD5, encodeSilk, isGIF} from "../common/utils/file";
|
import {calculateFileMD5, isGIF} from "../common/utils/file";
|
||||||
import {log} from "../common/utils/log";
|
import {log} from "../common/utils/log";
|
||||||
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video";
|
import {defaultVideoThumb, getVideoInfo} from "../common/utils/video";
|
||||||
|
import {encodeSilk} from "../common/utils/audio";
|
||||||
|
|
||||||
|
|
||||||
export class SendMsgElementConstructor {
|
export class SendMsgElementConstructor {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user