import { useEffect, useRef, useCallback } from 'react'; interface InterstellarTickerProps { isPlaying: boolean; } export function InterstellarTicker({ isPlaying }: InterstellarTickerProps) { const audioContextRef = useRef(null); const timerRef = useRef(null); const nextNoteTimeRef = useRef(0); // 初始化 AudioContext (必须在用户交互后触发) const initAudio = useCallback(() => { if (!audioContextRef.current) { const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; audioContextRef.current = new AudioContextClass(); } if (audioContextRef.current.state === 'suspended') { audioContextRef.current.resume(); } }, []); // 播放单次滴答声 const playTick = useCallback((time: number) => { const ctx = audioContextRef.current; if (!ctx) return; // 1. 创建振荡器 (声音源) const osc = ctx.createOscillator(); const gain = ctx.createGain(); // 2. 声音设计 - 模仿星际穿越的木鱼/秒表声 // 使用正弦波,频率较高 osc.type = 'sine'; osc.frequency.setValueAtTime(880, time); // A5 音高,清脆 // 3. 包络 (Envolope) - 极短的冲击感 // 0s: 静音 gain.gain.setValueAtTime(0, time); // 0.005s: 快速达到峰值 (Attack) gain.gain.linearRampToValueAtTime(0.8, time + 0.005); // 0.05s: 快速衰减 (Decay) gain.gain.exponentialRampToValueAtTime(0.001, time + 0.05); // 4. 连接并播放 osc.connect(gain); gain.connect(ctx.destination); osc.start(time); osc.stop(time + 0.1); }, []); // 调度器 (Lookahead Scheduler) const scheduler = useCallback(() => { const ctx = audioContextRef.current; if (!ctx) return; // 预读 0.1 秒 const lookahead = 0.1; const interval = 1.2; // 1.2 秒间隔 // 如果下一个音符的时间在当前时间 + 预读范围内,则调度它 while (nextNoteTimeRef.current < ctx.currentTime + lookahead) { playTick(nextNoteTimeRef.current); nextNoteTimeRef.current += interval; } timerRef.current = window.setTimeout(scheduler, 25); }, [playTick]); // 监听 isPlaying 变化 useEffect(() => { if (isPlaying) { // 开启声音 initAudio(); // 重置调度时间 if (audioContextRef.current) { // 稍微延迟一点开始,避免切歌时的爆音 nextNoteTimeRef.current = audioContextRef.current.currentTime + 0.1; scheduler(); } } else { // 关闭声音 if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } } }, [isPlaying, initAudio, scheduler]); // 组件卸载时清理 useEffect(() => { return () => { if (timerRef.current) { clearTimeout(timerRef.current); } if (audioContextRef.current) { audioContextRef.current.close(); } }; }, []); // 这是一个纯逻辑组件,不渲染任何 UI return null; }