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













