实时波形与频谱分析:一个交互式动画演示
在信号处理领域,时域波形和频域频谱是理解信号特性的重要工具。通过时域波形,我们可以直观地观察信号随时间的变化,而频域频谱则揭示了信号中所包含的频率成分及其幅值。为了帮助大家更好地理解这两者之间的关系,我开发了一个交互式的动画演示系统,展示波形与频谱的实时分析。
本文将详细介绍该系统的功能、实现原理以及如何使用它来探索信号的特性。
功能概述
这个演示系统实现了以下功能:
-
实时波形显示:
- 左侧显示信号的时域波形。
- 支持两个正弦波的叠加,并实时动态更新波形。
-
频谱分析:
- 右侧显示信号的频谱图。
- 通过频谱图可以直观地看到信号中包含的频率分量及其幅值。
-
交互控制:
- 为每个正弦波提供独立的频率和振幅控制。
- 可以通过复选框启用或禁用某个波形分量。
-
视觉效果:
- 网格背景便于观察波形和频谱。
- 使用不同颜色区分不同波形的频率分量,使频谱图更加直观。
系统界面与交互
1. 时域波形
时域波形展示信号随时间的变化。在演示中,时域波形是一个复合信号,它由两个正弦波叠加而成。通过滑块调整正弦波的频率和振幅,波形会实时更新,帮助用户观察不同参数对信号的影响。
2. 频谱图
频谱图展示信号的频率成分及其幅值。在演示中,频谱图会显示两个正弦波的频率分量,分别用不同颜色标注正频率的幅值。通过调整正弦波的参数,可以观察频谱图的变化,例如频率峰值的位置和高度。
3. 交互控制
用户可以通过以下方式与系统交互:
- 频率滑块:调节正弦波的频率(1-20 Hz)。
- 振幅滑块:调节正弦波的振幅(0-100)。
- 复选框:启用或禁用某个正弦波。
- 系统会实时响应用户的调整,并动态更新波形与频谱图。
实现原理
1. 时域波形计算
时域波形是两个正弦波的叠加,表达式如下:
y(t) = A₁ * sin(2πf₁t + φ₁) + A₂ * sin(2πf₂t + φ₂)
其中:
- A₁ 和 A₂ 是两个波形的振幅。
- f₁ 和 f₂ 是两个波形的频率。
- φ₁ 和 φ₂ 是相位,这里通过时间戳动态变化,实现波形的动画效果。
2. 频谱分析
对于单一正弦波,其频谱在频域中表现为两个对称的冲击峰,分别位于正频率和负频率处。通过分析复合波形的频率成分,我们可以在频谱图中绘制每个频率分量的幅值。
3. 动画与动态更新
通过 JavaScript 的 requestAnimationFrame
函数实现动画效果。系统每秒更新波形和频谱图,确保用户的调整能够实时反映在画布上。
使用方法
- 加载页面:将代码保存为 HTML 文件并在浏览器中打开。
- 调节波形参数:
- 使用滑块调整频率和振幅,观察时域波形的变化。
- 启用或禁用某个波形,查看复合波形的变化。
- 观察频谱图:
- 调节频率,观察频谱峰值的位置变化。
- 调节振幅,观察频谱峰值的高度变化。
实际应用
这个演示系统不仅可以帮助初学者理解时域与频域的关系,还可以用于以下场景:
-
信号分解:
- 通过调整参数,观察复合信号的组成成分。
- 理解如何通过频谱图分析信号的频率特性。
-
滤波器设计:
- 通过观察频谱图,思考如何设计滤波器以保留或去除特定频率成分。
-
教育与教学:
- 作为教学工具,帮助学生直观理解傅立叶变换和频谱分析的基本概念。
示例代码
以下是完整的 HTML 演示代码,可以直接复制并运行:
html"><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>波形与频谱分析</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-7xl mx-auto">
<h1 class="text-3xl font-bold text-center mb-6">波形与频谱实时分析</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 时域波形 -->
<div class="bg-white rounded-lg shadow-lg p-4">
<h2 class="text-xl font-semibold mb-4">时域波形</h2>
<canvas id="waveCanvas" width="600" height="300" class="border border-gray-300 rounded"></canvas>
</div>
<!-- 频谱图 -->
<div class="bg-white rounded-lg shadow-lg p-4">
<h2 class="text-xl font-semibold mb-4">频谱图</h2>
<canvas id="spectrumCanvas" width="600" height="300" class="border border-gray-300 rounded"></canvas>
</div>
</div>
<!-- 控制面板 -->
<div class="mt-6 bg-white rounded-lg shadow-lg p-6">
<h3 class="text-lg font-semibold mb-4">波形控制</h3>
<div id="waveControls" class="space-y-4">
<!-- 波形1 -->
<div class="p-4 border rounded-lg">
<div class="flex items-center mb-2">
<span class="w-20">波形 1</span>
<input type="checkbox" id="wave1Enable" checked class="ml-2">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm">频率 (Hz)</label>
<input type="range" id="freq1" min="1" max="20" value="2" class="w-full">
<span id="freq1Value" class="text-sm">2 Hz</span>
</div>
<div>
<label class="block text-sm">振幅</label>
<input type="range" id="amp1" min="0" max="100" value="50" class="w-full">
<span id="amp1Value" class="text-sm">50</span>
</div>
</div>
</div>
<!-- 波形2 -->
<div class="p-4 border rounded-lg">
<div class="flex items-center mb-2">
<span class="w-20">波形 2</span>
<input type="checkbox" id="wave2Enable" checked class="ml-2">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm">频率 (Hz)</label>
<input type="range" id="freq2" min="1" max="20" value="5" class="w-full">
<span id="freq2Value" class="text-sm">5 Hz</span>
</div>
<div>
<label class="block text-sm">振幅</label>
<input type="range" id="amp2" min="0" max="100" value="30" class="w-full">
<span id="amp2Value" class="text-sm">30</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const waveCanvas = document.getElementById('waveCanvas');
const spectrumCanvas = document.getElementById('spectrumCanvas');
const waveCtx = waveCanvas.getContext('2d');
const spectrumCtx = spectrumCanvas.getContext('2d');
// 控制参数
const controls = {
wave1: {
enabled: document.getElementById('wave1Enable'),
freq: document.getElementById('freq1'),
amp: document.getElementById('amp1'),
freqValue: document.getElementById('freq1Value'),
ampValue: document.getElementById('amp1Value')
},
wave2: {
enabled: document.getElementById('wave2Enable'),
freq: document.getElementById('freq2'),
amp: document.getElementById('amp2'),
freqValue: document.getElementById('freq2Value'),
ampValue: document.getElementById('amp2Value')
}
};
// 更新显示值
function updateValues() {
controls.wave1.freqValue.textContent = controls.wave1.freq.value + ' Hz';
controls.wave1.ampValue.textContent = controls.wave1.amp.value;
controls.wave2.freqValue.textContent = controls.wave2.freq.value + ' Hz';
controls.wave2.ampValue.textContent = controls.wave2.amp.value;
}
// 为所有控制添加事件监听
Object.values(controls).forEach(control => {
control.freq.addEventListener('input', updateValues);
control.amp.addEventListener('input', updateValues);
});
// 绘制网格
function drawGrid(ctx, width, height) {
ctx.beginPath();
ctx.strokeStyle = '#e5e5e5';
ctx.lineWidth = 0.5;
// 垂直线
for (let x = 0; x <= width; x += 50) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
// 水平线
for (let y = 0; y <= height; y += 50) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.stroke();
}
// 绘制时域波形
function drawWaveform(timestamp) {
waveCtx.clearRect(0, 0, waveCanvas.width, waveCanvas.height);
drawGrid(waveCtx, waveCanvas.width, waveCanvas.height);
// 绘制中心线
waveCtx.beginPath();
waveCtx.strokeStyle = '#666';
waveCtx.lineWidth = 1;
waveCtx.moveTo(0, waveCanvas.height / 2);
waveCtx.lineTo(waveCanvas.width, waveCanvas.height / 2);
waveCtx.stroke();
// 采样点数组
const samples = new Float32Array(waveCanvas.width);
const phase = timestamp / 1000;
// 计算复合波形
for (let x = 0; x < waveCanvas.width; x++) {
let sample = 0;
const t = x / 100;
// 波形1
if (controls.wave1.enabled.checked) {
const freq1 = parseFloat(controls.wave1.freq.value);
const amp1 = parseFloat(controls.wave1.amp.value);
sample += (amp1 / 100) * Math.sin(2 * Math.PI * freq1 * t + phase);
}
// 波形2
if (controls.wave2.enabled.checked) {
const freq2 = parseFloat(controls.wave2.freq.value);
const amp2 = parseFloat(controls.wave2.amp.value);
sample += (amp2 / 100) * Math.sin(2 * Math.PI * freq2 * t + phase);
}
samples[x] = sample;
}
// 绘制波形
waveCtx.beginPath();
waveCtx.strokeStyle = '#3b82f6';
waveCtx.lineWidth = 2;
for (let x = 0; x < samples.length; x++) {
const y = waveCanvas.height / 2 * (1 - samples[x]);
if (x === 0) {
waveCtx.moveTo(x, y);
} else {
waveCtx.lineTo(x, y);
}
}
waveCtx.stroke();
}
// 绘制频谱
function drawSpectrum() {
spectrumCtx.clearRect(0, 0, spectrumCanvas.width, spectrumCanvas.height);
drawGrid(spectrumCtx, spectrumCanvas.width, spectrumCanvas.height);
// 绘制中心线
spectrumCtx.beginPath();
spectrumCtx.strokeStyle = '#666';
spectrumCtx.lineWidth = 1;
spectrumCtx.moveTo(0, spectrumCanvas.height);
spectrumCtx.lineTo(spectrumCanvas.width, spectrumCanvas.height);
spectrumCtx.stroke();
// 频谱分析
const maxFreq = 20; // 最大显示频率
const freqScale = spectrumCanvas.width / maxFreq;
// 绘制频谱线
if (controls.wave1.enabled.checked) {
const freq1 = parseFloat(controls.wave1.freq.value);
const amp1 = parseFloat(controls.wave1.amp.value);
drawFrequencyPeak(freq1, amp1, '#ef4444');
}
if (controls.wave2.enabled.checked) {
const freq2 = parseFloat(controls.wave2.freq.value);
const amp2 = parseFloat(controls.wave2.amp.value);
drawFrequencyPeak(freq2, amp2, '#3b82f6');
}
function drawFrequencyPeak(freq, amp, color) {
const x = freq * freqScale;
const height = (amp / 100) * spectrumCanvas.height;
spectrumCtx.beginPath();
spectrumCtx.strokeStyle = color;
spectrumCtx.lineWidth = 2;
spectrumCtx.moveTo(x, spectrumCanvas.height);
spectrumCtx.lineTo(x, spectrumCanvas.height - height);
spectrumCtx.stroke();
// 添加频率标签
spectrumCtx.fillStyle = color;
spectrumCtx.font = '12px Arial';
spectrumCtx.textAlign = 'center';
spectrumCtx.fillText(
`${freq}Hz`,
x,
spectrumCanvas.height - height - 10
);
}
}
// 动画循环
function animate(timestamp) {
drawWaveform(timestamp);
drawSpectrum();
requestAnimationFrame(animate);
}
// 启动动画
updateValues();
requestAnimationFrame(animate);
</script>
</body>
</html>
总结
通过这个交互式的动画演示,我们可以直观地理解时域波形与频域频谱之间的关系。无论是学习信号处理的初学者,还是从事相关领域的专业人士,这个工具都可以作为一个有趣且实用的学习和研究工具。
如果你对这个演示有任何改进建议,或者希望了解更多相关内容,欢迎留言讨论!