まず過去記事では、UNOやSTM32でSDカード上のWAVファイルを再生するアプリケーションを作り、それをさらにDUEでも実行できるようにして見ました(↓)。
最後の「#12-10」で明らかになった、対策すべき点がありました。
上図は、DUEでWAVファイルを再生した時のDA変換器出力端子をSTM32+Logiscopeで観測した波形です。一定期間毎に、DA変換器出力が変化しない平坦な部分があります。恐らく、この期間はSDカードからデータを読み出している時間だと思われます。
色々なWAVファイルを再生した時の出力レートを測定した表を復習してみると、例えば8ビット,ステレオ,22.05kHzのWAVファイルを再生した場合、47.2kHzもの出力レートを観測しています。音楽CDの音声データは44.1kHzで記録されていますので、数字上はCDを凌駕しています。
にもかかわらず、CDの再生音とは比べ物にならないほど、音質が低く聞こえました。その最大の原因を上図の平坦部分だと考えています。出力が変化している部分と合わせた平均出力レートは47.2kHzもある様ですが、定期的に入る平坦部分によって、音質が低下しているのでしょう。
今回のテーマではこの点を改定して、もう少し高音質でWAVファイルを再生してみようと思います。
これまでにUNO,STM32,DUEで作って来たplay_wav.inoというプログラムは、ベストエフォートで出力レートが決まっていました。以下にWAVファイルを再生しているメインループ部分のコードを示します(関係のある処理のみ抜粋)。
// play wav file
usec=0;
usec_start=micros ();
while (Serial.available ()<=0)
{
wdat=fget_wave12 (fwav,usec);
if (wdat<0)
break;
dac12_write (wdat);
// continue
usec=micros () - usec_start;
}
以下、ステップを踏みながら解説します。
こうすることで、play_wav()関数を実行する環境(機体、MCU、動作周波数、等々)によらず、常にその環境下の最大処理速度で出力レートが決まることになります。これをベストエフォート方式と呼ぶことにします。
冒頭で述べた、DA変換器出力波形の平坦部分は、fget_wave12()関数の実行時間によって生じています。
これを対策するためには、一定周期で割り込みを起こし、割り込みサービスルーチン(ISR)内でDA変換器出力dac12_write()関数を実行するのがベストです。この方法は、あらかじめ決定した周期でDA変換器出力が変化するので、固定出力レート方式と呼びます。
固定出力レート方式では一定時間毎にタイマ割り込みを起こし、割り込みサービスルーチン(ISR)内でDA変換器に次々と新しいデータを書き込みます。ISR内で実行するのは、ただひたすら、バッファからデータを読み出してDA変換器に送る、それだけです(↓、一部抜粋)。
void TC3_Handler (void)
{
// TC1 channel#0 (TCC3) interrupt service routine
// interrupt flag clear
REG_TC1_SR0;
// output
dac12_write (wbuf[pwav_rptr]);
pwav_rptr++;
if (pwav_rptr>=pwav_max_buf)
pwav_rptr=0;
}
一方、メインのWAV再生ループ内では、SDカードからデータを読み出してバッファに積んで行きます。バッファはリングバッファとして、制御します(後述)。バッファがオーバフロウしないようにチェックしながら、ただひたすら、データの読み出しとバッファへのコピーを繰り返します。
// play wav file
while (Serial.available ()<=0)
{
usec=uint32 (usec_outr*pwav_cnt + 0.5);
wdat=fget_wave12 (fwav,usec);
if (wdat<0)
break;
// polling during buffer full
next_wptr=pwav_wptr + 1;
if (next_wptr>=pwav_max_buf)
next_wptr=0;
while (pwav_rptr==next_wptr)
;
// add data to the buffer
wbuf[pwav_wptr]=wdat;
pwav_wptr=next_wptr;
// continue
pwav_cnt++;
}
この方式では、データをDACへ出力する人と、SDカードからデータを読み出す人の、完全な分業体制です。
SDカードからの読み出し処理中にもタイマ割り込みは受け付けられるため、常に一定間隔でDA変換器出力が変化します(固定出力レート)。
SDカードからのデータ読み出しに関しては、新しいページの読み出しが必要になった場合と、前に読んだページからの読み出しだけで済む場合で、処理量に差が出ます。
これをバッファで吸収する仕組みです。
上図は、バッファ内の有効データ数の推移を表した模式図で、横軸に時間の経過、縦軸にバッファ内のデータ数を示しています。
以上の様なバッファをリングバッファとして実装します。次節ではリングバッファの制御についてもう少し詳しく解説します。
リングバッファwbuf[ ]の制御を下図で解説します💍。
バッファサイズを定数pwav_max_bufで定義します。上図では仮に、pwav_max_buf=24の例として作図しています。丸の中の数字が一次元配列wbuf[ ]の添え字となっています。
一次元配列の最初の添え字[0]と最後の添え字[23]を繋いで、リング状のバッファとして使用します。リング状に頭とお尻が繋がっているので、リングバッファと呼びます。最初に書き込んだデータが最初に読み出される形となるので、FIFO(First In First Out)バッファとも呼ばれます。
このバッファは、読み出しポインタrptrと書き込みポインタwptrで制御します。
従って上図の例では、添え字[3]〜[11]に有効データが入っています。
常にwptrの方がrptrよりも進んでいて、最低でも1個以上の有効データがバッファ内に存在することが必要です。
外付けの回路としては、「#12-10 DUEでWAVファイルを再生する」で使った回路が流用できます。以下図に再掲しておきます。
ブレッドボードなどに構築して、DUEと接続しておいてください。以下図はDUEのSPIピンヘッダの配置です。
次に、完成したプログラムをリンクしておきます。
完成プログラム:play_wav_due.ino
コンパイルしてDUEに書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。
以下、実験中の様子です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pwav,A12A_CLR.WAV】
DBG: fmt=1, 2, 8000, 16000, 2, 8
OK: play_wav: 22050[Hz] 28511[smpl] (0)
良い様です(1^−^;)v。旧プログラム(play_wav.ino)では、すごくざらついた音に聞こえていたのですが、とてもクリアに聞こえます。
STM32でLogiscopeを起動し、DUEのDA変換器出力を観測しておきましょう。STM32とDUEのGND同士、および、DUE.DAC0 → STM32.A0をジャンパ線で接続して、以下の実験を行いました。
実験サンプル:【 】内はシリアルモニタからの送信データ
【dvol,3】
OK: d-volume *3.00
↑波形の振幅が認識できるように、デジタルボリュームを少し上げておきます。出力が大きくなるので、イヤホン等には出力しないようにしてください。
【pwav,test.wav】
少し長めのWAVファイルを再生しておいて、再生中に以下のコマンドをSTM32のLogiscopeから送信します。
実験サンプル:【 】内はSTM32側Logiscopeからの送信データ
【osc,0,0,4,0】
上段は新プログラム(play_wav_due.ino)で観測したWAVファイル再生中のDA変換器出力波形、下段は旧プログラム(play_wav.ino)の波形です。新プログラムでは平坦な部分が無くなって、よどみなく音声波形が出力されていることを確認できました。
WAVファイルのフォーマット(ステレオ/モノラル、8/16ビット、サンプリングレート等)によって、データレートが異なったり、再生する時の出力レートが変わると、音質や聞こえ方などに差が出ます。いくつか、異なるフォーマットのWAVファイルを異なる出力レートで再生して見て、どのように違って聴こえるか、聞き比べてみました。
プログラム(play_wav_due.ino)の先頭付近にある「#define DEBUG_RING_BUF」を有効にして、デバッグ用の出力を有効にして実験しました。STM32との接続を解除して、元のサウンド出力回路を接続し、DUEにリセットをかけてプログラムの実行状態を初期化してください。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pwav,S8_11K.WAV,11025】
DBG: fmt=1, 2, 11025, 22050, 2, 8
DBG: cnt,ttl,min 1303198 662245548 410 0
OK: play_wav: 11025[Hz] 1303706[smpl] (0)
「DBG: cnt,ttl,min」で始まる出力が追加になったハズです。後ろに続く4つの数値の意味は以下の通りです。
2個目の数値÷1個目の数値を計算すると、バッファ中の平均データ数が求められます。加えて注目すべきは、3,4個目の数値です。表にまとめてみました。
WAVファイル |
チャン ネル数 |
bit |
サンプル レート [sample/s] |
出力レート [output/s] |
平均 データ 数[個] |
最小 データ 数[個] |
備考 | |
S8_11K.WAV | 2 | 8 | 11,025 | 11,025 | 508 | 410 | ||
22,050 | 498 | 200 | ||||||
S8_22K.WAV | 2 | 8 | 22,050 | 10,000 | 507 | 459 | ||
20,000 | 501 | 406 | ||||||
22,000 | 498 | 22 | ||||||
22,050 | 499 | 307 | ||||||
23,000 | 496 | 17 | ||||||
25,000 | 490 | 6 | ||||||
30,000 | 463 | 0 |
×バッファアンダラン エラー(7,999回)発生 |
|||||
S16_22K.WAV | 2 | 16 | 22,050 | 11,025 | 501 | 410 | ||
22,050 | 26 | 0 |
×バッファアンダラン エラー(94,997回)発生 |
|||||
A12A_CLR.WAV | 2 | 8 | 8,000 | 22,050 | 502 | 213 | ||
AR7A_SPL.WAV | 1 | 8 | 22,050 | 22,050 | 505 | 286 |
リンクの無いデータは、適切な音楽データ(再生時間2分ほど)を上表のフォーマットに変換してSDカードに書き込んでおいたものです。サイズが大きいのでリングしません。各自、適切なファイルを用意して実験してみてください。
以下雑感です。
相変わらず、ノイズだけはどうにもなりませぬ。どう考えても、DUEと言う機体は音楽などの再生用に作られたシステムではないので、仕方ないのかもしれません。ノイズを除くと、かなりいい線行っているので一度お試しください。