refactor: audio

This commit is contained in:
手瓜一十雪 2024-09-10 18:39:14 +08:00
parent f6da7da90b
commit b040c9b118
2 changed files with 54 additions and 59 deletions

View File

@ -1,66 +1,64 @@
import fs from 'fs';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from './log';
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
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);
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const EXIT_CODES = [0, 255];
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
let duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
logger.log('通过文件大小估算语音的时长:', duration);
return duration;
}
async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', err => {
logger.log('FFmpeg处理转换出错: ', err.message);
reject(err);
});
cp.on('exit', async (code, signal) => {
if (code == null || EXIT_CODES.includes(code)) {
try {
const data = await fsPromise.readFile(pcmPath);
await fsPromise.unlink(pcmPath);
resolve(data);
} catch (err) {
reject(err);
}
} else {
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
reject(new Error('FFmpeg处理转换失败'));
}
});
});
}
async function handleWavFile(file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
return await convert(filePath, pcmPath, logger);
}
return file;
}
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
try {
const file = await fsPromise.readFile(filePath);
const pttPath = path.join(TEMP_DIR, randomUUID());
if (!isSilk(file)) {
logger.log(`语音文件${filePath}需要转换成silk`);
const _isWav = isWav(file);
const pcmPath = pttPath + '.pcm';
let sampleRate = 0;
const convert = () => {
return new Promise<Buffer>((resolve, reject) => {
// todo: 通过配置文件获取ffmpeg路径
const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
const cp = spawn(ffmpegPath, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
cp.on('error', err => {
logger.log('FFmpeg处理转换出错: ', err.message);
return reject(err);
});
cp.on('exit', (code, signal) => {
const EXIT_CODES = [0, 255];
if (code == null || EXIT_CODES.includes(code)) {
sampleRate = 24000;
const data = fs.readFileSync(pcmPath);
fs.unlink(pcmPath, (err) => {
});
return resolve(data);
}
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
reject(Error('FFmpeg处理转换失败'));
});
});
};
let input: Buffer;
if (!_isWav) {
input = await convert();
} else {
input = file;
const allowSampleRate = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const { fmt } = getWavFileInfo(input);
// log(`wav文件信息`, fmt)
if (!allowSampleRate.includes(fmt.sampleRate)) {
input = await convert();
}
}
const silk = await encode(input, sampleRate);
fs.writeFileSync(pttPath, silk.data);
const pcmPath = `${pttPath}.pcm`;
const input = isWav(file) ? await handleWavFile(file, filePath, pcmPath, logger) : await convert(filePath, pcmPath, logger);
const silk = await encode(input, 24000);
await fsPromise.writeFile(pttPath, silk.data);
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
@ -68,15 +66,13 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
duration: silk.duration / 1000,
};
} else {
const silk = file;
let duration = 0;
try {
duration = getDuration(silk) / 1000;
duration = getDuration(file) / 1000;
} catch (e: any) {
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
duration = await guessDuration(filePath);
duration = await guessDuration(filePath, logger);
}
return {
converted: false,
path: filePath,
@ -87,4 +83,4 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
logger.logError('convert silk failed', error.stack);
return {};
}
}
}

View File

@ -33,7 +33,7 @@ export class NTQQFileApi {
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager('http://napcat-sign.wumiao.wang:2082/rkey', this.context.logger);
this.rkeyManager = new RkeyManager('https://llob.linyuchen.net/rkey', this.context.logger);
}
async copyFile(filePath: string, destPath: string) {
@ -346,8 +346,8 @@ export class NTQQFileApi {
if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
const imageAppid = parsedUrl.searchParams.get('appid');
const isNTFlavoredPic = imageAppid && ['1406', '1407'].includes(imageAppid);
if (isNTFlavoredPic) {
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
if (isNTV2) {
let rkey = parsedUrl.searchParams.get('rkey');
if (rkey) {
return IMAGE_HTTP_HOST_NT + url;
@ -356,7 +356,6 @@ export class NTQQFileApi {
rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `${rkey}`;
} else {
// 老的图片url不需要rkey
return IMAGE_HTTP_HOST + url;
}
} else if (fileMd5 || md5HexStr) {