Initial clean project import

This commit is contained in:
cat-shark
2026-06-19 18:41:41 +08:00
commit a13b804c7a
1306 changed files with 220568 additions and 0 deletions
+283
View File
@@ -0,0 +1,283 @@
/**
* 内置音效定义(主进程版本)
* 注意:这是 src/lib/subtitle/effects/SoundEffectLibrary.ts 的精简版本
* 仅包含主进程所需的元数据和命令生成功能
*/
import * as path from 'path';
import * as fs from 'fs';
import { app } from 'electron';
/**
* 音效 ID 到文件名的映射
* 这些音效文件存储在 resources/extra/common/soundEffects/ 目录下
*/
export const SOUND_EFFECT_FILES: Record<string, string> = {
// 原有音效
'swoosh': 'swoosh.mp3',
'magic': 'magic.mp3',
// 新增已下载的精选音效
'pop-1': '789790__quatricise__pop-1.wav',
'glass-collide': '122583__timkahn__glass-collide.wav',
'sweep-up-down': '157553__timbre__sine-log-sweep-up-down.wav',
'interference': '194201__coldwellw__text-interference.wav',
'knife-scrape': '244258__bajko__blade-scrape_knife.wav',
'pill-pop': '256314__kodack__popping-open-some-pills.wav',
'chalk-writing': '377832__newagesoup__writing-chalk-med-01.wav',
'chalk-one': '377840__newagesoup__writing-chalk-oneshot-03.wav',
'chalk-short': '377841__newagesoup__writing-chalk-short-04.wav',
'bubble-drink': '398029__swordofkings128__raw_bubblingdrink5.ogg',
'chain-pull': '405417__mrthenoronha__pulling-chain.wav',
'page-turn': '457916__phantastonia__turn-over-the-book-page-2.wav',
'lamp-switch': '541761__philliparthursimmons__desk-lamp-chain-pull-medium.wav',
'firecracker': '559842__walkingwithmicrophones__firecracker-corsair-4.wav',
'dove': '607243__d4xx__collared-dove.wav',
'buildup': '610309__brickdeveloper171__buildup.wav',
'click': '678248__pixeliota__mouse-click-sound.mp3',
'cash-register': '201159__kiddpark__cash-register.mp3',
'gasp': '341908__bacruz666__gasp.mp3',
'impact': '319590__hybrid_v__shield-bash-impact.wav',
'whip': '593660__eelke__whip-03-single-loud.wav',
'airhorn': '414208__jacksonacademyashmore__airhorn.wav',
'heartbeat': '485076__inspectorj__heartbeat-regular-single-01-01-loop.wav',
'magic-sfx': '264981__renatalmar__sfx-magic.wav',
'punch': '104183__ekokubza123__punch.wav',
'arrow-whoosh': 'mixkit-arrow-whoosh-1491.wav',
'blow-impact': 'mixkit-impact-of-a-blow-2150.wav',
'whistle': '39548__the_bizniss__whistle.wav',
// 🆕 新增中文音效
'ding': '叮.MP3',
'ding2': '叮2.MP3',
'transition-whoosh': '呼转场.MP3',
'shua': '唰.MP3',
'bo2': '啵2.MP3',
'bo3': '啵3.MP3',
'sousou': '嗖嗖.MP3',
'popup': '弹出1.MP3',
'suspense': '悬疑咚声.MP3',
'gun-bolt': '拉栓.MP3',
'spacebar': '按空格键.MP3',
'cash-sound': '收银声.MP3',
'waterdrop1': '水滴1.MP3',
'waterdrop': '水滴声.MP3',
'bubble': '泡泡声.MP3',
'di': '滴.MP3',
'click-cn': '点击.MP3',
'electric': '电流.MP3',
'book-flip': '翻书声.MP3',
'flashback': '闪回.MP3',
};
/**
* 获取音效文件所在目录
*/
export function getSoundEffectsDir(): string {
const bundledDir = path.join(process.resourcesPath || '', 'resources-bundles/soundEffects');
if (fs.existsSync(bundledDir)) {
return bundledDir;
}
const isDev = !app.isPackaged;
if (isDev) {
// 开发模式:使用 app.getAppPath() 获取项目根目录
return path.join(app.getAppPath(), 'electron/resources/extra/common/soundEffects');
} else {
// 生产模式:使用 process.resourcesPath
return path.join(process.resourcesPath, 'extra/common/soundEffects');
}
}
/**
* 获取内置音效文件路径
* @param effectId 音效ID
* @returns 音效文件路径,如果不存在则返回 null
*/
export function getBuiltinEffectFilePath(effectId: string): string | null {
const fileName = SOUND_EFFECT_FILES[effectId];
if (!fileName) {
return null;
}
const soundEffectsDir = getSoundEffectsDir();
const filePath = path.join(soundEffectsDir, fileName);
if (fs.existsSync(filePath)) {
return filePath;
}
console.warn(`[SoundEffect] 音效文件不存在: ${filePath}`);
return null;
}
/**
* 检查是否有预置的音效文件
*/
export function hasBuiltinEffectFile(effectId: string): boolean {
return getBuiltinEffectFilePath(effectId) !== null;
}
/**
* 内置音效列表 - 包含所有精选音效
*/
export const BUILTIN_SOUND_EFFECTS = [
// 基础音效
{ id: 'swoosh', name: '划过音', description: '快速划过的嗖声,适合滑动效果', category: 'basic' },
{ id: 'click', name: '点击音', description: '清脆的鼠标点击声', category: 'basic' },
{ id: 'impact', name: '撞击声', description: '有力的撞击声效', category: 'basic' },
{ id: 'punch', name: '打击声', description: '拳击打击效果', category: 'basic' },
{ id: 'whip', name: '鞭子声', description: '快速挥动的鞭子声', category: 'basic' },
{ id: 'blow-impact', name: '打击音', description: '重击效果声', category: 'basic' },
{ id: 'arrow-whoosh', name: '箭矢划过', description: '箭矢快速飞过的声音', category: 'basic' },
{ id: 'pop-1', name: '气泡音', description: '清脆的气泡破裂声', category: 'basic' },
{ id: 'glass-collide', name: '玻璃碰撞', description: '清脆的玻璃撞击声', category: 'basic' },
{ id: 'sweep-up-down', name: '扫频音', description: '上下起伏的扫频声', category: 'basic' },
{ id: 'knife-scrape', name: '刀刮声', description: '金属刮擦的声音', category: 'basic' },
{ id: 'pill-pop', name: '药片弹出', description: '药片从包装中顶出的声音', category: 'basic' },
{ id: 'chalk-writing', name: '粉笔写字', description: '粉笔在黑板上书写的声音', category: 'basic' },
{ id: 'bubble-drink', name: '饮料气泡', description: '饮料中气泡翻腾的声音', category: 'basic' },
{ id: 'chain-pull', name: '拉链声', description: '拉动金属链条的声音', category: 'basic' },
{ id: 'page-turn', name: '翻书声', description: '快速翻动书页的声音', category: 'basic' },
{ id: 'lamp-switch', name: '开关声', description: '台灯拉线开关的声音', category: 'basic' },
{ id: 'firecracker', name: '鞭炮声', description: '鞭炮爆炸的声音', category: 'basic' },
{ id: 'dove', name: '鸽子叫', description: '鸽子的咕咕声', category: 'basic' },
{ id: 'buildup', name: '积蓄音', description: '能量积蓄上升的声音', category: 'basic' },
{ id: 'interference', name: '干扰音', description: '电子信号干扰的声音', category: 'advanced' },
// 提示音效
{ id: 'cash-register', name: '收银机', description: '收银机叮当声', category: 'motion' },
{ id: 'airhorn', name: '气喇叭', description: '响亮的气喇叭声', category: 'motion' },
{ id: 'whistle', name: '哨子声', description: '清脆的哨子声', category: 'motion' },
{ id: 'gasp', name: '惊叹声', description: '惊叹吸气声效', category: 'motion' },
// 特效音效
{ id: 'magic', name: '魔法音', description: '神奇梦幻的音效,适合魔法效果', category: 'advanced' },
{ id: 'magic-sfx', name: '魔法特效', description: '另一种魔法音效', category: 'advanced' },
{ id: 'heartbeat', name: '心跳声', description: '有节奏的心跳声', category: 'advanced' },
// 🆕 新增中文音效
{ id: 'ding', name: '叮', description: '清脆的叮声提示音', category: 'basic' },
{ id: 'ding2', name: '叮2', description: '另一种叮声变体', category: 'basic' },
{ id: 'transition-whoosh', name: '呼转场', description: '转场过渡的呼声', category: 'motion' },
{ id: 'shua', name: '唰', description: '快速滑动的唰声', category: 'basic' },
{ id: 'bo2', name: '啵2', description: '气泡破裂的啵声', category: 'basic' },
{ id: 'bo3', name: '啵3', description: '另一种啵声变体', category: 'basic' },
{ id: 'sousou', name: '嗖嗖', description: '快速移动的嗖嗖声', category: 'motion' },
{ id: 'popup', name: '弹出', description: '元素弹出的提示音', category: 'basic' },
{ id: 'suspense', name: '悬疑咚声', description: '制造悬念的低沉咚声', category: 'advanced' },
{ id: 'gun-bolt', name: '拉栓', description: '枪械拉栓的金属声', category: 'advanced' },
{ id: 'spacebar', name: '按空格键', description: '键盘空格键按下的声音', category: 'basic' },
{ id: 'cash-sound', name: '收银声', description: '收银结算的声音', category: 'motion' },
{ id: 'waterdrop1', name: '水滴1', description: '水滴落下的声音', category: 'basic' },
{ id: 'waterdrop', name: '水滴声', description: '水滴的清脆声音', category: 'basic' },
{ id: 'bubble', name: '泡泡声', description: '气泡冒出的声音', category: 'basic' },
{ id: 'di', name: '滴', description: '简短的滴声提示', category: 'basic' },
{ id: 'click-cn', name: '点击', description: '鼠标点击的声音', category: 'basic' },
{ id: 'electric', name: '电流', description: '电流通过的声音效果', category: 'advanced' },
{ id: 'book-flip', name: '翻书声', description: '翻动书页的声音', category: 'basic' },
{ id: 'flashback', name: '闪回', description: '回忆闪回的特效音', category: 'advanced' },
];
/**
* 获取所有内置音效
*/
export function getAllBuiltinEffects() {
return BUILTIN_SOUND_EFFECTS.map(effect => ({
id: effect.id,
name: effect.name,
description: effect.description,
category: effect.category,
defaultParams: {
volume: 0.7,
pitch: 1.0,
fadeIn: 0,
fadeOut: 50,
triggerTime: 'start'
}
}));
}
/**
* 根据ID获取内置音效
*/
export function getBuiltinEffect(effectId: string) {
return BUILTIN_SOUND_EFFECTS.find(e => e.id === effectId);
}
/**
* 生成 FFmpeg 音效合成命令
* 为不同的音效生成不同的声音参数
*/
export function generateFFmpegCommand(
effectId: string,
params: any,
outputPath: string
): string {
const effect = getBuiltinEffect(effectId);
if (!effect) {
throw new Error(`未找到音效: ${effectId}`);
}
const volume = params?.volume || 0.7;
const duration = params?.duration || 0.3;
const pitch = params?.pitch || 1.0;
// 根据音效 ID 生成不同的声音参数
let soundParams = getSoundParams(effectId);
// 应用用户的音调参数
if (soundParams.frequency) {
soundParams.frequency = Math.round(soundParams.frequency * pitch);
}
// 使用不同的音效类型
if (soundParams.type === 'noise') {
// 噪音效果 - 使用 anoisesrc 生成噪音
return `ffmpeg -f lavfi -i "anoisesrc=d=${duration}:r=44100:c=stereo" -filter_complex "[0:a]volume=${volume}[a]" -map "[a]" -y "${outputPath}"`;
} else if (soundParams.type === 'sweep') {
// 扫频效果 - 从高频到低频的变化
const freq1 = soundParams.freq1 || 1000;
const freq2 = soundParams.freq2 || 400;
// 使用变频正弦波:从 freq1 扫到 freq2
return `ffmpeg -f lavfi -i "sine=f=${freq1}:d=${duration}:s=44100" -filter_complex "[0:a]atempo=0.5,volume=${volume}[a]" -map "[a]" -y "${outputPath}"`;
} else {
// 普通正弦波 - 这是最可靠的方式
const freq = soundParams.frequency || 440;
return `ffmpeg -f lavfi -i "sine=f=${freq}:d=${duration}" -filter_complex "volume=${volume}" -y "${outputPath}"`;
}
}
/**
* 获取不同音效的声音参数
*/
function getSoundParams(effectId: string): any {
const soundMap: Record<string, any> = {
// 基础音效 - 不同频率的正弦波
'pop': { type: 'sine', frequency: 800 }, // 高频爆裂音
'swoosh': { type: 'sweep', freq1: 2000, freq2: 400 }, // 从高到低的扫频
'ping': { type: 'sine', frequency: 1200 }, // 更高的叮当声
'ding': { type: 'sine', frequency: 600 }, // 低一点的钟声
'click': { type: 'sine', frequency: 1000 }, // 点击声
'snap': { type: 'sine', frequency: 950 }, // 啪嗒声
'bounce': { type: 'sine', frequency: 400 }, // 低频弹跳
'zoom': { type: 'sweep', freq1: 300, freq2: 1200 }, // 上升扫频
// 动画音效
'slide': { type: 'sweep', freq1: 500, freq2: 800 }, // 滑动效果
'spin': { type: 'sine', frequency: 700 }, // 旋转声
'swing': { type: 'sweep', freq1: 400, freq2: 900 }, // 摆动
'shake': { type: 'noise' }, // 震动 - 噪音
'wave': { type: 'sweep', freq1: 300, freq2: 700 }, // 波浪
'sparkle': { type: 'sine', frequency: 1500 }, // 高频闪烁
// 高级音效
'electric': { type: 'noise' }, // 电流 - 噪音
'explosion': { type: 'sweep', freq1: 1000, freq2: 100 }, // 下降扫频
'magic': { type: 'sweep', freq1: 800, freq2: 1500 }, // 魔法上升音
'heartbeat': { type: 'sine', frequency: 100 }, // 低频心跳
'rainbow': { type: 'sweep', freq1: 200, freq2: 2000 } // 广频扫频
};
return soundMap[effectId] || { type: 'sine', frequency: 440 };
}
+21
View File
@@ -0,0 +1,21 @@
/**
* 音效系统模块入口
*/
import { initSoundEffectDb, initBuiltinSoundEffects } from './main';
/**
* 初始化音效系统
* @param dbInstance 数据库实例
*/
async function init(dbInstance: any) {
// 初始化数据库连接
initSoundEffectDb(dbInstance);
// 初始化系统内置音效
await initBuiltinSoundEffects();
}
export default {
init
};
+516
View File
@@ -0,0 +1,516 @@
/**
* 音效系统 - 后端 IPC 处理器
*/
import { ipcMain } from 'electron';
import { v4 as uuidv4 } from 'uuid';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import StorageMain from '../storage/main';
import { SoundEffectDb } from '../db/soundEffectDb';
import { getAllBuiltinEffects, getBuiltinEffect, generateFFmpegCommand, getBuiltinEffectFilePath } from './builtinEffects';
import shellApi from '../shell/index';
let db: any = null;
let soundEffectDb: SoundEffectDb | null = null;
/**
* 初始化数据库连接
*/
export function initSoundEffectDb(dbInstance: any) {
db = dbInstance;
soundEffectDb = new SoundEffectDb(db);
}
/**
* 初始化系统内置音效
*/
export async function initBuiltinSoundEffects() {
if (!soundEffectDb) {
console.warn('[SoundEffect] 数据库未初始化,跳过内置音效初始化');
return;
}
try {
console.log('[SoundEffect] 开始初始化系统内置音效...');
// 先清除旧的系统音效数据,确保同步最新的音效列表
await soundEffectDb.clearBuiltinEffects();
console.log('[SoundEffect] 已清除旧的系统音效数据');
const effects = getAllBuiltinEffects();
for (const effect of effects) {
try {
const record = {
id: effect.id as string,
name: effect.name,
description: effect.description,
category: effect.category,
type: effect.id,
audio_source: null, // 使用预置音效文件,不需要合成配置
volume: effect.defaultParams.volume || 0.7,
duration: 0.5, // 默认时长
metadata: JSON.stringify(effect.defaultParams),
created_at: Date.now(),
updated_at: Date.now(),
is_system: 1
};
await soundEffectDb.saveBuiltinEffect(record);
console.log(`[SoundEffect] ✅ 初始化音效: ${effect.name}`);
} catch (error) {
console.warn(`[SoundEffect] ⚠️ 初始化音效失败: ${effect.name}`, error);
}
}
console.log(`[SoundEffect] ✅ 系统内置音效初始化完成,共 ${effects.length} 个音效`);
} catch (error) {
console.error('[SoundEffect] ❌ 初始化音效系统失败:', error);
}
}
// ==================== IPC 事件处理 ====================
/**
* 获取所有内置音效
*/
ipcMain.handle('soundEffect:getBuiltinEffects', async (event) => {
try {
if (!soundEffectDb) {
// 如果数据库未初始化,从库中获取
return getAllBuiltinEffects();
}
const effects = await soundEffectDb.getBuiltinEffects();
return effects.map((e) => ({
id: e.id,
name: e.name,
description: e.description,
category: e.category,
defaultParams: e.metadata ? JSON.parse(e.metadata) : {}
}));
} catch (error) {
console.error('[SoundEffect] 获取内置音效失败:', error);
return getAllBuiltinEffects();
}
});
/**
* 🆕 获取所有音效(内置 + 用户自定义)
* 用户音效会合并到列表中,并标记为自定义
*/
ipcMain.handle('soundEffect:getAllEffects', async (event) => {
try {
// 1. 获取内置音效
let builtinEffects: any[] = [];
if (soundEffectDb) {
const effects = await soundEffectDb.getBuiltinEffects();
builtinEffects = effects.map((e) => ({
id: e.id,
name: e.name,
description: e.description,
category: e.category,
isCustom: false,
defaultParams: e.metadata ? JSON.parse(e.metadata) : {}
}));
} else {
builtinEffects = getAllBuiltinEffects().map(e => ({
...e,
isCustom: false
}));
}
// 2. 获取用户自定义音效
let userEffects: any[] = [];
if (soundEffectDb) {
const effects = await soundEffectDb.getUserEffects();
userEffects = effects.map((e) => ({
id: e.id, // 已经是 user_xxx 格式
name: `🎵 ${e.name}`, // 添加标识
description: e.description || '自定义音效',
category: 'custom',
isCustom: true,
filePath: e.file_path,
fileSize: e.file_size
}));
}
// 3. 合并列表:内置在前,用户在后
const allEffects = [...builtinEffects, ...userEffects];
console.log(`[SoundEffect] 获取所有音效: ${builtinEffects.length} 个内置 + ${userEffects.length} 个自定义`);
return allEffects;
} catch (error) {
console.error('[SoundEffect] 获取所有音效失败:', error);
return getAllBuiltinEffects().map(e => ({ ...e, isCustom: false }));
}
});
/**
* 获取所有用户自定义音效
*/
ipcMain.handle('soundEffect:getUserEffects', async (event) => {
try {
if (!soundEffectDb) {
return [];
}
const effects = await soundEffectDb.getUserEffects();
return effects.map((e) => ({
id: e.id,
name: e.name,
description: e.description,
filePath: e.file_path,
duration: e.duration,
fileSize: e.file_size,
createdAt: e.created_at,
category: 'custom'
}));
} catch (error) {
console.error('[SoundEffect] 获取用户音效失败:', error);
return [];
}
});
/**
* 导入用户音效文件
*/
ipcMain.handle(
'soundEffect:importUserEffect',
async (event, { filePath, name, description }) => {
console.log(`[SoundEffect] 📥 开始导入用户音效:`, { filePath, name, description });
try {
if (!soundEffectDb) {
console.error('[SoundEffect] ❌ 数据库未初始化');
return { success: false, message: '数据库未初始化' };
}
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
console.error(`[SoundEffect] ❌ 音效文件不存在: ${filePath}`);
return { success: false, message: '音效文件不存在' };
}
// 检查名称是否已存在
const nameExists = await soundEffectDb.isNameExists(name);
if (nameExists) {
console.warn(`[SoundEffect] ⚠️ 音效名称已存在: ${name}`);
return { success: false, message: '音效名称已存在' };
}
// 创建用户音效目录
const userSoundsDir = StorageMain.getUserSoundsDir();
console.log(`[SoundEffect] 📁 用户音效目录: ${userSoundsDir}`);
if (!fs.existsSync(userSoundsDir)) {
fs.mkdirSync(userSoundsDir, { recursive: true });
console.log(`[SoundEffect] ✅ 创建用户音效目录成功`);
}
// 复制文件到用户目录
// 使用 user_ 前缀区分用户音效和内置音效
const effectId = `user_${uuidv4()}`;
const fileExtension = path.extname(filePath);
const destPath = path.join(userSoundsDir, `${effectId}${fileExtension}`);
console.log(`[SoundEffect] 📋 复制文件: ${filePath} -> ${destPath}`);
fs.copyFileSync(filePath, destPath);
console.log(`[SoundEffect] ✅ 文件复制成功`);
// 保存到数据库
const record = {
id: effectId,
name,
description: description || null,
file_path: destPath,
duration: null,
file_size: fs.statSync(destPath).size,
created_at: Date.now(),
updated_at: Date.now()
};
console.log(`[SoundEffect] 💾 保存到数据库:`, record);
await soundEffectDb.saveUserEffect(record);
console.log(`[SoundEffect] ✅ 导入用户音效成功: ${name} (ID: ${effectId})`);
return {
success: true,
id: effectId,
message: '音效导入成功'
};
} catch (error) {
console.error('[SoundEffect] ❌ 导入用户音效失败:', error);
return { success: false, message: `导入失败: ${error}` };
}
}
);
/**
* 删除用户自定义音效
*/
ipcMain.handle('soundEffect:deleteUserEffect', async (event, effectId) => {
try {
if (!soundEffectDb) {
return { success: false, message: '数据库未初始化' };
}
const effect = await soundEffectDb.getUserEffectById(effectId);
if (!effect) {
return { success: false, message: '音效不存在' };
}
// 删除文件
if (fs.existsSync(effect.file_path)) {
try {
fs.unlinkSync(effect.file_path);
} catch (error) {
console.warn('[SoundEffect] 删除音效文件失败:', error);
}
}
// 删除数据库记录
await soundEffectDb.deleteUserEffect(effectId);
console.log(`[SoundEffect] ✅ 删除用户音效: ${effectId}`);
return { success: true, message: '音效已删除' };
} catch (error) {
console.error('[SoundEffect] 删除用户音效失败:', error);
return { success: false, message: `删除失败: ${error}` };
}
});
/**
* 生成音效文件(使用 FFmpeg 合成)
*/
ipcMain.handle(
'soundEffect:generateEffect',
async (event, { effectId, params, outputPath }) => {
try {
// 检查是否是内置音效
const effect = getBuiltinEffect(effectId);
if (!effect) {
return {
success: false,
error: `未找到音效: ${effectId}`
};
}
// 生成 FFmpeg 命令
const command = generateFFmpegCommand(
effectId,
params,
outputPath
);
console.log(`[SoundEffect] 生成音效命令:`, command);
// 这里需要调用 FFmpeg 执行命令
// 暂时返回命令,实际执行由调用方负责
return {
success: true,
ffmpegCommand: command,
audioFile: outputPath
};
} catch (error) {
console.error('[SoundEffect] 生成音效失败:', error);
return {
success: false,
error: `生成失败: ${error}`
};
}
}
);
/**
* 预览音效
* 统一处理:根据 ID 前缀自动判断是用户音效 (user_*) 还是内置音效
*/
ipcMain.handle(
'soundEffect:previewEffect',
async (event, { effectId, userEffectId }) => {
try {
// 统一使用 effectId,兼容旧的 userEffectId 参数
const id = effectId || userEffectId;
if (!id) {
return { success: false, message: '需要提供 effectId' };
}
console.log(`[SoundEffect] 🎵 预览音效: ${id}`);
// 根据 ID 前缀判断类型
if (id.startsWith('user_')) {
// 用户自定义音效
if (!soundEffectDb) {
return { success: false, message: '数据库未初始化' };
}
const effect = await soundEffectDb.getUserEffectById(id);
if (!effect) {
console.error(`[SoundEffect] ❌ 用户音效不存在: ${id}`);
return { success: false, message: '音效不存在' };
}
if (!effect.file_path || !fs.existsSync(effect.file_path)) {
console.error(`[SoundEffect] ❌ 音效文件不存在: ${effect.file_path}`);
return { success: false, message: '音效文件不存在' };
}
console.log(`[SoundEffect] ✅ 预览用户音效: ${effect.file_path}`);
return { success: true, audioPath: effect.file_path };
} else {
// 内置音效 - 优先使用预置音效文件
const effectFilePath = getBuiltinEffectFilePath(id);
if (effectFilePath) {
console.log(`[SoundEffect] ✅ 预览内置音效: ${effectFilePath}`);
return { success: true, audioPath: effectFilePath };
}
// 降级到 FFmpeg 合成(当预置文件不存在时)
console.log('[SoundEffect] 预置音效不存在,降级到 FFmpeg 合成');
return { success: false, message: `内置音效文件不存在或映射错误: ${id}` };
const tempDir = os.tmpdir();
const tempPath = path.join(
tempDir,
`preview_${Date.now()}.wav`
);
const command = generateFFmpegCommand(
id,
{},
tempPath
);
console.log('[SoundEffect] 预览音效命令:', command);
try {
// 解析命令字符串为参数数组
const args = command
.replace(/^ffmpeg\s+/, '') // 移除 'ffmpeg' 前缀
.match(/(?:[^\s"]+|"[^"]*")+/g) // 分割参数,保留引号内的空格
.map(arg => arg.replace(/^"|"$/g, '')); // 移除参数两端的引号
console.log('[SoundEffect] FFmpeg 参数:', args);
// 执行 FFmpeg 命令
await shellApi.execFFmpegCommand(args);
console.log('[SoundEffect] 音效文件生成成功:', tempPath);
// 返回生成的音频文件路径
return {
success: true,
audioPath: tempPath
};
} catch (error) {
console.error('[SoundEffect] FFmpeg 执行失败:', error);
return {
success: false,
message: `音效生成失败: ${error}`
};
}
}
} catch (error) {
console.error('[SoundEffect] 预览音效失败:', error);
return { success: false, message: `预览失败: ${error}` };
}
}
);
/**
* 🆕 获取音效文件路径(用于视频生成时的音效混音)
* 统一处理:根据 ID 前缀自动判断是用户音效 (user_*) 还是内置音效
*/
ipcMain.handle(
'soundEffect:getFilePath',
async (event, { effectId, userEffectId }) => {
try {
// 统一使用 effectId,兼容旧的 userEffectId 参数
const id = effectId || userEffectId;
if (!id) {
return { success: false, message: '需要提供 effectId' };
}
// 根据 ID 前缀判断类型
if (id.startsWith('user_')) {
// 用户自定义音效
if (!soundEffectDb) {
return { success: false, message: '数据库未初始化' };
}
const effect = await soundEffectDb.getUserEffectById(id);
if (effect && effect.file_path && fs.existsSync(effect.file_path)) {
console.log(`[SoundEffect] ✅ 获取用户音效路径: ${id} -> ${effect.file_path}`);
return {
success: true,
filePath: effect.file_path
};
}
return { success: false, message: `用户音效文件不存在: ${id}` };
} else {
// 内置音效
const filePath = getBuiltinEffectFilePath(id);
if (filePath) {
console.log(`[SoundEffect] ✅ 获取内置音效路径: ${id} -> ${filePath}`);
return {
success: true,
filePath: filePath
};
}
return { success: false, message: `内置音效文件不存在: ${id}` };
}
} catch (error) {
console.error('[SoundEffect] 获取音效文件路径失败:', error);
return { success: false, message: `获取失败: ${error}` };
}
}
);
/**
* 🆕 批量获取音效文件路径(用于视频生成时的批量处理)
*/
ipcMain.handle(
'soundEffect:getBatchFilePaths',
async (event, { soundInstances }) => {
try {
const results: { effectId: string; filePath: string | null; triggerTime: number }[] = [];
for (const instance of soundInstances) {
const effectId = instance.effectId;
const filePath = getBuiltinEffectFilePath(effectId);
if (!filePath) {
return {
success: false,
message: `内置音效文件不存在或映射错误: ${effectId}`
};
}
results.push({
effectId,
filePath,
triggerTime: instance.triggerTime
});
if (filePath) {
console.log(`[SoundEffect] 获取音效路径: ${effectId} -> ${filePath}`);
} else {
console.warn(`[SoundEffect] 音效文件不存在: ${effectId}`);
}
}
return {
success: true,
results
};
} catch (error) {
console.error('[SoundEffect] 批量获取音效路径失败:', error);
return { success: false, message: `获取失败: ${error}` };
}
}
);
+135
View File
@@ -0,0 +1,135 @@
/**
* 音效系统 - 前端 IPC 调用包装
*/
const soundEffect = {
/**
* 获取所有内置音效
*/
async getBuiltinEffects() {
try {
return await ipcRenderer.invoke('soundEffect:getBuiltinEffects');
} catch (error) {
console.error('[soundEffect:getBuiltinEffects] IPC 调用失败:', error);
throw error;
}
},
/**
* 🆕 获取所有音效(内置 + 用户自定义)
*/
async getAllEffects() {
try {
return await ipcRenderer.invoke('soundEffect:getAllEffects');
} catch (error) {
console.error('[soundEffect:getAllEffects] IPC 调用失败:', error);
throw error;
}
},
/**
* 获取所有用户自定义音效
*/
async getUserEffects() {
try {
return await ipcRenderer.invoke('soundEffect:getUserEffects');
} catch (error) {
console.error('[soundEffect:getUserEffects] IPC 调用失败:', error);
throw error;
}
},
/**
* 导入用户音效文件
*/
async importUserEffect(filePath: string, name: string, description?: string) {
try {
return await ipcRenderer.invoke('soundEffect:importUserEffect', {
filePath,
name,
description
});
} catch (error) {
console.error('[soundEffect:importUserEffect] IPC 调用失败:', error);
throw error;
}
},
/**
* 删除用户自定义音效
*/
async deleteUserEffect(effectId: string) {
try {
return await ipcRenderer.invoke('soundEffect:deleteUserEffect', effectId);
} catch (error) {
console.error('[soundEffect:deleteUserEffect] IPC 调用失败:', error);
throw error;
}
},
/**
* 生成音效文件
*/
async generateEffect(effectId: string, params: any, outputPath: string) {
try {
return await ipcRenderer.invoke('soundEffect:generateEffect', {
effectId,
params,
outputPath
});
} catch (error) {
console.error('[soundEffect:generateEffect] IPC 调用失败:', error);
throw error;
}
},
/**
* 预览音效
* @param effectId 音效ID(内置或用户自定义,用户音效使用 user_ 前缀)
* @param userEffectId 已弃用,保留用于兼容
*/
async previewEffect(effectId?: string, userEffectId?: string) {
try {
// 统一使用 effectId,兼容旧的调用方式
const id = effectId || userEffectId;
return await ipcRenderer.invoke('soundEffect:previewEffect', {
effectId: id
});
} catch (error) {
console.error('[soundEffect:previewEffect] IPC 调用失败:', error);
throw error;
}
},
/**
* 🆕 获取音效文件路径(用于视频生成时混音)
*/
async getFilePath(effectId?: string, userEffectId?: string) {
try {
return await ipcRenderer.invoke('soundEffect:getFilePath', {
effectId,
userEffectId
});
} catch (error) {
console.error('[soundEffect:getFilePath] IPC 调用失败:', error);
return { success: false, message: `获取失败: ${error}` };
}
},
/**
* 🆕 批量获取音效文件路径(用于视频生成时的批量处理)
*/
async getBatchFilePaths(soundInstances: any[]) {
try {
return await ipcRenderer.invoke('soundEffect:getBatchFilePaths', {
soundInstances
});
} catch (error) {
console.error('[soundEffect:getBatchFilePaths] IPC 调用失败:', error);
return { success: false, message: `获取失败: ${error}` };
}
}
};
export default soundEffect;