Processingでオーディオビジュアライザ
動作イメージ
macにて動作確認
Macでのオーディオルーティング
使用するアプリ
いろいろ選択肢はあると思います
有料でいいなら
Audio Hijackを使うとか
無料でやりたいなら
BlackHoleを使うのをお勧めします
無料でやる場合のシステム環境設定
Audio MIDI設定.app
複数出力装置を作成
出力したいスピーカーやオーディオインターフェースとBlackHole両方にチェックを入れて音声を同時に送るようにする
プライマリ装置をMacやオーディオインターフェースに指定し、BlackHole側に音ずれ補正チェックを入れるといいと思います
システム設定.app
サウンド
出力に、先ほど作成した複数出力装置を指定
入力に、BlackHoleを指定
以上で、システムで再生する音声を同時にBlackHoleの入力に渡せます。
Processingでこの音声を拾って、オーディオリアクティブなコードで遊べるようになりました。
機能
数字キーでモード変更
1:LissajousCurveMode
2:SpectrumMode
3:BarGraphMode
4:FrequencyBarGraphMode
5:CombinedMode
コード
素人実装なので、改善点ありましたら教えてください
import processing.sound.*;
Waveform waveform;
FFT fft;
final int FFT_SIZE = 1024;
final int NUM_SAMPLES = 2048;
final float MIN_STANDARD_DEVIATION = 16.0f;
final int X_SCALE = 25;
final int Y_SCALE = 25;
final int FRAME_RATE = 120;
final int BAR_HUE_START = 240;
final int BAR_HUE_END = 0;
int strokeWeight;
color strokeColor;
color backgroundColor;
int barHueOffset = 200;
int barResolution = 4;
VisualizationMode currentMode;
void setup() {
noSmooth();
//fullScreen();
size(768, 384); // ウィンドウ
frameRate(FRAME_RATE);
initializeGraphics();
initializeAudio();
currentMode = new LissajousCurveMode();
colorMode(HSB, 255);
}
void initializeGraphics() {
strokeWeight = 1;
strokeColor = color(0, 252, 255);
backgroundColor = color(0, 25, 49);
stroke(strokeColor);
strokeWeight(strokeWeight);
}
void initializeAudio() {
AudioIn audioInput = new AudioIn(this, 0);
audioInput.start();
waveform = new Waveform(this, NUM_SAMPLES);
waveform.input(audioInput);
fft = new FFT(this, FFT_SIZE);
fft.input(audioInput);
}
void draw() {
background(backgroundColor);
float[] waveformData = getWaveformData();
currentMode.visualize(waveformData);
displayModeText();
}
float[] getWaveformData() {
waveform.analyze();
return waveform.data;
}
void keyPressed() {
switch (key) {
case '1':
currentMode = new LissajousCurveMode();
break;
case '2':
currentMode = new SpectrumMode();
break;
case '3':
currentMode = new BarGraphMode();
break;
case '4':
currentMode = new FrequencyBarGraphMode();
break;
case '5':
currentMode = new CombinedMode();
break;
case '+':
if (barResolution > 1) barResolution--;
break;
case '-':
barResolution++;
break;
case 'o':
barHueOffset += 10;
break;
case 'p':
barHueOffset -= 10;
break;
}
}
void displayModeText() {
fill(255);
textAlign(LEFT, TOP);
text("Mode: " + currentMode.getModeName(), 10, 10);
}
interface VisualizationMode {
void visualize(float[] waveformData);
String getModeName();
}
class LissajousCurveMode implements VisualizationMode {
public void visualize(float[] waveformData) {
float[] lissajousPoints = getLissajousPoints(waveformData);
stroke(strokeColor);
strokeWeight(strokeWeight);
noFill();
beginShape();
for (int i = 0; i < lissajousPoints.length; i += 2) {
vertex(lissajousPoints[i], lissajousPoints[i + 1]);
}
endShape(CLOSE);
}
float[] getLissajousPoints(float[] waveformData) {
float[] lissajousPoints = new float[NUM_SAMPLES * 2];
for (int i = 0; i < NUM_SAMPLES; i++) {
float x = waveformData[i];
float y = waveformData[(i + NUM_SAMPLES / 4) % NUM_SAMPLES];
float scaledX = (x - waveformData[(i + NUM_SAMPLES / 2) % NUM_SAMPLES]) * X_SCALE / MIN_STANDARD_DEVIATION;
float scaledY = (x + y) * Y_SCALE / MIN_STANDARD_DEVIATION;
lissajousPoints[i * 2] = scaledX * width / 2 + width / 2;
lissajousPoints[i * 2 + 1] = scaledY * height / 2 + height / 2;
}
return lissajousPoints;
}
public String getModeName() {
return "Lissajous Curve";
}
}
class SpectrumMode implements VisualizationMode {
public void visualize(float[] waveformData) {
stroke(strokeColor);
strokeWeight(2);
for (int i = 0; i < waveformData.length; i++) {
float x = map(i, 0, waveformData.length, 0, width);
float y = constrain(map(waveformData[i], -1, 1, height, 0), 0, height);
point(x, y);
}
}
public String getModeName() {
return "Spectrum";
}
}
class BarGraphMode implements VisualizationMode {
public void visualize(float[] waveformData) {
int numBars = waveformData.length / barResolution;
float barWidth = width / (float)numBars;
for (int i = 0; i < numBars; i++) {
int index = i * barResolution;
float barHeight = map(waveformData[index], 0, 1, 0, height);
float hue = (map(barHeight, 0, height, BAR_HUE_START, BAR_HUE_END) + barHueOffset) % 256;
noStroke();
fill(hue, 255, 255);
rect(i * barWidth, height - barHeight, barWidth, barHeight);
}
}
public String getModeName() {
return "Bar Graph";
}
}
float frequencyToMel(float frequency) {
return 2595 * log(1 + frequency / 700) / log(10);
}
float melToFrequency(float mel) {
return 700 * (pow(10, mel / 2595) - 1);
}
class FrequencyBarGraphMode implements VisualizationMode {
final int NUM_BARS = 64;
final float FREQ_MIN = 150; // 人間の聞こえる最小周波数
final float FREQ_MAX = 15000; // 人間の聞こえる最大周波数 (20kHz)
final int SAMPLE_RATE = 44100; // オーディオのサンプリングレート
float heightScaleFactor = 0.0000000000000000000000001f; // バーの高さのスケーリングファクター
float[] peakHeights; // ピークホールドのための配列
float peakDecayRate = 0.99f; // ピークの減衰率
float minGradient = 0.0f; // 左端のバーに乗算する勾配の最小値
float maxGradient = 250000000000000000000000000.0f; // 右端のバーに乗算する勾配の最大値
FrequencyBarGraphMode() {
peakHeights = new float[NUM_BARS];
}
public void visualize(float[] waveformData) {
fft.analyze();
float[] barHeights = new float[NUM_BARS];
float maxLog = log(FREQ_MAX);
float minLog = log(FREQ_MIN);
float logRange = maxLog - minLog;
// FFTの結果を用いて各バーの高さを計算
for (int i = 0; i < NUM_BARS; i++) {
float logFreqStart = minLog + i * logRange / NUM_BARS;
float logFreqEnd = minLog + (i + 1) * logRange / NUM_BARS;
float freqStart = exp(logFreqStart);
float freqEnd = exp(logFreqEnd);
int startIndex = freqToIndex(freqStart);
int endIndex = freqToIndex(freqEnd);
// FFT配列のインデックスが正しい範囲内にあることを確認
startIndex = Math.max(startIndex, 0);
endIndex = Math.min(endIndex, fft.spectrum.length - 1);
float sum = 0;
for (int j = startIndex; j <= endIndex; j++) {
sum += fft.spectrum[j];
}
// インデックスが実際に含まれている場合のみ平均を計算する
float average = (endIndex - startIndex + 1) > 0 ? sum / (endIndex - startIndex + 1) : 0;
barHeights[i] = average;
// ピークホールドの更新
if (barHeights[i] > peakHeights[i]) {
peakHeights[i] = barHeights[i];
} else {
peakHeights[i] *= peakDecayRate;
}
}
// バーを描画
float barWidth = width / (float)NUM_BARS;
for (int i = 0; i < NUM_BARS; i++) {
// 左から右にかけて勾配を計算
float gradient = map(i, 0, NUM_BARS - 1, minGradient, maxGradient);
float barHeight = map(barHeights[i], 0, 1, 0, height * heightScaleFactor) * gradient;
float peakHeight = map(peakHeights[i], 0, 1, 0, height * heightScaleFactor) * gradient;
float freq = exp(minLog + i * logRange / NUM_BARS);
float hue = map(log(freqToMel(freq)), log(freqToMel(FREQ_MIN)), log(freqToMel(FREQ_MAX)), 200, 0);
// バーの高さに応じて不透明度を計算
float opacity = map(barHeight, 0, height * heightScaleFactor * 1000000000000000000000000f, 0, 255);
fill(hue, 255, 255, opacity);
noStroke();
rect(i * barWidth, height - barHeight, barWidth, barHeight);
// ピークホールドの描画
fill(hue, 255, 255, 128);
rect(i * barWidth, height - peakHeight, barWidth, 12);
}
}
// 周波数をFFT配列のインデックスに変換する補助関数
int freqToIndex(float freq) {
if (freq > SAMPLE_RATE / 2) {
freq = SAMPLE_RATE / 2;
}
return Math.min(FFT_SIZE / 2, Math.round((freq / (SAMPLE_RATE / 2.0f)) * (FFT_SIZE / 2)));
}
// 周波数をメルスケールに変換する関数
float freqToMel(float freq) {
return 2595 * log10(1 + freq / 700);
}
float log(float value) {
return (float)Math.log(value);
}
float log10(float value) {
return (float)Math.log10(value);
}
float exp(float value) {
return (float)Math.exp(value);
}
float max(float[] array) {
float max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
return max;
}
public String getModeName() {
return "Frequency Bar Graph";
}
}
class CombinedMode implements VisualizationMode {
LissajousCurveMode lissajousCurveMode;
SpectrumMode spectrumMode;
FrequencyBarGraphMode frequencyBarGraphMode;
CombinedMode() {
lissajousCurveMode = new LissajousCurveMode();
spectrumMode = new SpectrumMode();
frequencyBarGraphMode = new FrequencyBarGraphMode();
}
public void visualize(float[] waveformData) {
lissajousCurveMode.visualize(waveformData);
spectrumMode.visualize(waveformData);
frequencyBarGraphMode.visualize(waveformData);
}
public String getModeName() {
return "Combined Mode";
}
}