console
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>音频波形可视化器</title>
<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
color: #fff;
margin: 0;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 20px;
}
h1 {
font-size: 32px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
margin-bottom: 5px;
}
.subtitle {
color: #aaccff;
font-size: 18px;
margin-bottom: 20px;
}
.visualizer-container {
background: rgba(0, 10, 30, 0.7);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
padding: 20px;
margin-bottom: 25px;
backdrop-filter: blur(5px);
border: 1px solid rgba(64, 128, 255, 0.3);
}
.canvas-wrapper {
display: flex;
justify-content: center;
margin: 0 auto;
}
canvas {
background: rgba(5, 15, 35, 0.6);
border-radius: 8px;
width: 100%;
max-width: 800px;
box-shadow: 0 0 20px rgba(64, 128, 255, 0.2);
height: 300px;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
margin-top: 25px;
}
.control-group {
background: rgba(0, 10, 30, 0.7);
border-radius: 10px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px);
border: 1px solid rgba(64, 128, 255, 0.2);
}
.control-title {
font-weight: 600;
margin-bottom: 12px;
color: #4facfe;
font-size: 18px;
}
.control-item {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-size: 14px;
}
.input-range {
width: 100%;
height: 8px;
-webkit-appearance: none;
border-radius: 4px;
background: rgba(100, 150, 255, 0.2);
outline: none;
}
.input-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #4facfe;
cursor: pointer;
box-shadow: 0 0 5px rgba(79, 172, 254, 0.8);
}
.value-display {
background: rgba(64, 128, 255, 0.25);
padding: 5px 10px;
border-radius: 5px;
font-size: 14px;
display: inline-block;
min-width: 40px;
text-align: center;
margin-top: 5px;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
button {
flex: 1;
background: linear-gradient(to right, #4facfe, #00f2fe);
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.5);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
#audio-source {
width: 100%;
padding: 10px;
background: rgba(0, 10, 30, 0.6);
border: 1px solid rgba(64, 128, 255, 0.4);
border-radius: 6px;
color: white;
margin-bottom: 15px;
}
.status {
background: rgba(0, 10, 30, 0.7);
padding: 10px;
border-radius: 6px;
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #aaccff;
}
.mode-selector {
display: flex;
gap: 10px;
margin-top: 15px;
}
.mode-btn {
flex: 1;
background: rgba(64, 128, 255, 0.2);
border: 1px solid rgba(64, 128, 255, 0.5);
text-align: center;
padding: 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn.active {
background: rgba(79, 172, 254, 0.5);
border-color: #4facfe;
box-shadow: 0 0 10px rgba(79, 172, 254, 0.4);
}
@media (max-width: 600px) {
.controls {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>音频波形可视化器</h1>
<div class="subtitle">动态柱状图效果,带间距、圆角与平滑渐弱</div>
</div>
<div class="visualizer-container">
<div class="canvas-wrapper">
<canvas id="visualizer"></canvas>
</div>
<div class="controls">
<div class="control-group">
<div class="control-title">波形设置</div>
<div class="control-item">
<label for="spacing-control"
>柱间距 (<span id="spacing-value">3</span>px)</label
>
<input
type="range"
id="spacing-control"
class="input-range"
min="0"
max="10"
value="3"
/>
</div>
<div class="control-item">
<label for="roundness-control"
>圆角半径 (<span id="roundness-value">4</span>px)</label
>
<input
type="range"
id="roundness-control"
class="input-range"
min="0"
max="12"
value="4"
/>
</div>
<div class="control-item">
<label for="width-control"
>柱宽 (<span id="width-value">8</span>px)</label
>
<input
type="range"
id="width-control"
class="input-range"
min="2"
max="20"
value="8"
/>
</div>
</div>
<div class="control-group">
<div class="control-title">外观设置</div>
<div class="control-item">
<label for="height-control"
>振幅 (<span id="height-value">100</span>%)</label
>
<input
type="range"
id="height-control"
class="input-range"
min="40"
max="200"
value="100"
/>
</div>
<div class="control-item">
<label for="color-mode">颜色模式</label>
<select id="color-mode" class="input-range">
<option value="gradient">渐变蓝</option>
<option value="energy">能量红</option>
<option value="ocean">海洋绿</option>
<option value="purple">紫罗兰</option>
</select>
</div>
<div class="control-item">
<label for="sensitivity"
>灵敏度 (<span id="sensitivity-value">50</span>%)</label
>
<input
type="range"
id="sensitivity"
class="input-range"
min="20"
max="100"
value="50"
/>
</div>
</div>
<div class="control-group">
<div class="control-title">音频源</div>
<div class="control-item">
<label for="audio-source">选择音频源</label>
<select id="audio-source">
<option value="test">测试音效</option>
<option value="mic">麦克风输入</option>
</select>
</div>
<div class="mode-selector">
<div class="mode-btn active" data-mode="sine">正弦波</div>
<div class="mode-btn" data-mode="peak">峰值波</div>
<div class="mode-btn" data-mode="pulse">脉冲波</div>
</div>
<div class="btn-group">
<button id="start-btn">开始</button>
<button id="reset-btn">重置参数</button>
</div>
<div class="status">点击"开始"按钮以启动可视化效果</div>
</div>
</div>
</div>
</div>
<script>
const canvas = document.getElementById("visualizer");
const ctx = canvas.getContext("2d");
const startBtn = document.getElementById("start-btn");
const resetBtn = document.getElementById("reset-btn");
function resizeCanvas() {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
const spacingControl = document.getElementById("spacing-control");
const roundnessControl = document.getElementById("roundness-control");
const widthControl = document.getElementById("width-control");
const heightControl = document.getElementById("height-control");
const sensitivityControl = document.getElementById("sensitivity");
const colorModeSelect = document.getElementById("color-mode");
let visualizationParams = {
barSpacing: 3,
barRoundness: 4,
barWidth: 8,
barHeightMultiplier: 1.0,
sensitivity: 0.5,
isAnimating: false,
animationId: null,
audioContext: null,
analyser: null,
audioSource: "test",
mode: "sine",
};
function updateDisplayValues() {
document.getElementById("spacing-value").textContent =
visualizationParams.barSpacing;
document.getElementById("roundness-value").textContent =
visualizationParams.barRoundness;
document.getElementById("width-value").textContent =
visualizationParams.barWidth;
document.getElementById("height-value").textContent = Math.round(
visualizationParams.barHeightMultiplier * 100
);
document.getElementById("sensitivity-value").textContent = Math.round(
visualizationParams.sensitivity * 100
);
}
function initParamsFromUI() {
visualizationParams.barSpacing = parseInt(spacingControl.value);
visualizationParams.barRoundness = parseInt(roundnessControl.value);
visualizationParams.barWidth = parseInt(widthControl.value);
visualizationParams.barHeightMultiplier =
parseInt(heightControl.value) / 100;
visualizationParams.sensitivity =
parseInt(sensitivityControl.value) / 100;
visualizationParams.mode =
document.querySelector(".mode-btn.active").dataset.mode;
updateDisplayValues();
}
const colorModes = {
gradient: {
low: "#3a7bd5",
mid: "#00d2ff",
high: "#ffffff",
},
energy: {
low: "#ff416c",
mid: "#ff4b2b",
high: "#fad961",
},
ocean: {
low: "#00b09b",
mid: "#96c93d",
high: "#3df0f0",
},
purple: {
low: "#654ea3",
mid: "#a367dc",
high: "#f4d0ff",
},
};
resetBtn.addEventListener("click", () => {
spacingControl.value = 3;
roundnessControl.value = 4;
widthControl.value = 8;
heightControl.value = 100;
sensitivityControl.value = 50;
colorModeSelect.value = "gradient";
document
.querySelectorAll(".mode-btn")
.forEach((btn) => btn.classList.remove("active"));
document
.querySelector('.mode-btn[data-mode="sine"]')
.classList.add("active");
initParamsFromUI();
startBtn.textContent = "开始";
document.querySelector(".status").textContent = "参数已重置";
});
spacingControl.addEventListener("input", function () {
visualizationParams.barSpacing = parseInt(this.value);
updateDisplayValues();
});
roundnessControl.addEventListener("input", function () {
visualizationParams.barRoundness = parseInt(this.value);
updateDisplayValues();
});
widthControl.addEventListener("input", function () {
visualizationParams.barWidth = parseInt(this.value);
updateDisplayValues();
});
heightControl.addEventListener("input", function () {
visualizationParams.barHeightMultiplier = parseInt(this.value) / 100;
updateDisplayValues();
});
sensitivityControl.addEventListener("input", function () {
visualizationParams.sensitivity = parseInt(this.value) / 100;
updateDisplayValues();
});
document.querySelectorAll(".mode-btn").forEach((btn) => {
btn.addEventListener("click", function () {
document
.querySelectorAll(".mode-btn")
.forEach((b) => b.classList.remove("active"));
this.classList.add("active");
visualizationParams.mode = this.dataset.mode;
});
});
function drawBars(dataArray, barCount) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const totalBarWidth =
visualizationParams.barWidth + visualizationParams.barSpacing;
barCount = Math.min(barCount, Math.floor(canvas.width / totalBarWidth));
const centerX = canvas.width / 2;
const halfBarCount = Math.floor(barCount / 2);
const colorMode = colorModes[colorModeSelect.value];
for (let i = 0; i < barCount; i++) {
const barIndex =
i < halfBarCount ? halfBarCount - i - 1 : i - halfBarCount;
const value = dataArray[barIndex] / 256;
let barHeight =
Math.pow(value, 1 + (1 - visualizationParams.sensitivity) * 2) *
canvas.height *
visualizationParams.barHeightMultiplier *
0.8;
const distanceFromCenter = Math.abs(i - halfBarCount);
barHeight =
barHeight * (1 - (0.4 * distanceFromCenter) / halfBarCount);
const barWidth = visualizationParams.barWidth;
const barRoundness = Math.min(
visualizationParams.barRoundness,
barHeight / 2
);
const x = centerX - halfBarCount * totalBarWidth + i * totalBarWidth;
const y = canvas.height - barHeight;
const gradient = ctx.createLinearGradient(x, y, x, canvas.height);
const gradientValue = Math.min(1, value * 3);
gradient.addColorStop(0, colorMode.high);
gradient.addColorStop(0.6, colorMode.mid);
gradient.addColorStop(1, colorMode.low);
ctx.fillStyle = gradient;
if (barRoundness > 0) {
ctx.beginPath();
ctx.moveTo(x + barRoundness, y);
ctx.lineTo(x + barWidth - barRoundness, y);
ctx.arcTo(
x + barWidth,
y,
x + barWidth,
y + barRoundness,
barRoundness
);
ctx.lineTo(x + barWidth, canvas.height);
ctx.lineTo(x, canvas.height);
ctx.lineTo(x, y + barRoundness);
ctx.arcTo(x, y, x + barRoundness, y, barRoundness);
ctx.closePath();
ctx.fill();
} else {
ctx.fillRect(x, y, barWidth, barHeight);
}
}
}
function generateSineData() {
const data = [];
const barsCount = 128;
const time = Date.now() * 0.002;
for (let i = 0; i < barsCount; i++) {
let value = Math.sin(i * 0.2 + time);
value += Math.sin(i * 0.1 + time * 0.5) * 0.3;
value += Math.sin(i * 0.05 + time * 0.2) * 0.2;
value += Math.random() * 0.1;
const normalized = (value + 2) * 40;
data.push(Math.min(255, Math.max(0, normalized)));
}
return data;
}
function generatePeakData() {
const data = [];
const barsCount = 128;
const time = Date.now() * 0.001;
const beat =
Math.sin(time * 2) > 0.8 ? Math.sin(time * 30) * 0.3 + 0.7 : 0;
for (let i = 0; i < barsCount; i++) {
const centerValue = Math.max(
0,
1 - Math.abs(i - barsCount / 2) / (barsCount / 4)
);
let value = centerValue;
if (i > barsCount / 2 - 6 && i < barsCount / 2 + 6) {
value += beat;
}
value += Math.sin(i * 0.2 + time) * 0.1;
value += Math.random() * 0.05;
const normalized = value * 200;
data.push(Math.min(255, Math.max(0, normalized)));
}
return data;
}
function generatePulseData() {
const data = [];
const barsCount = 128;
const time = Date.now() * 0.001;
const pulse = Math.abs(Math.sin(time)) > 0.9 ? 1 : 0;
for (let i = 0; i < barsCount; i++) {
let value = Math.sin(i * 0.3) * 0.5 + 0.5;
if (Math.abs(i - barsCount / 2) < 8) {
value = Math.min(1, value + pulse * 0.7);
}
if (Math.random() > 0.99) {
value = 1;
}
const normalized = value * 200;
data.push(Math.min(255, Math.max(0, normalized)));
}
return data;
}
function generateVisualizerData() {
switch (visualizationParams.mode) {
case "peak":
return generatePeakData();
case "pulse":
return generatePulseData();
case "sine":
default:
return generateSineData();
}
}
startBtn.addEventListener("click", function () {
if (!visualizationParams.isAnimating) {
visualizationParams.isAnimating = true;
startBtn.textContent = "停止";
document.querySelector(".status").textContent = "可视化效果运行中...";
initParamsFromUI();
animate();
} else {
visualizationParams.isAnimating = false;
startBtn.textContent = "开始";
document.querySelector(".status").textContent = "可视化已停止";
if (visualizationParams.animationId) {
cancelAnimationFrame(visualizationParams.animationId);
}
if (visualizationParams.audioContext) {
if (
visualizationParams.audioSource === "mic" &&
visualizationParams.audioContext.state !== "closed"
) {
visualizationParams.audioContext.close();
}
visualizationParams.audioContext = null;
}
}
});
function animate() {
if (!visualizationParams.isAnimating) return;
let dataArray;
const barCount = 64;
if (visualizationParams.audioSource === "mic") {
if (!visualizationParams.audioContext) {
initAudio();
}
if (visualizationParams.analyser) {
const bufferLength = visualizationParams.analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
visualizationParams.analyser.getByteFrequencyData(dataArray);
} else {
dataArray = generateVisualizerData();
}
} else {
dataArray = generateVisualizerData();
}
drawBars(dataArray, barCount);
visualizationParams.animationId = requestAnimationFrame(animate);
}
function initAudio() {
visualizationParams.audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
visualizationParams.analyser =
visualizationParams.audioContext.createAnalyser();
visualizationParams.analyser.fftSize = 256;
if (visualizationParams.audioSource === "mic") {
navigator.mediaDevices
.getUserMedia({ audio: true })
.then(function (stream) {
const source =
visualizationParams.audioContext.createMediaStreamSource(
stream
);
source.connect(visualizationParams.analyser);
})
.catch(function (err) {
console.error("麦克风访问失败:", err);
document.querySelector(".status").textContent =
"麦克风访问失败,使用测试音效";
visualizationParams.audioSource = "test";
});
}
}
document
.getElementById("audio-source")
.addEventListener("change", function () {
visualizationParams.audioSource = this.value;
if (visualizationParams.isAnimating && this.value === "mic") {
startBtn.click();
setTimeout(() => startBtn.click(), 100);
}
});
initParamsFromUI();
</script>
</body>
</html>