Unknown

Concept

So Takasugi
10 min readJan 31, 2024

『Unknown』 − 得体の知れない物質が、ジブンを支配している −

わたしの一部であり、愛着が湧くはずの脳波…

しかし実際に目で見ることはできないし、

思うようにコントロールすらできない。

Overview

脳波データをリアルタイムで計測し、脳波によって音、照明、映像が変化するのを楽しむデータドリブンなパフォーマンス作品。

作品の流れ: 加算される要素

  1. Lighting
  2. Sound
  3. Dance
  4. Visual

はじまりは何もない「無」からスタートする。

Flow

  1. イヤホン型脳波計「VIE Zone」もしくは「VIE Chill」を用いて脳波を計測。
  2. 脳波計測用のアプリケーション「VIE Streamer」を用いて脳波を取得
  3. OSCを用いてMaxに送信
  4. Max内で脳波を独自のプロセスで整形、Lightingに反映
  5. Maxから再度OSCを用いて、Processingに送信
  6. Processing内で脳波データを整形し、VisualとSoundに反映

My Section

上記のFlowの中で私が担当した部分は以下の3つである。

  1. Processingでの脳波データの処理、場合分け
  2. ProcessingでのVisualの再生
  3. ProcessingでのSoundの再生

これらについて詳しく説明していこうと思う。

Processingでの脳波データの処理、場合分け

まず初めに脳波を処理しVisualやSoundにアプローチするにあたって、脳波帯域をもとに観客にアプローチしようと考えた。

脳波帯域は5種類に分類され、各帯域ごとに増強されたときの状態と効用があり、それらをVisualおよびSoundに反映する方針であった。

そのため、帯域が増強されているときとされていないときの2種類に分類し、それらのタイミングでVisualとSoundが再生されるとコンセプトに沿ったパフォーマンスになると解釈した。

具体的に説明すると、まず各帯域ごとのHzを左右に分けて受信し、別々の変数に格納する。

float[] waveLeft = new float[5];
float[] waveRight = new float[5];

void oscEvent(OscMessage msg) {
if (msg.checkAddrPattern("/wave/L/1") == true) {
for (int i = 0; i < 5; i++) {
waveLeft[i] = msg.get(i).floatValue();
}
}
if (msg.checkAddrPattern("/wave/R/1") == true) {
for (int i = 0; i < 5; i++) {
waveRight[i] = msg.get(i).floatValue();
}
}
}

左右に分かれていると場合分けを行ったときに都合が悪いため、左右の平均をとり、それを配列に格納する関数を作成し、それをdraw関数内で実行。

float[] waveLeft = new float[5];
float[] waveRight = new float[5];
float[] waveAverage = new float[5];

void calcAvg(float[] arr1, float[] arr2, float[] result) {
int length = min(arr1.length, arr2.length);

for (int i = 0; i < length; i++) {
result[i] = (arr1[i] + arr2[i]) / 2.0;
}
}

void draw() {
calcAvg(waveLeft, waveRight, waveAverage);
}

これらの処理で整形された脳波データをもとに、各帯域ごとの値における閾値を20に設定し、20未満であればFlagに0を、20以上であればFlagに1を代入するようにした。

この20という数字は、20を下回ればその帯域は増強されていないが20以上であれば増強されているとチーム内で解釈したゆえの数字になっている。

void draw() {
for (int i = 0; i < 5; i++) {
// flag
if (waveAverage[i] < 30) {
waveFlag[i] = 0;
} else {
waveFlag[i] = 1;
}
}
}

そしてdraw関数の中で場合分けを行っているため、そのまま直接実行すると、その帯域で設定したVisualとSoundが1秒間に60回再生されてしまう。

それを避けるために、Flagが変化したときに一度だけ再生されるようにコードを書いた。直前のFlagの配列をProcessingのarrayCopy関数を用いてpreWaveFlagという配列に格納し、それを検証することで変化を取得する。

void draw() {
for (int i = 0; i < 5; i++) {
if (waveFlag[i] != preWaveFlag[i]) {
if (i == 0) {
if (waveFlag[i] == 0) {

}
else if (waveFlag[i] == 1) {

}
}
else if (i == 1) {
if (waveFlag[i] == 0) {

}
else if (waveFlag[i] == 1) {

}
}
else if (i == 2) {
if (waveFlag[i] == 0) {

}
else if (waveFlag[i] == 1) {

}
}
else if (i == 3) {
if (waveFlag[i] == 0) {

}
else if (waveFlag[i] == 1) {

}
}
else if (i == 4) {
if (waveFlag[i] == 0) {

}
else if (waveFlag[i] == 1) {

}
}
}
arrayCopy(waveFlag, preWaveFlag);
}

ProcessingでのVisualの再生

先ほどの処理で場合分けを行えたので、次はVisualの再生を書いていく。

チームで分担してVisualの作成を行う都合上、アニメーションをClassで作成してもらい、そのClass内のdisplay関数を実行することでVisualをつくることにした。

Visualが再生されるごとに前に再生されていたアニメーションが上書きされることを避けるため、animation用の配列をつくり、その配列にClassをaddしていくことにした。

ArrayList<Animation> animations;

void setup() {
animations = new ArrayList<Animation>;
}

void draw() {

for (Animation anim : animations) {
anim.display();
}

for (int i = 0; i < 5; i++) {
if (waveFlag[i] != preWaveFlag[i]) {
if (i == 0) {
if (waveFlag[i] == 0) {
animations.add(new Anim_00());
}
else if (waveFlag[i] == 1) {
animations.add(new Anim_01());
}
// 以下同様に
}
}
}
}

abstract class Animation {
abstract void display();
}

しかしこのままだとアニメーションが無限に追加され続けてしまうので配列のサイズの上限を設けるコードを加えた。

void draw() {
for (int i = 0; i < 5; i++) {
if (animations.size() >= 30) {
animations.remove(0);
}
}
}

ProcessingでのSoundの再生

Processingのminimライブラリを用いて、音声ファイルの再生を行った。デフォルトでは追加されていないので適宜インストールを行う。

まずサウンドファイルのロードをするコードを書く。audioPlayerではなくloadSnippetメソッドを用いる。

import ddf.minim.*;
Minim minim;

AudioSnippet[] soundFiles = new AudioSnippet[15];

void loadFiles() {
soundFiles[0] = minim.loadSnippet("./sound/00.mp3");
soundFiles[1] = minim.loadSnippet("./sound/01.mp3");
// 以下同様に10ファイル文書く
}

ロードしたファイルをVisualと同様に、場合分けされたif文の中で再生するコードを書く。loadSnippetメソッドを用いる場合、playの前にrewindを書かないと1度しか再生されないため注意。

void draw() {
for (int i = 0; i < 5; i++) {
if (waveFlag[i] != preWaveFlag[i]) {
if (i == 0) {
if (waveFlag[i] == 0) {
animations.add(new Anim_00());
soundFiles[0].rewind();
soundFiles[0].play();
}
else if (waveFlag[i] == 1) {
animations.add(new Anim_01());
soundFiles[1].rewind();
soundFiles[1].play();
}
// 以下同様に
}
}
}
}

しかしこのまま実行すると1秒間に60回再生されてしまうため、再生されているかの判別のためboolean型で変数を作成。結果以下のように変更された。

boolean[] soundPlayed = new boolean[5];

void draw() {
for (int i = 0; i < 5; i++) {
if (waveFlag[i] != preWaveFlag[i] && !soundPlayed[i]) {
if (i == 0) {
if (waveFlag[i] == 0) {
animations.add(new Anim_00());
soundFiles[0].rewind();
soundFiles[0].play();
soundPlayed[i] = true;
}
else if (waveFlag[i] == 1) {
animations.add(new Anim_01());
soundFiles[1].rewind();
soundFiles[1].play();
soundPlayed[i] = true;
}
// 以下同様に
}
}

if (waveFlag[i] == preWaveFlag[i] && soundPlayed[i]) {
soundPlayed[i] = false;
}
}
}

プログラムを停止したときに再生が止まるおまじないも忘れずに書く。

void stop() {
minim.stop();
super.stop();
}

acknowledgement

いままであまり触れてこなかったアート作品をグループという難しい環境で創ってみて、まったく知らなかったツールの数々、アートの奥深さ、そして同じ目標に向かって全員で努力するということの面白さを体感できた。自分が担当した内容は表側に出るような内容ではなかったものの、システムの根幹になるのでミスできないという緊張があったが、全員でカバーしあえる環境があったからこそ、のびのびお互いの良さを強調しあえたのだと思う。チームのメンバーに感謝を伝えたい。

--

--