音乐可视化频谱

Web Audio 合成音 + AnalyserNode 实时频谱与波形可视化,纯前端零音频文件。

音色:合成琶音

点击播放,观察频谱柱随旋律跳动。

使用体验

  1. 点击“播放演示音”,浏览器会生成一段合成琶音(无需任何音频文件)。
  2. 画布上半区显示频谱柱,下半区显示波形;点击“切换”在频谱 / 波形两种视图间切换。
  3. 再次点击“停止”结束音频并清空画面。该实现可作为音乐播放器动效、语音输入反馈的起点。

代码要点

js
// 核心链路:OscillatorNode -> GainNode -> AnalyserNode -> destination
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const analyser = ctx.createAnalyser();
analyser.fftSize = 256;               // 256 点 FFT,得到 128 个频段
const data = new Uint8Array(analyser.frequencyBinCount);

function draw() {
  requestAnimationFrame(draw);
  if (mode === "freq") {
    analyser.getByteFrequencyData(data); // 频谱
    // 按频段画柱:低频在左、高频在右,高度映射音量
  } else {
    analyser.getByteTimeDomainData(data); // 波形
  }
}
// 演示音:多个振荡器按和弦序列轮换,制造旋律感
const osc = ctx.createOscillator();
osc.type = "sawtooth";
osc.frequency.value = chord[step] * base;
osc.connect(gain); gain.connect(analyser); analyser.connect(ctx.destination);

完整可运行工程

可直接运行纯前端 · 零依赖需先点一次「播放」授权音频

把演示区的「音乐可视化频谱」抽成独立工程:Oscillator → Gain → Analyser 链路现场合成琶音,频谱 / 波形两种视图,支持滑杆改基频。
依赖清单:零第三方依赖(Web Audio API + Canvas 2D),无需 npm install,也不需要任何音频素材文件。

music-viz/ ├─ index.html 控制条 + 画布 ├─ app.js Web Audio 链路 + 可视化 ├─ styles.css 样式(跟随系统深浅色) └─ README.md 运行方式与扩展点

index.html

控制条(播放 / 模式 / 基频)+ 画布

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>音乐可视化频谱 · 独立工程版</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <main class="wrap">
    <h1>音乐可视化频谱</h1>
    <p class="sub">Web Audio 合成琶音 + AnalyserNode 实时频谱 / 波形,纯前端零音频文件。</p>
    <section class="panel">
      <div class="row">
        <button id="play" type="button" class="btn">▶ 播放演示音</button>
        <button id="mode" type="button" class="btn ghost">切换:波形</button>
        <input id="freq" type="range" min="110" max="440" step="10" value="220">
        <span class="msg" id="status" style="margin:0">已停止,点击播放重新开始。</span>
      </div>
    </section>
    <h2>可视化</h2>
    <div class="panel"><canvas id="canvas" height="300"></canvas></div>
  </main>
  <script src="app.js"></script>
</body>
</html>

app.js

Web Audio 链路 + 频谱/波形绘制

javascript
/* app.js —— Web Audio 合成音 + 频谱/波形可视化 */
var CHORD = [1, 1.25, 1.5, 2, 1.5, 1.25];

/* nextStep:琶音推进(纯函数,便于自测) */
function nextStep(step, len) { return (step + 1) % (len || CHORD.length); }

if (typeof module !== "undefined" && module.exports) {
  module.exports = { CHORD: CHORD, nextStep: nextStep };
}

if (typeof document !== "undefined") {
  document.addEventListener("DOMContentLoaded", function () {
    var canvas = document.getElementById("canvas"), ctx = canvas.getContext("2d");
    var playBtn = document.getElementById("play"), modeBtn = document.getElementById("mode"),
        freqInput = document.getElementById("freq"), status = document.getElementById("status");
    var actx = null, analyser = null, nodes = [], playing = false, mode = "freq", raf = 0, step = 0;

    function resize() {
      var dpr = window.devicePixelRatio || 1;
      canvas.width = Math.floor(canvas.clientWidth * dpr);
      canvas.height = 300 * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }

    function start() {
      if (!actx) {
        actx = new (window.AudioContext || window.webkitAudioContext)();
        analyser = actx.createAnalyser();
        analyser.fftSize = 256;
        analyser.smoothingTimeConstant = 0.82;
        analyser.connect(actx.destination);
      }
      actx.resume();
      var gain = actx.createGain();
      gain.gain.value = 0.12;
      gain.connect(analyser);
      var osc = actx.createOscillator();
      osc.type = "sawtooth";
      osc.frequency.value = Number(freqInput.value);
      osc.connect(gain);
      osc.start();
      nodes.push(osc, gain);
      nodes.push(setInterval(function () {
        step = nextStep(step);
        try { osc.frequency.setTargetAtTime(Number(freqInput.value) * CHORD[step], actx.currentTime, 0.03); } catch (e) {}
      }, 220));
      playing = true;
      playBtn.textContent = "■ 停止";
      status.textContent = "正在播放合成琶音…";
      draw();
    }

    function stop() {
      nodes.forEach(function (n) {
        if (n && typeof n.stop === "function") { try { n.stop(); } catch (e) {} }
        if (typeof n === "number") clearInterval(n);
      });
      nodes = [];
      playing = false;
      playBtn.textContent = "▶ 播放演示音";
      status.textContent = "已停止,点击播放重新开始。";
      cancelAnimationFrame(raf);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    function draw() {
      raf = requestAnimationFrame(draw);
      if (!analyser) return;
      var W = canvas.clientWidth, H = 300;
      var data = new Uint8Array(analyser.frequencyBinCount);
      ctx.clearRect(0, 0, W, H);
      if (mode === "freq") {
        analyser.getByteFrequencyData(data);
        var bars = 64, bw = W / bars;
        for (var i = 0; i < bars; i++) {
          var v = data[Math.floor(i * data.length / bars)] / 255;
          var h = v * (H * 0.86);
          ctx.fillStyle = "hsla(" + (155 + i * 2.2) + ", 70%, 62%, 0.9)";
          ctx.fillRect(i * bw + 1, H - h, bw - 2, h);
        }
      } else {
        analyser.getByteTimeDomainData(data);
        ctx.beginPath();
        ctx.strokeStyle = "hsla(155, 70%, 62%, 0.95)";
        ctx.lineWidth = 2;
        for (var x = 0; x < W; x++) {
          var y = (data[Math.floor(x / W * data.length)] / 255) * H;
          if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.stroke();
      }
    }

    playBtn.addEventListener("click", function () { playing ? stop() : start(); });
    modeBtn.addEventListener("click", function () {
      mode = mode === "freq" ? "wave" : "freq";
      modeBtn.textContent = mode === "freq" ? "切换:波形" : "切换:频谱";
    });
    window.addEventListener("resize", resize);
    resize();
    modeBtn.textContent = mode === "freq" ? "切换:波形" : "切换:频谱";
  });
}

styles.css

样式:跟随系统深浅色

css
:root {
  --bg: #0e1014; --panel: #171a20; --ink: #e8ecf1; --muted: #9aa4b2; --line: #262b33; --brand: #6fb3a8;
}
@media (prefers-color-scheme: light) {
  :root { --bg: #f6f7f8; --panel: #ffffff; --ink: #1a1d21; --muted: #5b636e; --line: #e3e6ea; }
}
* { box-sizing: border-box; }
body {
  margin: 0; padding: 24px 16px 48px; background: var(--bg); color: var(--ink);
  font: 15px/1.6 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
}
.wrap { max-width: 760px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin: 0 0 6px; }
h2 { font-size: 1.05rem; margin: 22px 0 10px; }
.sub { color: var(--muted); margin: 0 0 18px; }
.panel, .log-wrap { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
.q { font-size: 1.06rem; margin: 0 0 12px; }
.btns, .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.btn {
  padding: 8px 16px; border-radius: 8px; border: 1px solid var(--brand);
  background: var(--brand); color: #0b0f12; cursor: pointer; font-size: .92rem;
}
.btn.ghost { background: transparent; color: var(--brand); }
.msg { color: var(--muted); margin: 12px 0 0; font-size: .92rem; }
.log { max-height: 200px; overflow: auto; font-size: .88rem; color: var(--muted); }
.log div { padding: 3px 0; border-bottom: 1px dashed var(--line); }
input, select {
  padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg); color: var(--ink); font: inherit;
}
canvas { width: 100%; display: block; border-radius: 10px; }

README.md

运行方式与替换为真实音频的改造点

markdown
# 音乐可视化频谱(独立可运行工程)

作品站 fun 区「音乐可视化频谱」的独立工程版:纯前端、零依赖、无音频文件(现场合成)。

## 文件
- `index.html`:播放 / 模式切换 / 基频滑杆 + 画布
- `app.js`:Oscillator → Gain → Analyser 链路 + 频谱/波形绘制
- `styles.css`:样式

## 运行
1. 双击 `index.html`,点「播放演示音」(浏览器要求先有用户手势才能启动音频)
2. 逻辑自测:`node -e "console.log(require('./app.js').nextStep(5))"` → 期望 `0`

## 扩展点
- 换成真实音频:把 `OscillatorNode` 换成 `createMediaElementSource(audioEl)` 或 `decodeAudioData` 即可
- 更多视图:加 `getByteFrequencyData` 的峰值保持(peak hold)或环形极坐标绘制

运行方式

  1. 新建一个目录,把上面全部文件按文件名分别保存(用「一键复制整份工程」可一次拿全)。
  2. 前端部分:直接双击 index.html 即可运行;若浏览器限制本地文件读取,执行 python -m http.server 8000 后访问 http://localhost:8000/。
  3. 逻辑自测:各文件 README.md 中给出了对应的 Node / Python 自测命令(均为真实可跑的最小用例)。

标注:可直接运行、无需构建;要改造只需动 app.js 顶部的常量与 styles.css 里的变量。

复制结果为 # ===== 文件名 ===== 分隔的多文件文本,按文件名分别保存即可;也可逐块用右上角“复制代码”单独取用。

来源参考

实现涉及的官方文档(可直接访问):