公開日:2020/07/04
title.png

11th Season


#11-7 そうだ、ボイスレコーダを作ろう

 今回のテーマはボイスレコーダ🎤です。主に、SDカードI/F、サウンドセンサ(アナログマイク出力)を使います。
 結構、音声データを扱うテーマをこなして来た(↓)のだけど、今回はマイクで拾った音声データを長時間録音するアプリケーションとなります。

 昔、思い付きでポロっと書いたことがあったのだけど、今になって何となく、作って見ようと思い立ってしまいました。

基本設計

 それでは、基本設計から手を付けて行きます。

マイク

 まずは音声を拾うマイクですが、サウンドセンサ(下図)のアナログ出力を使います。

ar37_sound.jpg

 青いポテンシオメータを調整して、サウンド検出時に光る方のLEDが点くか点かないかギリギリの辺りに設定しておくと、バイアス点がVcc/2となります。マイク出力をオペアンプで増幅して使うので、バイアス点をVcc/2に合わせておかないと正しく増幅できません。

マイクアンプ

 マイクの出力はとても小さいので、オペアンプ(LM358、下図)を使って増幅回路を作成し、増幅してからArduinoのアナログ入力端子に接続します。

ar62_lm358.jpg

 今回は敢えて、反転増幅回路を使ってみます。後ほど詳細に説明します。なお、反転増幅回路を使ったマイクアンプはこちらで解説済みです。

 LM358をVcc=5V単一電源で使用する場合、入出力電圧の範囲は0〜Vcc−1.5Vです(LM358データシートより)。Vcc=5Vの場合、3.5Vくらいまでしか電圧が出力できないスペックです。実力でも4Vくらいが精いっぱいです。
 今回の様にバイアスをVcc/2=2.5Vで使う場合、VP-P=3Vが関の山なので、あんまり増幅率を上げ過ぎると音が歪みます。増幅率を決める際にはご注意ください。

SDカード

 音声データは結構なデータ量になるので、記録先はSDカードとします。筆者は自作のSDカードI/F(下図)を使いますが、市販のArduino用SDカードI/Fモジュールでも同じソフトウェアが使えるはずです(SDライブラリが使用可能なタイプならOK)。

ar65_sdsock.jpg

 SDカードの書き込み速度については、「#6-8 ATmegaのEEPROMを使ってみる」の中で計測済みです。6,399バイト/s程度でしたね。後で、サンプリングレートを検討する時に利いてくるので頭の片隅に入れておいてください。

 録音データの記録方式は、WindowsのWAVファイルとします。フォーマットをPCM/モノラル/8ビットにしておくと圧縮などの処理が不要で、測定したアナログ音声データをほぼそのまま書き込むだけで作成可能です。このフォーマットだと、1サンプル当たり1バイトのデータ書き込みが必要となります。

サンプリングレート

 サンプリングレートとは、マイクのアナログ音声データをArduinoのアナログ端子から取り込んで、SDカードに記録する頻度(周波数)を指します。例えば、一般的なCDのサンプリングレートは44.1kHzです。DVD等の音声データでは48kHz等のサンプリングレートが使用されることが多いです。
 基本的には、サンプリングレートが高いほど、高音質で記録できると言われています。

 一方、人間の耳で聞くことのできる音の周波数は20〜20k[Hz]と言われており、この範囲外の音は仮に記録されたとしても、耳で聞くことはできません。→ コウモリとかが出す超音波の周波数帯域など。
 この手のテーマを取り扱う時に度々登場する標本化定理と言うのが有って、この定理では「周波数f[Hz]の信号を再現するには、サンプリングレートが2×f[Hz]以上である必要がある」と述べられています。

 可聴域の上限を20kHzとして、その倍のサンプリングレート44.1kHz(+4.1kHzはマージン?)でサンプリングしたのがCDの規格で、「おや?もっとサンプリングレートを上げた方が良い音に聞こえるぞ。耳で聞こえないはずなのに変だな?」などと思いながらも、実際に良い音で聞こえる48kHzを採用したのが、DVD以降の規格みたいです。

※水が液体である温度を0〜100[℃]とした単位系などと異なり、可聴域が20〜20k[Hz]と言うのは、多くの人の平均的な値なので、「その外側は絶対聞こえない」と言う範囲ではありません。個人差によって高音が20kHzを超えて聴こえる人とかも居るかもしれないし、耳で音として聞こえていなくても空気の振動としては到達しているはずなので、20kHzを超える音波が人体に何らかの影響を与えている可能性も否定できません。

 本来であれば20kHz以上の音は耳では聴こえないはずなので、サンプリングレートを40kHz以上に上げることは、あまり意味を成しません。音声データとしては、48kHz辺りを上限と考えておけば良いという事です。

 「人の声の周波数範囲」で検索してみると、大体、男性100〜500[Hz]、女性200〜1,000[Hz]くらいの範囲で見つかります。多分、この周波数範囲は、歌などを歌う時の声域を含めた音域だと思います。∵女性の1,000Hzと言うのは、「オクターブ5のシ」辺りに相当し、話し声としては高すぎるので。
 「ボイス」レコーダと言うことで、今回のターゲットは「話し声」なので、男性100〜200[Hz]、女性200〜400[Hz]くらいに考えておけば良いみたいです。

 一番高い女性の周波数に少し余裕を持たせて500Hzなので、標本化定理に従うと500Hz×2倍=1,000Hz程度のサンプリングレートが確保できれば、ボイスレコーダとしては何とか使えるでしょう。
 今回使うWAVファイルのフォーマットでは1バイト/サンプルなので、サンプリングレート1,000Hzなら約1,000バイト/sの書き込み速度が有れば大丈夫です。SDカードの書き込み速度の測定結果を覚えていますか?約6,399バイト/sでしたので、破綻しません。
 SDカードへの書き込み処理だけなら6,000Hz程度まで上げられるという事ですが、それ以外の処理にも時間を取られるので、どこまでサンプリングレートを上げられるかが勝負です。

 今回のターゲットからは外れますが、音楽等の録音を目的としてより広い音域を記録したいと思った時、どこら辺までサンプリングレートを上げられるのか、Arduino処理能力の限界を実験で調べておきたいと思います(後ほど)。
 クロックアップした328duino等を使う場合にも、自動的にサンプリングレートを向上できるような仕組みを作り込んでおきたいと考えています。

 ファイルサイズ(記録時間)が許すなら、可能な限りサンプリングレートを上げておいた方が、人の声を変化させずに記録することが可能なので、サンプリングレート1,000Hzは「最低限」として、可能な限り上を目指したいと思います。

※昔の(今も?)固定電話では、帯域を300〜3.4k[Hz]に制限しているため、直接会って話す時と声が違って聴こえてしまいます。人の声の中心周波数が400Hzだったとしても、その周りの周波数にも音が含まれているため、3.4kHzで制限しただけでも声が変わって聞こえてしまうんですね。

詳細設計

 さらに詳細な仕様に落としていきます。完成プログラム内の変数名や定数名を交えて解説します。

 基本的には、タイマ割り込みを使って一定周期(サンプリングレート)毎に割り込みをかけ、割り込みサービスルーチン(ISR)内でAD変換器のデータ読み出しとサンプリングバッファ(vrc_buf[ ])への書き込みだけをひたすら繰り返します。
 録音関数内ではサンプリングバッファのデータを監視していて、書き込み可能なデータが見つかったら、その都度SDカードにデータを書き出します。

 サンプリングバッファはFIFOとして使うので、リングバッファを使って実現します。書き込みポインタvrc_wptrと読み出しポインタvrc_rptrを設定し、データが空の場合はvrc_wptr=vrc_rptrと定義します。
 ISR内でAD変換器から読み出したデータをvrc_buf[vrc_wptr]に書き込んだ後で、vrc_wptrをインクリメントします。もし、インクリメントした直後にvrc_wptr=vrc_rptrとなった場合には、FIFOの読み出しが間に合わず、書き込みポインタが読み出しポインタに周回差をつけてしまった → バッファオーバフロウ(ErrBufOvf)のエラーとなります。
 録音関数内でvrc_wptr≠vrc_rptrを検知した場合、vrc_buf[vrc_rptr]データをSDカードに書き出して、vrc_rptrをインクリメントします。
 録音中はずっと上処理の繰り返しです。その間、Serial.available()をモニタしていて、シリアルモニタから何らかのデータを受信した時点で、録音を停止する仕様とします。

 従って、シリアルモニタでの操作が前提となります。スイッチ回路などを取り付けて、独立動作させたい場合は各自対応してください。

割り込み周期(サンプリングレート)

 サンプリングレートを決定するタイマ割り込みには、TC2を使うことにしました。
 サンプリングレートは、CPUの周波数など処理能力によって変動するため、サンプリングレートに合わせてTC2のカウントクロックとカウンタ上限値(OCR2A)を適切に設定する必要があります。

 サンプリングレートfs[Hz]から、割り込み周期OCR2Aを計算するには:

  (OCR2A+1)×PStc2/fcpu=1/fs より、
  OCR2A=fcpu/PStc2/fs−1

 PStc2はTC2のカウントクロックをfcpu/PStc2にすることを表しています。タイマ(TC2)では、PStc2=1、8、32、64、128、256、1,024の中から選択して設定することが出来るため、以下の様にして最適値を計算します。

  // prescaler
  vrc_tc2ps=((F_CPU/1000000L)*itvl + 255)/256;
  if (vrc_tc2ps<=1)
    vrc_tc2ps=1;
  else if (vrc_tc2ps<=8)
    vrc_tc2ps=8;
  else if (vrc_tc2ps<=32)
    vrc_tc2ps=32;
  else if (vrc_tc2ps<=64)
    vrc_tc2ps=64;
  else if (vrc_tc2ps<=128)
    vrc_tc2ps=128;
  else if (vrc_tc2ps<=256)
    vrc_tc2ps=256;
  else if (vrc_tc2ps<=1024)
    vrc_tc2ps=1024;
  else
  {
    // ERROR:
    Serial.println (F("ERR: interrupt interval too long"));
    return (ErrIlleParam);
  }


※上コード中、変数itvl=1/(fs[MHz])です。

 TC2のレジスタ設定値をまとめておきます。

レジスタ名 アドレス 設定値 説明
TCCR2A 0x00b0 0x03 Fast PWM (OCR2A) mode,normal port
TCCR2B 0x00b1 0x08+PStc2 Fast PWM (OCR2A) mode,fcpu/PStc2
OCR2A 0x00b3 計算値を設定 カウンタ上限値(割り込み周期)
TIMSK2 0x0070 0x01 TOI2割り込み有効

AD変換器の設定

 ArduinoのanalogRead()関数では、AD変換器の変換時間は約104μsです。従ってサンプリングレートが1/(104μs)≒9,600Hzよりも低速なら、この設定のまま使用しても破綻することはありません。
 ただし、analogRead()関数ではAD変換の開始〜変換完了までの104μs間、処理がブロックされてしまい、他の処理を進めることが出来ず、サンプリングレートを低下させる要因となってしまいます。よって、「#10-1 サウンドレベルメータを作る」で作成したadc_start()関数とadc_read()関数をカスタマイズして使います。

  カスタマイズ内容:

 クロックアップ版328duinoなどでサンプリングレートを約9,600Hzを超えて設定できた時の為に、適切なAD変換クロックを設定できるようにしておきます。
 AD変換クロックはfcpu/PSadcであり、PSadc=2、4、8、16、32、64、128の中から選択可能です。デフォルトの変換時間約104μsの設定値は、一番遅いPSadc=128です。
 12ビットAD変換器の変換時間Tadcは以下式で求めることが出来ます。

  Tadc=13×(1/変換クロック周波数)=13/(fcpu/PSadc)

 サンプリングレートfs[Hz]から割り込み周期rs=1/(fs[Hz])が求まったら、rs>Tadcとなるプリスケーラ値PSadcを求めておくと、最適な変換クロックでAD変換を実施することが出来ます。

  // adc conversion clock
  itvl=int32 (ct + 1 - (vrc_isr + 1))*vrc_tc2ps/(F_CPU/1000000L);
  vrc_adcps=(itvl*3/4*(F_CPU/1000000L) + 12)/13;  // margin: *3/4
  if (vrc_adcps>=128)
    vrc_adcps=adc_FCPU_128;
  else if (vrc_adcps>=64)
    vrc_adcps=adc_FCPU_64;
  else if (vrc_adcps>=32)
    vrc_adcps=adc_FCPU_32;
  else if (vrc_adcps>=16)
    vrc_adcps=adc_FCPU_16;
  else if (vrc_adcps>=8)
    vrc_adcps=adc_FCPU_8;
  else if (vrc_adcps>=4)
    vrc_adcps=adc_FCPU_4;
  else if (vrc_adcps>=2)
    vrc_adcps=adc_FCPU_2;
  else
    vrc_adcps=adc_FCPU_2;
  if ((F_CPU/1000000L)>(1<<vrc_adcps))
  {
    // ERROR: adc conversion clock must be lower than 1MHz
    Serial.println (F("ERR: adc clock too fast"));
    return (ErrIlleParam);
  }


 ただし、fcpu/PSadc≦1MHzが12ビットAD変換器のスペックなので、割り込み周期がこれより短くなったらエラーとします。最後のif文が、そのエラー検出です。
 とは言え、少なくとも16MHzのUNOでは、ほとんどのケースでPSadc=128の一番遅いクロックが選択されるので、ここまでやる意味は無かったかも?です。

サンプリングレートの最適値を自動判定

 最後に残った課題が、サンプリングレートの自動判定です。
 16MHzのUNOに限定したら、「適切なサンプリングレートに決め打ち」で、終わりなのだけれども、クロックアップ版328duinoなどを使った場合には、クロック周波数に従って最適なサンプリングレートを自動計測したいので、その機能を作り込んでおきたいと思います。

 ただ問題は、記録フォーマットであるWAVファイルが、途中でサンプリングレートの変更には対応できないし、可変サンプリングレートみたいな記録方式も取れないので、録音開始時にサンプリングレートが確定している必要がある事です。

※従って、録音中アダプティブにサンプリングレートを自動調整することはできない。

 そこで、シリアルモニタからキャリブレーションコマンドを実行できるようにしておいて、このコマンドが実行された時に、16MHzのUNOで確実に記録可能なサンプリングレートに落として、1秒程度の間WAVファイルを記録してみます。この時、ISR内の処理時間と、SDカードへのデータ書き出しに掛かった時間を計測しておいて、そこから最適なサンプリングレートを自動計算することにしました。

 ISR内の処理はいつも必ずTCNT2=0の時に割り込みがかかっているので、ISRから復帰する時にTCNT2を読み出しておくと、ISR内の最大処理時間vrc_isrを知ることが出来ます(↓)。

  ISR (TIMER2_OVF_vect)
  {
    // TOI2 interrupt service routine
      :
      : (中略)
      :
    // load check
    if (vrc_isr<=TCNT2)
      vrc_isr=TCNT2;
    if (TIFR2&0x01)
    {
      // ERROR:
      vrc_err=ErrIntFast;
      TIFR2=0x01;
    }
  }


 最後のif文は、ISR内の処理が重すぎて、次の割り込みフラグが立ってしまった時にエラーフラグを立てる処理です。

 SDカードへのデータ書き込みに掛かる時間は、普通にmicros()関数値を参照して経過時間の総和usec_sdcを取っておきます(↓)。

  if (vrc_rptr!=vrc_wptr)
  {
    if (vrc_cali)
      usec_sdc=usec_sdc - micros ();  // ここから↓

    // write data
    fout.write (vrc_buf[vrc_rptr]);
    dsiz++;
      :
      : (中略)
      :
    // calibration
    if (vrc_cali)
    {
      usec_sdc=usec_sdc + micros ();  // ↑ここまでの時間を計測
      if (dsiz>vrc_cali_max)
        break;
    }
  }


 SDカードへの書き込みは書き込み単位毎に実行されるはずなので、上コードの様にバイト単位でwrite()メソッドを実行しても毎回SDカードに書き込むわけではありません。どこかで、データバッファが書き込み単位に到達したところで書き込むため、その時だけ一気に処理時間を取られることになるはずです。
 よって、1回1回の書き込み時間を計測するのではなく、全処理時間を総和しておいて、終了後に、書き込んだデータ数dsizで割り算すると、1サンプリングデータ当たりの処理時間を知ることが出来ます。キャリブレーション用の1秒程度のWAVファイル録音終了後、以下(↓)コードの様にして最適なサンプリング周波数を計算します。

  // samples per second
  usec_isr=(vrc_isr + 1)*vrc_tc2ps/(F_CPU/1000000L);  // TC2のカウント数→μs換算
  usec_sdc=(usec_sdc + dsiz - 1)/dsiz;  // SDカードへの書き込み時間[μs/バイト]
  vrc_sps=(1000000L*10/15)/(usec_isr + usec_sdc + 1);


 ここで、計測した処理時間に対して、1.5倍程度のマージンを見てサンプリングレートvrc_spsを計算しています。"*10/15"の所が、逆数になっていますが「1.5倍マージン」に相当しています。万一、マージン不足で、本番の録音時にエラーが発生するようなら、ここのマージンを×2倍マージンとか、大きい値に変更して見てください。

WAVファイル

 WAVファイルに関しては以下の記事で解説してありますので、そちらを参照して見てください。重複する説明は避けたいと思います。

 ここでは、WAVファイルの容量制限について述べておきます。上の記事を眺めてもらうとわかるのですが、ファイルを構成するチャンクと言う構造で、チャンクサイズを4バイトの整数(単位:バイト)で記録しているため、最大のサイズが2Gバイトに限定されています。

 今回の記録フォーマットでは、1バイト/1サンプルですので、仮に、サンプリングレートを10kHzに設定できたと仮定しても、1バイト×10kサンプル/s≒9.77Kバイト/sのデータ量です。2Gバイト=2×1,024Mバイト×1,024Kバイト×1,024バイトですので、連続して約60時間分の録音が可能です。
 さすがに、60時間の録音はテストしませんけど……(1^^;)、録音中WAVファイルのサイズをモニタしていて、vrc_MAX_WAVで指定したサイズを超えたところで、録音は自動的に停止するようにしておきます。

  const uint32 vrc_MAX_WAV=0x80000000UL - 1024;  // vrc: max wav size [byte]

 2Gバイトに対して、WAVファイルのヘッダ情報なども含む必要があるので、余裕を見て1,024バイトを減じています。

チャンクサイズの書き込み

 最終的なデータサイズは録音開始時にはわからず、録音を停止した時に初めてサイズが確定します。しかし、WAVファイルのヘッダ部分にデータサイズを書き込まなくてはならないため、録音開始時、ヘッダ情報を書き込む際にはダミーのデータを書き込んでおき、録音終了してサイズが確定したところでサイズ情報を改めて書き込む必要がありました。

 この時、SD.open()メソッドで普通にFILE_WRITEを指定してしまうと、FILE_WRITEの中に「データの追記」を意味するO_APPEND属性が含まれてしまうことで、write()メソッドが常にファイルの末尾に書き込みを実行してしまうようになっていました。
 そこで、WAVファイルをopen()する時に、以下の様にO_APPEND属性を除く様にします。

  // open file
  fout=SD.open (vrc_fnam, FILE_WRITE&(~O_APPEND));


 こうすることで、seek()メソッドとwrite()メソッドを使ってファイルポインタを巻き戻し、データを上書きできる様になります。具体的なコーディングは完成プログラムでご確認ください。
 でも、今回はここまで。完成プログラムと、回路の設計については次回に持ち越します。


 周波数としてはそれほど高くない音声信号なのですが、16MHzのArduino UNOで、かつ、ソフトウェアで処理しようとすると、中々の高負荷処理となります。録音音質を上げるためにはサンプリングレートを上げることが必要なのだけど、16MHzのMCUパワーでどこまでできるでしょうか?次回、実験で確かめてみます。


btn_arduino.png

btn_prev.png btn_next.png

home