サンプル音(PCM)の再生

  1. サンプル音(PCM)の再生

    1. サンプル音とは

      サンプル音は音の波形を一定時間間隔でAD変換した数値の列です。この数値の列にヘダ情報を付加した波形ファイルは、Windowsでは .wav ファイルとして保存されます。音の再生でよく利用される .MP3 ファイルはこの波形ファイルを圧縮したファイルになります。画像ファイルでは .BMP と .JPG ファイルに対応します。このように、サンプルされたディジタル値を利用して作成した音源をPCM(Pulse Coded Modulation)音源と呼ぶ場合があります。
       サンプルした音をそのまま再生する方式は、手軽に自然な音の再生が可能ですが、大きなメモリが必要になります。CD並みの音質にするには、サンプル速度44K個、サンプル当たりのデータ量を2バイトとする必要があります。実際にはCDでは左右2チャンネル(ステレオ)を記録しますから、この倍の記憶が必要です。
       ここでは、サンプル速度を6K個、サンプル当たりのデータ量を1バイトとし、モノラル録音とします。この場合、1Mビットのフラッシュメモリで約40秒の録音が可能です。また、1バイトの読み出しを1/6k = 160 μS 以下で行う必要があります。

    2. DAコンバータ

       ここでは、DAコンバータとしてAD557を利用します。これは、5V単一電源で動作する8ビットDAコンバータです。1ピンから8ピンにディジタル信号を接続します。ただし、8ピンがLSB(最低桁)ですからPICのRB0に接続します。BITの番号付けが逆ですから注意してください。11ピンと、12,13ピンに電源を接続します。電圧の範囲は4.5〜5.5、電流は20mA程度です。



      9,10はこのチップを有効にする制御信号で、共に0にすれば、ディジタル信号のアナログ値がVout(16ピン)に出力されます。VoutSENSEAとVoutSENSEBは出力のレベルを設定する場合使用します。ここでは、両者を Vout に接続します。この場合出力は 0Vから 2.5V の範囲になります。

    3. I2Cメモリ

       波形の記録には外付けのフラッシュ方 I2C メモリを利用します。 64Kbitから1Mbitまでのメモリが利用できます。I2C 方式で2本のシリアル信号で接続できますが、データは直列に伝送されるため、アクセスは遅くなります。下図は ATMEL社の AT24C1024 です。CMOS型ですから電源電圧は 2.7〜5.5V 、静止時の電流は数μAです。読み取り時には2mA、書き込み時には5mA程度の電流が流れます。フラッシュメモリのなで、書き込みには10mSが必要です。クロックは400KHzで1MHzの高速I2Cには対応してないようです。
       読み出しは、I2Cのクロックが 400KHz 、PICのクロックが20MHzの場合、C言語の関数を利用して100μS 程度の時間が必要です。この時間は、アドレスを指定しないで連続番地を読み出す場合です。アドレスを指定してランダムに読み出す場合はこの数倍の時間が必要です。


       書き込みは、アドレスを指定して1バイト単位に行うこともできますが、ページ単位に連続バイトを書き込むことも可能で、この方が高速です。ただし、ページのサイズは、AT24C1024 の場合256バイトですが、記憶容量が小さい場合ページサイズも小さくなります。ページは境界をまたがって書き込むことはできません。例えばページサイズが64の場合、10番地から73番地までを連続して書くことはできません。まず、0〜63バイトまでを書き込み、次に、全アドレスを指定した後、次のページに書き込む必要があります。
       読み出しの場合、ページの制限はありません。最初に番地を指定すれば、可能なアドレスの範囲で連続読み出しが可能です。

  2. システムの構成

    1. 回路構成

      PIC16F873のBポートとADコンバータAD557のデータ信号を接続します。AD557はBIT8が最下位ビットですから、これを16F873のB1に接続します。AD557のVOUTとVoutSA,VoutSBを接続し、VOUTは外部スピーカに接続します。ある程度の音質のスピーカを接続しないと音質の違いがはっきりしません。PC用の外部スピーカに接続するとよいでしょう。
       外部メモリはI2Cで接続します。PICのC3/SCLとC4/SAをAT24C1024のSCLtpSDAを接続します。バス接続ですから、各線を1kΩでVCCに接続する(プルアップする)必要があります。
       PICとRS232Cとの接続には、レベルコンバータMAX232(相当)を利用します。これは、マイクロローダによるプログラム書き込みとデータの送受信に利用します。プログラムを送りつけるときは、PICのCLRピンに接続するスイッチをオンにする必要があります。
       PICのA1に接続するスイッチは再生開始のスイッチで、フラッシュメモリにデータが書き込まれている場合は、このスイッチをオンにすることで再生できます。A0に接続するLEDは動作の確認用です。PCからPICにコマンドを送り付けたとき、PICがメモリを読んで音声の再生中に点灯します。


    2. wavファイルの構成
      再生するには、予め、メモリにサンプルデータを書き込む必要があります。I2Cメモリに直接ファイルを書き込むことができる ライタ もありますが、ここでは、PC でのソフトを利用して シリアル(RS232C) 経由で .WAV ファイルの内容をメモリに書き込みます。
       ここでは、モノラル、8ビット、6000サンプル/秒 の.wav ファイルを再生の対象にします。フォーマットは CoolEdit などの 音声編集ツールで設定可能です。 .wav ファイルはRIFFファイルの構成です。先頭から 0x28,0x29 バイトにファイルのデータ数が記録され、音声データは 0X2C バイトから始まります。また、ox1C、0x1D に秒当たりのサンプル数が記録されています。
    3. wav ファイルの転送プロトコル

      PC側で wavファイルを選択し、次のようなプロトコルで シリアル経由で PIC にページ単位にデータを送信します。PICでは、ページのデータをメモリに保存後、I2Cを利用してメモリにページ書き込みを行います。PICのメモリの制限、メモリがわのページサイズの条件を考慮して、標準では 64バイト をページサイズとします。
       
      プロトコル
       'l' num ;下位アドレスの指定
       'h' num ; 上位アドレスの指定
       'w' num d1 d2 ...  dn ; num バイトのデータ書き出し
       'r' num ; num バイトのデータ読み出し

       ページ単位の書き込みを行うには、まず 'l' コマンドと 'h' コマンドで、下位と上位のアドレスを指定します。次に、'w' コマンドで 書き込みを指示し、続けて num バイトのデータを送ります。numをメモリにページサイズにすれば、1回の 'w'コマンドで1ページを記録できます。ファイル全体をページに分割して送るのは、PC側のプログラムで行います。
       メモリの値を読み出すには、'l' コマンドと 'h' コマンドで、下位と上位のアドレスを指定した後、r コマンドを出します。PIC 側で、このコマンドを受け取ると、連続したメモリの値を num バイト送り出します。

    4. PIC のプログラム

      main()の先頭での、output_low(PIN_C0); は動作チェック用のLEDの初期設定です。このLEDはコマンドを受信したとき、再生中のメモリアクセスで点灯します。
       while() で繰り返しに入ります。kbhit()関数は、シリアルからデータが到着したとき1になります。これをチェックしないで、getc()を行うと、シリアルからデータが到着するまでこの関数の実行が終了しません。main() では、スイッチボタンでも「再生」処理を実行したいのですが、getc()で「はまって」しまうとスイッチのチェックまで制御ができません(割り込みを利用するとこの問題はありませんが、あいにく、外部割込み端子RB0をDAコンバータのポートで使ってしまいました)。
       受信していた場合、最初と次のバイトをcmndとnumに読みます。cmndが 'h'や'l'の場合 adres をセットします。また、受信の応答としてコマンドを PC側に返します。'w'の場合、とりあえず受信データを buf[] に保存します。受信後、writemem()でI2C経由で、メモリにページ書きをします。一度バッファに保存しないと、受信が遅れる心配があります。
       'r'の場合、readMem() で連続するnumバイトのメモリを読み、PCに値をおくります。送信の場合は、少々遅くなってもかまいませんから、バッファは不要です。
      //Serial File Transmission
      //PC>SerialCommand
      //PIC > I2C > FlashMem
      
      //PIC RS232 xmit:C6 ,rcv:C7
      //I2C CL:C3 ,DA:C4
      
      //PC command 
      //h num :アドレスの上位バイトを設定する
      //l num :アドレスの下位バイトを設定する
      //w num :PCから続くnumバイトのデータをROMに書く
      //r num :現在のアドレスからnumバイトのデータをPCに送る
      
      #include <16f873a.h>
      #fuses HS,NOWDT,NOLVP //内部クロック、WDT,LVPなし
      #use delay(CLOCK=20000000)
      #use RS232(BAUD=9600,xmit=PIN_C6,rcv=PIN_C7)//use delayの後に配置する
      #use i2c(MASTER, SDA=PIN_C4, SCL=PIN_C3, FORCE_HW) // I2C使用宣言
      
      void writeMem(long address);
      void readMem(long address);
      void playwave();
      
      unsigned long adrs;
      char buf[64];
      char num,cmnd,dat;
      long wsize,wcnt;
      
      
      main()
      {
      
          int i;
              //set_tris_b(0b11110010);
              adrs=0;
              output_low(PIN_C0);             
              
              while(1){
                
                cmnd=' ';
                if(kbhit()) {
                  cmnd = getc();//get command & num
                  output_high(PIN_C0);//check light
                  num = getc();
                  output_low(PIN_C0);
                }
                
                switch (cmnd){
                
                case 'w'://write command
                 for(i=0;i<num;i++){                  
                      buf[i]=getc();//get data
                 }          
                  writeMem(adrs);//write memory vir I2c
                  adrs +=  num;
                  putc('1');//put response
                  break;
                case 'h'://high Address
                  adrs = num<<8 ;
                  putc('h');
                  break;
                case 'l' :
                  adrs = (adrs & 0xF0) + num;//low address
                  putc('l');
                  break;
                case 'r'://read command
                  readMem(adrs);//read memory and send
                   adrs += num;
                   break;
                      
                case 'p':
                  playwave();
                  //putc('p');
                }
      
              
              if(! input_state(PIN_A0))
                playwave();
                                        
                //putc(cmnd); //printf("1")ではバグル
                //write ROM
                         
              }
              return 0;
      }
      
      checkWrite() はi2c_isr_state()で書き込みの完了を確認してから、i2c_write(data); で次の書き込みを行います。writeMem()は指定addressからnumバイトとI2Cを通して連続書き込みをします。i2c_start(); で開始準備、i2c_write(0xA0);でメモリチップのデバイスアドレスを指定します。次に、addressの下位と上位のバイトを送ります。続けて、buf[]の値を書き込みます。ここで、連続するアドレスがページアドレスを跨がないよう注意する必要があります。この場合、0番地から64バイトづつ書き込みますから問題ありません。
       最後にi2c_stop();でI2Cの利用を終了します。
      //状態をチェックしてから書き込む
      void checkWrite(int data){
        int state;
        while(1){
            state=i2c_isr_state();
             if(state>=0x80) {
                 i2c_write(data);
                 //delay_ms(2);
              return ;
            }
        }
      }
      
      //addressからROMに書き込む
      //chipアドレスは0に固定
      void writeMem(long address)
      {
              //chipのメモリのaddress番地にdataを書く
              int i;       
              i2c_start();
              i2c_write(0xA0);
              i2c_write(address>>8 );
              i2c_write(address);
              
              for(i=0;i<num;i++){
                      checkWrite(buf[i]);
              }
              
              i2c_stop();
              delay_ms(5);
              
              //enable_interrupts(INT_RDA);
              //printf("wadrs:%lx\r\n",address);      
      }
      以下は、メモリの読み出しです。readMem() はPCからの確認用の読み出しです。最初にaddressを送り、次に、i2c_write(0xA0 | 0x01); で、読み出しを指示します。i2c_read(1) で1を指示して読むと、続けて読み出すことを意味します。num-1 バイトを読み出した後、i2c_read(0) で読み出し完了を指示します。putc() では読み出した内容を PC 側に送ることを指示します。
       readRandomByte()は指定アドレスから1バイトを読み出します。アドレスを送り、読み出した1バイトを戻り値とします。playWave() は、メモリから波形データを読み出し、再生します。最初の putc(readRandomByte(0x0)); はこのplayコマンドに対する応答です。波形メモリの先頭には'R'が記録されていますので、確認に利用できます。メモリの28,29 番地に波形データのバイト数が記録されています。この2つのバイトを合成して、2バイト整数にします。
       (((long)readRandomByte(0x29))<<8) + wsize
      の (long)のキャストに注意してください。これがないと、readRandomByte(0x29) を1バイトのデータとして8ビット右シフトしますから(<<8)、値が消えてしまいます。(long)で2バイトのデータにしてからシフトして上位バイトに送り込みます(うっかりこれを忘れて、暫く悩みました)。
       波形データは、先頭から 0x2c 番地以後に記録されています。読み出しはページの制限がありませんから、無制限に連続読み出しができます。読み出す間隔を、サンプルした時間間隔にあわす必要があります。ここでは、6KHzサンプルした波形データを利用していますから、約170μS間隔でDAコンバータに送り出す必要があります。問題は、i2c_read(1) による読み出し時間です。この前後で RC0 ピンに1,0をセットし、この間隔をオシロで測定すると、ほぼ100μSでした(この時間は PIC のクロックや I2Cのクロックでも変化します)。そこで、70μSを追加して時間を調整します。i2c_read(1)の時間は80μS以下と推測していましたが、やや長めの値です。したがって、この方式では10KHz以上でサンプルした音声ファイルは再生できません。I2Cの関数をアセンブラで書けばもう少し早くなると思います。

      //addressからデータを読みPCに送る
      void readMem(long address)
      {
              //chipのメモリのaddressからnumバイトをよみシリアルに送る
              int i;
              
              i2c_start();
              i2c_write(0xA0);
              i2c_write(address>>8);
              i2c_write(address);
              
              i2c_start();
              i2c_write(0xA0 | 0x01);
              
              for(i=0;i<num-1;i++){
                 putc(i2c_read(1));//read & put
              }
              putc(i2c_read(0));//last data
              i2c_stop();
      
      }
      
      int readRandomByte(long address){
           //sddress1バイトを読む
           i2c_start();
              i2c_write(0xA0);
              i2c_write(address>>8);
              i2c_write(address);
              
              //printf("radrs:%lx:",address);
              i2c_start();
              i2c_write(0xA0 | 0x01);
              dat= i2c_read(0);
              i2c_stop();
              return dat;
      }
              
      
      void playwave(){
          putc(readRandomByte(0x0));
          wsize = readRandomByte(0x28);
          wsize = (((long)readRandomByte(0x29))<<8) + wsize;
              
          adrs = 0x2c;
          output_b(readRandomByte(adrs));
              
          i2c_start();//sequential read
          i2c_write(0xA0 | 0x01);
          for(wcnt=0;wcnt<wsize;wcnt++){
               //この間ほぼ100μ秒
               output_high(PIN_C0);//check light
               output_b(i2c_read(1));
               output_low(PIN_C0);//check light
               delay_us(70);
               adrs++;
            }
            output_b(i2c_read(0));
            i2c_stop();     
      }

  3. PC側のプログラム

    1. 波形ファイルの送信

      PC側で行うことは、波形ファイルを ページ単位でRS232C経由で送信することです。ダイアログの画面は下のようです。起動すると、RS232Cでの接続を行います(COM1、9600BPS、8bit、パリティなし)。openボタンで、.wav ファイルを選択します。Sendボタンを押すと、64バイト(ページ)単位でファイルをRS232Cに送ります。
       setAdrsH、setAdrsL、は接続メモリの上位、下位のアドレスを設定します。アドレス設定後RCVボタンを押すと、指定アドレスからnum(64)バイトを受信し、16進で表示します。
       送受信のページサイズ(64)はPC側で変更できますが、PIC側では64バイトが最大で128や256の設定はできません。
      Playボタンを押すと、PICに再生開始コマンドを送ります。



    2. RS232Cの設定

       OnInitDialog()の中で、下のように設定しています。RsはCRs232Cクラスのインスタンスです。PCのCOMポートの番号はRsOpenで COM1 を設定しています。必要なら変更をしてください。
         Rs.RsPort(0);
          Rs.RsOpen();
          rc=Rs.InitCommPort(0,1024L,1024L);//バッファ長
          if(rc) TRACE("RSInitError:%d\n",rc);
      
           rc=Rs.SetCommPort(9600,8,0,0,false,false);//設定
           if(rc) TRACE("RSSetError:%d\n",rc);
           //Rs.RsConfig();
      
           m_font.CreatePointFont(9*10, _T("MS ゴシック"));
           GetDlgItem(IDC_MSG)->SetFont(&m_font,TRUE);
      SetFont()で、ダイアログのフォントを設定しています。RCVで受信したメモリの内容を表示するのに、固定幅フォントが必要です。

    3. タイマー処理

       CRs232Cクラスの受信はイベントではなく、タイマーによるタイマーイベント1で定期的な受信チェックを行っています。タイマーイベント2では、送信コマンドに対するPIC側からの応答チェック(タイムアウト処理)を行います。コマンドを出した後、タイマーイベント2を設定し、指定時間以内に受信がない場合、タイマーイベント2が発生し、タイムアウトの表示を行います。
       タイマーイベント1では mode 変数で、メモリからの送受信の処理を行います。送信の場合、mode='f' とし この場合、ファイルの次のページを送信します。メモリ受信の場合、buffer[] に受信データを保存し、dumpBuffer(buffer); で下のようにメッセージ領域にデータを16進で表示します。行の先頭がアドレスです。



    4. プロジェクト

      このプロジェクトはダウンロード可能です。ファイルをダブルクリックして解凍できます。