/**
* 基于 Web Audio 频谱能量驱动进度指示器的呼吸效果。
* 仅负责视觉变量更新,不参与播放逻辑。
*/
export class SeekReactiveIndicator {
private readonly _audio: HTMLAudioElement;
private _seekWrapper: HTMLElement | null = null;
private _audioContext: AudioContext | null = null;
private _analyser: AnalyserNode | null = null;
private _mediaElementSource: MediaElementAudioSourceNode | null = null;
private _analyserData: Uint8Array<ArrayBuffer> | null = null;
private _rafId: number | null = null;
private _energy: number = 0;
constructor(audio: HTMLAudioElement) {
this._audio = audio;
}
/**
* 绑定进度条容器。
*/
public attach(seekWrapper: HTMLElement): void {
this._seekWrapper = seekWrapper;
this.resetVisual();
}
/**
* 开始频谱驱动动画。
*/
public start(): void {
if (!this._seekWrapper) {
return;
}
this._seekWrapper.classList.add('music-player__seek-wrapper--playing');
if (this._rafId !== null) {
return;
}
const hasGraph = this.ensureAudioGraph();
if (hasGraph && this._audioContext && this._audioContext.state === 'suspended') {
void this._audioContext.resume();
}
const loop = () => {
if (!this._seekWrapper || this._audio.paused) {
this._rafId = null;
return;
}
const energy = hasGraph ? this.readFrequencyEnergy() : this.readFallbackEnergy();
// 峰值保持 + 衰减,增强节拍可见性。
this._energy = Math.max(energy, this._energy * 0.84);
const beatScale = 1 + Math.min(1, this._energy * 1.45) * 0.38;
const beatGlow = 0.45 + Math.min(1.4, this._energy * 2.2);
const lineGlow = 0.2 + Math.min(1, this._energy * 1.9) * 0.8;
this._seekWrapper.style.setProperty('--music-seek-beat-scale', beatScale.toFixed(3));
this._seekWrapper.style.setProperty('--music-seek-beat-glow', beatGlow.toFixed(3));
this._seekWrapper.style.setProperty('--music-seek-line-glow', lineGlow.toFixed(3));
this._rafId = window.requestAnimationFrame(loop);
};
this._rafId = window.requestAnimationFrame(loop);
}
/**
* 停止动画并复位视觉变量。
*/
public stop(): void {
if (this._rafId !== null) {
window.cancelAnimationFrame(this._rafId);
this._rafId = null;
}
if (this._seekWrapper) {
this._seekWrapper.classList.remove('music-player__seek-wrapper--playing');
}
this._energy = 0;
this.resetVisual();
}
private resetVisual(): void {
if (!this._seekWrapper) {
return;
}
this._seekWrapper.style.setProperty('--music-seek-beat-scale', '1');
this._seekWrapper.style.setProperty('--music-seek-beat-glow', '0.45');
this._seekWrapper.style.setProperty('--music-seek-line-glow', '0.35');
}
private ensureAudioGraph(): boolean {
if (
this._audioContext &&
this._analyser &&
this._mediaElementSource &&
this._analyserData
) {
return true;
}
if (typeof window.AudioContext === 'undefined') {
return false;
}
try {
this._audioContext = new window.AudioContext();
this._analyser = this._audioContext.createAnalyser();
this._analyser.fftSize = 256;
this._analyser.smoothingTimeConstant = 0.72;
this._analyserData = new Uint8Array(new ArrayBuffer(this._analyser.frequencyBinCount));
this._mediaElementSource = this._audioContext.createMediaElementSource(this._audio);
this._mediaElementSource.connect(this._analyser);
this._analyser.connect(this._audioContext.destination);
return true;
} catch (e) {
console.warn('Seek reactive indicator init failed:', e);
this._audioContext = null;
this._analyser = null;
this._mediaElementSource = null;
this._analyserData = null;
return false;
}
}
private readFrequencyEnergy(): number {
if (!this._analyser || !this._analyserData) {
return 0;
}
this._analyser.getByteFrequencyData(this._analyserData);
// 低频到中低频更接近“鼓点感”,并加权前几个 bin。
const bins = Math.min(24, this._analyserData.length);
let sum = 0;
let weightSum = 0;
for (let i = 0; i < bins; i++) {
const weight = 1.35 - i / (bins * 1.8);
sum += this._analyserData[i] * weight;
weightSum += weight;
}
return weightSum > 0 ? sum / weightSum / 255 : 0;
}
private readFallbackEnergy(): number {
const t = performance.now() / 1000;
const wave = (Math.sin(t * 2.6) + 1) * 0.18;
const jitter = Math.random() * 0.05;
return Math.min(0.35, wave + jitter);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这是一个基于 Web Audio API 的音乐播放器视觉效果类,用于创建随音频频谱变化的进度条"呼吸"动画效果。让我详细解释一下各个部分:
类的整体设计
这是一个纯视觉效果类,只负责更新 CSS 变量来驱动动画,不参与音频播放逻辑。
主要属性
// 核心音频元素
private readonly _audio: HTMLAudioElement;
// 进度条容器元素
private _seekWrapper: HTMLElement | null = null;
// Web Audio API 相关
private _audioContext: AudioContext | null = null;
private _analyser: AnalyserNode | null = null;
private _mediaElementSource: MediaElementAudioSourceNode | null = null;
private _analyserData: Uint8Array<ArrayBuffer> | null = null;
// 动画控制
private _rafId: number | null = null;
private _energy: number = 0; // 当前能量值
核心方法解析
1. attach() - 绑定 DOM 元素
public attach(seekWrapper: HTMLElement): void {
this._seekWrapper = seekWrapper;
this.resetVisual(); // 初始化视觉状态
}
2. start() - 开始动画循环
这是核心方法,包含以下步骤:
- 添加播放状态的 CSS 类
- 确保 Web Audio 图形已创建
- 启动 requestAnimationFrame 循环
动画循环逻辑:
const loop = () => {
// 读取音频能量(频谱分析或备用方案)
const energy = hasGraph ? this.readFrequencyEnergy() : this.readFallbackEnergy();
// 峰值保持 + 衰减算法,让节拍更明显
this._energy = Math.max(energy, this._energy * 0.84);
// 计算视觉效果参数
const beatScale = 1 + Math.min(1, this._energy * 1.45) * 0.38;
const beatGlow = 0.45 + Math.min(1.4, this._energy * 2.2);
const lineGlow = 0.2 + Math.min(1, this._energy * 1.9) * 0.8;
// 更新 CSS 自定义属性
this._seekWrapper.style.setProperty('--music-seek-beat-scale', beatScale.toFixed(3));
this._seekWrapper.style.setProperty('--music-seek-beat-glow', beatGlow.toFixed(3));
this._seekWrapper.style.setProperty('--music-seek-line-glow', lineGlow.toFixed(3));
};
3. ensureAudioGraph() - 创建音频分析图
private ensureAudioGraph(): boolean {
try {
this._audioContext = new window.AudioContext();
this._analyser = this._audioContext.createAnalyser();
this._analyser.fftSize = 256; // 频谱分辨率
this._analyser.smoothingTimeConstant = 0.72; // 平滑度
// 创建音频处理链: 音频元素 -> 分析器 -> 扬声器
this._mediaElementSource = this._audioContext.createMediaElementSource(this._audio);
this._mediaElementSource.connect(this._analyser);
this._analyser.connect(this._audioContext.destination);
return true;
} catch (e) {
// 降级处理
return false;
}
}
4. 能量读取方法
频谱能量读取:
private readFrequencyEnergy(): number {
this._analyser.getByteFrequencyData(this._analyserData);
// 重点关注低频到中低频(鼓点感强)
const bins = Math.min(24, this._analyserData.length);
let sum = 0;
let weightSum = 0;
for (let i = 0; i < bins; i++) {
// 给前面的频率 bin 更高权重
const weight = 1.35 - i / (bins * 1.8);
sum += this._analyserData[i] * weight;
weightSum += weight;
}
return weightSum > 0 ? sum / weightSum / 255 : 0;
}
备用能量方案(无 Web Audio 支持时):
private readFallbackEnergy(): number {
const t = performance.now() / 1000;
const wave = (Math.sin(t * 2.6) + 1) * 0.18; // 模拟波动
const jitter = Math.random() * 0.05; // 添加随机性
return Math.min(0.35, wave + jitter);
}
设计亮点
- 峰值保持算法:
this._energy = Math.max(energy, this._energy * 0.84)让节拍更明显 - 频率加权:专注低频段,突出鼓点和低音
- 优雅降级:Web Audio API 不支持时使用数学函数模拟
- 性能优化:使用 requestAnimationFrame 和适当的频谱分辨率
- 分离关注点:只负责视觉效果,不干预音频播放
这个类创建了一个随音乐节拍"跳动"的视觉效果,增强了用户的听觉体验。
AI 正在分析代码…
评论加载中...