読者です 読者をやめる 読者になる 読者になる

よいちろ日記

忘れないようにメモ。

ArduinoでBluetoothマイク作ろうとした。

先日「Baby」の記事をみて、音声コミュニケーションだったり音声認識サービスの普及のボトルネックになるのはハードウェアなんじゃないかなと思って、小型のBluetoothマイクって作れるのかなと思って実験してみた。結論から言うと、「オリジナルで作る手間を考えたら既製品で良い。というか既製品すごい。」だった。

音声データをハード側で加工せずに単純にモバイル端末に送りたいだけならば、自前でハードウェアを用意するメリットも大してなさそうという所感。電池の持ちをよくしたいとか、ハード操作起因でなにかしたいとかってなったら必要だけど。 

プロファイルについて。

今回関係しそうなプロファイルは3つ。それぞれ実装上のメリット・デメリットを書いてみる。各プロファイルの詳細はこちら。
Bluetoothプロファイルの一覧 - Wikipedia

GATT

■ メリット
こいつはiOSのCorebluetoothで制御できるのでGATTプロファイルを使って音声データのやり取りができるならばそれが一番手っ取り早い。手持ちのLightBlue Beanもこのプロファイルにしか対応していない。
Bean - Punch Through

■ デメリット
元々の用途が低頻度、長期間のデータ取得なので、音声という高頻度でデータをやり取りしたいものには向いていなさそう。(案の定向いていないらしい。後述。)

SPP

■ メリット
通信速度がある程度の範囲ならbaudrateで設定でき、音声データやりとりにも耐えられそう。デバッグが楽。(シリアル通信モニタ)
■ デメリット
バイルで実装することを考えると、Android一択。(iOSはMFi審査をパスしないと、SPPできるライブラリを提供してもらえないっぽい。)

HFP

■ メリット
音声通信用のプロファイルなのでやりたいことはすべてできそう。
■ デメリット
クラシックBluetoothなので、電池持ちとかは気にしていられない。クラシックBluetoothが使えるBluetoothモジュールがあまりない。ここに手をだすなら(学習コスト的な意味で)市販のBluetoothヘッドセットで間に合わせるというのも検討。

Bluetoothモジュールの選択肢。

すでに持っていたもの、手に入りやすそうで実装も困らなそうなの(記事があるかとか重要)で3つピックアップ。

LightBlue Bean

扱いが慣れているし、楽。GATTプロファイルしかサポートしていない。小型なのでマイクをつけるだけで良さそうなのも良い。

BluetoothモジュールRBT-001

これももってた。SPPに対応している。あとから知るのだが、baudrate=9,600以上にしたい場合はちょっと面倒。

つなぎ方はこれを参考に。
Bluetooth通信する(Bluetoothモジュール使用)

自分のEvernoteには魚拓があったけど、「Arduinoミニ四駆でラジコンカーを作ろうとしています -Part2- Bluetooth編」ってのがシンプルでよかったのに。

RN52

もってないけど、HFP対応している。「トランジスタ技術 2014年 2月号」でBluetoothのMP3プレイヤー作る際に用いられていたのでなんとかなりそう。
MicroChip RN-52 Bluetooth モジュールを使ったオーディオ再生 | RVF/RC45 blog
TANZVOLK – WIRE IS DEAD!!! Bluetoothオーディオレシーバーを作る -The Making of Bluetooth Audio Receiver- part3
学研付録「真空管アンプ」を改造する(5) ―BluetoohモジュールでiPhoneから受信する― | にゃんきちの日曜DIY

どのOSで実装するか。

最終的には、Bluetooth通信でモバイル端末とつなぎたい。iOSで実装できると一番良い。(Corebluetoothは使い方わかるので。)二番手にAndroidデバッグ用にPC(Mac)で開発というか実験という感じ。なのでGATTプロファイルでどこまで通信速度出せるかが鍵になる。(結果、出せない。)

baudrateについて。

今回は音声をほぼリアルタイムで取得したいという前提でいじっていく。waveファイルの生データであるマイクの電圧の値をいかに狭い間隔(サンプリング周波数)で取得するかが音質に関わってくるはず。シンプルにシリアル通信でSerial.printする方針に。となるとbaudrateが高いほうがマイクのデータを逃さず取得できるはず。(1loopにつき1数値取得される。loopで取得できる単位時間あたりデータ量 >> baudrateで指定できる単位時間あたりデータ転送量、という前提で考えている。)このbaudrateの設定をどのくらい低い値まで許容できるかは後ほど実験。音声取得をリアルタイムにこだわらない場合、ArduinoにSDカードつけて、そこに送っておくとかもあるけれど、毎loopごとにSDカードへの書き込み処理してたんじゃだめだし、結局ArduinoのRAM増やすしかなくて(配列にもデータ持たせておく)、SDカードぶっ刺してRAM拡張はそれだけで大変そうだったので見送り。

ボーレート(baudrate)と転送速度(bps) | KEI SAKAKI's PAGE.

「シリアル・ポートに限れば、1回に1ビット転送するので必ずしも誤りではありませんが、概念的に単位を bps とするのであれば、それをボーレートとするのは不適切なのです」

今回はシリアルポートに限っているので、baudrate=bpsとしてすすめる。

実験してみた。

手元にある部品で試せる部分をまず試してみることにした。SPP、GATTでPC(Mac)と通信するところからはじめてみる。(はじめてみるといっても、これだけ試してHPFでやりとりするの必須だなという結論にはなるのだが。)

Arduino(RBT-001)でECM情報取得→(SPP、有線)→PC(Mac)で受ける→Processingでwaveファイルへ

ここを参考にして全く同じことをトレースしてみた。マイクは秋月電子で買ったこちらを使用。

アンプ:OPA344
マイク:C9767(P-01810)

仕様詳細についてはこちらを参考に。
Arduino - SparkFun マイク・モジュール: SatE-O

だいたいbaudrate=115,200くらいからまあ聞けるレベルの音声データ(waveファイル)が取れる。それ以下になるとくぐもったような音になる。baudrate=9,600だと音楽のベースが「ぼわーんぼわーん」と聞こえるレベルで、何を言っているかまでは聞き取れない。音楽のビートが判断できるかなくらい。

次に、上記の無線版。

ArduinoECM情報取得→(SPP、無線=Bluetooth)→PC(Mac)で受ける→Processingでwaveファイルへ

これは単にBluetooth機器としてMac(システム環境設定→Bluetooth)で検出したArduino(RBT-001)に繋げば良い。

baudrate=115,200だと、そもそもRBT-001が対応していない。変更も面倒。単なる数字を順繰りにおくることすらうまく行かなかったのでそうっぽい。あと、同じサンプル数を受け取るのに、有線シリアル通信だと20秒くらいなものが、無線Bluetoothシリアル通信だと5分くらいかかった。baudrate=9,600だともはや音にならない。どっちにしろiphoneBluetoothのSPP扱うにはMFi必要って言う。
iOS のBluetooth対応 - サーリューション日記
bluetoothのspp接続(Appleコミュニティ)

Arduino iphone Bluetooth シリアル通信」でヒットした記事に、一瞬光が見えたけど、ソースコードみたらCorebluetooth(GATT)ガッツリ使ってた。

ちょっとハマったのが、「マイクモジュールをArduinoに繋ぎPCでProcessingを使って録音する」のサンプルコードだと、waveファイルのサンプリングレートを処理の最初と最後の時間差から計算しているけど、Bluetoothでデータ送る場合、受け取り側の時間を見ると間延びしてしまう。実データとしては5秒分のものを1分くらいかけて送っているためである。なのでこの場合は、サンプリングレートを算出するサンプリング時間をハード側から取得しないと行けない。(実際には、有線接続時につくったwaveファイルをFinderでみるとサンプリングレートがでてくるのでそれを決め打ちして入れていた。)また、GATTに関しては「安定して出た通信速度は 5kbps 程度」ということで、baudrate=5,000。ここに様々な処理が加わって更に遅くなるとしたら全然お話しにならなそう。GATTで音声データをリアルタイムに扱うという方針は分が悪そうなので保留。

そもそもの動機から、無線にしないといけない。かつ、その先は雑音処理だったり、小型にする必要があったりする。でHFPプロファイルをアプリで扱うにはとか、回路設計しなきゃ…とか考えた末、市販のBluetoothヘッドセットでいいかという元も子もない結論になったとさ。
これとかすごいよ。こんなにちっこくて。

他にメモ。

回路とコード

RBT-001の接続方法とサンプルコードを記載。

f:id:yoichiro0903:20161211000157p:plain
Arduinoの5VとGNDをRBT-001の5VとGNDに。RXとTXはお互い逆につなぐ。

疎通確認用のsketchはこれ。sketch書き込み時にRXとTX繋いでるとエラーになることがあるので注意。データ受信確認用なので、送信確認にしたいときはSerial.readでStringを読み取り、Lチカすればいい。

//RBT-001動作確認用
#include <SoftwareSerial.h>
#define BT_TX 11
#define BT_RX 10
 
SoftwareSerial btSerial(BT_RX, BT_TX);
int count = 0;
 
void setup(){
  btSerial.begin(9600);
}
 
void loop(){
  btSerial.write(count);
  count++;
}
最終的にマイクも含めた接続とサンプルコードを記載。

f:id:yoichiro0903:20161211000306p:plain
OPA344のGND、VCCはArduinoのGND、5Vへ接続。OPA344のOUTをArduinoのanalogピンへ接続。

 
//RBT-001でOPA344のデータを送信。Arrayにためてから送る方式もコメント。
#include <SoftwareSerial.h>
#define BAUD 9600
#define BT_TX 11
#define BT_RX 10
#define PIN 0
//#define ARRAY_SIZE 800
 
SoftwareSerial btSerial(BT_RX, BT_TX);
int data;
//int data_array[ARRAY_SIZE];
//int count;
 
void setup(){
  btSerial.begin(BAUD);
  pinMode(PIN, INPUT);
}
 
void loop(){
    data = analogRead(PIN);
    btSerial.write(data >> 2);
 
  /*
  if (count < ARRAY_SIZE){
    data_array[count] = analogRead(PIN);
    digitalWrite(13,HIGH);
    count++;
  }
 
  if (count >= ARRAY_SIZE){
    digitalWrite(13,LOW);
    for (int i=0; i <= ARRAY_SIZE; i++){
      btSerial.write(data_array[count] >> 2); 
      i++;
    }
  count = 0;
  }
  */
}
Processingでwaveファイル作るコード。

例によって疎通確認用コード。Arduino側のbaudrateとProcessing側のbaudrateはそろえないといけない。つなぐシリアルポートはMacとRBT-001を繋いだときにArduinoIDEなどで調べておく。

import processing.serial.*;

final int BAUD = 9600;
final String SERIALPORT = "/dev/cu.EasyBT-COM1”; //Bluetooth
//final String SERIALPORT = "/dev/cu.usbmodem1421”; //Wired

int y;

Serial myPort;

void setup(){
  myPort = new Serial(this, SERIALPORT, BAUD);
}

void draw(){
  background(0);
  text(y,50,50);
}

void serialEvent(Serial p){
  y = p.read(); //debug
  //print("count", y); //debug on console
}
import processing.serial.*;
 
final int BAUD = 9600;
final String SERIALPORT = "/dev/cu.EasyBT-COM1”; //Bluetooth
//final String SERIALPORT = "/dev/cu.usbmodem1421”; //Wired
 
int y;
 
Serial myPort; 
 
void setup(){
  myPort = new Serial(this, SERIALPORT, BAUD);
}
 
void draw(){ 
  background(0);
  text(y,50,50); 
}
 
void serialEvent(Serial p){
  y = p.read(); //debug
  //print("count", y); //debug on console
}

 

wave変換のコードはこちらのサイトを参考にさせていただきました。

以下転載です。

import processing.serial.*;

final int BAUD = 57600;            //シリアル通信の転送速度。送信側と合わせる
final int ARYSIZE = 100000;        //サンプル数-44。100000で20秒弱になった
final String SERIALPORT = "COM3";  //シリアルポート。環境に合わせる
final String OUTFILE = "test.wav"; //出力WAVファイル名

Serial myPort;
byte x;
int cnt;
byte data;  //受信データ保存用配列

float time_start; //サンプリング周波数計算用
float time_end;  //同上

boolean outputflag;

void setup(){
  myPort = new Serial(this, SERIALPORT, BAUD);  //シリアルポート初期化
  data = new byte[ARYSIZE];  //受信データ保存用配列
  cnt = 0;                  //カウンタ初期化
  time_start = millis();
  outputflag = false;
}

void draw(){
 
}

//Waveヘッダ書込用。4バイト整数をbyte型4つ分書き出し
void set4bytes(byte
ary, int idx, int val){
  ary[idx  ] = (byte)(val % 256);
  ary[idx+1] = (byte)(val / 0x100 % 256);
  ary[idx+2] = (byte)(val / 0x10000 % 256);
  ary[idx+3] = (byte)(val / 0x1000000);
}

//Waveヘッダ書込用。2バイト整数をbyte型2つ分書き出し
void set2bytes(byte[] ary, int idx, int val){
  ary[idx  ] = (byte)(val % 256);
  ary[idx+1] = (byte)(val / 0x100 % 256);
}

//Waveファイルに書き出し
void writeWav(){
  time_end = millis();
 
  //WAVEヘッダの書き込み

  data[0] = 'R';
  data[1] = 'I';
  data[2] = 'F';
  data[3] = 'F';
 
  int fsizemin8 = ARYSIZE - 8;
  set4bytes(data, 4, fsizemin8);
 
  data[8] = 'W';
  data[9] = 'A';
  data[10] = 'V';
  data[11] = 'E';
 
  data[12] = 'f';
  data[13] = 'm';
  data[14] = 't';
  data[15] = ' ';
 
  int fmtsize = 16;
  set4bytes(data, 16, fmtsize);
 
  int fmtcode = 1;
  set2bytes(data, 20, fmtcode);
 
  int numch = 1;                //モノラル
  set2bytes(data, 22, numch);
 
  //サンプリング周波数
  int samprate = int(ARYSIZE / *1;
  set4bytes(data, 24, samprate);
 
  int bytepersec = samprate;    //秒あたりバイト数
  set4bytes(data, 28, bytepersec);
 
  int blockboundary = 1;
  set2bytes(data, 32, blockboundary);
 
  int bitpersample = 8;        //8bit
  set2bytes(data, 34, bitpersample);
 
  data[36] = 'd';
  data[37] = 'a';
  data[38] = 't';
  data[39] = 'a';
 
  int sizeremained = ARYSIZE - 126;
  set4bytes(data, 40, sizeremained);
 
  //出力
  saveBytes(OUTFILE, data);
  exit();
}

//シリアル通信で受信すると呼び出される
void serialEvent(Serial p){
  x = (byte) p.read(); //受信データ
  println(cnt, x);
  if (cnt < ARYSIZE){
    data[cnt] = x;
    cnt++;
  }else{
    if (!outputflag) { //複数回実行されないように
      outputflag = true;
      writeWav();
    }
  }
}

 

*1:time_end - time_start) / 1000