UNPKG

esp-ai

Version:

Provide a complete set of AI dialogue solutions for your development board, including but not limited to the IAT+LLM+TTS integration solution for the ESP32 series development board. | 为你的开发板提供全套的AI对话方案,包括但不限于 `ESP32` 系列开发板的 `IAT+LLM+TTS` 集成方案。

312 lines (272 loc) 10.3 kB
const fs = require('fs'); const path = require('path'); const log = require("./log.js"); class AudioSaver { /** * 初始化音频保存器 * @param {string} deviceId - 设备ID * @param {string} baseDir - 基础保存目录 */ constructor(deviceId, baseDir = path.join(__dirname, 'audio_analysis')) { this.config = { enabled: false, baseDir, timestamp: null, rawStream: null, processedStream: null, rawFilePath: '', processedFilePath: '', deviceId: (deviceId || 'unknown_device').replace(/[^a-zA-Z0-9_-]/g, '_') }; } /** * 启用或关闭音频保存 * @param {boolean} enable - true 启用,false 关闭 */ toggle(enable = false) { try { // 若当前状态与目标状态一致,直接返回 if (this.config.enabled === enable) { this.config.enabled && log.info(`音频保存状态未变(当前: ${enable})`); return; } if (enable) { this.config.enabled = true; this.initialize(); } else { this.config.enabled = false; this.close(); } } catch (error) { log.error(`音频保存操作失败: ${error.message}`); this.config.enabled = false; throw error; } } /** * 初始化音频保存配置 */ initialize() { // 仅在未初始化且启用状态下执行 if (this.config.rawStream || !this.config.enabled) return; // 确保目录存在 this.ensureDirectory(); // 检查目录可写性 this.checkDirectoryWritable(); // 初始化文件路径 this.initFilePaths(); // 创建写入流 this.createWriteStreams(); // 测试写入 this.testWrite(); log.info(`音频保存已启用: 原始文件=${this.config.rawFilePath}, 处理后文件=${this.config.processedFilePath}`); } /** * 确保保存目录存在 */ ensureDirectory() { if (!fs.existsSync(this.config.baseDir)) { try { fs.mkdirSync(this.config.baseDir, {recursive: true, mode: 0o755}); log.info(`已创建音频保存目录: ${this.config.baseDir}`); } catch (err) { throw new Error(`创建目录失败: ${err.message} (路径: ${this.config.baseDir})`); } } } /** * 检查目录是否可写 */ checkDirectoryWritable() { try { fs.accessSync(this.config.baseDir, fs.constants.W_OK); log.info(`音频保存目录验证通过: ${this.config.baseDir}`); } catch (err) { throw new Error(`目录不可写: ${this.config.baseDir} (错误: ${err.message})`); } } /** * 初始化文件路径 */ initFilePaths() { this.config.timestamp = new Date().getTime(); const fileNamePrefix = `device_${this.config.deviceId}_${this.config.timestamp}`; this.config.rawFilePath = path.join(this.config.baseDir, `${fileNamePrefix}_raw.mp3`); this.config.processedFilePath = path.join(this.config.baseDir, `${fileNamePrefix}_processed.mp3`); } /** * 创建写入流 */ createWriteStreams() { // 创建原始音频文件写入流 log.info(`创建原始音频流: ${this.config.rawFilePath}`); this.config.rawStream = fs.createWriteStream(this.config.rawFilePath); this.config.rawStream.on('error', (err) => { log.error(`原始音频流错误: ${err.message}`); }); // 创建处理后音频文件写入流 log.info(`创建处理后音频流: ${this.config.processedFilePath}`); this.config.processedStream = fs.createWriteStream(this.config.processedFilePath); this.config.processedStream.on('error', (err) => { log.error(`处理后音频流错误: ${err.message}`); }); } /** * 测试写入功能 */ testWrite() { try { const testBuffer = Buffer.from('TEST_INIT', 'utf-8'); // 同步写入测试数据 fs.writeFileSync(this.config.rawFilePath, testBuffer); // 同步清空测试数据 fs.truncateSync(this.config.rawFilePath, 0); log.info(`音频保存初始化完成: 原始文件=${this.config.rawFilePath}`); } catch (err) { // 清理测试文件 if (fs.existsSync(this.config.rawFilePath)) { fs.unlinkSync(this.config.rawFilePath); } throw new Error(`测试写入失败: ${err.message}`); } } /** * 关闭所有音频流 */ close() { // 记录关闭前的状态(是否曾经启用) const wasEnabled = this.config.enabled; this.closeStream(this.config.rawStream, this.config.rawFilePath, '原始'); this.closeStream(this.config.processedStream, this.config.processedFilePath, '处理后'); this.config.rawStream = null; this.config.processedStream = null; // 仅当曾经启用过时,才打印关闭日志 if (wasEnabled) { log.info(`音频保存已禁用: device=${this.config.deviceId}`); } } /** * 关闭单个流 * @param {stream.Writable} stream - 要关闭的流 * @param {string} filePath - 文件路径 * @param {string} streamType - 流类型描述 */ closeStream(stream, filePath, streamType) { // 增加对已销毁流的判断 if (stream && !stream.destroyed) { try { if (!stream.closed) { stream.end(() => { log.info(`关闭${streamType}音频流: ${filePath}`); }); } } catch (err) { log.error(`关闭${streamType}音频流失败: ${err.message}`); } } } /** * 写入原始音频数据 * @param {Buffer} data - 音频数据 * @returns {boolean} 写入结果(未初始化时返回false) */ writeRaw(data) { // 校验数据类型 if (!(data instanceof Buffer)) { log.error(`写入原始音频失败: 数据必须为Buffer类型(实际类型: ${typeof data})`); return false; } // 未启用 或 流未初始化时,直接返回不报错 if (!this.config.enabled || !this.config.rawStream) { // 仅在调试模式下输出,避免干扰正常日志 log.info(`原始音频流未初始化或功能未启用,跳过写入(长度: ${data?.length || 0})`); return false; } // 确认流处于可写状态 if (this.config.rawStream.closed || this.config.rawStream.destroyed) { log.info(`原始音频流已关闭,跳过写入(长度: ${data?.length || 0})`); return false; } // 正常写入逻辑 const result = this.config.rawStream.write(data); log.info(`写入 rawStream: 长度=${data.length}, 写入结果=${result}`); if (!result) { this.config.rawStream.once('drain', () => { log.info(`rawStream 缓存已清空`); }); } return result; } /** * 写入处理后的音频数据 * @param {Buffer} data - 处理后的音频数据 */ writeProcessed(data) { log.info(`处理后音频输入跟踪: 数据长度=${data?.length || 0} bytes, 保存功能状态=${this.config.enabled}`); if (!(data instanceof Buffer)) { log.error(`写入处理后音频失败: 数据必须为Buffer类型(实际类型: ${typeof data})`); return false; } if (!data) { log.info(`跳过写入: 处理后音频数据为空`); return; } if (!this.config.enabled) { log.info(`跳过写入: 音频保存功能未启用`); return; } if (!this.config.processedStream) { log.info(`跳过写入: 处理后音频流未初始化`); return; } try { // 正常写入逻辑 const result = this.config.processedStream.write(data); log.info(`已写入处理后音频: 长度=${data.length} bytes, 写入结果=${result}`); if (!result) { this.config.processedStream.once('drain', () => { log.info(`processedStream 缓存已清空`); }); } } catch (err) { log.error(`处理后音频写入失败: ${err.message}`); } } /** * 标记原始音频保存完成 */ finishRaw() { if (this.config.enabled && this.config.rawStream) { this.config.rawStream.end(() => { log.info(`原始音频保存完成: device=${this.config.deviceId}, timestamp=${this.config.timestamp}`); }); this.config.rawStream = null; } } /** * 标记处理后音频保存完成 */ finishProcessed() { if (this.config.enabled && this.config.processedStream) { this.config.processedStream.end(() => { log.info(`处理后音频保存完成: device=${this.config.deviceId}, timestamp=${this.config.timestamp}`); }); this.config.processedStream = null; } } /** * 启用或禁用保存功能 * @param {boolean} enabled - 是否启用 */ setEnabled(enabled) { this.config.enabled = enabled; } /** * 检查是否启用 * @returns {boolean} 是否启用 */ isEnabled() { return this.config.enabled; } } module.exports = AudioSaver;