WORDS BANDaircord発、共有体験を生む
腕時計型デバイス『WORDS BAND』登場!

aircord

aircord発、共有体験を生む
腕時計型デバイス『WORDS BAND』登場!

Play!

「Intel® Edison」× 日本屈指の表現者たちーー両者の融合から始まる、かつてない “未来体験” へ、ようこそ!
8/1(土)、2(日)の2日間にわたって開催された、最先端の “ものづくり” の祭典『Maker Faire Tokyo 2015』(MFT 2015)。その一角、Intelブースの目玉企画のひとつとして、最前線の作り手たちによる展示企画『Artist Showcase』の作品が、今年もその全貌を現した。
過去3回にわたって5組の参加アーティストたちに密着し、発想のヒントや完成へと至る道のりをお届けしてきたこのレポート。
ついにお披露目を迎えた彼らの作品は、MFT 2015の会場でどんな光を放ったのか。彼らのチャレンジ精神と最新テクノロジーにかける想いを、システム構成やソースコードとともに解き明かしていこう……!

超音波でGO!
量産体制の試みから拓ける、
新たなビジョン

音楽イベントなど大勢の人々が集まる場所で、居合わせた人々が同じ体験を分かち合い、新たな共感を生み出すために、“言葉を伝える” ウェアラブルデバイスを作りたいーー。
これまでの取材で、aircordのクリエイティブプロデューサー橋本俊行(はしもと・としゆき)は、作品の構想をそう語った。
無線通信が混線しがちなイベント会場を想定して彼らが選んだのは、なんと超音波(!)。
しかも、腕時計サイズにEdison、表示用のOLEDディスプレイ、自作基板やバッテリーを集約し、設計からデザイン、CNCミリングマシンによる削り出しをも自社で行うという。果たして、その成果は……?
体験者が装着できる腕時計型デバイスと、バンドを除いた部分50台を並べた表示部分が、流れる音楽に応じて歌詞の言葉を表示。プロダクト(製品)レベルの展示を前に、メカニカルデザインエンジニアの岩崎 修(いわさき・おさむ)と、デザインエンジニアの赤川智洋(あかがわ・ともひろ)に、現在の心境と今後の展望を聞いてみた。

赤川智洋
シンプルな機能のウェアラブルデバイスで、電源を入れさえすれば自動で動くようなものにすることが一つのハードルだったわけですが、無事にクリアできました。さらに、展示した50台のうち1台にプログラムを書き込むと、残りすべてが自動で書き換わるようなシステムを予め組んでおくなど、実際の運用を想定したものに仕上がっていると思います。
岩崎 修
超音波による通信に関しては、嬉しい発見がありました。制作段階ではコンクリートの壁に乱反射してしまい、思うようにつながらなかったのですが、MFT 2015のような広い空間では逆に、数十メートルは優に飛ばすことができることがわかった。イベント会場での使用を想定しているだけに、これは大きな手応えですね。

超音波で言葉を送受信するウェアラブルデバイスーー
『WORDS BAND』の仕組みを大公開!

aircord「WORDS BAND」のシステムは4種のデバイスで構成されている:

  • 1)音声認識 / 超音波送信デバイス x 1個
  • 2)バンド型デバイス x 2個
  • 3)表示デバイス x 50個
  • 4)開発用Edison(受信マスター) x 1個

全体のシステムは以下の通りだ:

1)音声認識 / 超音波送信デバイス x 1個(モードスイッチャー)

このデバイスはIntel® Edison にデフォルトでインストールされているLinuxベースのYoctoに代わって、ubilinux for Intel Edison によって作動している。
また、このデバイスは異なる表示モードを選べるスイッチを持っている。事前にプログラムされたロゴアニメーションの表示、楽曲に合わせた歌詞アニメーションの表示、または音声認識による話し言葉の文字表示など用途に応じて切り替えが可能。
音声認識による表示の場合、高感度指向性マイクが Edison に接続されたUSBオーディオインターフェイスに接続される。Edison が音声をGoogle Chrome上のGoogle Web Speech API に送り、音声を文字データに変換する。この時はWi-FiもしくはUSBハブを介したLANによるインターネットのネットワークが必要だ。
モードのスイッチのためのデータや、音声認識の結果の文字データは超音波信号のエンコードモジュール(ソフトウェア)でオーディオ信号に変換され、アンプを介してスピーカーから超音波が出力される。

超音波を使う利点としては、MFT 2015が開催された東京ビックサイトのような、Wi-Fiが不安定になりやすい環境や、話し声などの雑音がある場所でも音(人には聞こえない周波数)が届くことだ。会場では30メートル先でも超音波が届いていた。

2)バンド型デバイス x 2個

超音波はバンド型デバイス上のシリコンマイクで受信される。
マイクで拾われた信号はデコードモジュール(ソフトウェア)で文字データに復号され、共有メモリに復号結果が保存される。
Edison 上のArduinoスケッチがそのデータにアクセスし、そして以下を制御している:
・モード管理(ロゴ表示/歌詞表示/音声認識 )
・各モードに応じたOLED描画プログラム
これらは、通常のLinuxベースの Yocto OS バージョン上で作動している。

3)表示デバイス x 50個

独自のオリジナルボードに載った Edison で作動しているArduinoスケッチが、後述のマスター Edison から入ってくるデータを処理し、適切なアニメーションをGPOを介してOLEDディスプレイに表示している。

RS485のマルチドロップ接続で繋がることで、50個すべての表示デバイスが同時に制御される。50個の Edison は個別のシリアル番号を持っているため、すべて同じアニメーションを同期し、ディスプレイに表示することもできるし、シリアル番号に従って個別に異なる表示をすることもできる。また事前に決められたパターン(画像や文字)だけでなく、マイクに話された言葉をライブで文字として表示することも可能だ。

4)開発用Edison(受信マスター)

50個の Edison を素早く効率的にプログラムするために、aircordチームはIntel® Edison キット For Arduino で開発用 Edisonを1台用意し、マスターの Edison のプログラムに変更があれば、50個の Edison に同期される仕組みにした。

マスターにはlsyncdが使われており、ローカル・ディレクトリ・ツリーを監視している「監視デーモン」がファイルに変更を検知した際、一定時間待った後、他の50のデバイスへ変更を同期する。

50個の Edison ではファイルの更新監視デーモンとスケッチの自動再起動プロセスを走らせていて、マスターに変更があれば、ほぼ即座にその他のデバイスすべてに反映される。

このマスターデバイスは、バンド型デバイスと同様の要素を持ち、さらに50個の表示デバイスへ超音波信号からのデータをRS485経由でスルーアウトする受信マスターの役割も担っている。

材料

音声認識 / 超音波送信デバイス x 1個

バンド型デバイス x 2個

表示デバイス x 50個

開発機用edison(受信マスター) x 1個

クラウドサービス

  • Google Web Speech API

ソースコード

プログラムとして複雑になってしまう部分を抜いて、おおまかにまとめた擬似コードをご紹介。

Learn More on GitHub

[AutoRestartフォルダ]
Arduinoスケッチファイルが更新されると、自動的にスケッチを再起動するプログラム

autoRestart.sh
スケッチファイルの更新監視プログラムをデーモンとして起動するためのスクリプト
                #!/bin/sh
sketchFile="/home/root/Sketch/sketch.elf"
sketchCopy='systemctl stop clloader ; cp /home/root/Sketch/sketch.elf /sketch/sketch.elf ; systemctl start clloader'

INTERVAL=1 # watch interval
last=`ls -e $sketchFile | awk '{print $8"-"$9}'`
while true; do
        sleep $INTERVAL
        current=`ls -e $sketchFile | awk '{print $8"-"$9}'`
        if [ "$last" != "$now" ]; then
                echo "updated: $now"
                last=$now
                eval $sketchCopy
        fi
done
                
              
AutoSketchReLoad.sh
Arduinoのスケッチファイル更新を監視するプログラム
ネットワーク経由でホームフォルダにsyncされるファイルを監視し、
更新があったら実行用フォルダにコピーしてスケッチを再起動する。
                #!/bin/bash
#
# AutoSketchReload: Starts the AutoSketchReload Daemon
#
# chkconfig: 345 99 02
# description: Auto Reload for Arduino Sketch which updated by lsyncd
#              over a network.
# processname: AutoSketchReload

. /etc/init.d/functions

autorestart="/home/root/intel_edison/AutoRestart/autoRestart.sh"
pidfile="/var/run/autorestart.pid"
prog="autoRestart.sh"
RETVAL=0

start() {
        pid=`/bin/pidof autoRestart.sh`
        if [ $? -eq 0 ] && [ ! -z "$pid" ]; then
        echo "autoRestart.sh (pid $pid) is already running..."
        exit
        fi

        eval $autorestart

        pid=`/bin/pidof autoRestart.sh`
        if [ $? -eq 0 ] && [ ! -z "$pid" ]; then
        echo "Starting autoRestaert.sh (pid $pid)"
        else
        echo "Error autoRestart is not running"
        fi
}

stop()
{
        /bin/kill -9 `/bin/pidof autoRestart.sh`
        until [ -z "$(/bin/pidof autoRestart.sh)" ]; do :; done
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        stop
        start
        ;;
    status)
        pid=`/bin/pidof autoRestart.sh`
        if [ $? -eq 0 ] && [ ! -z "$pid" ]; then
        echo "autoRestart.sh (pid $pid) is running..."
        else
        echo "autoRestart.sh is not running"
        fi
        ;;
    *)
        echo "Usage: AutoSketchReLoad.sh {start|stop|restart|status}"
        exit 1
esac


exit $?
                
              

[wordsbandSketchフォルダ]

wordsbandSketch.ino
メインのArduinoスケッチ
                #include 
#include 
#include 
#include 
#include "OLED.h"
#include "systemFunctions.h"
#include "logo.h"

using namespace std;

// ------------------------------------------------------------------------------
// using shared memory map for communication for processes
#include "mmap.h"
struct mmapData* p_mmapData;

// ------------------------------------------------------------------------------
// MODE
enum {
        MODE_STOP = 0,
        MODE_MUSIC,
        MODE_VOICE,
        MODE_INIT
};
const char mode_chars[3] = {';', '*', ':'};
int getMode(char c) {
  switch(c){
  case mode_chars[0]: return MODE_STOP;
  case mode_chars[1]: return MODE_MUSIC;
  case mode_chars[2]: return MODE_VOICE;
  }
};
int mode = 0;

// ------------------------------------------------------------------------------
// data from ultrasonic
#define MYBUFSZ 1024
char mmapStr[MYBUFSZ+1] = {'\0'};
char ResultStr[MYBUFSZ+1] = {'\0'};
int mmapStrArrow = 0;

// ------------------------------------------------------------------------------
// Misc
int posX = 0;
int preposX = 0;
int posY = 0;
int preposY = 0;

// for logo
float stop_x = -104;

// for voice recognition
std::queue strQueue;

// ------------------------------------------------------------------------------
void setup() 
{
  mode = MODE_INIT;

  setupMemoryMap();

  Serial.begin(9600);
    
  // init OLED
  OLED_init();

  // display Host Name
  OLED_locate(10,10);
  OLED_write(getHostname().c_str());

  // display Network SSID info
  OLED_locate(10,25);
  OLED_write(getSSID().c_str()); // SSID
  OLED_locate(3,25);

  // display Network IP info
  OLED_locate(10,40);
  OLED_write(getIpAddress().c_str()); // IP
  OLED_locate(3,40);

  // wait
  delay(3000); 
  
  // clear OLED
  OLED_background( 0x0000 );
  OLED_cls();

  {
    // check process
    if(isRunning("decoder")) { Serial.println("killall decoder"); system("killall decoder"); }
    delay(1000);
    if(isRunning("mmap/mmap")) { Serial.println("killall mmap"); system("killall mmap"); }
    delay(1000); 
    
    // kick C programs
    system("/home/root/ultrasonic/decoder plughw:2,0 | /home/root/mmap/mmap &");
  }

} // setup

// ------------------------------------------------------------------------------
int CheckMode()
{
  int m;
  
  if (pthread_mutex_lock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_lock");
  
  m = getMode(p_mmapData->mode);
  
  if (pthread_mutex_unlock(&(p_mmapData->mutex)) != 0) exitError("pthread_mutex_unlock");
 
  return m;
}

// ------------------------------------------------------------------------------
void loop() 
{
  OLED_locate(0,0);

  mode = CheckMode();

  switch(mode)
  {
    case MODE_STOP:
    {
      // display Logo (marquee left to right)
      {
        static int stop_count = 100;
        if(stop_x <= 8.99 && stop_x >= 8.0 && stop_count >= 0)
        {
          stop_count --;
          if(stop_count < 0)
          { 
            stop_count = 100;
            stop_x += 0.5; 
          }
        }
        else
        {
          stop_x += 0.5;
        }
        if(stop_x > 128) stop_x = -104;
        int stop_y = 60;

        // set OLED color
        OLED_foreground(0x0415);
        
        // LOGO data transfer 
        OLED_bitblit(stop_x, stop_y, logoWidth, logoHeight, logoData);
        OLED_bitblit(stop_x-230, stop_y, logoWidth, logoHeight, logoData);

      }

      break;
    }
    case MODE_MUSIC:
    {
      DisplayLyrics();
      
      break;
    }
    case MODE_VOICE:
    {
      DisplayVoiceRecog();

      break;
    }
    
  } // switch(mode)

} // loop()
                
              
logo.h
aircordロゴ表示用データ
                //////////////////////////
// Aircord Logo Data

#define logoWidth 112
#define logoHeight 15

const unsigned char logoData [] = {
0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
0x00, 0x00, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
0x00, 0x00, 0x00, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78,
0x03, 0xFF, 0xFE, 0xF1, 0xFF, 0xFD, 0xFF, 0xFD, 0xFF, 0xF0, 0xFF, 0xFE, 0xFF, 0xF8,
0x07, 0xFF, 0xFE, 0xF3, 0xFF, 0xFB, 0xFF, 0xFB, 0xFF, 0xF1, 0xFF, 0xFD, 0xFF, 0xF8,
0x0F, 0xFF, 0xFC, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF3, 0xFF, 0xFB, 0xFF, 0xF8,
0x1F, 0xFF, 0xF8, 0xF7, 0xFF, 0xEF, 0xFF, 0xE7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF8,
0x1E, 0x01, 0xE0, 0xF7, 0x80, 0x0F, 0x00, 0x0F, 0x00, 0xF7, 0x80, 0x07, 0x80, 0x78,
0x1E, 0x01, 0xE0, 0xF7, 0x80, 0x0F, 0x00, 0x0F, 0x00, 0xF7, 0x80, 0x07, 0x80, 0x78,
0x1E, 0x01, 0xE0, 0xF7, 0x80, 0x0F, 0x00, 0x0F, 0x00, 0xF7, 0x80, 0x07, 0x80, 0x78,
0x1F, 0xFF, 0xE0, 0xF7, 0x80, 0x0F, 0xFF, 0xCF, 0xFF, 0xF7, 0x80, 0x07, 0xFF, 0xF8,
0x1F, 0xFF, 0xE0, 0xF7, 0x80, 0x0F, 0xFF, 0xCF, 0xFF, 0xF7, 0x80, 0x07, 0xFF, 0xF8,
0x1F, 0xFF, 0xC0, 0xF7, 0x80, 0x0F, 0xFF, 0x8F, 0xFF, 0xE7, 0x80, 0x07, 0xFF, 0xF8,
0x1F, 0xFF, 0x80, 0xF7, 0x80, 0x0F, 0xFF, 0x0F, 0xFF, 0xC7, 0x80, 0x07, 0xFF, 0xE0,
};
                
              
systemFunctions.h
Linuxシステムから情報を得る関数群
                #include 
#include 
#include 

bool isRunning(std::string processName)
{
  FILE *fid;
  std::string ipCommand = "ps | grep " + processName + " | grep -v grep";
  char commandres[80] = {0};
  
  if((fid = popen (ipCommand.c_str(), "r")) == NULL)
  {
    Serial.write("error.");
  }
  
  std::string ret;
  while(fgets(commandres, 80, fid) != NULL) 
  {
    pclose(fid);
    ret = std::string(commandres);
  }
  
  pclose(fid);

  if(ret.length() != 0)
    return true;
  else
    return false;

}

std::string getHostname()
{
  FILE *fid;
  std::string ipCommand = "cat /etc/hostname";
  char commandres[80] = {0};
  
  if((fid = popen (ipCommand.c_str(), "r")) == NULL)
  {
    Serial.write("error.");
    return "";
  }
  
  std::string ret;
  while(fgets(commandres, 80, fid) != NULL) 
  {

    std::string str(commandres);
    if (str[str.size()-1] == '\n') str.erase(str.size()-1);
    if (str[str.size()-1] == '\r') str.erase(str.size()-1);

    ret = str;
  }
  pclose(fid);

  return ret;
}

std::string getIpAddress()
{
  FILE *fid;
  std::string ipCommand = "ifconfig wlan0 | grep inet | sed 's/.*inet[^6][^0-9]*\\([0-9.]*\\)[^0-9]*.*/\\1/'";
  char commandres[80] = {0};
  
  if((fid = popen (ipCommand.c_str(), "r")) == NULL)
  {
    Serial.write("error.");
    return "";
  }
  std::string ret;
  while(fgets(commandres, 80, fid) != NULL) 
  {
    std::string str(commandres);
    if (str[str.size()-1] == '\n') str.erase(str.size()-1);
    if (str[str.size()-1] == '\r') str.erase(str.size()-1);
    ret = str;
  }
  
  pclose(fid);
  return ret;
} // getIpAddress

std::string getSSID()
{
  FILE *fid;
  std::string ssidCommand = "iwconfig wlan0 | grep ESSID: | sed 's/.*ESSID:\"\\(.*\\)\"/\\1/'";
  char commandres[80] = {0};
  
  if((fid = popen (ssidCommand.c_str(), "r")) == NULL)
  {
    Serial.write("error.");
    return "";
  }
  
  std::string ret;
  while(fgets(commandres, 80, fid) != NULL)
  {
    ret = std::string(commandres);
  }
  
  pclose(fid);
  return  ret;
}
                
              

「例えばライブ会場で、ファンが装着したこのデバイスにミュージシャンが発した言葉が一斉に表示されたなら、
これまでにない一体感につながるはず」(橋本)
「音声認識や各種プログラム、ハードウェアとのつなぎ込みなど……
社内のさまざまなスキルを連携させる上でも、とても面白いプロジェクトになりました」(赤川)
「CNCミリングマシンで外装まで削り出してみて、
『こういう加工をみんながやるようになれば、本当に時代が変わる』と実感しました」(岩崎)
(※いずれもこれまでの『Artist Showcase』密着記事より)
これまでも音と光を自在に操り、未来的なインタラクション表現を打ち出してきた実力と、
Intel® Edisonというテクノロジーが出会うことで実現した、“体験を分かち合うための新しいカタチ”。
このEdison搭載デバイスを数千、いや数万人の大観衆が装着し、大いなる共感に包まれるーー
そんな歴史的な1日を目指して、彼らの挑戦は続いていく……!

8月1日、MFT 2015内 Intel『Artist Showcase』会場にて

ARTIST PROFILE

aircord

映像や光、テクノロジーを用いて開発を行うクリエイティブチーム。インタラクティブ、デバイス、ロボティクスなどのデザインや企画、開発などを主軸に、ハードとソフト双方向からのアプローチで、新しい可能性に積極的にチャレンジしている。受賞歴は、Cannes Lionsゴールド、D&ADイエロー・ペンシル、文化庁メディア芸術祭審査委員会推薦作品など。

http://www.aircord.co.jp

クレジット

プロデュース/クリエイティブディレクション : Toshiyuki Hashimoto 橋本 俊行
筐体デザイン : Seiya Nakano 中野 誠也
システム設計/ソフトウェア開発 : Tomohiro Akagawa 赤川 智洋
回路設計/基板設計制作 : Osamu Iwasak i 岩崎 修
超音波通信開発 : Shin Aoyama 青山 新
筐体設計/制作 : Maki Hitomi 人見麻紀
OLEDモーショングラフィック開発 : Hisaki Ito 伊藤 久記
展示台デザイン制作 : Masahito Uchino 内野 真仁