🔊
ReactとWebAudio APIで本物っぽいVUメーターを作る
vu-meter-react これは何?
ちょっと今作ってるアプリで必要に迫られまして、VUメーターのライブラリを作りました。
複数の基準レベルをターゲットポイントとして設けて、針の角度を補完する力技実装ですが、挙動はそれなりに正確なはずです。
正確なVUのバリスティクス(約300msのアタック/リリース)、ステレオ/モノ、テーマ(light/dark)、可変サイズに対応。React 16.8+ 〜 19 で動作。
動作デモ
インストール
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。フレーム毎に針角のみ更新。
- カスタマイズ: テーマや色、フォント、サイズ(幅/高さの片方指定でアスペクト比維持)。
リポジトリ
サンプル
サンプルコードいくつか
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