From b040c9b118c85a62c5ba5b47ed6a1482392c9e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Tue, 10 Sep 2024 18:39:14 +0800 Subject: [PATCH] refactor: audio --- src/common/audio.ts | 106 ++++++++++++++++++++---------------------- src/core/apis/file.ts | 7 ++- 2 files changed, 54 insertions(+), 59 deletions(-) diff --git a/src/common/audio.ts b/src/common/audio.ts index c37c3442..b45e7240 100644 --- a/src/common/audio.ts +++ b/src/common/audio.ts @@ -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 { + return new Promise((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 { + 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((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 {}; } -} +} \ No newline at end of file diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index d1b8260a..4f93f250 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -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) {