From f9a1aefff625f3c56be8ac6f72cc98dc15c81771 Mon Sep 17 00:00:00 2001 From: fofolee Date: Fri, 10 Jan 2025 00:07:41 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E6=8E=92=E6=96=B0=E5=A2=9E=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E5=88=86=E7=B1=BB=EF=BC=8C=E6=94=AF=E6=8C=81=E6=9C=97?= =?UTF-8?q?=E8=AF=BB=E6=96=87=E6=9C=AC=E3=80=81=E6=92=AD=E6=94=BE=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E3=80=81=E5=BD=95=E5=88=B6=E9=9F=B3=E9=A2=91=E3=80=81?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E7=B3=BB=E7=BB=9F=E9=9F=B3=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/quickcomposer.js | 1 + plugin/lib/quickcomposer/audio/index.js | 9 + plugin/lib/quickcomposer/audio/media.js | 164 +++++++++++++ plugin/lib/quickcomposer/audio/record.js | 44 ++++ plugin/lib/quickcomposer/audio/speech.js | 59 +++++ src/components/composer/common/ParamInput.vue | 2 + src/js/composer/commands/audioCommands.js | 227 ++++++++++++++++++ src/js/composer/commands/fileCommands.js | 19 ++ src/js/composer/commands/index.js | 3 + src/js/composer/commands/systemCommands.js | 15 +- 10 files changed, 539 insertions(+), 4 deletions(-) create mode 100644 plugin/lib/quickcomposer/audio/index.js create mode 100644 plugin/lib/quickcomposer/audio/media.js create mode 100644 plugin/lib/quickcomposer/audio/record.js create mode 100644 plugin/lib/quickcomposer/audio/speech.js create mode 100644 src/js/composer/commands/audioCommands.js diff --git a/plugin/lib/quickcomposer.js b/plugin/lib/quickcomposer.js index 3c3d650..bdc6083 100644 --- a/plugin/lib/quickcomposer.js +++ b/plugin/lib/quickcomposer.js @@ -7,6 +7,7 @@ const quickcomposer = { coding: require("./quickcomposer/coding"), math: require("./quickcomposer/math"), ui: require("./quickcomposer/ui"), + audio: require("./quickcomposer/audio"), }; module.exports = quickcomposer; diff --git a/plugin/lib/quickcomposer/audio/index.js b/plugin/lib/quickcomposer/audio/index.js new file mode 100644 index 0000000..0ddb262 --- /dev/null +++ b/plugin/lib/quickcomposer/audio/index.js @@ -0,0 +1,9 @@ +const speech = require("./speech"); +const media = require("./media"); +const record = require("./record"); + +module.exports = { + speech, + media, + ...record, +}; diff --git a/plugin/lib/quickcomposer/audio/media.js b/plugin/lib/quickcomposer/audio/media.js new file mode 100644 index 0000000..dd4c0d1 --- /dev/null +++ b/plugin/lib/quickcomposer/audio/media.js @@ -0,0 +1,164 @@ +const { spawn } = require("child_process"); +const fs = require("fs"); + +// 存储当前播放的音频实例 +let currentAudio = null; + +// 系统音效映射 +const SYSTEM_SOUNDS = { + beep: { + win: "Beep", + mac: "Ping.aiff", + }, + error: { + win: "Asterisk", + mac: "Basso.aiff", + }, + warning: { + win: "Exclamation", + mac: "Sosumi.aiff", + }, + notification: { + win: "Notification", + mac: "Glass.aiff", + }, + complete: { + win: "SystemAsterisk", + mac: "Hero.aiff", + }, + click: { + win: "MenuCommand", + mac: "Tink.aiff", + }, +}; + +/** + * 播放音频文件 + * @param {string} file 音频文件路径 + * @param {number} volume 音量 (0-1) + * @param {boolean} loop 是否循环播放 + * @param {boolean} autoplay 是否自动播放 + */ +async function play(file, volume = 1, loop = false, autoplay = true) { + // 停止当前音频 + stop(); + + // 检查文件是否存在 + if (!fs.existsSync(file)) { + throw new Error(`音频文件不存在: ${file}`); + } + + // 创建新的音频实例 + const audio = new Audio(); + audio.src = `file://${file}`; + audio.volume = parseFloat(volume) || 1; + audio.loop = !!loop; + + // 保存当前实例 + currentAudio = audio; + + // 如果设置了自动播放 + if (autoplay !== false) { + audio.play(); + } + + // 返回 Promise,在播放结束时 resolve + return new Promise((resolve, reject) => { + audio.onended = () => { + if (!audio.loop) { + currentAudio = null; + resolve(); + } + }; + audio.onerror = (error) => { + currentAudio = null; + reject(error); + }; + }); +} + +/** + * 播放系统音效 + * @param {string} type 音效类型 + * @param {number} volume 音量 (0-1) + */ +async function beep(type = "beep", volume = 1) { + // 在 Windows 上使用 PowerShell 播放系统音效 + if (process.platform === "win32") { + const soundName = SYSTEM_SOUNDS[type]?.win || SYSTEM_SOUNDS.beep.win; + // 使用系统音效 + const script = `[System.Media.SystemSounds]::${soundName}.Play()`; + + return new Promise((resolve, reject) => { + const ps = spawn("powershell", ["-Command", script]); + ps.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`PowerShell 命令执行失败,退出码: ${code}`)); + }); + }); + } + // 在 macOS 上使用 afplay 播放系统音效 + else if (process.platform === "darwin") { + const soundName = SYSTEM_SOUNDS[type]?.mac || SYSTEM_SOUNDS.beep.mac; + volume = parseFloat(volume) || 1; + return new Promise((resolve, reject) => { + const afplay = spawn("afplay", [ + `/System/Library/Sounds/${soundName}`, + "-v", + volume, + ]); + afplay.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`afplay 命令执行失败,退出码: ${code}`)); + }); + }); + } + // 在其他平台上使用 utools.shellBeep + else { + utools.shellBeep(); + return Promise.resolve(); + } +} + +/** + * 停止音频播放 + */ +function stop() { + if (currentAudio) { + currentAudio.pause(); + currentAudio.currentTime = 0; + currentAudio = null; + } +} + +/** + * 分析音频文件 + * @param {string} file 音频文件路径 + * @returns {Promise} 音频信息 + */ +async function analyze(file) { + if (!fs.existsSync(file)) { + throw new Error(`音频文件不存在: ${file}`); + } + + const audio = new Audio(); + audio.src = `file://${file}`; + + return new Promise((resolve, reject) => { + audio.onloadedmetadata = () => { + resolve({ + duration: audio.duration, + channels: audio.mozChannels || 2, + sampleRate: audio.mozSampleRate || 44100, + }); + }; + audio.onerror = reject; + }); +} + +module.exports = { + play, + beep, + stop, + analyze, +}; diff --git a/plugin/lib/quickcomposer/audio/record.js b/plugin/lib/quickcomposer/audio/record.js new file mode 100644 index 0000000..64ec1cb --- /dev/null +++ b/plugin/lib/quickcomposer/audio/record.js @@ -0,0 +1,44 @@ +const fs = require("fs"); + +/** + * 录制音频 + * @param {number} duration 录制时长(ms) + * @param {string} savePath 保存路径 + */ +async function record(duration = 5000, savePath) { + const format = "audio/webm"; + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const mediaRecorder = new MediaRecorder(stream, { mimeType: format }); + const chunks = []; + + return new Promise((resolve, reject) => { + mediaRecorder.ondataavailable = (e) => chunks.push(e.data); + mediaRecorder.onstop = async () => { + try { + const blob = new Blob(chunks, { type: format }); + if (savePath) { + // 使用 FileReader 读取 blob 数据 + const reader = new FileReader(); + reader.onload = () => { + const buffer = Buffer.from(reader.result); + fs.writeFileSync(savePath, buffer); + }; + reader.readAsArrayBuffer(blob); + } + stream.getTracks().forEach((track) => track.stop()); + resolve(blob); + } catch (error) { + reject(error); + } + }; + mediaRecorder.onerror = reject; + + mediaRecorder.start(); + setTimeout(() => mediaRecorder.stop(), duration); + }); +} + +module.exports = { + record, +}; diff --git a/plugin/lib/quickcomposer/audio/speech.js b/plugin/lib/quickcomposer/audio/speech.js new file mode 100644 index 0000000..508025b --- /dev/null +++ b/plugin/lib/quickcomposer/audio/speech.js @@ -0,0 +1,59 @@ +// 存储当前朗读实例 +let currentSpeech = null; + +/** + * 朗读文本 + * @param {string} text 要朗读的文本 + * @param {Object} options 朗读选项 + * @param {number} options.rate 语速 (0.1-10) + * @param {number} options.pitch 音调 (0-2) + * @param {number} options.volume 音量 (0-1) + * @param {string} options.lang 语言 + */ +async function speak(text, options = {}) { + // 停止当前朗读 + stop(); + + // 创建新的语音合成实例 + const speech = new window.SpeechSynthesisUtterance(); + speech.text = text; + speech.rate = parseFloat(options.rate) || 1; + speech.pitch = parseFloat(options.pitch) || 1; + speech.volume = parseFloat(options.volume) || 1; + speech.lang = options.lang || "zh-CN"; + + // 保存当前实例 + currentSpeech = speech; + + // 开始朗读 + window.speechSynthesis.speak(speech); + + // 返回 Promise,在朗读结束时 resolve + return new Promise((resolve, reject) => { + speech.onend = () => { + currentSpeech = null; + resolve(); + }; + speech.onerror = (error) => { + currentSpeech = null; + reject(error); + }; + }); +} + +/** + * 停止文本朗读 + */ +function stop() { + if (window.speechSynthesis) { + window.speechSynthesis.cancel(); + } + if (currentSpeech) { + currentSpeech = null; + } +} + +module.exports = { + speak, + stop, +}; diff --git a/src/components/composer/common/ParamInput.vue b/src/components/composer/common/ParamInput.vue index 6084c64..dbcbe05 100644 --- a/src/components/composer/common/ParamInput.vue +++ b/src/components/composer/common/ParamInput.vue @@ -45,6 +45,7 @@