mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
360 lines
7.8 KiB
Vue
360 lines
7.8 KiB
Vue
<template>
|
||
<div class="eq-control">
|
||
<div class="eq-header">
|
||
<h3>{{ t('player.eq.title') }}
|
||
<n-tag type="warning" size="small" round v-if="!isElectron">
|
||
桌面版可用,网页端不支持
|
||
</n-tag>
|
||
</h3>
|
||
<div class="eq-controls">
|
||
<n-switch v-model:value="isEnabled" @update:value="toggleEQ">
|
||
<template #checked>{{ t('player.eq.on') }}</template>
|
||
<template #unchecked>{{ t('player.eq.off') }}</template>
|
||
</n-switch>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="eq-presets">
|
||
<n-scrollbar x-scrollable>
|
||
<n-space :size="6" :wrap="false">
|
||
<n-tag
|
||
v-for="preset in presetOptions"
|
||
:key="preset.value"
|
||
:type="currentPreset === preset.value ? 'success' : 'default'"
|
||
:bordered="false"
|
||
size="medium"
|
||
round
|
||
clickable
|
||
@click="applyPreset(preset.value)"
|
||
>
|
||
{{ preset.label }}
|
||
</n-tag>
|
||
</n-space>
|
||
</n-scrollbar>
|
||
</div>
|
||
|
||
<div class="eq-sliders">
|
||
<div v-for="freq in frequencies" :key="freq" class="eq-slider">
|
||
<div class="freq-label">{{ formatFreq(freq) }}</div>
|
||
<n-slider
|
||
v-model:value="eqValues[freq.toString()]"
|
||
:min="-12"
|
||
:max="12"
|
||
:step="0.1"
|
||
vertical
|
||
:disabled="!isEnabled"
|
||
@update:value="updateEQ(freq.toString(), $event)"
|
||
/>
|
||
<div class="gain-value">{{ eqValues[freq.toString()] }}dB</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { onMounted, ref } from 'vue';
|
||
import { useI18n } from 'vue-i18n';
|
||
|
||
import { audioService } from '@/services/audioService';
|
||
import { isElectron } from '@/utils';
|
||
|
||
const { t } = useI18n();
|
||
|
||
const frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
|
||
const eqValues = ref<{ [key: string]: number }>({});
|
||
const isEnabled = ref(audioService.isEQEnabled());
|
||
const currentPreset = ref(audioService.getCurrentPreset() || 'flat');
|
||
|
||
// 预设配置
|
||
const presets = {
|
||
flat: {
|
||
label: t('player.eq.presets.flat'),
|
||
values: Object.fromEntries(frequencies.map((f) => [f, 0]))
|
||
},
|
||
pop: {
|
||
label: t('player.eq.presets.pop'),
|
||
values: {
|
||
31: -1.5,
|
||
62: 3.5,
|
||
125: 5.5,
|
||
250: 3.5,
|
||
500: -0.5,
|
||
1000: -1.5,
|
||
2000: 1.5,
|
||
4000: 2.5,
|
||
8000: 2.5,
|
||
16000: 2.5
|
||
}
|
||
},
|
||
rock: {
|
||
label: t('player.eq.presets.rock'),
|
||
values: {
|
||
31: 4.5,
|
||
62: 3.5,
|
||
125: 2,
|
||
250: 0.5,
|
||
500: -0.5,
|
||
1000: -1,
|
||
2000: 0.5,
|
||
4000: 2,
|
||
8000: 2.5,
|
||
16000: 3.5
|
||
}
|
||
},
|
||
classical: {
|
||
label: t('player.eq.presets.classical'),
|
||
values: {
|
||
31: 3.5,
|
||
62: 3,
|
||
125: 2.5,
|
||
250: 1.5,
|
||
500: -0.5,
|
||
1000: -1.5,
|
||
2000: -1.5,
|
||
4000: 0.5,
|
||
8000: 2,
|
||
16000: 3
|
||
}
|
||
},
|
||
jazz: {
|
||
label: t('player.eq.presets.jazz'),
|
||
values: {
|
||
31: 3,
|
||
62: 2,
|
||
125: 1.5,
|
||
250: 2,
|
||
500: -1,
|
||
1000: -1.5,
|
||
2000: -0.5,
|
||
4000: 1,
|
||
8000: 2.5,
|
||
16000: 3
|
||
}
|
||
},
|
||
hiphop: {
|
||
label: t('player.eq.presets.hiphop'),
|
||
values: {
|
||
31: 5,
|
||
62: 4.5,
|
||
125: 3,
|
||
250: 1.5,
|
||
500: -0.5,
|
||
1000: -1,
|
||
2000: 0.5,
|
||
4000: 1.5,
|
||
8000: 2,
|
||
16000: 2.5
|
||
}
|
||
},
|
||
vocal: {
|
||
label: t('player.eq.presets.vocal'),
|
||
values: {
|
||
31: -2,
|
||
62: -1.5,
|
||
125: -1,
|
||
250: 0.5,
|
||
500: 2,
|
||
1000: 3.5,
|
||
2000: 3,
|
||
4000: 1.5,
|
||
8000: 0.5,
|
||
16000: 0
|
||
}
|
||
},
|
||
dance: {
|
||
label: t('player.eq.presets.dance'),
|
||
values: {
|
||
31: 4,
|
||
62: 3.5,
|
||
125: 2.5,
|
||
250: 1,
|
||
500: 0,
|
||
1000: -0.5,
|
||
2000: 1.5,
|
||
4000: 2.5,
|
||
8000: 3,
|
||
16000: 2.5
|
||
}
|
||
},
|
||
acoustic: {
|
||
label: t('player.eq.presets.acoustic'),
|
||
values: {
|
||
31: 2,
|
||
62: 1.5,
|
||
125: 1,
|
||
250: 1.5,
|
||
500: 2,
|
||
1000: 1.5,
|
||
2000: 2,
|
||
4000: 2.5,
|
||
8000: 2,
|
||
16000: 1.5
|
||
}
|
||
}
|
||
};
|
||
|
||
const presetOptions = Object.entries(presets).map(([value, preset]) => ({
|
||
label: preset.label,
|
||
value
|
||
}));
|
||
|
||
const toggleEQ = (enabled: boolean) => {
|
||
audioService.setEQEnabled(enabled);
|
||
};
|
||
|
||
const applyPreset = (presetName: string) => {
|
||
currentPreset.value = presetName;
|
||
audioService.setCurrentPreset(presetName);
|
||
const preset = presets[presetName as keyof typeof presets];
|
||
if (preset) {
|
||
Object.entries(preset.values).forEach(([freq, gain]) => {
|
||
updateEQ(freq, gain);
|
||
});
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
// 恢复 EQ 设置
|
||
const settings = audioService.getAllEQSettings();
|
||
eqValues.value = settings;
|
||
|
||
// 如果有保存的预设,应用该预设
|
||
const savedPreset = audioService.getCurrentPreset();
|
||
if (savedPreset && presets[savedPreset as keyof typeof presets]) {
|
||
currentPreset.value = savedPreset;
|
||
}
|
||
});
|
||
|
||
const updateEQ = (frequency: string, gain: number) => {
|
||
audioService.setEQFrequencyGain(frequency, gain);
|
||
eqValues.value = {
|
||
...eqValues.value,
|
||
[frequency]: gain
|
||
};
|
||
|
||
// 检查当前值是否与任何预设匹配
|
||
const currentValues = eqValues.value;
|
||
let matchedPreset: string | null = null;
|
||
|
||
// 检查是否与任何预设完全匹配
|
||
Object.entries(presets).forEach(([presetName, preset]) => {
|
||
const isMatch = Object.entries(preset.values).every(
|
||
([freq, value]) => Math.abs(currentValues[freq] - value) < 0.1
|
||
);
|
||
if (isMatch) {
|
||
matchedPreset = presetName;
|
||
}
|
||
});
|
||
|
||
// 更新当前预设状态
|
||
if (matchedPreset !== null) {
|
||
currentPreset.value = matchedPreset;
|
||
audioService.setCurrentPreset(matchedPreset);
|
||
} else if (currentPreset.value !== 'custom') {
|
||
// 如果与任何预设都不匹配,将状态设置为自定义
|
||
currentPreset.value = 'custom';
|
||
audioService.setCurrentPreset('custom');
|
||
}
|
||
};
|
||
|
||
const formatFreq = (freq: number) => {
|
||
if (freq >= 1000) {
|
||
return `${freq / 1000}kHz`;
|
||
}
|
||
return `${freq}Hz`;
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.eq-control {
|
||
@apply p-6 rounded-lg;
|
||
@apply bg-light dark:bg-dark;
|
||
width: 100%;
|
||
max-width: 700px;
|
||
|
||
.eq-header {
|
||
@apply flex justify-between items-center mb-4;
|
||
|
||
h3 {
|
||
@apply text-xl font-semibold;
|
||
@apply text-gray-800 dark:text-gray-200;
|
||
}
|
||
}
|
||
|
||
.eq-presets {
|
||
@apply mb-2 relative;
|
||
height: 40px;
|
||
|
||
:deep(.n-scrollbar) {
|
||
@apply -mx-2 px-2;
|
||
}
|
||
|
||
:deep(.n-tag) {
|
||
@apply cursor-pointer transition-all duration-200;
|
||
text-align: center;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
}
|
||
|
||
:deep(.n-space) {
|
||
flex-wrap: nowrap;
|
||
padding: 4px 0;
|
||
}
|
||
}
|
||
|
||
.eq-sliders {
|
||
@apply flex justify-between items-end;
|
||
@apply bg-gray-50 dark:bg-gray-800 gap-1;
|
||
@apply rounded-lg p-2;
|
||
height: 300px;
|
||
|
||
.eq-slider {
|
||
@apply flex flex-col items-center;
|
||
width: 45px;
|
||
height: 100%;
|
||
|
||
.n-slider {
|
||
flex: 1;
|
||
margin: 12px 0;
|
||
min-height: 180px;
|
||
}
|
||
|
||
.freq-label {
|
||
@apply text-xs font-medium text-center;
|
||
@apply text-gray-600 dark:text-gray-400;
|
||
white-space: nowrap;
|
||
margin: 8px 0;
|
||
height: 20px;
|
||
}
|
||
|
||
.gain-value {
|
||
@apply text-xs font-medium text-center;
|
||
@apply text-gray-600 dark:text-gray-400;
|
||
white-space: nowrap;
|
||
margin: 4px 0;
|
||
height: 16px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.n-slider) {
|
||
--n-rail-height: 4px;
|
||
--n-rail-color: theme('colors.gray.200');
|
||
--n-rail-color-hover: theme('colors.gray.300');
|
||
--n-fill-color: theme('colors.green.500');
|
||
--n-fill-color-hover: theme('colors.green.600');
|
||
--n-handle-color: theme('colors.green.500');
|
||
--n-handle-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
|
||
.n-slider-handle {
|
||
@apply transition-all duration-200;
|
||
&:hover {
|
||
transform: scale(1.2);
|
||
}
|
||
}
|
||
}
|
||
</style>
|