Processingで音声チャットを作ってみる

仕事の成り行きでTV会議システムについて少し調べていたのですが、「これってProcessingで作ったら色々できて面白そう!」と私のゴーストが囁いたので、まずはネットワーク越しに音声を送る仕組みを作ってみました。

結構込み入ったことをしていて、すべてを丁寧に説明しきる自信がないので、今回は説明少なめ、コード多めの手抜きコースでお送りします。

音声チャットの流れ

今回はコードをシンプルにするために、音声送信用のスケッチと受信用のスケッチを別々に作ることにします。処理の流れは以下のとおりです。

  1. 音声送信用スケッチで、マイクに入力された音声情報をfloat型の配列として取得
  2. 取得した音声情報の波形を画面に描画
  3. 取得した音声情報をshort型の配列に変換してサイズを1/2に圧縮
  4. UDPで圧縮した音声データを受信用スケッチに送信
  5. 音声受信用スケッチで、UDPで送られて来た音声データを受信
  6. 取得した音声データをfloat型の配列に戻す
  7. 展開した音声情報をスピーカーから出力
  8. 再生した音声情報の波形を画面に描画

使用する外部ライブラリ

外部ライブラリとして、音声情報の取得と再生にminimを、UDPの通信にUDP(そのまんまの名前ですが…)を使用します。

minimはメニューのSketchImport Libraryminimから追加します。

UDPも同じくメニューのSketchImport LibraryAdd Library...からudpで検索すると追加できます。

音声送信用スケッチ

音声送信用のスケッチのコードは以下になります。

冒頭のIPPORT定数を変更することで送信先と利用するポート番号を変更できます。ポート番号変更の際には、受信用スケッチ側のポート番号も併せて変更する必要があるので注意してください。

import java.nio.*;
import hypermedia.net.*;
import ddf.minim.*;

final String IP = "localhost";
final int PORT = 6000;
final int SAMPLE_NUM = 1024;

UDP udp;

Minim minim;
AudioInput in;
UDPListener listener;

void setup() {
  size(300, 100);

  udp = new UDP(this);

  minim = new Minim(this);
  in = minim.getLineIn(Minim.MONO, SAMPLE_NUM);
  listener = new UDPListener();
  in.addListener(listener);
}

void draw() {
  background(0);
  stroke(255, 255, 0);

  for (int i = 0; i < in.bufferSize () - 1; i++) {
    point(i, (in.left.get(i) + 1) * height / 2);
  }
}

class UDPListener implements AudioListener {
  ByteBuffer buffer = ByteBuffer.allocate(SAMPLE_NUM * Short.SIZE);

  public synchronized void samples(float[] samp) {
    ShortBuffer sb = buffer.asShortBuffer();

    for (int i = 0; i < samp.length; i++) {
      sb.put(i, (short)(samp[i] * Short.MAX_VALUE));
    }

    udp.send(buffer.array(), IP, PORT);
  }

  public synchronized void samples(float[] sampL, float[] sampR) {}
}

音声受信用スケッチ

音声受信用のスケッチのコードは以下になります。

import java.nio.*;
import hypermedia.net.*;
import ddf.minim.*;

final int PORT = 6000;
final int SAMPLE_NUM = 1024;

UDP udp;

Minim minim;
AudioOutput out;
UDPSignal signal;

void setup() {
  size(300, 100);

  udp = new UDP(this, PORT);
  udp.listen(true);

  minim = new Minim(this);
  out = minim.getLineOut(Minim.MONO, SAMPLE_NUM);
  signal = new UDPSignal();
  out.addSignal(signal);
}

void draw() {
  background(0);
  stroke(0, 255, 255);

  for (int i = 0; i < out.bufferSize () - 1; i++) {
    point(i, (out.left.get(i) + 1) * height / 2);
  }
}

void receive(byte[] data) {
  signal.receive(data);
}

class UDPSignal implements AudioSignal {
  ByteBuffer buffer = ByteBuffer.allocate(Short.SIZE * SAMPLE_NUM);

  void receive(byte[] data) {
    arrayCopy(data, buffer.array());
  }

  void generate(float[] signal) {
    ShortBuffer sb = buffer.asShortBuffer();

    for (int i = 0; i < signal.length; i++) {
      signal[i] = (float)sb.get(i) / Short.MAX_VALUE;
      sb.put(i, (short)0);
    }
  }

  void generate(float[] left, float[] right) {}
}

動作確認

実際の動作画面がこちらです。左が音声送信用スケッチ、右が音声受信用スケッチの画面です。

f:id:tkitao:20140913225204p:plain

同じ波形が表示されているだけでなく、スピーカーからマイクに入力した音声も再生されています。(音量を上げるとハウリングがすぐ起きます)

終わりに

実はビデオで同様のことを行うスケッチもすでに完成していますので、近いうちにビデオチャット編も投稿したいと思います。