音乐增加均衡器配置
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,7 @@
|
|||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
import AddToPlaylistModal from '@/components/AddToPlaylistModal';
|
import AddToPlaylistModal from '@/components/AddToPlaylistModal';
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
import LyricsPiPWindow from '@/components/LyricsPiPWindow';
|
import LyricsPiPWindow from '@/components/LyricsPiPWindow';
|
||||||
@@ -19,6 +20,36 @@ const SPECTRUM_EDGE_TRIM = 8;
|
|||||||
const SPECTRUM_REFERENCE_VOLUME = 10;
|
const SPECTRUM_REFERENCE_VOLUME = 10;
|
||||||
const SPECTRUM_MIN_VOLUME = 5;
|
const SPECTRUM_MIN_VOLUME = 5;
|
||||||
const SPECTRUM_MAX_REFERENCE_VOLUME = 15;
|
const SPECTRUM_MAX_REFERENCE_VOLUME = 15;
|
||||||
|
const EQ_BANDS = [
|
||||||
|
{ label: '31', frequency: 31 }, { label: '62', frequency: 62 },
|
||||||
|
{ label: '125', frequency: 125 }, { label: '250', frequency: 250 },
|
||||||
|
{ label: '500', frequency: 500 }, { label: '1K', frequency: 1000 },
|
||||||
|
{ label: '2K', frequency: 2000 }, { label: '4K', frequency: 4000 },
|
||||||
|
{ label: '8K', frequency: 8000 }, { label: '16K', frequency: 16000 },
|
||||||
|
] as const;
|
||||||
|
const DEFAULT_EQ_GAINS = EQ_BANDS.map(() => 0);
|
||||||
|
const EQ_PRESETS = {
|
||||||
|
Flat: DEFAULT_EQ_GAINS,
|
||||||
|
流行: [4, 3, -1, -2, 1, 2, 3, 2, 4, 3], 摇滚: [5, 4, 2, 1, -1, -2, 2, 3, 4, 5],
|
||||||
|
古典: [4, 3, 1, 1, 0, 1, -1, -2, -3, -4], 人声: [-3, -2, -1, 1, 3, 4, 3, 2, -1, -2],
|
||||||
|
电子: [5, 4, 1, -2, -2, 0, 3, 4, 4, 3], 重低音: [7, 6, 4, 3, 1, 0, 0, 0, 0, 0],
|
||||||
|
} as const;
|
||||||
|
const REVERB_PRESETS = {
|
||||||
|
none: { label: '关闭', source: null, main: 1, send: 0 },
|
||||||
|
telephone: { label: '电话', source: 'filter-telephone.wav', main: 0, send: 3 },
|
||||||
|
church: { label: '教堂', source: 's2_r4_bd.wav', main: 1.8, send: 0.9 },
|
||||||
|
hall: { label: '大厅', source: 'bright-hall.wav', main: 0.8, send: 2.4 },
|
||||||
|
cinema: { label: '电影院', source: 'cinema-diningroom.wav', main: 0.6, send: 2.3 },
|
||||||
|
dining: { label: '餐厅', source: 'dining-living-true-stereo.wav', main: 0.6, send: 1.8 },
|
||||||
|
living: { label: '卫生间', source: 'living-bedroom-leveled.wav', main: 1.6, send: 2.1 },
|
||||||
|
spreader: { label: '室内', source: 'spreader50-65ms.wav', main: 1, send: 2.5 },
|
||||||
|
stereo: { label: '立体声', source: 'cardiod-35-10-spread.wav', main: 1.8, send: 0.6 },
|
||||||
|
matrix1: { label: '矩阵混响 (1)', source: 'matrix-reverb1.wav', main: 1.5, send: 0.9 },
|
||||||
|
matrix2: { label: '矩阵混响 (2)', source: 'matrix-reverb2.wav', main: 1.3, send: 1 },
|
||||||
|
cardiod: { label: '心形扩散', source: 'cardiod-35-10-spread.wav', main: 1.8, send: 0.6 },
|
||||||
|
magnetic: { label: '磁性立体声', source: 'tim-omni-35-10-magnetic.wav', main: 1, send: 0.2 },
|
||||||
|
spring: { label: '反馈弹簧', source: 'feedback-spring.wav', main: 1.8, send: 0.8 },
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface PlayRecord {
|
interface PlayRecord {
|
||||||
platform: MusicSource;
|
platform: MusicSource;
|
||||||
@@ -245,6 +276,19 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
const [showPlaylist, setShowPlaylist] = useState(false);
|
const [showPlaylist, setShowPlaylist] = useState(false);
|
||||||
const [playlistIndex, setPlaylistIndex] = useState(-1); // 当前在播放列表中的索引
|
const [playlistIndex, setPlaylistIndex] = useState(-1); // 当前在播放列表中的索引
|
||||||
const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单
|
const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单
|
||||||
|
const [showAudioEffectsMenu, setShowAudioEffectsMenu] = useState(false);
|
||||||
|
const [equalizerEnabled, setEqualizerEnabled] = useState(false);
|
||||||
|
const [eqGains, setEqGains] = useState<number[]>(DEFAULT_EQ_GAINS);
|
||||||
|
const [loudnessEnabled, setLoudnessEnabled] = useState(false);
|
||||||
|
const [reverbEnabled, setReverbEnabled] = useState(false);
|
||||||
|
const [reverbMix, setReverbMix] = useState(12);
|
||||||
|
const [reverbPreset, setReverbPreset] = useState<keyof typeof REVERB_PRESETS>('none');
|
||||||
|
const [reverbMainGain, setReverbMainGain] = useState(100);
|
||||||
|
const [eqPreset, setEqPreset] = useState('Flat');
|
||||||
|
const [pitchRate, setPitchRate] = useState(1);
|
||||||
|
const [surroundEnabled, setSurroundEnabled] = useState(false);
|
||||||
|
const [surroundSpeed, setSurroundSpeed] = useState(25);
|
||||||
|
const [surroundDistance, setSurroundDistance] = useState(5);
|
||||||
const [showSleepTimerMenu, setShowSleepTimerMenu] = useState(false); // 睡眠定时菜单
|
const [showSleepTimerMenu, setShowSleepTimerMenu] = useState(false); // 睡眠定时菜单
|
||||||
const [sleepTimerEndAt, setSleepTimerEndAt] = useState<number | null>(null); // 睡眠定时结束时间
|
const [sleepTimerEndAt, setSleepTimerEndAt] = useState<number | null>(null); // 睡眠定时结束时间
|
||||||
const [sleepTimerRemaining, setSleepTimerRemaining] = useState(0); // 睡眠定时剩余秒数
|
const [sleepTimerRemaining, setSleepTimerRemaining] = useState(0); // 睡眠定时剩余秒数
|
||||||
@@ -291,6 +335,41 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
() => Array.from({ length: SPECTRUM_BIN_COUNT }, () => SPECTRUM_IDLE_LEVEL)
|
() => Array.from({ length: SPECTRUM_BIN_COUNT }, () => SPECTRUM_IDLE_LEVEL)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem('musicEqGains') || 'null');
|
||||||
|
if (Array.isArray(saved) && saved.length === EQ_BANDS.length) setEqGains(saved.map(value => Math.max(-12, Math.min(12, Number(value) || 0))));
|
||||||
|
setEqualizerEnabled(localStorage.getItem('musicEqEnabled') === '1');
|
||||||
|
setLoudnessEnabled(localStorage.getItem('musicLoudnessEnabled') === '1');
|
||||||
|
setReverbEnabled(localStorage.getItem('musicReverbEnabled') === '1');
|
||||||
|
setReverbPreset((localStorage.getItem('musicReverbPreset') as keyof typeof REVERB_PRESETS) || 'none');
|
||||||
|
setReverbMainGain(Math.max(0, Math.min(150, Number(localStorage.getItem('musicReverbMainGain')) || 100)));
|
||||||
|
setReverbMix(Math.max(0, Math.min(40, Number(localStorage.getItem('musicReverbMix')) || 12)));
|
||||||
|
setEqPreset(localStorage.getItem('musicEqPreset') || 'Flat');
|
||||||
|
setPitchRate(Math.max(0.5, Math.min(1.5, Number(localStorage.getItem('musicPitchRate')) || 1)));
|
||||||
|
setSurroundEnabled(localStorage.getItem('musicSurroundEnabled') === '1');
|
||||||
|
setSurroundSpeed(Math.max(5, Math.min(60, Number(localStorage.getItem('musicSurroundSpeed')) || 25)));
|
||||||
|
setSurroundDistance(Math.max(1, Math.min(10, Number(localStorage.getItem('musicSurroundDistance')) || 5)));
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
localStorage.setItem('musicEqGains', JSON.stringify(eqGains));
|
||||||
|
localStorage.setItem('musicEqEnabled', equalizerEnabled ? '1' : '0');
|
||||||
|
localStorage.setItem('musicLoudnessEnabled', loudnessEnabled ? '1' : '0');
|
||||||
|
localStorage.setItem('musicReverbEnabled', reverbEnabled ? '1' : '0');
|
||||||
|
localStorage.setItem('musicReverbMix', String(reverbMix));
|
||||||
|
localStorage.setItem('musicReverbPreset', reverbPreset);
|
||||||
|
localStorage.setItem('musicReverbMainGain', String(reverbMainGain));
|
||||||
|
localStorage.setItem('musicEqPreset', eqPreset);
|
||||||
|
localStorage.setItem('musicPitchRate', String(pitchRate));
|
||||||
|
localStorage.setItem('musicSurroundEnabled', surroundEnabled ? '1' : '0');
|
||||||
|
localStorage.setItem('musicSurroundSpeed', String(surroundSpeed));
|
||||||
|
localStorage.setItem('musicSurroundDistance', String(surroundDistance));
|
||||||
|
}, [eqGains, equalizerEnabled, loudnessEnabled, reverbEnabled, reverbMix, reverbPreset, reverbMainGain, eqPreset, pitchRate, surroundEnabled, surroundSpeed, surroundDistance]);
|
||||||
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const lyricsContainerRef = useRef<HTMLDivElement>(null);
|
const lyricsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const sleepHoursPickerRef = useRef<HTMLDivElement>(null);
|
const sleepHoursPickerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -302,6 +381,14 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
const analyserRef = useRef<AnalyserNode | null>(null);
|
const analyserRef = useRef<AnalyserNode | null>(null);
|
||||||
const mediaSourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
const mediaSourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
||||||
const spectrumDataRef = useRef<Uint8Array | null>(null);
|
const spectrumDataRef = useRef<Uint8Array | null>(null);
|
||||||
|
const eqFiltersRef = useRef<BiquadFilterNode[]>([]);
|
||||||
|
const loudnessGainRef = useRef<GainNode | null>(null);
|
||||||
|
const compressorRef = useRef<DynamicsCompressorNode | null>(null);
|
||||||
|
const convolverRef = useRef<ConvolverNode | null>(null);
|
||||||
|
const dryGainRef = useRef<GainNode | null>(null);
|
||||||
|
const wetGainRef = useRef<GainNode | null>(null);
|
||||||
|
const pannerRef = useRef<PannerNode | null>(null);
|
||||||
|
const pannerTimerRef = useRef<number | null>(null);
|
||||||
const spectrumFrameRef = useRef<number | null>(null);
|
const spectrumFrameRef = useRef<number | null>(null);
|
||||||
const currentTimeRef = useRef(0);
|
const currentTimeRef = useRef(0);
|
||||||
const volumeRef = useRef(volume);
|
const volumeRef = useRef(volume);
|
||||||
@@ -1304,6 +1391,68 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 音频事件监听
|
// 音频事件监听
|
||||||
|
useEffect(() => {
|
||||||
|
eqFiltersRef.current.forEach((filter, index) => { filter.gain.value = equalizerEnabled ? (eqGains[index] || 0) : 0; });
|
||||||
|
if (compressorRef.current) {
|
||||||
|
compressorRef.current.threshold.value = loudnessEnabled ? -24 : 0;
|
||||||
|
compressorRef.current.knee.value = loudnessEnabled ? 12 : 0;
|
||||||
|
compressorRef.current.ratio.value = loudnessEnabled ? 2 : 1;
|
||||||
|
}
|
||||||
|
if (loudnessGainRef.current) loudnessGainRef.current.gain.value = loudnessEnabled ? 1.04 : 1;
|
||||||
|
if (dryGainRef.current) dryGainRef.current.gain.value = reverbEnabled ? reverbMainGain / 100 : 1;
|
||||||
|
if (wetGainRef.current) wetGainRef.current.gain.value = reverbEnabled ? reverbMix / 100 : 0;
|
||||||
|
const audio = audioRef.current as (HTMLAudioElement & { preservesPitch?: boolean }) | null;
|
||||||
|
if (audio) {
|
||||||
|
audio.playbackRate = pitchRate;
|
||||||
|
audio.preservesPitch = false;
|
||||||
|
}
|
||||||
|
if (pannerRef.current && !surroundEnabled) {
|
||||||
|
pannerRef.current.positionX.value = 0;
|
||||||
|
pannerRef.current.positionY.value = 0;
|
||||||
|
pannerRef.current.positionZ.value = 0;
|
||||||
|
}
|
||||||
|
}, [eqGains, equalizerEnabled, loudnessEnabled, reverbEnabled, reverbMix, reverbMainGain, pitchRate, surroundEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const convolver = convolverRef.current;
|
||||||
|
const context = audioContextRef.current;
|
||||||
|
const preset = REVERB_PRESETS[reverbPreset];
|
||||||
|
if (!convolver || !context || !preset || !preset.source || !reverbEnabled) {
|
||||||
|
if (convolver) convolver.buffer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
fetch(`/music/effects/${preset.source}`)
|
||||||
|
.then(response => response.arrayBuffer())
|
||||||
|
.then(data => context.decodeAudioData(data))
|
||||||
|
.then(buffer => {
|
||||||
|
if (!cancelled && convolver) convolver.buffer = buffer;
|
||||||
|
})
|
||||||
|
.catch(error => console.warn('加载环境混响音效失败:', error));
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [reverbPreset, reverbEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pannerTimerRef.current !== null) window.clearInterval(pannerTimerRef.current);
|
||||||
|
if (!surroundEnabled || !pannerRef.current) return;
|
||||||
|
let angle = 0;
|
||||||
|
const update = () => {
|
||||||
|
const panner = pannerRef.current;
|
||||||
|
if (!panner) return;
|
||||||
|
angle = (angle + 360 / (surroundSpeed * 10)) % 360;
|
||||||
|
const radians = angle * Math.PI / 180;
|
||||||
|
panner.positionX.value = Math.sin(radians) * surroundDistance;
|
||||||
|
panner.positionY.value = 0;
|
||||||
|
panner.positionZ.value = Math.cos(radians) * surroundDistance;
|
||||||
|
};
|
||||||
|
update();
|
||||||
|
pannerTimerRef.current = window.setInterval(update, 100);
|
||||||
|
return () => {
|
||||||
|
if (pannerTimerRef.current !== null) window.clearInterval(pannerTimerRef.current);
|
||||||
|
pannerTimerRef.current = null;
|
||||||
|
};
|
||||||
|
}, [surroundEnabled, surroundSpeed, surroundDistance]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = audioRef.current;
|
const audio = audioRef.current;
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
@@ -1608,6 +1757,18 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
return getSourceDisplayLabel(currentSource, false);
|
return getSourceDisplayLabel(currentSource, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetAudioEffects = () => {
|
||||||
|
setEqGains([...DEFAULT_EQ_GAINS]); setEqPreset('Flat'); setEqualizerEnabled(false);
|
||||||
|
setLoudnessEnabled(false); setReverbEnabled(false); setReverbMix(12); setReverbPreset('none'); setReverbMainGain(100);
|
||||||
|
setPitchRate(1); setSurroundEnabled(false); setSurroundSpeed(25); setSurroundDistance(5);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyEqPreset = (name: string) => {
|
||||||
|
const preset = EQ_PRESETS[name as keyof typeof EQ_PRESETS];
|
||||||
|
if (!preset) return;
|
||||||
|
setEqPreset(name); setEqGains([...preset]); setEqualizerEnabled(name !== 'Flat');
|
||||||
|
};
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
if (isNaN(seconds) || seconds === 0) return '0:00';
|
if (isNaN(seconds) || seconds === 0) return '0:00';
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
@@ -1752,13 +1913,30 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!analyserRef.current) {
|
if (!analyserRef.current) {
|
||||||
const analyser = audioContextRef.current.createAnalyser();
|
const context = audioContextRef.current;
|
||||||
analyser.fftSize = 512;
|
const eqFilters = EQ_BANDS.map((band) => {
|
||||||
analyser.smoothingTimeConstant = 0.8;
|
const filter = context.createBiquadFilter();
|
||||||
mediaSourceRef.current.connect(analyser);
|
filter.type = 'peaking'; filter.frequency.value = band.frequency; filter.Q.value = 1.4; filter.gain.value = 0;
|
||||||
analyser.connect(audioContextRef.current.destination);
|
return filter;
|
||||||
analyserRef.current = analyser;
|
});
|
||||||
spectrumDataRef.current = new Uint8Array(analyser.frequencyBinCount);
|
eqFilters.forEach((filter, index) => index === 0 ? mediaSourceRef.current?.connect(filter) : eqFilters[index - 1].connect(filter));
|
||||||
|
const lastEq = eqFilters[eqFilters.length - 1];
|
||||||
|
const dryGain = context.createGain(); const wetGain = context.createGain();
|
||||||
|
const convolver = context.createConvolver();
|
||||||
|
const impulse = context.createBuffer(2, Math.floor(context.sampleRate * 1.6), context.sampleRate);
|
||||||
|
for (let channel = 0; channel < impulse.numberOfChannels; channel += 1) {
|
||||||
|
const data = impulse.getChannelData(channel);
|
||||||
|
for (let i = 0; i < data.length; i += 1) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / data.length, 2.2);
|
||||||
|
}
|
||||||
|
convolver.buffer = impulse; lastEq.connect(dryGain); lastEq.connect(convolver); convolver.connect(wetGain);
|
||||||
|
const mixer = context.createGain(); dryGain.connect(mixer); wetGain.connect(mixer);
|
||||||
|
const compressor = context.createDynamicsCompressor(); compressor.threshold.value = 0; compressor.knee.value = 0; compressor.ratio.value = 1; compressor.attack.value = 0.003; compressor.release.value = 0.25; mixer.connect(compressor);
|
||||||
|
const loudnessGain = context.createGain(); compressor.connect(loudnessGain);
|
||||||
|
const panner = context.createPanner();
|
||||||
|
panner.panningModel = 'HRTF'; panner.distanceModel = 'inverse'; panner.refDistance = 1; panner.maxDistance = 20; panner.rolloffFactor = 0.5;
|
||||||
|
panner.positionX.value = 0; panner.positionY.value = 0; panner.positionZ.value = 0;
|
||||||
|
const analyser = context.createAnalyser(); analyser.fftSize = 512; analyser.smoothingTimeConstant = 0.8; loudnessGain.connect(panner); panner.connect(analyser); analyser.connect(context.destination);
|
||||||
|
eqFiltersRef.current = eqFilters; convolverRef.current = convolver; dryGainRef.current = dryGain; wetGainRef.current = wetGain; compressorRef.current = compressor; loudnessGainRef.current = loudnessGain; pannerRef.current = panner; analyserRef.current = analyser; spectrumDataRef.current = new Uint8Array(analyser.frequencyBinCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioContextRef.current.state === 'suspended') {
|
if (audioContextRef.current.state === 'suspended') {
|
||||||
@@ -1842,6 +2020,8 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
window.cancelAnimationFrame(spectrumFrameRef.current);
|
window.cancelAnimationFrame(spectrumFrameRef.current);
|
||||||
}
|
}
|
||||||
analyserRef.current?.disconnect();
|
analyserRef.current?.disconnect();
|
||||||
|
eqFiltersRef.current.forEach(filter => filter.disconnect());
|
||||||
|
convolverRef.current?.disconnect(); dryGainRef.current?.disconnect(); wetGainRef.current?.disconnect(); compressorRef.current?.disconnect(); loudnessGainRef.current?.disconnect(); pannerRef.current?.disconnect();
|
||||||
mediaSourceRef.current?.disconnect();
|
mediaSourceRef.current?.disconnect();
|
||||||
audioContextRef.current?.close().catch(() => undefined);
|
audioContextRef.current?.close().catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -2567,6 +2747,7 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); setShowAudioEffectsMenu(true); }} className={`transition-colors ${equalizerEnabled || loudnessEnabled || reverbEnabled || pitchRate !== 1 || surroundEnabled ? 'text-green-400' : 'text-zinc-500 hover:text-white'}`} title="均衡器和音效" aria-label="均衡器和音效"><SlidersHorizontal className="h-4 w-4 md:h-5 md:w-5" /></button>
|
||||||
{/* PiP 歌词按钮 */}
|
{/* PiP 歌词按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={toggleSpectrum}
|
onClick={toggleSpectrum}
|
||||||
@@ -2939,6 +3120,25 @@ export default function MusicClient({ children: _children }: { children?: React.
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showAudioEffectsMenu && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[100] flex items-center justify-center p-4" onClick={() => setShowAudioEffectsMenu(false)}>
|
||||||
|
<div className="relative w-full max-w-5xl max-h-[90vh] bg-zinc-900 rounded-2xl border border-white/10 shadow-2xl overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="p-5 border-b border-white/10 flex items-center justify-between"><div><h3 className="text-lg font-bold text-white">均衡器与音效调节</h3></div><button onClick={() => setShowAudioEffectsMenu(false)} className="w-8 h-8 rounded-lg text-zinc-400 hover:text-white hover:bg-white/10">✕</button></div>
|
||||||
|
<div className="p-5 overflow-y-auto max-h-[calc(90vh-80px)]"><div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8">
|
||||||
|
<section><div className="flex items-center justify-between mb-4"><div><h4 className="text-sm font-bold text-white">10 段均衡器</h4></div><label className="flex items-center gap-2 text-xs text-zinc-300"><input type="checkbox" checked={equalizerEnabled} onChange={(e) => setEqualizerEnabled(e.target.checked)} className="h-4 w-4 accent-green-500" />启用 EQ</label></div>
|
||||||
|
<div className="rounded-xl border border-white/5 p-4 overflow-x-auto overscroll-x-contain"><div className="min-w-[760px] grid grid-cols-10 gap-3 items-end h-56 sm:h-64">{EQ_BANDS.map((band, index) => (<div key={band.frequency} className="h-full min-w-0 flex flex-col items-center gap-2"><span className="text-[10px] font-mono text-zinc-500">{(eqGains[index] || 0) > 0 ? '+' : ''}{(eqGains[index] || 0).toFixed(1)}</span><div className="flex-1 w-full min-h-0 flex items-center justify-center"><input type="range" min="-12" max="12" step="0.5" value={eqGains[index] || 0} onChange={(e) => { setEqPreset('自定义'); const value = Number(e.target.value); setEqGains(current => current.map((gain, i) => i === index ? value : gain)); }} disabled={!equalizerEnabled} className="w-40 sm:w-52 h-5 -rotate-90 accent-green-500 [direction:rtl] disabled:opacity-40" /></div><span className="text-[10px] font-mono text-zinc-400">{band.label}</span></div>))}</div></div>
|
||||||
|
<div className="mt-5"><div className="flex items-center justify-between mb-3"><h4 className="text-sm font-bold text-white">预设</h4><button onClick={() => applyEqPreset('Flat')} className="text-xs text-green-400 hover:text-green-300">Flat 重置</button></div><div className="flex flex-wrap gap-2">{Object.keys(EQ_PRESETS).map(name => (<button key={name} onClick={() => applyEqPreset(name)} className={`px-3 py-1.5 rounded-lg text-xs transition-colors ${eqPreset === name ? 'bg-green-500/20 text-green-300 border border-green-500/50' : 'bg-white/5 text-zinc-300 border border-white/10 hover:bg-white/10'}`}>{name}</button>))}</div></div>
|
||||||
|
</section>
|
||||||
|
<aside className="space-y-5"><section className="rounded-xl border border-white/10 bg-white/[0.03] p-4"><div className="flex items-center justify-between"><div><h4 className="text-sm font-bold text-white">响度增强</h4><p className="text-xs text-zinc-500 mt-1">轻微压缩,增强人声和细节</p></div><input type="checkbox" checked={loudnessEnabled} onChange={(e) => setLoudnessEnabled(e.target.checked)} className="h-4 w-4 accent-green-500" /></div></section>
|
||||||
|
<section className="rounded-xl border border-white/10 bg-white/[0.03] p-4"><div className="flex items-center justify-between mb-3"><div><h4 className="text-sm font-bold text-white">环境混响音效</h4><p className="text-xs text-zinc-500 mt-1">模拟不同空间的回声效果</p></div><input type="checkbox" checked={reverbEnabled} onChange={(e) => setReverbEnabled(e.target.checked)} className="h-4 w-4 accent-green-500" /></div><div className="flex flex-wrap gap-2">{Object.entries(REVERB_PRESETS).map(([id, preset]) => (<button key={id} onClick={() => { const selected = id as keyof typeof REVERB_PRESETS; const selectedPreset = REVERB_PRESETS[selected]; setReverbPreset(selected); setReverbEnabled(selected !== 'none'); setReverbMainGain(Math.round(selectedPreset.main * 100)); setReverbMix(Math.min(100, Math.round(selectedPreset.send * 30))); }} className={`rounded-lg border px-2.5 py-1 text-xs ${reverbPreset === id ? 'border-green-500/50 bg-green-500/20 text-green-300' : 'border-white/10 bg-white/5 text-zinc-300 hover:bg-white/10'}`}>{preset.label}</button>))}</div><label className="mt-3 block text-xs text-zinc-400">原始音量 {reverbMainGain}%<input type="range" min="0" max="150" value={reverbMainGain} onChange={(e) => setReverbMainGain(Number(e.target.value))} disabled={!reverbEnabled} className="mt-2 w-full accent-green-500 disabled:opacity-40" /></label><label className="mt-3 block text-xs text-zinc-400">环境音效 {reverbMix}%<input type="range" min="0" max="100" value={reverbMix} onChange={(e) => setReverbMix(Number(e.target.value))} disabled={!reverbEnabled} className="mt-2 w-full accent-green-500 disabled:opacity-40" /></label></section>
|
||||||
|
<section className="rounded-xl border border-white/10 bg-white/[0.03] p-4"><div className="flex items-center justify-between mb-3"><div><h4 className="text-sm font-bold text-white">音调升降调节</h4><p className="text-xs text-zinc-500 mt-1">当前 {pitchRate.toFixed(2)}x</p></div><button onClick={() => setPitchRate(1)} className="text-xs text-green-400">重置</button></div><input type="range" min="0.5" max="1.5" step="0.01" value={pitchRate} onChange={(e) => setPitchRate(Number(e.target.value))} className="w-full accent-green-500" /><div className="mt-2 flex justify-between text-xs text-zinc-500"><span>低</span><span>原速</span><span>高</span></div></section>
|
||||||
|
<section className="rounded-xl border border-white/10 bg-white/[0.03] p-4"><div className="flex items-center justify-between mb-3"><div><h4 className="text-sm font-bold text-white">3D 立体环绕</h4><p className="text-xs text-zinc-500 mt-1">需使用耳机,模拟声源环绕移动</p></div><input type="checkbox" checked={surroundEnabled} onChange={(e) => setSurroundEnabled(e.target.checked)} className="h-4 w-4 accent-green-500" /></div><label className="block text-xs text-zinc-400">旋转速度 {surroundSpeed}<input type="range" min="5" max="60" value={surroundSpeed} onChange={(e) => setSurroundSpeed(Number(e.target.value))} disabled={!surroundEnabled} className="mt-2 w-full accent-green-500 disabled:opacity-40" /></label><label className="block mt-3 text-xs text-zinc-400">声场距离 {surroundDistance}<input type="range" min="1" max="10" value={surroundDistance} onChange={(e) => setSurroundDistance(Number(e.target.value))} disabled={!surroundEnabled} className="mt-2 w-full accent-green-500 disabled:opacity-40" /></label></section>
|
||||||
|
<button onClick={() => setShowAudioEffectsMenu(false)} className="w-full rounded-xl bg-green-500 p-3 text-sm font-medium text-black hover:bg-green-400">完成</button>
|
||||||
|
</aside></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Quality Selection Menu */}
|
{/* Quality Selection Menu */}
|
||||||
{showQualityMenu && (
|
{showQualityMenu && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user