import fsPromise from 'fs/promises'; import path from 'node:path'; import { randomUUID } from 'crypto'; import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm'; import { LogWrapper } from '@/napcat-core/helper/log'; import { EncodeArgs } from 'napcat-common/src/audio-worker'; import { FFmpegService } from '@/napcat-core/helper/ffmpeg/ffmpeg'; import { runTask } from 'napcat-common/src/worker'; import { fileURLToPath } from 'node:url'; const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; function getWorkerPath () { // return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href; return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs'); } async function guessDuration (pttPath: string, logger: LogWrapper) { const pttFileInfo = await fsPromise.stat(pttPath); const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s logger.log('通过文件大小估算语音的时长:', duration); return duration; } async function handleWavFile ( file: Buffer, filePath: string, pcmPath: string ): Promise<{ input: Buffer; sampleRate: number }> { const { fmt } = getWavFileInfo(file); if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) { const result = await FFmpegService.convert(filePath, pcmPath); return { input: await fsPromise.readFile(pcmPath), sampleRate: result.sampleRate }; } return { input: file, sampleRate: fmt.sampleRate }; } 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 pcmPath = `${pttPath}.pcm`; // const { input, sampleRate } = isWav(file) ? await handleWavFile(file, filePath, pcmPath): { input: await FFmpegService.convert(filePath, pcmPath) ? await fsPromise.readFile(pcmPath) : Buffer.alloc(0), sampleRate: 24000 }; let input: Buffer; let sampleRate: number; if (isWav(file)) { const result = await handleWavFile(file, filePath, pcmPath); input = result.input; sampleRate = result.sampleRate; } else { const result = await FFmpegService.convert(filePath, pcmPath); input = await fsPromise.readFile(pcmPath); sampleRate = result.sampleRate; } const silk = await runTask(getWorkerPath(), { input, sampleRate }); fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e)); await fsPromise.writeFile(pttPath, Buffer.from(silk.data)); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); return { converted: true, path: pttPath, duration: silk.duration / 1000, }; } else { let duration = 0; try { duration = getDuration(file) / 1000; } catch (e: unknown) { logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, (e as Error).stack); duration = await guessDuration(filePath, logger); } return { converted: false, path: filePath, duration, }; } } catch (error: unknown) { logger.logError('convert silk failed', error); return {}; } }