Skip to content

📊 音频频谱

需要 capability:audioSpectrum: true

ctx.audio.spectrum 提供对 EchoMusic 实时音频频谱数据的访问。底层由 echo-ffmpeg-player 播放引擎内置的频谱分析驱动,从播放管线直接获取 FFT 数据。

API 概览

API说明返回
getStatus()获取频谱捕获状态Promise<AudioSpectrumStatus>
getSnapshot()获取当前单帧频谱数据Promise<AudioSpectrumFrame | null>
subscribe(options, handler, meta?)订阅实时频谱数据流() => void(取消函数)

getStatus()

获取频谱捕获引擎的当前状态。

js
const status = await ctx.audio.spectrum.getStatus();
// {
//   capturing: true,      // 是否正在捕获
//   sampleRate: 44100,    // 当前采样率
//   fftSize: 2048,        // 当前 FFT 窗口大小
//   subscriptions: 2,     // 活跃订阅数
// }
字段类型说明
capturingboolean频谱捕获是否处于活动状态
sampleRatenumber音频采样率(Hz)
fftSizenumberFFT 变换窗口大小
subscriptionsnumber当前活跃的订阅数

getSnapshot()

获取一帧即时的频谱数据。如果频谱捕获未启动,返回 null

js
const frame = await ctx.audio.spectrum.getSnapshot();
if (frame) {
  // frame.frequencyData: Float32Array — 频域数据
  // frame.timeData: Float32Array      — 时域数据
  // frame.sampleRate: number          — 采样率
  console.log("频域数据长度:", frame.frequencyData.length);
}

适用场景: 只需要偶尔读取频谱数据(如制作截图表),而不是持续实时更新。


subscribe(options, handler)

订阅实时频谱数据。每当系统捕获到新一帧频谱时,会调用 handler

参数

参数类型必选说明
options.fftSizenumberFFT 窗口大小(32-32768,必须是 2 的幂),默认 2048
options.smoothingTimeConstantnumber平滑系数(0-1),默认 0.8。值越小响应越快
options.intervalnumber数据推送最小间隔(ms),默认 50
handler(frame) => void频谱数据回调
meta.pluginIdstring订阅者标识,默认自动填充当前插件 ID

频谱帧结构(AudioSpectrumFrame)

ts
interface AudioSpectrumFrame {
  frequencyData: Float32Array;  // 频域数据(长度 = fftSize / 2)
  timeData: Float32Array;       // 时域数据(长度 = fftSize)
  sampleRate: number;           // 采样率(Hz)
}
字段说明
frequencyData[0, fftSize/2) — 每个 bin 代表一段频率区间的幅度(非 dB,线性值 0-1)
timeData[0, fftSize) — 原始 PCM 波形采样值
sampleRate实际捕获的音频采样率

返回值

subscribe() 返回一个取消函数。调用它即可停止此订阅。插件禁用/卸载时也会自动取消所有订阅:

js
const unsubscribe = ctx.audio.spectrum.subscribe(
  { fftSize: 1024, interval: 30 },
  (frame) => {
    drawBars(frame.frequencyData);
  }
);

// 稍后手动取消
unsubscribe();

按需启停频谱捕获

为避免不必要的性能开销,建议在播放时才订阅频谱:

js
export function activate(ctx) {
  let unsubscribe = null;

  ctx.events.onPlayStateChange((isPlaying) => {
    if (isPlaying && !unsubscribe) {
      unsubscribe = ctx.audio.spectrum.subscribe(
        { fftSize: 2048 },
        (frame) => renderSpectrum(frame)
      );
    } else if (!isPlaying && unsubscribe) {
      unsubscribe();
      unsubscribe = null;
    }
  });
}

完整示例

示例一:频谱柱状图

js
export function activate(ctx) {
  // 创建 Canvas
  const canvas = document.createElement("canvas");
  canvas.width = 300;
  canvas.height = 100;
  canvas.style.cssText = `
    position: fixed; bottom: 60px; right: 20px; z-index: 9999;
    border-radius: 8px; background: rgba(0,0,0,0.6);
  `;
  document.body.appendChild(canvas);
  const ctx2d = canvas.getContext("2d");

  ctx.audio.spectrum.subscribe(
    { fftSize: 256, interval: 50 },
    (data) => {
      const { frequencyData } = data;
      const width = canvas.width;
      const height = canvas.height;
      const barWidth = width / frequencyData.length;

      ctx2d.clearRect(0, 0, width, height);

      for (let i = 0; i < frequencyData.length; i++) {
        const barHeight = frequencyData[i] * height;
        const x = i * barWidth;
        const hue = (i / frequencyData.length) * 120 + 200;

        ctx2d.fillStyle = `hsl(${hue}, 80%, 60%)`;
        ctx2d.fillRect(x, height - barHeight, barWidth - 1, barHeight);
      }
    }
  );

  ctx.dispose(() => canvas.remove());
}

示例二:波形可视化

使用 timeData 绘制原始波形:

js
ctx.audio.spectrum.subscribe(
  { fftSize: 1024, smoothingTimeConstant: 0, interval: 30 },
  (frame) => {
    const { timeData } = frame;
    ctx2d.clearRect(0, 0, width, height);
    ctx2d.beginPath();
    ctx2d.strokeStyle = "#00ff88";
    ctx2d.lineWidth = 1;

    for (let i = 0; i < timeData.length; i++) {
      const x = (i / timeData.length) * width;
      const y = (0.5 + timeData[i] * 0.45) * height;
      if (i === 0) ctx2d.moveTo(x, y);
      else ctx2d.lineTo(x, y);
    }
    ctx2d.stroke();
  }
);

示例三:VU 表(响度监测)

js
let peak = 0;

ctx.audio.spectrum.subscribe(
  { fftSize: 512, interval: 100 },
  (frame) => {
    // 计算 RMS 作为响度指标
    let sum = 0;
    for (let i = 0; i < frame.timeData.length; i++) {
      sum += frame.timeData[i] * frame.timeData[i];
    }
    const rms = Math.sqrt(sum / frame.timeData.length);
    peak = Math.max(peak, rms);

    // 更新 VU 表 UI
    updateVUMeter(rms, peak);

    // 逐渐衰减峰值
    peak *= 0.99;
  }
);

常见问题

频谱数据的 bin 如何对应频率?

对于 FFT 大小 N:frequencyData[k] 对应频率约为 k * sampleRate / N Hz。

例如 fftSize=2048sampleRate=44100

  • frequencyData[0] → ~0 Hz(DC)
  • frequencyData[1] → ~21.5 Hz
  • frequencyData[511] → ~11,025 Hz(奈奎斯特频率)

低频的 bin 频率分辨率更高。如需在低频段获得更细致的分频,请增大 fftSize。

interval 和实际帧率的关系?

interval 是最小推送间隔。实际帧率取决于系统音频缓冲区大小:

  • 默认 50ms → 每秒最多 20 帧
  • 设为 16 → 每秒最多 ~60 帧(高刷新率可视化)
  • 设为 100 → 每秒最多 10 帧(省电模式)

频谱捕获对性能有多大影响?

  • 仅在有活跃订阅时才会启动底层系统音频环回捕获
  • 所有订阅共享同一帧捕获(不会为每个订阅单独捕获)
  • fftSize 越大 → CPU 使用越高,但更精确;建议根据需求选最小值
  • 100ms interval + fftSize=512 的 CPU 开销非常低,适合后台运行

下一步

文档内容
播放器与音频引擎 →播放控制、EQ、空间音效、IR 卷积
API 总览 →ctx 完整 API 列表
窗口与系统 →浮窗、桌面歌词、Mini 播放器