Skip to content

Commit 32c4aa6

Browse files
committed
🐞 fix: 切换引擎自动重启 & 修复进度定时器 & 添加一些注释
1 parent c5cc64a commit 32c4aa6

7 files changed

Lines changed: 272 additions & 27 deletions

File tree

electron/main/ipc/ipc-system.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ const initSystemIpc = (): void => {
3333
app.quit();
3434
});
3535

36+
// 重启应用
37+
ipcMain.on("restart-app", () => {
38+
ipcLog.info("🔄 Restarting application...");
39+
app.relaunch();
40+
app.exit(0);
41+
});
42+
3643
// 获取系统全部字体
3744
ipcMain.handle("get-all-fonts", async () => {
3845
try {

src/components/Setting/PlaySetting.vue

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@
102102
</n-text>
103103
</div>
104104
<n-select
105-
v-model:value="settingStore.audioEngine"
105+
:value="settingStore.audioEngine"
106106
:options="Object.values(audioEngineData)"
107107
class="set"
108+
@update:value="handleEngineChange"
108109
/>
109110
</n-card>
110111
<n-card v-if="!isElectron" class="set-item">
@@ -117,13 +118,20 @@
117118
<n-card v-if="isElectron" class="set-item">
118119
<div class="label">
119120
<n-text class="name">音频输出设备</n-text>
120-
<n-text class="tip" :depth="3">新增或移除音频设备后请重新打开设置</n-text>
121+
<n-text class="tip" :depth="3">
122+
{{
123+
settingStore.audioEngine === "ffmpeg"
124+
? "FFmpeg 引擎不支持切换输出设备"
125+
: "新增或移除音频设备后请重新打开设置"
126+
}}
127+
</n-text>
121128
</div>
122129
<n-select
123130
v-model:value="settingStore.playDevice"
124131
class="set"
125132
:options="outputDevices"
126133
:render-option="renderOption"
134+
:disabled="settingStore.audioEngine === 'ffmpeg'"
127135
@update:value="playDeviceChange"
128136
/>
129137
</n-card>
@@ -487,6 +495,34 @@ const showSpectrumsChange = (value: boolean) => {
487495
settingStore.showSpectrums = value;
488496
};
489497
498+
// 处理音频引擎切换
499+
const handleEngineChange = (newEngine: "element" | "ffmpeg") => {
500+
const oldEngine = settingStore.audioEngine;
501+
if (newEngine === oldEngine) return;
502+
503+
// 先保存新的引擎设置
504+
settingStore.audioEngine = newEngine;
505+
506+
// 弹出确认对话框
507+
window.$dialog.warning({
508+
title: "切换音频引擎",
509+
content: "音频引擎切换需要重启应用才能生效。是否立即重启?",
510+
positiveText: "立即重启",
511+
negativeText: "稍后",
512+
onPositiveClick: () => {
513+
// 立即重启应用
514+
if (isElectron) {
515+
window.electron.ipcRenderer.send("restart-app");
516+
} else {
517+
window.location.reload();
518+
}
519+
},
520+
onNegativeClick: () => {
521+
window.$message.info("引擎将在下次启动时生效");
522+
},
523+
});
524+
};
525+
490526
onMounted(() => {
491527
if (isElectron) {
492528
getOutputDevices();

src/core/audio-player/AudioEffectManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ export class AudioEffectManager {
9292
return dataArray;
9393
}
9494

95+
/**
96+
* 获取低频音量
97+
* @returns 低频音量 (0-1)
98+
*/
9599
public getLowFrequencyVolume(): number {
96100
if (!this.analyserNode) return 0;
97101

src/core/audio-player/AudioElementPlayer.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import { AUDIO_EVENTS, BaseAudioPlayer, type AudioEventType } from "./BaseAudioP
22

33
/**
44
* 基于 HTMLAudioElement 的播放器实现
5+
*
6+
* 使用原生 HTML5 Audio 元素进行音频播放,支持大多数常见格式
7+
* 通过 MediaElementAudioSourceNode 连接到 Web Audio API 音频图谱
58
*/
69
export class AudioElementPlayer extends BaseAudioPlayer {
10+
/** 内部 Audio 元素 */
711
private audioElement: HTMLAudioElement;
12+
/** MediaElementAudioSourceNode 用于连接 Web Audio API */
813
private sourceNode: MediaElementAudioSourceNode | null = null;
914

10-
/** Seek 锁 */
15+
/** Seek 锁,用于在 seek 过程中返回稳定的 currentTime */
1116
private isInternalSeeking = false;
12-
/** 目标时间缓存 */
17+
/** 目标时间缓存,用于在 seek 过程中返回稳定的 currentTime */
1318
private targetSeekTime = 0;
1419

1520
constructor() {
@@ -23,6 +28,10 @@ export class AudioElementPlayer extends BaseAudioPlayer {
2328
});
2429
}
2530

31+
/**
32+
* 当音频图谱初始化完成时调用
33+
* 创建 MediaElementAudioSourceNode 并连接到输入节点
34+
*/
2635
protected onGraphInitialized(): void {
2736
if (!this.audioCtx || !this.inputNode) return;
2837

@@ -35,66 +44,107 @@ export class AudioElementPlayer extends BaseAudioPlayer {
3544
}
3645
}
3746

47+
/**
48+
* 加载音频资源
49+
* @param url 音频地址
50+
*/
3851
public async load(url: string): Promise<void> {
3952
this.audioElement.src = url;
4053
this.audioElement.load();
4154
}
4255

56+
/**
57+
* 执行底层播放
58+
* @returns 播放 Promise
59+
*/
4360
protected async doPlay(): Promise<void> {
4461
return this.audioElement.play();
4562
}
4663

64+
/**
65+
* 执行底层暂停
66+
*/
4767
protected doPause(): void {
4868
this.audioElement.pause();
4969
}
5070

71+
/**
72+
* 跳转到指定时间
73+
* @param time 目标时间(秒)
74+
*/
5175
public async seek(time: number): Promise<void> {
5276
this.isInternalSeeking = true;
5377
this.targetSeekTime = time;
5478

5579
await super.seek(time);
5680
}
5781

82+
/**
83+
* 执行底层 Seek
84+
* @param time 目标时间(秒)
85+
*/
5886
protected doSeek(time: number): void {
5987
if (Number.isFinite(time)) {
6088
this.audioElement.currentTime = time;
6189
}
6290
}
6391

92+
/**
93+
* 设置播放速率
94+
* @param value 速率值 (0.5 - 2.0)
95+
*/
6496
public setRate(value: number): void {
6597
this.audioElement.playbackRate = value;
6698
}
6799

100+
/**
101+
* 获取当前播放速率
102+
* @returns 当前速率值
103+
*/
68104
public getRate(): number {
69105
return this.audioElement.playbackRate;
70106
}
71107

108+
/**
109+
* 设置音频输出设备
110+
* @param deviceId 设备 ID
111+
*/
72112
protected async doSetSinkId(deviceId: string): Promise<void> {
73113
if (typeof this.audioElement.setSinkId === "function") {
74114
await this.audioElement.setSinkId(deviceId);
75115
}
76116
}
77117

118+
/** 获取当前音频源地址 */
78119
public get src(): string {
79120
return this.audioElement.src || "";
80121
}
81122

123+
/** 获取音频总时长(秒) */
82124
public get duration(): number {
83125
return this.audioElement.duration || 0;
84126
}
85127

128+
/**
129+
* 获取当前播放时间(秒)
130+
* 如果正在 Seek,返回目标时间以避免进度跳回
131+
*/
86132
public get currentTime(): number {
87-
// 如果正在 Seek (无论是等待淡出,还是正在缓冲),强制返回目标时间以避免进度跳回
88133
if (this.isInternalSeeking) {
89134
return this.targetSeekTime;
90135
}
91136
return this.audioElement.currentTime || 0;
92137
}
93138

139+
/** 获取是否暂停状态 */
94140
public get paused(): boolean {
95141
return this.audioElement.paused;
96142
}
97143

144+
/**
145+
* 获取错误码
146+
* @returns 错误码 (0: 无错误, 1: ABORTED, 2: NETWORK, 3: DECODE, 4: SRC_NOT_SUPPORTED)
147+
*/
98148
public getErrorCode(): number {
99149
if (!this.audioElement.error) return 0;
100150
switch (this.audioElement.error.code) {
@@ -113,6 +163,7 @@ export class AudioElementPlayer extends BaseAudioPlayer {
113163

114164
/**
115165
* 监听原生 DOM 事件并转发为标准事件
166+
* 将 HTMLAudioElement 的事件转换为 BaseAudioPlayer 的统一事件格式
116167
*/
117168
private bindInternalEvents() {
118169
const events: AudioEventType[] = Object.values(AUDIO_EVENTS);

src/core/audio-player/BaseAudioPlayer.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,14 @@ export abstract class BaseAudioPlayer extends EventTarget {
172172
*/
173173
public async seek(time: number) {
174174
this.cancelPendingPause();
175-
176175
// 如果已经暂停,直接跳转
177176
if (this.paused) {
178177
this.doSeek(time);
179178
return;
180179
}
181-
182180
this.applyFadeTo(0, SEEK_FADE_TIME);
183-
184181
await new Promise((resolve) => setTimeout(resolve, SEEK_FADE_TIME * 1000));
185-
186182
this.doSeek(time);
187-
188183
this.applyFadeTo(this.volume, SEEK_FADE_TIME);
189184
}
190185

@@ -278,18 +273,22 @@ export abstract class BaseAudioPlayer extends EventTarget {
278273
}
279274
}
280275

276+
/** 获取频率数据 */
281277
public getFrequencyData(): Uint8Array {
282278
return this.effectManager ? this.effectManager.getFrequencyData() : new Uint8Array(0);
283279
}
284280

281+
/** 获取低频音量 */
285282
public getLowFrequencyVolume(): number {
286283
return this.effectManager ? this.effectManager.getLowFrequencyVolume() : 0;
287284
}
288285

286+
/** 设置滤波器增益 */
289287
public setFilterGain(index: number, value: number) {
290288
this.effectManager?.setFilterGain(index, value);
291289
}
292290

291+
/** 获取滤波器增益 */
293292
public getFilterGains(): number[] {
294293
return this.effectManager ? this.effectManager.getFilterGains() : [];
295294
}
@@ -321,6 +320,11 @@ export abstract class BaseAudioPlayer extends EventTarget {
321320
public abstract get paused(): boolean;
322321
public abstract getErrorCode(): number;
323322

323+
/**
324+
* 触发事件
325+
* @param type 事件类型
326+
* @param detail 事件详情
327+
*/
324328
protected emit(type: Exclude<AudioEventType, "error">): void;
325329
protected emit(type: typeof AUDIO_EVENTS.ERROR, detail: AudioErrorDetail): void;
326330
protected emit(type: AudioEventType, detail?: AudioErrorDetail): void {
@@ -331,6 +335,12 @@ export abstract class BaseAudioPlayer extends EventTarget {
331335
}
332336
}
333337

338+
/**
339+
* 添加事件监听
340+
* @param type 事件类型
341+
* @param listener 事件监听器
342+
* @param options 事件选项
343+
*/
334344
public override addEventListener<K extends keyof AudioEventMap>(
335345
type: K,
336346
listener: (this: BaseAudioPlayer, ev: AudioEventMap[K]) => unknown,
@@ -339,6 +349,12 @@ export abstract class BaseAudioPlayer extends EventTarget {
339349
super.addEventListener(type, listener as EventListenerOrEventListenerObject, options);
340350
}
341351

352+
/**
353+
* 移除事件监听
354+
* @param type 事件类型
355+
* @param listener 事件监听器
356+
* @param options 事件选项
357+
*/
342358
public override removeEventListener<K extends keyof AudioEventMap>(
343359
type: K,
344360
listener: (this: BaseAudioPlayer, ev: AudioEventMap[K]) => unknown,

0 commit comments

Comments
 (0)