🔊

ReactとWebAudio APIで本物っぽいVUメーターを作る

に公開

vu-meter-react これは何?

ちょっと今作ってるアプリで必要に迫られまして、VUメーターのライブラリを作りました。
複数の基準レベルをターゲットポイントとして設けて、針の角度を補完する力技実装ですが、挙動はそれなりに正確なはずです。

正確なVUのバリスティクス(約300msのアタック/リリース)、ステレオ/モノ、テーマ(light/dark)、可変サイズに対応。React 16.8+ 〜 19 で動作。

動作デモ

https://codesandbox.io/p/sandbox/2kwnxh

インストール

npm i vu-meter-react
# or
yarn add vu-meter-react
pnpm add vu-meter-react

特徴

  • 本物感のある挙動: 300msバリスティクス、ピークランプ、目盛り・ラベルの描画。
  • 入力に忠実: AnalyserNode#getFloatTimeDomainData() からRMS→dBFS→VU変換。
  • ステレオ/モノ: ChannelSplitterNode/内部でのチャンネル処理に対応。
  • 軽量・依存少: SVG + CSS transform。フレーム毎に針角のみ更新。
  • カスタマイズ: テーマや色、フォント、サイズ(幅/高さの片方指定でアスペクト比維持)。

リポジトリ

https://github.com/Jun-Murakami/vu-meter-react

サンプル

サンプルコードいくつか
import { useState, useRef } from "react";
import { VUMeter } from "vu-meter-react";

/**
 * 最小構成の例
 * - 再生ボタンでAudioContextをユーザー操作から生成(自動再生ポリシー対策)
 * - 簡易オシレータ→ゲイン→Destination を作り、ゲインノードを sourceNode として渡す
 * - ステレオ表示(L/R)、0VU = -18 dBFS、ダークテーマ、幅300px
 */
export default function App() {
  const [audioContext, setAudioContext] = useState<AudioContext | null>(null);
  const sourceNodeRef = useRef<AudioNode | null>(null);

  const start = async () => {
    // 1) AudioContextはユーザー操作から生成(モバイル/ブラウザ対策)
    const Ctx = window.AudioContext ?? (window as any).webkitAudioContext;
    const ctx = new Ctx();

    // 2) 信号源(220Hzの矩形波)とエンベロープ(ゲイン)を作成
    const osc = ctx.createOscillator();
    osc.type = "square";
    osc.frequency.value = 220;

    const gain = ctx.createGain();
    // 初期は無音。短いアタック→ゆるやかなディケイを数回繰り返す
    gain.gain.setValueAtTime(0, ctx.currentTime);
    const burstCount = 5;
    const repeatInterval = 0.7;
    const attackTime = 0.02;
    const decayTime = 0.5;
    for (let i = 0; i < burstCount; i++) {
      const t0 = ctx.currentTime + i * repeatInterval;
      gain.gain.cancelScheduledValues(t0);
      gain.gain.setValueAtTime(0.0001, t0);
      gain.gain.linearRampToValueAtTime(0.8, t0 + attackTime);
      gain.gain.exponentialRampToValueAtTime(0.0001, t0 + attackTime + decayTime);
    }

    // 3) 接続(osc → gain → 出力)
    osc.connect(gain);
    gain.connect(ctx.destination);

    // 4) 再生/停止のスケジュール
    const totalDur = burstCount * repeatInterval + decayTime;
    osc.start();
    osc.stop(ctx.currentTime + totalDur);

    // 5) VUメーターに渡すノード(ここでは gain)を保持
    sourceNodeRef.current = gain;
    setAudioContext(ctx);
  };

  return (
    <div>
      <button onClick={start}>Play</button>
      <VUMeter
        audioContext={audioContext}
        sourceNode={sourceNodeRef.current}
        mono={false}                    // falseでステレオ(L/R)を表示
        referenceLevel={-18}            // 0VU = -18 dBFS
        options={{
          width: 300,                   // 幅。高さ未指定時はアスペクト比から自動算出
          theme: "dark",                // light/dark
          // needleColor, labelColor, backgroundColor, boxColor, fontFamily も調整可能
        }}
      />
    </div>
  );
}

マイク入力をメータリングする

import { useEffect, useState } from "react";
import { VUMeter } from "vu-meter-react";

/**
 * getUserMedia のストリームを VU メーターに渡す例
 * - MediaStream を AudioContext に取り込み、MediaStreamAudioSourceNode を sourceNode にする
 * - 終了時は AudioContext/トラックを停止して解放
 */
export default function MicMeter() {
  const [ctx, setCtx] = useState<AudioContext | null>(null);
  const [src, setSrc] = useState<AudioNode | null>(null);

  const start = async () => {
    const Ctx = window.AudioContext ?? (window as any).webkitAudioContext;
    const audioCtx = new Ctx();

    // マイク入力の取得(必要に応じて { echoCancellation: false } など指定)
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const mediaSrc = audioCtx.createMediaStreamSource(stream);

    setCtx(audioCtx);
    setSrc(mediaSrc);
  };

  const stop = async () => {
    // AudioContextの停止とMediaStreamトラックの停止
    if (ctx) await ctx.close();
    setCtx(null);
    setSrc(null);
  };

  useEffect(() => {
    return () => {
      // アンマウント時にクリーンアップ
      if (ctx) ctx.close();
    };
  }, [ctx]);

  return (
    <div>
      <button onClick={start}>Mic Start</button>
      <button onClick={stop}>Stop</button>
      <VUMeter audioContext={ctx} sourceNode={src} mono referenceLevel={-20} />
    </div>
  );
}

API(型とデフォルト)

export interface VUMeterProps {
  /** 計測に使用するAudioContext。nullの場合は描画のみ行い、メータは停止 */
  audioContext: AudioContext | null;
  /** メータ対象のAudioNode(例: GainNode, MediaStreamSource など)。nullで停止 */
  sourceNode: AudioNode | null;
  /** trueでモノメータ。false(デフォルト)でL/Rのステレオ表示 */
  mono?: boolean;                  // default: false
  /** ラベル(モノ時 "MONO"、ステレオ時 "L"/"R" がデフォルト) */
  label?: string;
  /** 0 VU とみなす dBFS 値 */
  referenceLevel?: number;         // default: -18
  /** 見た目やサイズのオプション */
  options?: VUMeterOptions;
}

export interface VUMeterOptions {
  /** テーマ(配色のベース) */
  theme?: "dark" | "light";        // default: 'light'
  /** 針の色 */
  needleColor?: string;
  /** 文字/ラベルの色 */
  labelColor?: string;
  /** 盤面の背景色 */
  backgroundColor?: string;
  /** 外枠ボックスの色 */
  boxColor?: string;
  /** フォントファミリ */
  fontFamily?: string;
  /** 幅(px)。高さ未指定時はアスペクト比から自動算出 */
  width?: number;
  /** 高さ(px)。幅未指定時はアスペクト比から自動算出 */
  height?: number;
}

めちゃくちゃニッチですが、よければ使ってみてね

Discussion