foobar2000 上基于 webview 组件的频谱可视化模版

智享阁
智享阁
管理员
115
文章
17
粉丝
随记杂谈评论12字数 1814阅读6分2秒阅读模式

foobar2000上基于webview组件的频谱可视化模版,效果如下:

你可以直接从下面的地址直接下载立即可用的 html 文件:
WV 频谱可视化 By 智享阁文章源自《智享阁》智享阁-https://www.esnpc.com/spectrum-visualization-template-based-on-webview-component-on-foobar2000/

注意:
1、需要安装先安装 webview 组件,可以从 智享阁 foobar2000 组件汉化版介绍合集 下载已汉化的组件包。
2、在 foobar2000 的 webview 组件参数中将窗口大小设置为 20 毫秒(添加面板并加载模版后,从面板上右键-WebView-参数选项进入设置)。文章源自《智享阁》智享阁-https://www.esnpc.com/spectrum-visualization-template-based-on-webview-component-on-foobar2000/

代码如下:文章源自《智享阁》智享阁-https://www.esnpc.com/spectrum-visualization-template-based-on-webview-component-on-foobar2000/

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebView - 频谱可视化 By 智享阁,修改使用请保留!</title>
<style>
	body {
		margin: 0;
		background-color: #0f0f0f;
		font-family: Arial, sans-serif;
		width: 100vw;
		height: 100vh;
		overflow: hidden;
	}
	
	#spectrum-container {
		width: 100%;
		height: 100%;
		display: flex;
		align-items: center;
		justify-content: center;
		background-color: #0f0f0f;
	}
	
	#spectrum-canvas {
		width: 100%;
		height: 100%;
		background-color: #0e0e0e;
		border: none;
		display: block;
	}
</style>
</head>
<body>
	<div id="spectrum-container">
		<canvas id="spectrum-canvas"></canvas>
	</div>

<script>
	const SPECTRUM_CONFIG = {
		BAR_COUNT: 40, // 频谱条数,如果修改,请同步修改下面 FREQUENCY_POINTS 中的频率点
		BAR_HEIGHT: 1.3, // 校准频谱条高度
		SMOOTH_FACTOR: 0.6, // 平滑系数
		DECAY_RATE: 0.8, // 衰减率
		DB_MIN: -60, // 最小显示 dB 值
		DB_MAX: 0, // 最大显示 dB 值
		FFT_SIZE: 16384, // FFT 大小
		PEAK_DECAY_RATE: 0.99, // 峰值衰减率
		PEAK_HOLD_TIME: 500, // 峰值保持时间
		UPDATE_INTERVAL_MS: 20 // 更新间隔
	};

	const FREQUENCY_POINTS = [
		50, 59, 69, 80, 94, 110, 129, 150, 176, 206,
		241, 282, 331, 387, 453, 530, 620, 726, 850, 1000,
		1200, 1400, 1600, 1900, 2200, 2600, 3000, 3500, 4100, 4800,
		5600, 6600, 7700, 9000, 11000, 12000, 14000, 17000, 20000, 23000
	];

	const FONT_STYLE = '0.6pc Arial';
	const HHPC_FOOBAR_SPECIAL = true;
	const BAR_COUNT = SPECTRUM_CONFIG.BAR_COUNT;
	const BAR_HEIGHT = SPECTRUM_CONFIG.BAR_HEIGHT;
	const SMOOTH_FACTOR = SPECTRUM_CONFIG.SMOOTH_FACTOR;
	const DECAY_RATE = SPECTRUM_CONFIG.DECAY_RATE;
	const DB_MIN = SPECTRUM_CONFIG.DB_MIN;
	const DB_MAX = SPECTRUM_CONFIG.DB_MAX;
	const PEAK_DECAY_RATE = SPECTRUM_CONFIG.PEAK_DECAY_RATE;
	const PEAK_HOLD_TIME = SPECTRUM_CONFIG.PEAK_HOLD_TIME;

	let SharedBuffer;
	let Samples;
	let xSampleRate = 44100;
	let xChannelCount = 2;
	let xChannelConfig = 0;
	let windowSizeTextMS = '窗口大小获取失败';
	let tid;
	let isPlaying = false;
	let isPaused = false;
	let isStopped = true;
	let spectrumData = new Array(BAR_COUNT).fill(SPECTRUM_CONFIG.DB_MIN);
	let fftSize = SPECTRUM_CONFIG.FFT_SIZE;
	let frequencies = [];
	let peakData = new Array(SPECTRUM_CONFIG.BAR_COUNT).fill(SPECTRUM_CONFIG.DB_MIN);
	let peakLastUpdate = new Array(SPECTRUM_CONFIG.BAR_COUNT).fill(0);
	let UPDATE_INTERVAL_MS = SPECTRUM_CONFIG.UPDATE_INTERVAL_MS;

	function OnSharedBufferReceived(e) {
		if (SharedBuffer) {
			window.chrome.webview.releaseBuffer(SharedBuffer);
		}

		if (!e.additionalData) return;
		
		SharedBuffer = e.getBuffer();
		Samples = new Float64Array(SharedBuffer);
		xSampleRate = e.additionalData.SampleRate;
		xChannelCount = e.additionalData.ChannelCount;
		xChannelConfig = e.additionalData.ChannelConfig;
		
		isPlaying = true;
		isPaused = false;
		isStopped = false;
		
		const windowSizeMS = Math.ceil(Samples.length/xChannelCount/xSampleRate*1000);
		
		if (UPDATE_INTERVAL_MS != windowSizeMS) {
			if (windowSizeMS > 1000) {
				return;
			}
			UPDATE_INTERVAL_MS = windowSizeMS;
			clearInterval(tid);
			tid = setInterval(OnTimer, UPDATE_INTERVAL_MS);
		}
		
		windowSizeTextMS = '窗口大小: ' + UPDATE_INTERVAL_MS + ' 毫秒';
		
		calculateFrequencies();
	}

	function calculateFrequencies() {
		frequencies = [];
		for (let i = 0; i < fftSize/2; i++) {
			frequencies.push(i * xSampleRate / fftSize);
		}
	}

	function OnTimer() {
		drawSpectrum();
	}

	function drawSpectrum() {
		const canvas = document.getElementById('spectrum-canvas');
		const ctx = canvas.getContext('2d');
		const dpr = window.devicePixelRatio || 1;
		const displayWidth = canvas.clientWidth;
		const displayHeight = canvas.clientHeight;
		
		canvas.width = displayWidth * dpr;
		canvas.height = displayHeight * dpr;
		
		ctx.scale(dpr, dpr);
		ctx.textRendering = 'optimizeLegibility';
		ctx.clearRect(0, 0, canvas.width, canvas.height);
		ctx.fillStyle = '#0e0e0e';
		ctx.fillRect(0, 0, canvas.width, canvas.height);

			const currentTime = Date.now();
		
		if ((isPlaying || isPaused) && Samples && Samples.length >= 2) {
			if (isPlaying && !isPaused) {
				const newSpectrumData = calculateSpectrumFromSamples();
				
				for (let i = 0; i < BAR_COUNT; i++) {
					spectrumData[i] = SMOOTH_FACTOR * spectrumData[i] + (1 - SMOOTH_FACTOR) * newSpectrumData[i];
					if (spectrumData[i] >= peakData[i]) {
						peakData[i] = spectrumData[i];
						peakLastUpdate[i] = currentTime;
					} else {
						if (currentTime - peakLastUpdate[i] > PEAK_HOLD_TIME) {
							peakData[i] -= 0.5;
							if (peakData[i] < DB_MIN) {
								peakData[i] = DB_MIN;
							}
						}
					}
				}
			}
		} else if (isStopped) {
			for (let i = 0; i < BAR_COUNT; i++) {
				spectrumData[i] *= DECAY_RATE;
				if (spectrumData[i] < 1.0) {
					spectrumData[i] = DB_MIN;
				}
				if (currentTime - peakLastUpdate[i] > PEAK_HOLD_TIME) {
					peakData[i] *= PEAK_DECAY_RATE;
					if (peakData[i] < 1.0) {
						peakData[i] = DB_MIN;
					}
				}
			}
		}
		
		drawSpectrumBars(ctx, spectrumData, displayWidth, displayHeight);
		
		const infoText = 'WV 频谱可视化 By 智享阁 // ' + windowSizeTextMS;
		const textWidth = ctx.measureText(infoText).width;

		ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
		ctx.font = FONT_STYLE;
		ctx.fillText(infoText, textWidth + 10, 12);
	}
	
	function fft(real, imag) {
		const N = real.length;
		if (N <= 1) return;
		
		const evenReal = [];
		const evenImag = [];
		const oddReal = [];
		const oddImag = [];
		
		for (let i = 0; i < N; i += 2) {
			evenReal.push(real[i]);
			evenImag.push(imag[i]);
			oddReal.push(real[i + 1]);
			oddImag.push(imag[i + 1]);
		}
		
		fft(evenReal, evenImag);
		fft(oddReal, oddImag);
		
		for (let k = 0; k < N / 2; k++) {
			const angle = -2 * Math.PI * k / N;
			const tReal = Math.cos(angle) * oddReal[k] - Math.sin(angle) * oddImag[k];
			const tImag = Math.sin(angle) * oddReal[k] + Math.cos(angle) * oddImag[k];
			
			real[k] = evenReal[k] + tReal;
			imag[k] = evenImag[k] + tImag;
			real[k + N / 2] = evenReal[k] - tReal;
			imag[k + N / 2] = evenImag[k] - tImag;
		}
	}

	function generateFrequencyBands() {
		const bands = [];
		for (let i = 0; i < FREQUENCY_POINTS.length - 1; i++) {
			bands.push({
				start: FREQUENCY_POINTS[i],
				end: FREQUENCY_POINTS[i + 1]
			});
		}
		return bands;
	}

	function calculateSpectrumFromSamples() {
		const bandEnergy = new Array(BAR_COUNT).fill(DB_MIN);
		
		if (!Samples || Samples.length < 2) {
			return bandEnergy;
		}
		
		const monoBuffer = [];
		for (let i = 0; i < Samples.length; i += xChannelCount) {
			let left = 0, right = 0;
			
			switch (xChannelConfig) {
				case 0x3: // Stereo
					left = Samples[i] || 0;
					right = Samples[i + 1] || 0;
					break;
				case 0x7: // 3.0
				case 0xB: // 3.0
					left = (Samples[i] || 0) + ((Samples[i + 2] || 0) * 0.5);
					right = (Samples[i + 1] || 0) + ((Samples[i + 2] || 0) * 0.5);
					break;
				case 0x33: // 4.0
					left = ((Samples[i] || 0) + (Samples[i + 2] || 0)) * 0.7;
					right = ((Samples[i + 1] || 0) + (Samples[i + 3] || 0)) * 0.7;
					break;
				case 0x3F: // 5.1
					if (HHPC_FOOBAR_SPECIAL) {
						left = (Samples[i] || 0) + (Samples[i + 2] || 0);
						right = (Samples[i + 1] || 0) + (Samples[i + 2] || 0);
					} else {
						left = ((Samples[i] || 0) + (Samples[i + 2] || 0) * 0.6 + (Samples[i + 4] || 0) * 0.5) * 0.7;
						right = ((Samples[i + 1] || 0) + (Samples[i + 2] || 0) * 0.6 + (Samples[i + 5] || 0) * 0.5) * 0.7;
					}
					break;
				case 0xFF: // 7.1
					left = ((Samples[i] || 0) + (Samples[i + 6] || 0) + (Samples[i + 2] || 0) * 0.6 + (Samples[i + 4] || 0) * 0.4) * 0.6;
					right = ((Samples[i + 1] || 0) + (Samples[i + 7] || 0) + (Samples[i + 2] || 0) * 0.6 + (Samples[i + 5] || 0) * 0.4) * 0.6;
					break;
				default: // 默认立体声
					left = Samples[i] || 0;
					right = Samples[i + 1] || 0;
			}
			
			left = Math.max(-1, Math.min(1, left));
			right = Math.max(-1, Math.min(1, right));
			
			const mono = (left + right) / 2;
			monoBuffer.push(mono);
		}
		
		if (monoBuffer.length < fftSize) {
			while (monoBuffer.length < fftSize) {
				monoBuffer.push(0);
			}
		} else if (monoBuffer.length > fftSize) {
			monoBuffer.splice(fftSize);
		}
		
		for (let i = 0; i < fftSize; i++) {
			const windowValue = 0.5 * (1 - Math.cos(2 * Math.PI * i / (fftSize - 1)));
			monoBuffer[i] *= windowValue;
		}
		
		const real = [...monoBuffer];
		const imag = new Array(fftSize).fill(0);
		
		fft(real, imag);
		
		const magnitude = [];
		for (let i = 0; i < fftSize/2; i++) {
			const mag = Math.sqrt(real[i] * real[i] + imag[i] * imag[i]);
			magnitude.push(mag);
		}
		
		const freqBands = generateFrequencyBands();
		
		for (let band = 0; band < BAR_COUNT && band < freqBands.length; band++) {
			const startFreq = freqBands[band].start;
			const endFreq = freqBands[band].end;
			
			let energy = 0;
			let count = 0;
			
			for (let i = 0; i < frequencies.length; i++) {
				const freq = frequencies[i];
				if (freq >= startFreq && freq < endFreq) {
					energy += magnitude[i] * magnitude[i];
					count++;
				}
			}
			
			if (count > 0) {
				const totalEnergy = energy;
				const rms = Math.sqrt(totalEnergy / fftSize);
				const dbValue = rms > 0 ? Math.max(DB_MIN, 20 * Math.log10(rms)) : DB_MIN;
				bandEnergy[band] = dbValue;
			} else {
				let closestBin = 0;
				let minDiff = Math.abs(frequencies[0] - (startFreq + endFreq) / 2);
				
				for (let i = 1; i < frequencies.length; i++) {
					const diff = Math.abs(frequencies[i] - (startFreq + endFreq) / 2);
					if (diff < minDiff) {
						minDiff = diff;
						closestBin = i;
					}
				}
				
				const mag = magnitude[closestBin] / Math.sqrt(fftSize);
				const dbValue = mag > 0 ? Math.max(DB_MIN, 20 * Math.log10(mag)) : DB_MIN;
				bandEnergy[band] = dbValue;
			}
		}
		
		return bandEnergy;
	}
	
	function drawSpectrumBars(ctx, spectrumData, width, height) {
		const barWidth = width / BAR_COUNT * 0.7;
		const barSpacing = width / BAR_COUNT * 0.3;
		const maxHeight = height * BAR_HEIGHT;
		const spectrumAreaHeight = height * 0.99 - 15;

		ctx.textRendering = 'optimizeLegibility';
		ctx.font = FONT_STYLE;
		
		const colors = [];
		const baseColors = [
			'#207FDF', '#20CFCF', '#00EF00', 
			'#77EF00', '#EFDF1C', '#FF9900',
			'#FF6600', '#FF3300', '#FF0000',
			'#CC0066', '#9900CC', '#6600FF'
		];
		
		for (let i = 0; i < BAR_COUNT; i++) {
			colors.push(baseColors[i % baseColors.length]);
		}

		ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
		ctx.lineWidth = 1;

		const dBLabels = [
			{ text: '0 dB', y: height * 0.10 },
			{ text: '-20 dB', y: height * 0.35 },
			{ text: '-40 dB', y: height * 0.60 },
			{ text: '-60 dB', y: height * 0.85 }
		];

		const sampleText = '-20 dB-2';
		const textMetrics = ctx.measureText(sampleText);
		const textWidth = ctx.measureText(sampleText).width;
		const textHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;

		for (let label of dBLabels) {
			const centerY = label.y - textMetrics.actualBoundingBoxDescent - textHeight / 2;
			
			ctx.beginPath();
			ctx.moveTo(0, centerY);
			ctx.lineTo(width - textWidth, centerY);
			ctx.stroke();
		}
		
		ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
		ctx.textAlign = 'center';
		
		const freqLabels = [];
		for (let i = 0; i < FREQUENCY_POINTS.length; i++) {
			const freq = FREQUENCY_POINTS[i];
			if (freq >= 1000) {
				freqLabels.push((freq / 1000).toFixed(freq % 1000 === 0 ? 0 : 1) + 'K');
			} else {
				freqLabels.push(freq.toString());
			}
		}
		
		for (let i = 0; i < BAR_COUNT; i++) {
			const dbValue = spectrumData[i];
			const heightRatio = (dbValue - DB_MIN) / (DB_MAX - DB_MIN);			
			const barHeight = Math.max(1, heightRatio * maxHeight);
			const x = i * (barWidth + barSpacing) + barSpacing / 2;
			const y = spectrumAreaHeight - barHeight;		
			const gradient = ctx.createLinearGradient(0, y, 0, height);
			
			gradient.addColorStop(0, colors[i]);
			gradient.addColorStop(1, colors[i] + 'CC');
			
			ctx.fillStyle = gradient;
			ctx.fillRect(x, y, barWidth, barHeight);	
			ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
			ctx.lineWidth = 1;
			ctx.strokeRect(x, y, barWidth, barHeight);

			const peakDbValue = peakData[i];
			const peakHeightRatio = (peakDbValue - DB_MIN) / (DB_MAX - DB_MIN);
			const peakBarHeight = Math.max(1, peakHeightRatio * maxHeight);
			const peakY = spectrumAreaHeight - peakBarHeight;
			
			ctx.fillStyle = '#FFFFFF';
			ctx.fillRect(x, peakY, barWidth, 1);

			if (i < freqLabels.length) {
				ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
				ctx.fillText(freqLabels[i], x + barWidth/2, height - 4);
			}
		}
		
		ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
		ctx.textAlign = 'right';
		ctx.fillText('0 dB', width - 4, height * 0.10);
		ctx.fillText('-20 dB', width - 4, height * 0.35);
		ctx.fillText('-40 dB', width - 4, height * 0.60);
		ctx.fillText('-60 dB', width - 4, height * 0.85);
	}

	function onPlaybackStop() {
		isPlaying = false;
		isPaused = false;
		isStopped = true;
	}

	function onPlaybackPause(paused) {
		isPaused = paused;
		if (paused) {
			isPlaying = false;
		} else {
			isPlaying = true;
		}
		isStopped = !isPlaying && !isPaused;
	}

	function onPlaybackNewTrack() {
		isPlaying = true;
		isPaused = false;
		isStopped = false;
		spectrumData = new Array(BAR_COUNT).fill(DB_MIN);
			peakData = new Array(BAR_COUNT).fill(DB_MIN);
			peakLastUpdate = new Array(BAR_COUNT).fill(0);
	}

	function main() {
		window.addEventListener('resize', drawSpectrum);
		window.chrome.webview.addEventListener("sharedbufferreceived", OnSharedBufferReceived);
		
		tid = setInterval(OnTimer, UPDATE_INTERVAL_MS);
		if (window.chrome && window.chrome.webview) {
			try {
				window.onPlaybackStop = onPlaybackStop;
				window.onPlaybackPause = onPlaybackPause;
				window.onPlaybackNewTrack = onPlaybackNewTrack;
			} catch (e) {
				console.warn("无法注册播放控制事件:", e);
			}
		}
	}

	main();
</script>
</body>
</html>
文章源自《智享阁》智享阁-https://www.esnpc.com/spectrum-visualization-template-based-on-webview-component-on-foobar2000/文章源自《智享阁》智享阁-https://www.esnpc.com/spectrum-visualization-template-based-on-webview-component-on-foobar2000/

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布、售卖本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

智享阁
  • 本文由 智享阁 发表于2025年12月10日 10:08:14
  • 转载请保留本文链接:https://www.esnpc.com/spectrum-visualization-template-based-on-webview-component-on-foobar2000/

发表评论