公開日:2019/10/12
title.png

7th Season


#7-9 WAVファイルを再生する

 今回は、DA変換器を使って、SDカード上のWAVファイルを再生してみようと思います。Arduino UNO(ATmega328 @16MHz)の処理能力で、どこまでできるでしょうか??あんまり期待しないでご覧くださいm(1__)m。

DA変換器

 WAVファイルを再生するためには、何らかのDA変換器が必要です。広義には、「#5-8 MMLで音楽を奏でる」で使った、PWM+LPFも、一種のDA変換器だと見ることができます。今回は、そのものズバリの「DA変換器」を使って、WAVファイルを再生してみたいと思います。

 以下の様な、12ビットDA変換器モジュールを購入しました。

ar79_dac12.jpg

 今回はこれを使います。ご覧の通り、1.5cm□程度の小さな基板上に、MCP4725というDA変換ICが載っているようです(マークからは判断できず、シルクの文字より)。

  MCP4725のデータシートを検索する(別ウインドウが開きます)

※以下、MCP4725モジュールまたは、単にMCP4725と称します。

 ピンヘッダのシルクに書かれた信号名(SCL,SDA)からも明らかですが、データシートによると、I2Cで通信するタイプのDA変換ICみたいです。Standard(100kHz)、Fast(400kHz)、High-speed(3.4MHz)の転送モードに対応しています。
 MCP4725は、電圧範囲2.7V(MIN)〜5.5V(MAX)なので、3.3V、5V共、適用可能ですが、この後、オペアンプLM358を通そうと考えているので、DA変換器出力のフルスケールを3.3Vにしたいと思います。MCP4725は、デジタル・アナログ共通の電源端子しか持っていないので、デジタル信号もつられて3.3Vに落ちてしまいますが、止むを得ません(I2C通信にレベルシフタが必要)。

※MCP4725モジュールのピンヘッダには、GND端子が2本出ていて、「アナログとデジタルのGNDが分離されているのか?」と、思いきや、抵抗を測ってみると、どうも、ショートしている様です。そもそも、MCP4725にGND端子が1本しかないので、分離しようが無いのですよ……。

I2Cスレーブアドレス

 MCP4725のスレーブアドレスは、以下の様になっています。

スレーブアドレス(7ビット)
6 5 4 3 2 1 0
1 1 0 0 0 0 A0
固定 出荷時設定 端子

 「出荷時設定」の2ビットは、顧客が指定可能な出荷時設定で、データシートには「特に指定のない場合は"00"」と記載がありました。実際、手元のチップは、"00"でした。問題は、最下位ビットで、実はMCP4725の端子の1本にA0端子が割り当てられていて、この端子のロジックレベルで、スレーブアドレスが決定されます。

 前節の写真で、基板中央の黒い6ピンICがMCP4725です。その下に見えるはんだジャンパパタンで、A0端子のロジックレベルが決まります。写真で、中央と上側のはんだジャンパをショートするとA0=LOW、中央と下側のはんだジャンパをショートするとA0=HIGHとなります。
 写真ではわかり難いかもしれませんが、購入した状態では、はんだジャンパはショートされておらず、A0端子が浮いています。使用する前に、どちらかにはんだを盛って、A0端子のロジックレベルを確定させる必要があります。

※……こんな小さなはんだジャンパ、うまくショートできる自信がない〜。ちょっと緊張しましたが、A0=LOWに設定しました(GND側は、10kΩの抵抗を介してGNDに落ちているみたいです)。と、言うことで、このページでは、スレーブアドレス=0x60です。

I2C信号線のプルアップ抵抗

 写真には写っていませんが、基板の裏面に、I2C端子(SCL,SDA)用のプルアップジャンパ(らしきもの)が付いています。スレーブアドレスを決定するA0端子と同様に、はんだジャンパっぽくなっているのですが、よくよく見てみると、基板のパタンでつながっているみたいです。通電する前に、「#2-9 抵抗値を測定する」の抵抗測定回路とプログラムで測定したところ、SCL,SDA端子とも、VCC端子との間に数kΩの抵抗が見えました。
 基板パタンを追いかけたところ、プルアップ抵抗値は"472"=4.7kΩでした。ジャンパになっていて、選択できるならともかく、既にパタンが繋がっている以上、どうにもならないので、「また大きなお世話でプルアップ抵抗繋いであるのね」ってことで、先に進みます。

I2Cでの通信仕様

 MCP4725の内部レジスタに、12ビットのデジタル値を書き込むと、その値に対応したアナログ電圧が出力されます。MCP4725には、内部レジスタ以外に、電源投入時のデフォルト値を書き込むEEPROMがあります。I2Cによるデータ転送で、どちらに対して書き込むか?を決定するコマンドが3通り用意されています(詳しくはデータシートをご覧ください)。

 EEPROMの初期値は、フルスケールの1/2が出力される値になっており、この値で問題ないので、EEPROMへの書き込みコマンドについては、実装しないこととしました(デフォルトのまま使います)。
 MCP4725の内部レジスタ値を読み出す機能もありますが、これも、基本的に使い道が無いので、実装しません。

 よって、必要なのは、12ビットのデジタル値を設定するためのFast Mode Command書き込みシーケンス(下図)だけです。

ar79_mcpi2c.png

 ArduinoのWireライブラリを使う場合には、最初のスレーブアドレスバイトはライブラリが自動的に送信するので、第2、第3バイトに対するWire.write()を実行すれば良いことになります(以下コード抜粋)。

  // write dat to 12 bit dac (MCP4725)
  Wire.beginTransmission (i2c_MCP4725);
  Wire.write ((dat>>8)&0x0f);
  Wire.write (dat&0xff);
  Wire.endTransmission ();


 datは、MCP4725に出力する12ビットデジタル値を格納した変数です。P1=0,P0=0(Normal Mode)を送るコードになっています。

 I2Cのプロトコルでは、データを受信した側が、送信した側に対して、ACK/NACKを送信する規定となっています。従って、UARTの様に、「データを送信した、終わり」と言う訳には行きません。ちゃんと、相手からのACK/NACK信号を判定して、エラーフラグを立てるなり、何らかの処置が必要になるからです。
 今回は、ArduinoがI2Cマスタとなって、スレーブのMCP4725へデータを転送します。よってArduinoは、上図のデータ転送中、ずっとMCUタイムを拘束されます。つまり、他の処理を並行して実行することができない、と言うことです。

 上図の物理的な信号転送時間は、データだけで24ビット分、その他、START/STOPコンディションやACK/NACKの分などを考慮すると、29+αビット分の通信時間がかかっているものと推定することができます。この通信時間は、ATmega328のI2Cユニット最速の、Fast Mode 400kHzで通信したとして、およそ80μsとなります。
 その他、転送中および前後処理分の時間がかかるはずなので、MCP4725へのデータ転送には、もう少し時間がかかるでしょう(おそらく、100μs以上)。

 この通信時間が、後々、音声データの出力レートを律速することになるので、頭の隅っこに入れておいてください。

WAVファイル

 WAVファイルのフォーマット(形式)については、以下のページに解説がありますので、そちらをご覧ください。

 今回、Arduinoで再生可能なWAVファイルの形式は、以下とします。

 これ以外のWAVファイルを指定された場合は、エラーで弾きます。

音声データの生成

 基本的なところとして、WAVファイルと言うのは、スピーカなどを鳴らすためのアナログ電圧波形を、そのままデジタル化したデータで、PCMフォーマット(wFormatTag=1)の場合には、圧縮もされていない、生の電圧値(ただし、デジタル)の羅列になっています。
 よって、WAVファイルからデジタルの電圧値を読み出して、それをDA変換器でアナログ電圧に変換してから、スピーカなどに出力すれば、音は鳴ります。原理的には、これだけで終了です。

 ただし、スピーカは、非常に重たい負荷となりますので、それなりにパワーのあるアンプを通さないと鳴らせません。よって今回は、「#5-8 MMLで音楽を奏でる」で使ったのと同様に、イヤホンへ出力して音を聞くようにします。「#6-7 Arduinoはサイコロを振る」の様に、アンプ内蔵のスピーカに繋げば、スピーカで再生音を聞くことも可能です。

 WAVファイルを再生する手順をまとめておきます。

 よって、出力レート(DA変換器の出力を変化する時間間隔)はベストエフォートです。出力レートは、MCP4725へのデータ転送時間も含めた、Arduinoの処理速度に依存します。
 出力レートの逆数が出力周波数となり、標本化定理によって、出力周波数の1/2の周波数までしか再現できないことがわかっているので、処理を最適化して、出力レートを高速化することが、音質を向上する方法のキーパラメータとなります。

 以下、各処理について、少し詳しく解説します。

WAVデータの読み出し

 WAVファイルのフォーマットに従って波形データを読み込み、MCP4725に合わせて符号無12ビットデータを返す、fget_wave12()関数のコードを追いながら、コードの下に解説を加えて行きます。

  int fget_wave12 (
  File &fwav,
  uint32 usec)
  {
    // get 12 bit dac data from wav file fwav
    // return: 12 bit wave data / pwav_EOF
    int16 dat;
    uint32 fpos;


 再生開始時間からの経過時間usec[μs]と、再生するWAVファイルのFileクラスfwavを引数として受け取って、12ビット符号無整数(デジタル値)を返す関数です。

    // seek data
    fpos=fpos_dat + (uint32 (usec*pwav_spus)<<pwav_shft);
    if (fpos>=fpos_end)
    {
      // ERROR:
      return (pwav_EOF);
    }
    fwav.seek (fpos);


 再生開始時間からの経過時間usec[μs]から、読み出すべきファイルポインタfposを計算します。データの末尾fpos_endを超えたら、再生終了なので、pwav_EOFを返します。再生を続行する場合は、計算したファイルポインタ位置へseek()します。

    // get data
    if (nChannels==1)
    {
      // 1 channel
      if (wBitsPerSample==8)
      {
        // 8 bit
        dat=fwav.read ();
        dat=(dat - 0x80)<<4;
      }


 1チャンネル(モノラル)8ビットデータの場合の処理です。SDカードからのリードは1バイトだけです。
 読み込んだ8ビットデータは、0x80の下駄(バイアス)を履いた符号無整数なので、0x80を減じることで8ビット符号付整数に変換してから、4ビット左シフトして12ビット符号付整数に変換しています。

      else
      {
        // 16 bit
        dat=fget_uint16 (fwav);
        dat=dat>>4;
      }
    }


 1チャンネル(モノラル)16ビットデータの場合の処理です。SDカードからのリードは2バイトになります。
 読み込んだデータは、16ビット符号付整数なので、今度は4ビット右シフトすることで、12ビット符号付整数に変換しています。

    else
    {
      // 2 channel
      int16 dat2;

      if (wBitsPerSample==8)
      {
        // 8 bit
        dat=fwav.read ();
        dat2=fwav.read ();
        dat=(dat + dat2 - (2*0x80))<<4;
      }


 2チャンネル(ステレオ)8ビットデータの場合の処理です。SDカードからのリードは2バイトです。
 読み込んだ2チャンネル分の8ビットデータは、下駄を差っ引いた上で、そのまま加算、シフトによって12ビット符号付整数に変換しています。

※もし、ステレオ→モノラル変換の際に、L/Rチャンネルを足して、「2で割るべき」とお考えなら、ここで、2で除しておいてください。次の16ビットの場合も同様です。

      else
      {
        // 16 bit
        dat=fget_uint16 (fwav);
        dat2=fget_uint16 (fwav);
        dat=(dat>>4) + (dat2>>4);
      }
    }


 2チャンネル(ステレオ)16ビットデータの場合の処理です。SDカードからのリードは4バイトになります。
 読み込んだ2チャンネル分の16ビットデータは、最初にシフトしてから、加算します。さもないと、16ビットintがオーバフロウするので、longにキャストする必要が出てきます。計算結果は、12ビット符号付整数です。

    // digital volume
    dat*=pwav_dvol;


 デジタルボリュームの処理です。後ほど、別途解説します。

    // convert unsigned (+dac_HALF) & clipping
    dat=dat + dac_HALF;
    dat=(dat<0)? 0: (dat>dac_MAX)? dac_MAX: dat;

    return (dat);
  }


 最後に、12ビット符号無整数値に変換するため、0x0800(=2,048=dac_HALF)を加算して下駄(バイアス)を履かせます。数値が0〜4,095(12ビット符号無整数)に収まるようにクリッピングして、返します。

 「#6-8 ATmegaのEEPROMを使ってみる」の記事で、SDカードのアクセス速度を測定しました。今回は、2GバイトのSDカードでSPIクロックを8MHzにして使いましたので、この時の読み出し速度を表から参照すると、28,693[バイト/s]でした。逆数を取って、1バイトの読み出しに必要な時間に換算してみると、約35[μs/バイト]となります。
 1チャンネル8ビットデータの場合だと、1バイト読み出すだけで済みますが、2チャンネル16ビットデータの場合だと、4バイト読み出す必要があるので、読み出しだけで35〜140[μs](=4×35μs)の処理時間変動があります。
 本節の最初に示したアルゴリズムで、出力レートはベストエフォートなので、処理時間が多くかかると、出力レートが遅くなり、音質が低下します。後で実験する時に利いてきますので、覚えておいてください。

デジタルボリューム

 今回使用するMCP4725は、0〜3.3[V]の範囲で電圧を出力可能です。つまり、出力電圧範囲VP-P=3.3Vです。「#5-8 MMLで音楽を奏でる」で解説した様に、イヤホンに送る電圧波形は、VP-P=0.5〜1.0[V]くらいの範囲なので、VP-P=3.3Vで出力してしまうと、音量が大きすぎます。そこで、WAVファイルから読み込んだ波形データdatに、適切な係数を掛けることで、VP-P=0.5〜1.0[V]に絞って使います。
 折角の12ビットDA変換器(デジタル範囲0〜4,095)の、一部分(※)だけを使うことになって、ちょっともったいないのですが、Arduinoの遅さ故、時間軸方向の解像度(出力レート)がそれほど期待できません。電圧軸方向の解像度が12ビットもあったとて、使い切れないのが現状なので、特に問題なしと判断しました。

※例えば、VP-P=1.0Vで使う時には、12ビットDA変換器のデジタル値1,428〜2,668(2,048±620)程度の範囲だけを使うことになります。

 前節で示したコードの中で、デジタルボリュームを処理する時点の変数datは、12ビット符号付整数、なので、0をセンタとして、+側に11ビット分、−側に11ビット分の振れ幅を持っていることに留意してください。例えばここに、×0.1倍(=×1/10倍)の係数を掛けると、0をセンタとして、正負の値が×0.1倍となります。

 直後に下駄(バイアス、dac_HALF=2,048)を履かせて、12ビット符号無整数に変換していますが、その前に係数を掛ける必要があります。∵そうしないと、バイアスにも係数が掛かってしまうため。

アクティブLPF

 12ビットDA変換器なので、電圧軸方向の解像度は高いのですが、時間軸方向はArduinoの処理速度で律速するので、解像度は低くなります。つまり、出力波形は階段状となります。階段状の波形には高周波数成分が多量に含まれるので、音として聞いた時にノイズとなって発現します。

 階段状の高周波数イズを軽減するために、LPFを通して、多少波形を成形しようと思います。今回使うのは、以下図の様なアクティブLPFです。

ar79_lpf_sch.png

 「#6-2 オペアンプにspice」でやったように、VoとViの関係式を作ったところで、「LPFの関係式って、どんな形?」って感じなので、もう少し感覚的に理解してみようと思います。

 どの程度の音程(周波数)まで通過させるか?と言うより、DA変換器への出力レートtDACに起因する階段状の電圧変化を鈍らせて、波形を平滑化することに主眼を置きます。よって、tDAC≒K×Cf×Rf前後に調整するのが良いと考えられます。
 時定数Cf×Rfは、充電電圧が最終到達電圧の63.2%程度に達するまでの時間でしたね。この時、K×Cf×Rfで到達する電圧値は、最終到達電圧のL=1−(1−63.2%)K[%]となります。

 仮に、tDAC=300μsを想定して、K=3(この時のL≒95%)を選択すると、Cf=0.1μF(手持ちのコンデンサから選択)の時に必要な抵抗Rf=1kΩとなり、この場合の遮断周波数fc≒1.6kHzとなります。
 実際にコーディング後、tDACの実力値を測定して、Rfを再調整することとします。

 上のサンプルパラメータでシミュレーションした結果は以下の通りです。

ar79_lpf_sim.png

 Viが、DA変換器への出力レートtDACに起因する階段状の電圧変化に模した矩形波です。ゲインがマイナス(−Rf/Rs=−1)の回路なので、LPFが掛かっていなければ、VoはViを反転した矩形波になるはずです。
 電圧Viの変化点で、Voにも尖った波形が出力されていますが、Viの急峻な立ち上がり/立ち下がり波形が、なだらかに補正されているのがわかります。

※LTspiceを使ってシミュレーションしました。LTspiceによるシミュレーションについては、以下の記事で紹介しています。

今回のお買い物

 今回のお買い物は、以下の通りです。


 粗方、必要な検討は終わらせたつもりですが、全てを刈り取ってまとめるのは、次回に譲ります。長くなってきて疲れたので、今回はこの辺りで失礼💦


btn_arduino.png

btn_prev.png btn_next.png

home