見出し画像

Processingでオーディオビジュアライザ

動作イメージ

  • macにて動作確認

Combined Modeでの動作

Macでのオーディオルーティング

使用するアプリ

無料でやる場合のシステム環境設定

  • 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";
  }
}


この記事が気に入ったらサポートをしてみませんか?