mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
✨ feat: 新增 均衡器
This commit is contained in:
1
src/assets/icons/Eq.svg
Normal file
1
src/assets/icons/Eq.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 17V7q0-.425.288-.712T8 6t.713.288T9 7v10q0 .425-.288.713T8 18t-.712-.288T7 17m4 4V3q0-.425.288-.712T12 2t.713.288T13 3v18q0 .425-.288.713T12 22t-.712-.288T11 21m-8-8v-2q0-.425.288-.712T4 10t.713.288T5 11v2q0 .425-.288.713T4 14t-.712-.288T3 13m12 4V7q0-.425.288-.712T16 6t.713.288T17 7v10q0 .425-.288.713T16 18t-.712-.288T15 17m4-4v-2q0-.425.288-.712T20 10t.713.288T21 11v2q0 .425-.288.713T20 14t-.712-.288T19 13"/></svg>
|
||||
|
After Width: | Height: | Size: 650 B |
165
src/components/Modal/Equalizer.vue
Normal file
165
src/components/Modal/Equalizer.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="equalizer">
|
||||
<n-flex align="center" justify="space-between" :size="8">
|
||||
<n-flex wrap :size="8" class="eq-presets">
|
||||
<n-tag
|
||||
v-for="(preset, key) in presetList"
|
||||
:key="key"
|
||||
:type="currentPreset === key ? 'primary' : 'default'"
|
||||
:bordered="currentPreset === key"
|
||||
:disabled="!enabled"
|
||||
round
|
||||
@click="applyPreset(key as PresetKey)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-switch v-model:value="enabled" :round="false" :disabled="!isElectron" />
|
||||
</n-flex>
|
||||
|
||||
<div class="eq-sliders">
|
||||
<div v-for="(freq, i) in frequencies" :key="freq" class="eq-col">
|
||||
<div class="eq-freq">{{ freqLabels[i] }}</div>
|
||||
<n-slider
|
||||
v-model:value="bands[i]"
|
||||
:min="-12"
|
||||
:max="12"
|
||||
:step="0.5"
|
||||
:tooltip="false"
|
||||
vertical
|
||||
:disabled="!enabled || !isElectron"
|
||||
@update:value="onBandChange(i, $event)"
|
||||
/>
|
||||
<div class="eq-value">{{ formatDb(bands[i]) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isElectron } from "@/utils/helper";
|
||||
import { useStatusStore } from "@/stores";
|
||||
import player from "@/utils/player";
|
||||
|
||||
const statusStore = useStatusStore();
|
||||
|
||||
type PresetKey = keyof typeof presetList;
|
||||
|
||||
// 10 段中心频率
|
||||
const frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
|
||||
const freqLabels = [
|
||||
"31Hz",
|
||||
"62Hz",
|
||||
"125Hz",
|
||||
"250Hz",
|
||||
"500Hz",
|
||||
"1kHz",
|
||||
"2kHz",
|
||||
"4kHz",
|
||||
"8kHz",
|
||||
"16kHz",
|
||||
];
|
||||
|
||||
// 预设(单位 dB),范围建议在 [-12, 12]
|
||||
const presetList = {
|
||||
acoustic: { label: "原声", bands: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
|
||||
pop: { label: "流行", bands: [0, 2, 4, 4, 1, -1, -1, 1, 2, 2] },
|
||||
rock: { label: "摇滚", bands: [4, 3, 2, 0, -1, 1, 2, 3, 3, 2] },
|
||||
classical: { label: "古典", bands: [0, 0, 1, 2, 3, 3, 2, 1, 0, 0] },
|
||||
jazz: { label: "爵士", bands: [0, 2, 3, 2, 0, 1, 2, 2, 1, 0] },
|
||||
vocal: { label: "人声", bands: [-2, -1, 0, 2, 4, 4, 2, 0, -1, -2] },
|
||||
dance: { label: "舞曲", bands: [5, 4, 3, 1, -1, 0, 2, 3, 3, 2] },
|
||||
custom: { label: "自定义", bands: [] as number[] },
|
||||
} as const;
|
||||
|
||||
const enabled = ref<boolean>(statusStore.eqEnabled);
|
||||
const currentPreset = ref<PresetKey>((statusStore.eqPreset as PresetKey) || "custom");
|
||||
const bands = ref<number[]>(
|
||||
statusStore.eqBands?.length === 10 ? [...statusStore.eqBands] : Array(10).fill(0),
|
||||
);
|
||||
|
||||
/** 格式化 dB 文本 */
|
||||
const formatDb = (v: number) => `${v >= 0 ? "+" : ""}${v}dB`;
|
||||
|
||||
/**
|
||||
* 应用预设
|
||||
*/
|
||||
const applyPreset = (key: PresetKey) => {
|
||||
if (!enabled.value) return;
|
||||
currentPreset.value = key;
|
||||
statusStore.setEqPreset(key);
|
||||
// 自定义不覆盖当前频段
|
||||
if (key !== "custom") {
|
||||
const arr = presetList[key].bands;
|
||||
bands.value = [...arr];
|
||||
statusStore.setEqBands(bands.value);
|
||||
if (enabled.value) player.updateEq({ bands: bands.value });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据当前开关状态应用/移除 EQ
|
||||
*/
|
||||
const applyEq = () => {
|
||||
if (!isElectron) return;
|
||||
statusStore.setEqEnabled(enabled.value);
|
||||
statusStore.setEqBands(bands.value);
|
||||
if (enabled.value) {
|
||||
player.enableEq({ bands: bands.value, frequencies });
|
||||
} else {
|
||||
player.disableEq();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 单段变更处理:实时更新 EQ
|
||||
*/
|
||||
const onBandChange = (index: number, value: number) => {
|
||||
bands.value[index] = value;
|
||||
statusStore.setEqBands(bands.value);
|
||||
// 任何手动拖动都切换为自定义
|
||||
if (currentPreset.value !== "custom") {
|
||||
currentPreset.value = "custom";
|
||||
statusStore.setEqPreset("custom");
|
||||
}
|
||||
if (enabled.value) player.updateEq({ bands: bands.value });
|
||||
};
|
||||
|
||||
watch(enabled, () => applyEq());
|
||||
|
||||
onMounted(() => {
|
||||
// 初始状态:若持久化为开启,则直接应用
|
||||
if (isElectron && enabled.value) player.enableEq({ bands: bands.value, frequencies });
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.equalizer {
|
||||
.eq-sliders {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
.eq-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.eq-freq {
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
:deep(.n-slider) {
|
||||
height: 160px;
|
||||
}
|
||||
.eq-value {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -64,7 +64,7 @@ import type { DropdownOption } from "naive-ui";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { isElectron, renderIcon } from "@/utils/helper";
|
||||
import player from "@/utils/player";
|
||||
import { openAutoClose, openChangeRate } from "@/utils/modal";
|
||||
import { openAutoClose, openChangeRate, openEqualizer } from "@/utils/modal";
|
||||
|
||||
const dataStore = useDataStore();
|
||||
const musicStore = useMusicStore();
|
||||
@@ -92,6 +92,14 @@ const playModeOptions: DropdownOption[] = [
|
||||
|
||||
// 其他控制:播放速度下拉菜单
|
||||
const controlsOptions = computed<DropdownOption[]>(() => [
|
||||
{
|
||||
label: "均衡器",
|
||||
key: "equalizer",
|
||||
icon: renderIcon("Eq"),
|
||||
props: {
|
||||
onClick: () => openEqualizer(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "自动关闭",
|
||||
key: "autoClose",
|
||||
|
||||
@@ -72,6 +72,12 @@ interface StatusState {
|
||||
personalFmMode: boolean;
|
||||
/** 更新检查 */
|
||||
updateCheck: boolean;
|
||||
/** 均衡器是否开启 */
|
||||
eqEnabled: boolean;
|
||||
/** 均衡器 10 段增益(dB) */
|
||||
eqBands: number[];
|
||||
/** 均衡器当前预设 key */
|
||||
eqPreset: string;
|
||||
/** 自动关闭 */
|
||||
autoClose: {
|
||||
/** 自动关闭 */
|
||||
@@ -118,6 +124,9 @@ export const useStatusStore = defineStore("status", {
|
||||
showDesktopLyric: false,
|
||||
showPlayerComment: false,
|
||||
updateCheck: false,
|
||||
eqEnabled: false,
|
||||
eqBands: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
eqPreset: "acoustic",
|
||||
autoClose: {
|
||||
enable: false,
|
||||
time: 30,
|
||||
@@ -196,6 +205,28 @@ export const useStatusStore = defineStore("status", {
|
||||
delete this.currentTimeOffsetMap[songId];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 设置 EQ 开关
|
||||
* @param enabled 是否开启
|
||||
*/
|
||||
setEqEnabled(enabled: boolean) {
|
||||
this.eqEnabled = enabled;
|
||||
},
|
||||
/**
|
||||
* 设置 EQ 10 段增益(dB)
|
||||
* @param bands 长度 10 的 dB 数组
|
||||
*/
|
||||
setEqBands(bands: number[]) {
|
||||
if (Array.isArray(bands) && bands.length === 10) {
|
||||
this.eqBands = [...bands];
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 设置 EQ 预设名
|
||||
*/
|
||||
setEqPreset(preset: string) {
|
||||
this.eqPreset = preset;
|
||||
},
|
||||
},
|
||||
// 持久化
|
||||
persist: {
|
||||
@@ -220,6 +251,9 @@ export const useStatusStore = defineStore("status", {
|
||||
"playHeartbeatMode",
|
||||
"personalFmMode",
|
||||
"autoClose",
|
||||
"eqEnabled",
|
||||
"eqBands",
|
||||
"eqPreset",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import UpdateApp from "@/components/Modal/UpdateApp.vue";
|
||||
import ExcludeKeywords from "@/components/Modal/ExcludeKeywords.vue";
|
||||
import ChangeRate from "@/components/Modal/ChangeRate.vue";
|
||||
import AutoClose from "@/components/Modal/AutoClose.vue";
|
||||
import Equalizer from "@/components/Modal/Equalizer.vue";
|
||||
|
||||
// 用户协议
|
||||
export const openUserAgreement = () => {
|
||||
@@ -279,3 +280,17 @@ export const openAutoClose = () => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** 打开均衡器弹窗 */
|
||||
export const openEqualizer = () => {
|
||||
window.$modal.create({
|
||||
preset: "card",
|
||||
transformOrigin: "center",
|
||||
autoFocus: false,
|
||||
style: { width: "600px" },
|
||||
title: "均衡器",
|
||||
content: () => {
|
||||
return h(Equalizer);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
233
src/utils/player-utils/context.ts
Normal file
233
src/utils/player-utils/context.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { isElectron } from "@/utils/helper";
|
||||
|
||||
/**
|
||||
* 针对单个 HTMLMediaElement 的音频图节点集合
|
||||
* - context: 全局共享的 AudioContext 实例
|
||||
* - source: 由 mediaElement 创建的源节点
|
||||
* - analyser: 频谱分析节点(频谱显示/后续处理的观察点)
|
||||
* - preGain: 均衡器前级增益(可选,启用 EQ 时存在)
|
||||
* - filters: 均衡器滤波器链(可选,启用 EQ 时存在,首尾为 shelf,中间为 peaking)
|
||||
*/
|
||||
export type AudioGraphNodes = {
|
||||
context: AudioContext;
|
||||
source: MediaElementAudioSourceNode;
|
||||
analyser: AnalyserNode;
|
||||
preGain?: GainNode;
|
||||
filters?: BiquadFilterNode[];
|
||||
};
|
||||
|
||||
/**
|
||||
* AudioContextManager
|
||||
* - 单例管理器:维护一个全局 AudioContext 与每个 mediaElement 的音频图
|
||||
* - 提供:
|
||||
* 1) 基础图创建与缓存(source -> analyser)
|
||||
* 2) EQ 启用、更新、禁用(构建/修改 source -> preGain -> filters... -> analyser 链)
|
||||
* 3) 图销毁与全局销毁
|
||||
*/
|
||||
class AudioContextManager {
|
||||
private static instance: AudioContextManager | null = null;
|
||||
private context: AudioContext | null = null;
|
||||
private nodeMap: WeakMap<HTMLMediaElement, AudioGraphNodes> = new WeakMap();
|
||||
|
||||
// 默认 10 段均衡器中心频率 (Hz)
|
||||
private readonly defaultFrequencies: number[] = [
|
||||
31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000,
|
||||
];
|
||||
// 默认 Q 值(首尾 shelf 不使用 Q)
|
||||
private readonly defaultQ: number = 1.0;
|
||||
|
||||
/** 获取管理器单例 */
|
||||
static getInstance() {
|
||||
if (!AudioContextManager.instance) {
|
||||
AudioContextManager.instance = new AudioContextManager();
|
||||
}
|
||||
return AudioContextManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建全局 AudioContext
|
||||
* 说明:仅在 Electron 环境中生效;浏览器端维持与现有逻辑一致不启用
|
||||
*/
|
||||
getContext(): AudioContext | null {
|
||||
if (!isElectron) return null;
|
||||
if (!this.context) {
|
||||
this.context = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
}
|
||||
return this.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取(或为该 mediaElement 创建)基础音频图:source -> analyser
|
||||
* 不在此处连接 destination,由调用方决定是否连接到输出以避免与 HTMLAudioElement 直出叠加
|
||||
*/
|
||||
getOrCreateBasicGraph(mediaElement: HTMLMediaElement): AudioGraphNodes | null {
|
||||
if (!isElectron) return null;
|
||||
const existing = this.nodeMap.get(mediaElement);
|
||||
if (existing) return existing;
|
||||
const context = this.getContext();
|
||||
if (!context) return null;
|
||||
const source = context.createMediaElementSource(mediaElement);
|
||||
const analyser = context.createAnalyser();
|
||||
analyser.fftSize = 512;
|
||||
// 默认仅连接到 analyser,是否接到 destination 由上层控制
|
||||
source.connect(analyser);
|
||||
const nodes: AudioGraphNodes = { context, source, analyser };
|
||||
this.nodeMap.set(mediaElement, nodes);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// 计算 dB 到线性增益
|
||||
private dbToLinear(db: number): number {
|
||||
return Math.pow(10, db / 20);
|
||||
}
|
||||
|
||||
/** 启用均衡器:source -> preGain -> filters... -> analyser(不连接 destination) */
|
||||
enableEq(
|
||||
mediaElement: HTMLMediaElement,
|
||||
options?: {
|
||||
bands?: number[]; // dB 值,与频率一一对应
|
||||
frequencies?: number[]; // 自定义中心频率
|
||||
q?: number; // peaking Q
|
||||
preamp?: number; // dB
|
||||
},
|
||||
) {
|
||||
if (!isElectron) return null;
|
||||
const nodes = this.getOrCreateBasicGraph(mediaElement);
|
||||
if (!nodes) return null;
|
||||
const { context, source, analyser } = nodes;
|
||||
// 断开 source 直连 analyser,改为走 EQ 链
|
||||
// 如果已存在 EQ 链,先彻底移除,避免重复并联导致增益叠加
|
||||
try {
|
||||
if (nodes.filters && nodes.filters.length) nodes.filters.forEach((f) => f.disconnect());
|
||||
if (nodes.preGain) nodes.preGain.disconnect();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
try {
|
||||
source.disconnect();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
// 创建 preGain(前级增益:整体增益控制,避免各段叠加导致失真)
|
||||
const preGain = context.createGain();
|
||||
preGain.gain.value = this.dbToLinear(options?.preamp ?? 0);
|
||||
// 创建滤波器(10 段:首尾 shelf,中间 peaking)
|
||||
const freqs = options?.frequencies ?? this.defaultFrequencies;
|
||||
const gains = options?.bands ?? new Array(freqs.length).fill(0);
|
||||
const q = options?.q ?? this.defaultQ;
|
||||
const filters: BiquadFilterNode[] = freqs.map((f, i) => {
|
||||
const filter = context.createBiquadFilter();
|
||||
if (i === 0) filter.type = "lowshelf";
|
||||
else if (i === freqs.length - 1) filter.type = "highshelf";
|
||||
else filter.type = "peaking";
|
||||
filter.frequency.value = f;
|
||||
if (filter.type === "peaking") filter.Q.value = q;
|
||||
filter.gain.value = gains[i] ?? 0; // dB
|
||||
return filter;
|
||||
});
|
||||
// 连接链路:source -> preGain -> f0 -> f1 ... -> fn -> analyser
|
||||
// 注意:不在此处连接到 destination,交由上层(player)决定,防止与 HTML 元素直出叠加
|
||||
source.connect(preGain);
|
||||
let current: AudioNode = preGain;
|
||||
for (const f of filters) {
|
||||
current.connect(f);
|
||||
current = f;
|
||||
}
|
||||
current.connect(analyser);
|
||||
// 保存
|
||||
nodes.preGain = preGain;
|
||||
nodes.filters = filters;
|
||||
this.nodeMap.set(mediaElement, nodes);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新均衡器参数(不重建链路)
|
||||
* - bands: 各频段 dB 值(与 frequencies 对齐),直接写入 filter.gain
|
||||
* - preamp: 前级增益 dB,转换为线性增益写入 preGain.gain
|
||||
* - q: peaking 类型的 Q 值统一更新(shelf 不适用 Q)
|
||||
*/
|
||||
updateEq(
|
||||
mediaElement: HTMLMediaElement,
|
||||
options: { bands?: number[]; preamp?: number; q?: number } = {},
|
||||
) {
|
||||
const nodes = this.nodeMap.get(mediaElement);
|
||||
if (!nodes || !nodes.filters || !nodes.preGain) return;
|
||||
const { filters, preGain } = nodes;
|
||||
if (typeof options.preamp === "number") {
|
||||
preGain.gain.value = this.dbToLinear(options.preamp);
|
||||
}
|
||||
if (Array.isArray(options.bands)) {
|
||||
filters.forEach((f, idx) => {
|
||||
if (typeof options.bands![idx] === "number") f.gain.value = options.bands![idx] as number;
|
||||
});
|
||||
}
|
||||
if (typeof options.q === "number") {
|
||||
filters.forEach((f) => {
|
||||
if (f.type === "peaking") f.Q.value = options.q as number;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用均衡器
|
||||
* - 断开 preGain 与所有 filters,并恢复为 source -> analyser
|
||||
* - 不直接连接到 destination,由调用方按需处理
|
||||
*/
|
||||
disableEq(mediaElement: HTMLMediaElement) {
|
||||
const nodes = this.nodeMap.get(mediaElement);
|
||||
if (!nodes) return;
|
||||
const { source, analyser, preGain, filters } = nodes;
|
||||
try {
|
||||
if (filters && filters.length) filters.forEach((f) => f.disconnect());
|
||||
if (preGain) preGain.disconnect();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
try {
|
||||
source.disconnect();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
source.connect(analyser);
|
||||
nodes.preGain = undefined;
|
||||
nodes.filters = undefined;
|
||||
this.nodeMap.set(mediaElement, nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开并移除指定元素的图(不关闭全局 context)
|
||||
* - 用于元素销毁或完全停止可视化/处理时的清理
|
||||
*/
|
||||
disposeGraph(mediaElement: HTMLMediaElement) {
|
||||
const nodes = this.nodeMap.get(mediaElement);
|
||||
if (!nodes) return;
|
||||
try {
|
||||
nodes.source.disconnect();
|
||||
nodes.analyser.disconnect();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
this.nodeMap.delete(mediaElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁整个上下文(谨慎调用)
|
||||
* - 关闭全局 AudioContext,并清空所有节点缓存
|
||||
* - 仅在应用退出或需要彻底重建时调用
|
||||
*/
|
||||
destroyAll() {
|
||||
if (this.context) {
|
||||
try {
|
||||
this.context.close();
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
this.context = null;
|
||||
}
|
||||
this.nodeMap = new WeakMap();
|
||||
}
|
||||
}
|
||||
|
||||
const audioContextManager = AudioContextManager.getInstance();
|
||||
export default audioContextManager;
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getUnlockSongUrl,
|
||||
} from "./player-utils/song";
|
||||
import { getLyricData } from "./player-utils/lyric";
|
||||
import audioContextManager from "@/utils/player-utils/context";
|
||||
import blob from "./blob";
|
||||
|
||||
// 播放器核心
|
||||
@@ -38,7 +39,6 @@ class Player {
|
||||
private audioContext: AudioContext | null = null;
|
||||
private analyser: AnalyserNode | null = null;
|
||||
private dataArray: Uint8Array<ArrayBuffer> | null = null;
|
||||
private source: MediaElementAudioSourceNode | null = null;
|
||||
/** 其他数据 */
|
||||
private testNumber: number = 0;
|
||||
private message: MessageReactive | null = null;
|
||||
@@ -243,7 +243,15 @@ class Player {
|
||||
// 允许跨域
|
||||
if (settingStore.showSpectrums) {
|
||||
const audioDom = this.getAudioDom();
|
||||
audioDom.crossOrigin = "anonymous";
|
||||
if (audioDom) audioDom.crossOrigin = "anonymous";
|
||||
}
|
||||
// 恢复均衡器:如持久化为开启,则在音频节点可用后立即构建 EQ 链
|
||||
if (isElectron && statusStore.eqEnabled) {
|
||||
try {
|
||||
this.enableEq({ bands: statusStore.eqBands });
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
// 恢复进度(仅在明确指定且大于0时才恢复,避免切换歌曲时意外恢复进度)
|
||||
if (seek && seek > 0) {
|
||||
@@ -439,12 +447,14 @@ class Player {
|
||||
/**
|
||||
* 获取 Audio Dom
|
||||
*/
|
||||
private getAudioDom() {
|
||||
const audioDom = (this.player as any)._sounds[0]._node;
|
||||
if (!audioDom) {
|
||||
throw new Error("Audio Dom is null");
|
||||
private getAudioDom(): HTMLMediaElement | null {
|
||||
try {
|
||||
const sounds = (this.player as any)?._sounds;
|
||||
const node = sounds && sounds.length ? sounds[0]?._node : null;
|
||||
return node || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return audioDom;
|
||||
}
|
||||
/**
|
||||
* 获取本地歌曲元信息
|
||||
@@ -1071,21 +1081,18 @@ class Player {
|
||||
initSpectrumData() {
|
||||
try {
|
||||
if (this.audioContext || !isElectron) return;
|
||||
// AudioContext
|
||||
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
// 获取音频元素
|
||||
const audioDom = this.getAudioDom();
|
||||
// 媒体元素源
|
||||
this.source = this.audioContext.createMediaElementSource(audioDom);
|
||||
// AnalyserNode
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
// 频谱分析器 FFT
|
||||
this.analyser.fftSize = 512;
|
||||
// 连接源和分析节点
|
||||
this.source.connect(this.analyser);
|
||||
// 连接分析节点到 AudioContext
|
||||
if (!audioDom) return;
|
||||
// 通过统一管理器创建/获取基础图
|
||||
const nodes = audioContextManager.getOrCreateBasicGraph(audioDom);
|
||||
if (!nodes) return;
|
||||
// 记录节点
|
||||
this.audioContext = nodes.context;
|
||||
this.analyser = nodes.analyser;
|
||||
// 可视化保持与原有行为一致:连接到输出
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
// 配置 AnalyserNode
|
||||
// 配置数据缓冲
|
||||
const bufferLength = this.analyser.frequencyBinCount;
|
||||
this.dataArray = new Uint8Array(bufferLength);
|
||||
// 更新频谱数据
|
||||
@@ -1095,6 +1102,61 @@ class Player {
|
||||
console.error("🎼 Initialize music spectrum failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用均衡器
|
||||
* @param options 配置
|
||||
* @param options.bands 各频段 dB 值(与 frequencies 对齐),直接写入 filter.gain
|
||||
* @param options.preamp 前级增益 dB,转换为线性增益写入 preGain.gain
|
||||
* @param options.q peaking 类型的 Q 值统一更新(shelf 不适用 Q)
|
||||
* @param options.frequencies 自定义中心频率
|
||||
*/
|
||||
enableEq(options?: { bands?: number[]; preamp?: number; q?: number; frequencies?: number[] }) {
|
||||
if (!isElectron) return;
|
||||
const audioDom = this.getAudioDom();
|
||||
if (!audioDom) return;
|
||||
const nodes = audioContextManager.enableEq(audioDom, options);
|
||||
if (!nodes) return;
|
||||
// 连接到输出,确保声音从 WebAudio 输出
|
||||
try {
|
||||
nodes.analyser.connect(nodes.context.destination);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新均衡器参数
|
||||
* @param options 配置
|
||||
* @param options.bands 各频段 dB 值(与 frequencies 对齐),直接写入 filter.gain
|
||||
* @param options.preamp 前级增益 dB,转换为线性增益写入 preGain.gain
|
||||
* @param options.q peaking 类型的 Q 值统一更新(shelf 不适用 Q)
|
||||
*/
|
||||
updateEq(options: { bands?: number[]; preamp?: number; q?: number }) {
|
||||
if (!isElectron) return;
|
||||
const audioDom = this.getAudioDom();
|
||||
if (!audioDom) return;
|
||||
audioContextManager.updateEq(audioDom, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用均衡器并恢复直出(保持频谱可用)
|
||||
*/
|
||||
disableEq() {
|
||||
if (!isElectron) return;
|
||||
const audioDom = this.getAudioDom();
|
||||
if (!audioDom) return;
|
||||
audioContextManager.disableEq(audioDom);
|
||||
// 恢复 analyser 输出
|
||||
const nodes = audioContextManager.getOrCreateBasicGraph(audioDom);
|
||||
if (nodes) {
|
||||
try {
|
||||
nodes.analyser.connect(nodes.context.destination);
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 切换桌面歌词
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user