特定の周波数帯(主にキックドラムなどの低音域)の急激な音量変化を検知してビジュアルを反応させるサンプル。
p5.js 2.0 p5.sound
Audio Reactive Beat Detection Peak Detect
View Source Code
let mic, fft, peakDetect;
let nyquist = 22050; // Cache nyquist globally to avoid AudioContext lockups
let isStarted = false;
// UIコントロール要素
let freqMinSlider, freqMaxSlider, thresholdSlider;
let freqMinValue, freqMaxValue, thresholdValue;
// ビートに合わせて変化させる図形のプロパティ
let ellipseSize = 50;
let ellipseColor = 0;
function setup() {
createCanvas(windowWidth, windowHeight);
mic = new p5.AudioIn();
fft = new window.p5.FFT(1024);
// mic.connect(fft)はAudioContext起動後に実行するためここでは行わない
// マイクからの入力(特にスマホやPCマイク)は低音が拾いにくいため、帯域を少し広げます(40〜160Hz)
// 初期閾値も少し下げます
peakDetect = new CustomPeakDetect(40, 160, 0.5, 20);
// ビートを検出したときに呼ばれるコールバック関数を登録
peakDetect.onPeak(triggerBeat);
// UI要素の取得とイベントリスナーの登録
freqMinSlider = select("#freqMin");
freqMaxSlider = select("#freqMax");
thresholdSlider = select("#threshold");
freqMinValue = select("#freqMinValue");
freqMaxValue = select("#freqMaxValue");
thresholdValue = select("#thresholdValue");
// スライダーの初期値を反映
updateParamsFromUI();
// スライダー変更時にリアルタイムで反映
freqMinSlider.input(updateParamsFromUI);
freqMaxSlider.input(updateParamsFromUI);
thresholdSlider.input(updateParamsFromUI);
colorMode(HSB, 360, 100, 100);
noStroke();
}
function updateParamsFromUI() {
// スライダーから値を取得
let fMin = float(freqMinSlider.value());
let fMax = float(freqMaxSlider.value());
let thresh = float(thresholdSlider.value());
// 表示の更新
freqMinValue.html(fMin);
freqMaxValue.html(fMax);
thresholdValue.html(thresh.toFixed(2));
// PeakDetectインスタンスへ反映 (freq1が最大周波数を超えないように制御)
if (fMin >= fMax) {
fMin = fMax - 10;
freqMinSlider.value(fMin);
freqMinValue.html(fMin);
}
if (peakDetect) {
peakDetect.freq1 = fMin;
peakDetect.freq2 = fMax;
peakDetect.threshold = thresh;
}
}
function draw() {
// 背景は徐々に暗くする(残像)
background(20, 30);
if (!isStarted) {
fill(255);
textAlign(CENTER, CENTER);
textSize(24);
text("Click to Start Microphone", width / 2, height / 2);
return;
}
// 取得したFFTデータをCustomPeakDetectに渡してピークを監視
// (CustomPeakDetect内部で fft.analyze() を呼ぶ想定)
peakDetect.update(fft);
// 毎フレーム、円のサイズと色相を少しずつ戻す(減衰効果)
ellipseSize = ellipseSize * 0.95; // サイズを小さくしていく
ellipseColor = ellipseColor * 0.98; // 色相を0に近づけていく
// 最小サイズを設定
if (ellipseSize < 50) {
ellipseSize = 50;
}
// 円を描画
fill(ellipseColor, 80, 100);
circle(width / 2, height / 2, ellipseSize);
// デバッグ用:現在ピークを検知中か(peakDetect.isDetected)を画面に表示
fill(255);
textAlign(LEFT, TOP);
textSize(24);
text("Beat Detect (Bass Peak)", 20, 20);
textSize(14);
fill(200);
text("Use UP/DOWN arrow keys to adjust threshold", 20, 50);
// ===== デバッグ情報の追加 =====
fill(255);
textSize(14);
text(`Threshold: ${peakDetect.threshold}`, 20, 50);
text(
`Current Energy (Normalized): ${peakDetect.currentEnergy ? peakDetect.currentEnergy.toFixed(4) : 0}`,
20,
70,
);
text(
`Raw Result (Linear Avg): ${peakDetect.rawResult ? peakDetect.rawResult.toFixed(4) : 0}`,
20,
90,
);
text(`Frames since last peak: ${peakDetect.framesSinceLastPeak}`, 20, 110);
// エナジーのバーを描画
fill(50);
rect(20, 140, 200, 20);
fill(120, 100, 100);
rect(20, 140, 200 * (peakDetect.currentEnergy || 0), 20);
// 閾値の線を描画
stroke(255);
strokeWeight(2);
line(
20 + 200 * peakDetect.threshold,
130,
20 + 200 * peakDetect.threshold,
170,
);
noStroke();
textSize(16);
if (peakDetect.isDetected) {
fill(0, 100, 100); // ピーク中は赤文字
text("BEAT DETECTED!", 20, 180);
} else {
fill(255);
text("Waiting for beat...", 20, 180);
}
// ============================
}
// ビートが検出された瞬間に実行される関数
function triggerBeat() {
// 円のサイズと色相を跳ね上げる
ellipseSize = 300;
ellipseColor = random(0, 360); // ランダムな色相に
}
function mousePressed() {
if (!isStarted) {
userStartAudio();
// AudioContextが開始された後に正確なナイキスト周波数を取得
const ctx = getAudioContext();
if (ctx && ctx.sampleRate) {
nyquist = ctx.sampleRate / 2;
}
mic.start();
mic.connect(fft);
isStarted = true;
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
// v2で廃止された p5.PeakDetect の代替クラス
class CustomPeakDetect {
constructor(freq1 = 20, freq2 = 20000, threshold = 0.1, framesPerPeak = 20) {
this.freq1 = freq1;
this.freq2 = freq2;
this.threshold = threshold; // 0.0〜1.0 の間で反応する強さ
this.framesPerPeak = framesPerPeak; // 連続で検知しないためのフレーム間隔
this.isDetected = false;
this.framesSinceLastPeak = framesPerPeak;
this.onPeakCallback = null;
// デバッグ用変数
this.currentEnergy = 0;
this.rawResult = 0;
}
onPeak(callback) {
this.onPeakCallback = callback;
}
update(fft) {
let spectrum = fft.analyze();
let energy = this.getEnergy(spectrum, this.freq1, this.freq2);
this.currentEnergy = energy;
if (
energy > this.threshold &&
this.framesSinceLastPeak >= this.framesPerPeak
) {
this.isDetected = true;
this.framesSinceLastPeak = 0;
if (this.onPeakCallback) this.onPeakCallback();
} else {
this.isDetected = false;
this.framesSinceLastPeak++;
}
}
getEnergy(spectrum, freq1, freq2) {
if (!spectrum || spectrum.length === 0) return 0;
const bins = spectrum.length;
let i1 = Math.round((freq1 / nyquist) * bins);
let i2 = Math.round((freq2 / nyquist) * bins);
i1 = constrain(i1, 0, bins - 1);
i2 = constrain(i2, 0, bins - 1);
if (i1 === i2) return spectrum[i1] || 0;
let sum = 0;
let count = i2 - i1 + 1;
for (let i = i1; i <= i2; i++) {
sum += spectrum[i] || 0;
}
let result = sum / count;
this.rawResult = result; // デバッグ用に記録
if (isNaN(result)) return 0;
// v2の仕様に依存:もしresultがすでに正規化されたdBや別のスケールなら、この計算が狂う可能性がある
// 一旦今のロジックのままデバッグ数値を観察する
// リニア振幅をデシベル(dB)に変換し、0.0〜1.0スケールに正規化して返す
let db = result > 0 ? 20 * Math.log10(result) : -100;
// マイク入力は直接入力に比べてdBが下がるため、マッピング範囲を広げます (-80dB to -30dB)
let normalized = map(db, -80, -30, 0, 1);
return constrain(normalized, 0, 1);
}
}