2011年4月1日金曜日

ARM Cortex-M3 (NXP LPC1769)を用いたオーディオ処理の効率改善

概要

今回はプロセッサを用いた信号処理における効率改善に関するトピックです。


先日完成したARM Cortex-M3 (NXP LPC1769)を用いたオーディオ基板は @suikan_blackfin さんがお作りになったサンプルアプリケーションのおかげで素早く動作確認することができました。本当にありがとうございます。


氏はリアルタイムOSのポーティングから信号処理まで多岐に渡る分野で活躍されていて、言わば私の心の師匠ですが、恐れ多くもサンプルアプリケーションで気になったオーディオ処理の効率改善について記すことにしました。

巨人の肩に乗りまくりです。

オリジナルの設計

まず初めにオリジナルの設計がどのようなオーディオ処理ステップを踏むのかを整理してみます。
  • DMA転送されたオーディオバッファサイズ分のデータがrxbufに入っている。
  • 並び替えながらrxbufからaudio_data.inputBufferにコピーする。
  • オーディオ処理を実行する。
    • ここでは処理に応じてaudio_data.inputBufferからaudio_data.outputBufferへのコピーが発生する。
  • 並び替えながらaudio_data.outputBufferからtxbufにコピーする。
図を用いて整理すると以下のようになります。

要するに主記憶上におけるメモリコピーが少なくとも3回発生していることになります。
このメモリコピーはforループで実装されており、3回のforループによる性能への影響も気になるところです。

通常、性能という観点で見た場合、主記憶上でのメモリコピーや重複したforループは処理性能低下の主要な要因のひとつとなります。

そこで、今回は上記処理の効率改善を考えてみます。

最小限の処理から考えてみる

まず初めに、最小限の処理について考えます。

入力をそのまま出力に伝達する場合、単なるメモリコピーで済みます。
入力に何らかの処理を加え、出力に伝える場合でもこの入出力間のメモリコピーの間に何らかの処理を追加するだけで済みますので、本質的に上記と変わりません。

オリジナルの実装ではコーデックのデータ形式を踏まえて処理を行なっています。

まず初めに入力されたデータを内部で扱いやすい形式にメモリコピーします。

これはrxbufからinputBufferへのコピーです。


次にオーディオの処理を実行します。
これはinputBufferからoutputBufferへのコピーです。

オーディオの処理を実装する過程で、ここに様々な演算が入ることになります。

最後に結果を出力バッファに書き込みます。
これはoutputBufferからtxbufへのコピーです。

この作業はオーディオバッファの内容を、前段で都合の良い形式に並び替えた結果発生する作業と言えます。


まとめると以下のようになります。
  • コーデックから得られたデータ形式は扱いにくいので並び替える。
  • 並び替えは主記憶上でMCUが実行する。
  • 並び替えたデータは、コーデックがそのまま扱えないので再変換する。
コードブロックは以下のようになっていました。(一部はオリジナルと少し異なります。)

   index = 0;
   for (sample = 0; sample < AUDIOBUFSIZE / 2; sample++) {
       for (ch = 0; ch < 2; ch++) {
           audio_data.inputBuffer[ch][sample] = rxbuf[index++];
       }
   }
   audio_effect_through(
           &effect_param,
           audio_data.inputBuffer,
           audio_data.outputBuffer,
           AUDIOBUFSIZE / 2);
   index = 0;
   for (sample = 0; sample < AUDIOBUFSIZE / 2; sample++) {
       for (ch = 0; ch < 2; ch++) {
           txbuf[index++] = audio_data.outputBuffer[ch][sample];
       }
   }

オーディオエフェクト処理の前後でデータ形式変換を行なっていることがわかります。
前段と後段で各((AUDIOBUFSIZE / 2) x 2)回分のメモリコピーを行なっています。

実際にオーディオ処理関数内部の実装も見てみます。(一部はオリジナルと少し異なります。)

   void audio_effect_through(
           effect_param_t *param,
           AUDIOSAMPLE input[2][AUDIOBUFSIZE / 2],
           AUDIOSAMPLE output[2][AUDIOBUFSIZE / 2],
           int count)
   {
       int i;
 
       const int var0 = param->var0;
       const int var1 = param->var1;
       for (i = 0; i < count; i++)
       {
           output[LCH][i] = (input[LCH][i] >> 10) * var0;
           output[RCH][i] = (input[RCH][i] >> 10) * var1;
       }
   }

ここで上位から渡されるcountは(AUDIOBUFSIZE / 2)です。
よって、ここでも((AUDIOBUFSIZE / 2) x 2)回分のメモリコピーを行なっていることになります。

改善の提案

ここまではオリジナルの設計について整理しました。
それでは実際にオーディオ処理の効率改善をしてみます。

基本的な思想は以下の通りです。
  • 主記憶上におけるメモリコピーは性能に対して著しい劣化を伴う。
  • より多くの処理を実現するためにはメモリコピーを排除すれば良い。
  • メモリコピーを行なっている主な理由はデータ形式変換である。
  • データ形式変換が不要となるような枠組みを用意すれば、データ形式変換が不要となるはずである。
  • データ形式変換が不要となれば、必要となるバッファも削減することができ、RAM容量という観点から見ても有利である。
「データ形式変換」を実現しながらも、「メモリコピー」を発生させないという一見矛盾した課題を解決すれば良い事になります。この中でforループについても削減可能と判断しました。

オリジナルの実装ではオーディオ処理関数に渡るデータ形式が重要でした。
この点は改善案でも特に変わるものではありません。

オリジナルと異なるのはその実現手法です。
ここで実際のコードを示します。

   for (index = 0; index < AUDIOBUFSIZE; index+=2) {
       audio_effect_through(
               &effect_param,
               rxbuf + (index + 0), rxbuf + (index + 1),
               txbuf + (index + 0), txbuf + (index + 1));
   }

オーディオ処理関数へL-Rのステレオデータを揃えて渡す部分はコールバック関数とし、オーディオ処理関数内部で直接出力データを格納させる形式としました。

オリジナルに存在した前段と後段における形式変換用メモリコピーを排除することができます。
これにより内部作業用バッファinputBufferとoutputBufferも不要となりました。

audio_effect_throught関数はオーディオデータをスルーコピーする関数です。

   void audio_effect_through(
           const effect_param_t *param,
           const AUDIOSAMPLE *in_left,
           const AUDIOSAMPLE *in_right,
           AUDIOSAMPLE *out_left,
           AUDIOSAMPLE *out_right)
   {
       const int var0 = param->var0;
       const int var1 = param->var1;
       *out_left = ((*in_left) >> 10) * var0;
       *out_right = ((*in_right) >> 10) * var1;
   }

オーディオエフェクト関数には1サンプル毎に処理を依頼します。
処理結果は関数に渡されたバッファへのポインタを用いて直接格納します。


要するに冒頭にあった最小限の処理に近くなる仕組みです。



上記により中間バッファを排除しながらも、渡されるデータ形式はL-Rのステレオで揃っているという状況を作ることができます。
また、バッファサイズ分のforループも1回で済むようになり、効率改善が期待できます。

改善の効果

ここで実際の処理時間に与える効果を調査しました。
処理時間を測定するために、オーディオ処理ブロックに差し掛かったところでGPIOをハイレベルにし、オシロスコープにより観測します。

ここでの処理対象は1オーディオサンプルブロックでAUDIOBUFSIZEバイト分のデータです。
初めにオリジナル実装でオーディオスルーにかかっている処理時間を示します。
1オーディオサンプルブロックの処理に約70[us]の時間を要しています。


次に改良した実装でオーディオスルーを行なった場合の処理時間を調べます。
同じ1オーディオサンプルブロックの処理を約25[us]の時間で処理していることがわかります。


オリジナルの実装に対して約35%の時間で同等の処理が実現できる事が確認できました。
削減できた約45[us]は別の演算に割り当てることができます。
従来より高度な演算も可能な他、他のタスクにプロセッサを素早く譲ることができるようになります。

改善のまとめ

オーディオ処理関数の呼び出し方を変更し、主記憶上におけるメモリコピーを大幅に削減しました。
結果的に処理時間を削減でき、従来よりもシステムプロセッサを効率的に運用することが可能となりました。

今回の改善でオーディオエフェクト部分の設計者はデータ長を気にすることなく、1サンプルの処理に集中して記述できるようにもなりました。
オーディオエフェクト関数を同じパラメータで作成すれば、関数ポインタの切り替えのみでオーディオエフェクト処理を切り替えることが可能となります。


当然のことですが、オーディオ処理では特定サンプルに対して、時間軸方向前後のデータも用いてフィルタリング処理を行ないます。
提案手法ではオーディオ処理関数に渡ってくるデータは1サンプル分のみですが、オーディオエフェクト関数内部で静的メモリを保持し、バッファリングしながら処理をすることで、時間軸の前後方向のデータも用いて処理することが可能です。

RTOSを用いたシステムを構築する場合、局所的に見た性能改善とシステム全体を見た性能改善の双方の視点が欠かせません。
今回行なった性能改善は局所的なものですが、タスクの性質上高いプライオリティで動作します。
このタスクの処理時間を削減するだけで、他のタスクで行うことの出来るサービスが飛躍的に増えます。
例えば、ディスプレイに表示したい内容を別のタスクに伝達しようとか、そういった付加的なサービスにプロセッサ時間を使う事ができます。こういった要素が「他と違う何か(=付加価値)」に繋がっていきます。

なお、実験結果はコンパイラの最適化オプションを外した状態で行ないました。
最適化を施すことで、場合によってはより大きな改善が得られる可能性もあります。

0 件のコメント:

コメントを投稿