Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 59 additions & 24 deletions src/components/Setting/config/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ export const usePlaySettings = (): SettingConfig => {
get: () => settingStore.useNextPrefetch,
set: (v) => (settingStore.useNextPrefetch = v),
}),
forceIf: {
condition: computed(() => settingStore.useGaplessPlayback),
forcedValue: true,
forcedDescription: "无缝播放已启用,需要保持预载开启",
},
},
{
key: "memoryLastSeek",
Expand Down Expand Up @@ -425,50 +430,80 @@ export const usePlaySettings = (): SettingConfig => {
],
},
{
key: "enableAutomix",
label: "启用自动混音",
type: "switch",
key: "songTransitionMode",
label: "切歌过渡模式",
type: "select",
tags: [{ text: "Beta", type: "warning" }],
description: computed(() =>
settingStore.playbackEngine === "web-audio"
? "是否启用自动混音功能"
: "自动混音功能仅在使用 Web Audio 引擎时可用",
? "选择歌曲切换时的过渡方式"
: "过渡模式仅在使用 Web Audio 引擎时可用",
),
options: computed(() => [
{ label: "关闭", value: "off" },
{
label: "自动混音 (Auto Mix)",
value: "automix",
disabled: !isElectron,
},
{
label: "无缝播放 (Gapless)",
value: "gapless",
disabled: settingStore.audioEngine !== "element",
},
]),
value: computed({
get: () => settingStore.enableAutomix,
get: () => settingStore.songTransitionMode,
set: (v) => {
if (v) {
if (v === "automix") {
window.$dialog.warning({
title: "启用自动混音 (Beta)",
content:
"可能出现兼容性问题,该功能在早期测试,遇到问题请反馈issue,不保证可以及时处理。效果可能因为歌曲而异,保守策略。",
positiveText: "开启",
negativeText: "取消",
onPositiveClick: () => {
settingStore.enableAutomix = true;
settingStore.songTransitionMode = "automix";
},
});
} else if (v === "gapless") {
window.$dialog.warning({
title: "启用无缝播放 (Beta)",
content:
"无缝播放会预解码下一首歌曲的音频数据,每首歌曲约占用 50-150MB 内存。如果设备内存较小,可能影响性能。该功能目前处于预览状态,有任何问题请提交 Issues。",
positiveText: "开启",
negativeText: "取消",
onPositiveClick: () => {
settingStore.songTransitionMode = "gapless";
},
});
} else {
settingStore.enableAutomix = v;
settingStore.songTransitionMode = v;
}
Comment on lines +458 to 482
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了减少代码重复并提高可维护性,可以对 automixgapless 的处理逻辑进行合并。这两个分支的结构几乎完全相同,只是标题和内容不同。

                if (v === "automix" || v === "gapless") {
                  const titles = {
                    automix: "启用自动混音 (Beta)",
                    gapless: "启用无缝播放 (Beta)",
                  };
                  const contents = {
                    automix:
                      "可能出现兼容性问题,该功能在早期测试,遇到问题请反馈issue,不保证可以及时处理。效果可能因为歌曲而异,保守策略。",
                    gapless:
                      "无缝播放会预解码下一首歌曲的音频数据,每首歌曲约占用 50-150MB 内存。如果设备内存较小,可能影响性能。该功能目前处于预览状态,有任何问题请提交 Issues。",
                  };
                  window.$dialog.warning({
                    title: titles[v],
                    content: contents[v],
                    positiveText: "开启",
                    negativeText: "取消",
                    onPositiveClick: () => {
                      settingStore.songTransitionMode = v;
                    },
                  });
                } else {
                  settingStore.songTransitionMode = v;
                }

},
}),
disabled: computed(() => settingStore.playbackEngine !== "web-audio"),
children: [
{
key: "automixMaxAnalyzeTime",
label: "最大分析时间",
type: "input-number",
description: "单位秒,越长越精准但更耗时 (建议 60s)",
min: 5,
max: 300,
suffix: "s",
value: computed({
get: () => settingStore.automixMaxAnalyzeTime,
set: (v) => (settingStore.automixMaxAnalyzeTime = v),
}),
},
],
condition: () => settingStore.songTransitionMode !== "off",
children: computed(() => {
if (settingStore.songTransitionMode === "automix") {
return [
{
key: "automixMaxAnalyzeTime",
label: "最大分析时间",
type: "input-number" as const,
description: "单位秒,越长越精准但更耗时 (建议 60s)",
min: 5,
max: 300,
suffix: "s",
value: computed({
get: () => settingStore.automixMaxAnalyzeTime,
set: (v: number) => (settingStore.automixMaxAnalyzeTime = v),
}),
},
];
}
return [];
}),
},
],
},
Expand Down
18 changes: 12 additions & 6 deletions src/core/audio-player/BaseAudioPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,20 @@ export abstract class BaseAudioPlayer
}

const duration = options.fadeIn ? (options.fadeDuration ?? 0.5) : 0;
const target = this.volume * this.replayGain;

// 修复:如果是渐入,强制从 0 开始
// 渐入时直接操作 gainNode,绕过 applyFadeTo 避免 cancelScheduledValues 取消刚设置的值
if (duration > 0 && this.gainNode && this.audioCtx) {
this.gainNode.gain.setValueAtTime(0, this.audioCtx.currentTime);
const ct = this.audioCtx.currentTime;
this.gainNode.gain.cancelScheduledValues(ct);
this.gainNode.gain.setValueAtTime(0, ct);
const safeStart = ct + 0.02;
this.gainNode.gain.setValueAtTime(0, safeStart);
this.gainNode.gain.linearRampToValueAtTime(target, safeStart + duration);
} else {
this.applyFadeTo(target, 0);
}

this.applyFadeTo(this.volume * this.replayGain, duration, options.fadeCurve);

try {
await this.doPlay();
} catch (e) {
Expand Down Expand Up @@ -274,8 +280,8 @@ export abstract class BaseAudioPlayer
*/
public stop() {
this.cancelPendingPause();
// 捕获可能产生的异步错误
Promise.resolve(this.pause({ fadeOut: false })).catch(() => {});
// 使用 keepContextRunning 防止挂起共享 AudioContext
Promise.resolve(this.pause({ fadeOut: false, keepContextRunning: true })).catch(() => {});
Promise.resolve(this.doSeek(0)).catch(() => {});
}

Expand Down
34 changes: 2 additions & 32 deletions src/core/automix/AutomixManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getSharedAudioContext } from "./SharedAudioContext";
import { useAudioManager } from "../player/AudioManager";
import { useSongManager } from "../player/SongManager";
import { usePlayerController } from "../player/PlayerController";
import { useDataStore, useMusicStore, useSettingStore, useStatusStore } from "@/stores";
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
import type {
AudioAnalysis,
AutomixPlan,
Expand Down Expand Up @@ -1063,37 +1063,7 @@ export class AutomixManager {
}

public getNextSongForAutomix(): { song: SongType; index: number } | null {
const dataStore = useDataStore();
const statusStore = useStatusStore();
const playerController = usePlayerController();

if (dataStore.playList.length === 0) return null;

// 单曲循环模式下,下一首就是当前这首
if (statusStore.repeatMode === "one") {
const currentSong = dataStore.playList[statusStore.playIndex];
if (currentSong) {
return { song: currentSong, index: statusStore.playIndex };
}
}

if (dataStore.playList.length <= 1) return null;

let nextIndex = statusStore.playIndex;
let attempts = 0;
const maxAttempts = dataStore.playList.length;

while (attempts < maxAttempts) {
nextIndex++;
if (nextIndex >= dataStore.playList.length) nextIndex = 0;

const nextSong = dataStore.playList[nextIndex];
if (!playerController.shouldSkipSong(nextSong)) {
return { song: nextSong, index: nextIndex };
}
attempts++;
}
return null;
return usePlayerController().getNextSongInfo();
}
}

Expand Down
Loading