にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

マイクモジュールをArduinoに繋ぎPCでProcessingを使って録音する

Arduinoに乗っけて音に反応する何かをつくれないかと、「エレクトレットマイクアンプモジュール ADA-1063」を買った。
www.switch-science.com
これは、

使い方はシンプルで、電源を与えるだけ。増幅された信号が、VCCの半分でバイアスされてOUTピンから出てきます。

だそうである。


とりあえず、マイクモジュールの動作確認を兼ねて、出力をArduino経由でPCに転送し録音してみた。

概略

  • Arduinoでマイクモジュールの出力を読み取り、シリアル通信で送信する。
    • 簡単のため、シリアル通信で送受信するデータを1バイト(0~255)の範囲とした。
  • PCで受信側ではProcessingを使うことにした。
  • 受信したデータを8bitモノラルのWAVEファイルとして書き出す。

使ったもの

ハード

  • Arduino Uno R3
  • エレクトレットマイクアンプモジュール ADA-1063

ソフト

接続

Arduino Unoとの接続はこんな感じ。
f:id:nixeneko:20160914050500j:plain

  • 5V ↔ Vcc
  • GND ↔ GND
  • A0 ↔ OUT

として結んでいる。出力はAnalog InのA0ピンとした。

素人考えで直結しただけだけどこんなんでいいのだろうか。

ソースコード

Arduino

#define BAUD 57600 //シリアル通信の転送速度。受信側と合わせる
#define PIN 0      //使用するアナログ入力ピンの指定 A0…0.
int d;

void setup() {
  // put your setup code here, to run once:
  pinMode(PIN, INPUT);
  Serial.begin(BAUD);
}

void loop() {
  // put your main code here, to run repeatedly:
  d = analogRead(PIN);
  Serial.write(d>>2);
  //delay(100);
}

analogRead関数では0~1023のデータが返るっぽいので、4で割って8bitに収まる様にしてシリアル出力に書き出している。
マイクモジュールのVccに5Vを与えているので、無音なら2.5Vとなるようである。アナログ入力が0~5Vの範囲だろうから、ちょうど真ん中になる。8bit WAVEファイルは符号なし整数0~255で無音が128と真ん中にくるので、読み取ったデータを8bitにマップしてWaveファイルにそのまま突っ込めばいい。

Processing

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 / ((time_end - time_start) / 1000));
  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();
    }
  }
}

シリアル通信で受信したデータを配列に貯め、予め決めたサンプル数だけ貯まったらWAVEファイルとして書き出すことをしている。

WAVEファイルのヘッダを書き込む部分が大部分である。泥臭い。ヘッダについては次のページを参考にした。

Processingにはunsignedなintやbyteがないので、多少ややこしくなっている。

録音例

mp3にエンコードしています。

1.1倍速で再生してちょうどよいくらいの速度っぽい。プログラムのボーレート変えると再生速度も微妙に変化するので、シリアル通信回りは少なくともある程度影響してるのだろうが…。あるいは、サンプリング周波数の計算や送受信するデータの方が悪いのかもしれない…?

今回はとりあえずマイクが動いてるのを確認できたのでよしとする。

(2016-09-15追記)
サンプリング周波数の計算のために時間を計測しているが、これが実際より1.5秒程度長くなっているのが原因だったらしい。
具体的には開始時間の計測に難があった。

Processingの開始時には1.5秒ほどシリアル通信が何も受信しない期間があるようなので、これを含めないように時間の取得をしないといけない。

draw()が最初に呼ばれたときにはserial.available()が200~300あたりになっているが、これがその後1.5秒ほど変化しない(新しいデータが来ない?)っぽいので、安定してデータが受信できるようになるまで待ってから時間の計測とデータの取得をするといいようだ。

改変したprocessingコードが次である。

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;  //受信データ保存用配列

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

boolean outputflag;
boolean initflag;

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

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(){
  //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 / ((time_end - time_start) / 1000.0));
  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);
  println("start:", time_start, "end:", time_end, "samprate:", samprate);
  exit();
}

//シリアル通信で受信すると呼び出される
void serialEvent(Serial p){
  if(initflag && cnt > 1000){ //受信が安定した頃に一回だけ実行
    time_start = millis();  //開始時間の計測
    println("time!");
    initflag = false;
    cnt = 0;  //カウンタの初期化→今までの受信データは上書きして捨てられる
  }
  x = (byte) p.read(); //受信データ
  if (cnt % 100 == 1) println(cnt, x); //見づらいのでコンソール出力を間引いた
  if (cnt < ARYSIZE){
    data[cnt] = x;
    cnt++;
  }else{
    if (outputflag) { //複数回実行されないように
      time_end = millis();
      outputflag = false;
      writeWav();
    }
  }
}

ここでは最初に受信したデータの累積数が1000個(=カウンタが1000以上)になるまで待って、そこから開始時間の計測とデータの取得を開始している。これで大体等倍速が実現できた。めでたしめでたし。

別の方法

serialEventイベントハンドラで受信データを扱う以外にも、drawループの中で受信データを取得することもできる。
上のコードの void serialEvent(){ ~ } 部分を削除し、 void draw(){ } を次のように変更すると同じような結果が得られる。

void draw(){
  if(initflag && (cnt > 1000)){
    cnt = 0;
    time_start = millis();
    initflag = false;
    println("time!");
  }
  while(myPort.available() > 0){
    x = (byte) myPort.read();
        
    if (cnt < ARYSIZE){
      data[cnt] = x;
      cnt++;
    }else{
      if (outputflag) {
        time_end = millis();
        println("time!");
        outputflag = false;
        writeWav();
      }
    }
  }
  println(cnt, x);
}

(追記終)

感想

ノイズがでかい。かなりマイクに近づけて録音してもこのようにノイズが大きめに聞こえるので、このままでは実用は難しいかもしれない。

このページによると同じモジュールではないが電源切り離し等すればノイズが減ったとのことである。今手元にちょうどいい電源がないのでそのうち検証したい。

(2016-09-16追記)
マイクモジュールにつなぐ電源を、単4電池4本直列→三端子レギュレータで5Vに降圧したものに切り替えたところ、ノイズが気にならない位になった。この場合、レギュレータの出力の5VをArduinoのAREFと接続する必要があるようだ。

(2017-02-17追記)

Using it is simple: connect GND to ground, VCC to 2.4-5VDC. For the best performance, use the "quietest" supply available (on an Arduino, this would be the 3.3V supply).

https://www.adafruit.com/products/1063

……。
はい、Arduinoの3.3V端子から電源供給したら見事にノイズが少なくなりました。ノイズに敏感なものを扱う際にはArduinoの3.3Vを使いましょう、5Vではなく……。