mirror of
https://github.com/fofolee/uTools-quickcommand.git
synced 2025-09-23 20:44:42 +08:00
编排新增音频分类,支持朗读文本、播放音频、录制音频、播放系统音效
This commit is contained in:
@@ -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;
|
||||
|
9
plugin/lib/quickcomposer/audio/index.js
Normal file
9
plugin/lib/quickcomposer/audio/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const speech = require("./speech");
|
||||
const media = require("./media");
|
||||
const record = require("./record");
|
||||
|
||||
module.exports = {
|
||||
speech,
|
||||
media,
|
||||
...record,
|
||||
};
|
164
plugin/lib/quickcomposer/audio/media.js
Normal file
164
plugin/lib/quickcomposer/audio/media.js
Normal file
@@ -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<Object>} 音频信息
|
||||
*/
|
||||
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,
|
||||
};
|
44
plugin/lib/quickcomposer/audio/record.js
Normal file
44
plugin/lib/quickcomposer/audio/record.js
Normal file
@@ -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,
|
||||
};
|
59
plugin/lib/quickcomposer/audio/speech.js
Normal file
59
plugin/lib/quickcomposer/audio/speech.js
Normal file
@@ -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,
|
||||
};
|
Reference in New Issue
Block a user