お知らせ:この記事はJLCPCBの提供でお送りしています。

ESP32+FPGAオーディオ基板の解説記事(第1回第2回第3回第4回第5回)、いよいよ最終回です。今回はPCM音源の中核をなしている演算パイプラインの構成について解説します。

2種類の「サンプル」

演算パイプラインの構成について説明する前に、この後の説明で出てくる「サンプル」という単語の使い方について説明しておきます。

ここまでの記事を読んでくださっている方であれば、「サンプル」がどういうものかという認識は持っていただいていると思います。しかし、今回の記事では、この単語が指すものが2種類出てくるため、混乱を避けるためにあらかじめここで説明しておきます。

まず、1つ目の「サンプル」はSPI Flashに格納されている楽器音のデータです。一般的には数秒~数十秒の音のデータ全体(例えばピアノのラの音を弾いて音が消えるまでのデータ)も「サンプル」と呼ばれますが、特に断りがない場合この記事ではそのような音のデータの集まりは「サンプル列」と表記し、サンプル列の中の1要素を「サンプル」と呼びます。

2つ目の「サンプル」は最終的にDACに送り込まれるデータを指す「サンプル」です。1つ目の説明に書いた「サンプル」を演算パイプライン内で処理した結果得られるのがこちらの「サンプル」です。この記事ではこちらの意味での「サンプル」は「出力サンプル」と表記します。

演算パイプラインの処理ブロック

用語の定義が済んだところで、演算パイプライン内部の解説に進みます。今回は上の図に示す演算パイプライン内の処理ブロックを、左から右に順に説明していきます。

タイミングコントローラ

まずはタイミングコントローラです。タイミングコントローラはこの先の演算パイプラインの各処理が破綻しない適当な間隔でパイプラインにデータを流していく役割を担っています。今回の実装では、

  • 音声出力のサンプリング周波数:48kHz
  • 同時発音数:16音
  • クロック周波数:49.152MHz

という条件なので、出力サンプルを1つ生成するのに使えるクロック数は49.152M / 48k = 1024クロックとなります。この1024クロックの間に16音分の処理を順繰りに行う必要があるので、単純計算だと1音に使えるクロック数は最大で64クロックと求まります。今回は処理をパイプライン化しているので、処理全体が64クロックで終わる必要はなく、各処理ブロックが64クロック以下で処理を終えれば全体の処理が破綻せず進む1、ということになります。

一方、今回の演算パイプラインの中で最も処理クロック数が必要となるのはSPI Flashコントローラで、処理1回につき62クロックを必要とします。ここから、今回は1スロットのクロック数を62クロックとして、演算パイプラインを動かしています。また、タイミングコントローラ自体が動作を開始するためのトリガはI2Sマスタから得ています。前回の記事で紹介したI2Sマスタには、出力サンプルを左右チャンネル分出力し終わったタイミングでトリガ信号を生成する機能を持たせています。左右チャンネル分の出力サンプルを出力し終わるごとにトリガが発生しますので、タイミングコントローラには48kHz周期でトリガがかかることになります。このトリガを受けた後、タイミングコントローラは62クロック(1スロット)ごとに、パイプラインの後続の要素に対して、16音分のデータを順次処理するようにトリガをかけていきます。

タイミングコントローラはコマンドデコーダから受け取った各音源スロットごとの発音開始・終了情報を保持しています。発音の開始時と終了時は演算パイプラインの後段で通常の処理に加えて特別な処理が必要になるため、タイミングコントローラは演算パイプラインにトリガを掛ける際に、処理対象の音源スロット番号と、発音開始・終了フラグ情報もサンプルカウンタに送出しています。

サンプルカウンタ

サンプルカウンタは次にSPI Flashから読み出すべきサンプルのアドレスを算出するブロックです。サンプルカウンタはカウント値(読み出すべきサンプルのアドレス)の他に、次に述べる設定値を保持します。

  • 再生開始位置
  • ループ開始位置
  • サンプル列の長さ
  • 再生速度

サンプルカウンタは基本的には再生速度に応じた速度でカウント値をカウントアップする動作を行います。得られたカウント値は後段のSPI Flashコントローラに渡され、カウント値が指すアドレスに格納されているサンプルが読み出されます。カウント値が順次カウントアップされていくと、ひとつながりのサンプル列が読み出されていくことになり、楽器音として我々の耳に聞こえるようになります。1度にカウントアップする量を増減することで、サンプル列を読み出すスピードを変えることができます。サンプル列を読み出すスピードが変われば音程も変わるので、これを使ってMIDI入力の音程に合わせた音程の楽器音を生成することができます。

一般的な十二平均律では、ある音とそれより半音高い音の周波数の比率は1:1.059463(2の1/12乗)となります。つまり、サンプルカウンタのカウントアップ速度を1.059463倍にすると、元のカウントアップ速度の時より半音高い音が生成されることになります。1.059463という細かい倍率をきちんとセットできるように、カウント値および1度にカウントアップする量の設定は整数ではなく小数値で持つようにしています。

また、今回は単純にサンプル列をワンショットで再生するだけでなく、サンプル列の一部の区間をループ再生できる設計にしています。一般的に、ピアノなどの減衰音を出す楽器の音は発音の初期(ピアノであればキーが押されてハンマーが弦を叩いた直後)に複雑な周波数成分を持った、楽器ごとの特徴が表れる音が鳴り、次第に正弦波のような単純な音に近づいていくことが知られています。そのため、発音の前半の楽器の特徴が出る領域はそのままサンプルを順次読み出していき、後半の比較的単純な波形はループ再生とすることで必要となるメモリ領域を節約することができます。

SPI Flashコントローラ

サンプルカウンタで算出されたカウント値はSPI Flashコントローラに送られます。SPI Flashコントローラは受け取ったカウント値をもとに、FPGAに接続されたSPI Flashと通信しサンプルを読み出します。

SPI Flashは一般的には8ピンのICで、SPIバスのSCK,MISO,MOSI,SSの4つの信号線を使ってデータを読み書きできるFlashメモリです。SPIバスはマスタからスレーブ方向のデータ線がMOSI、その逆がMISOと、1bit単位での通信となります。ただし、SPI Flashの品種によっては通信速度の高速化のために、SPI Flashの8ピンのうち4ピンを双方向のデータ線として使って4bit単位で通信するQPIと呼ばれるモードをサポートしている場合があります。2今回の実装でも、QPIモードに対応しているSPI Flashを採用し、高速にサンプルを読み出せるようにしています。

QPIモードを使うことで、SPIモードに比べて高速にサンプルが読み出せるようになってはいますが、それでも4bit単位での通信となるため、あるサンプルを読み出す場合に、

  • コマンド発行 8bit:8bit/4 = 2サイクル
  • メモリアドレス 24bit:24bit/4 = 6サイクル
  • データ出力待ち:4サイクル
  • データ読み出し 16bit:16bit/4 = 4サイクル

と、合計16サイクルが必要となります。1サイクル=2クロックとなるので、16bitのサンプルを1つ読み出すのに、32クロックかかることになります。また、今回の実装では、線形補間器の節で後程説明する通り、1スロットあたり2つのサンプルを読み出す必要があります。そうなると合計で64クロックかかることになります。上記の通信の他にSSピンの制御の時間を加えると、1スロットのクロック数の制約である64クロックをオーバーするため、ここからさらにサイクル数を削る必要があります。幸いQPIモードには、読み出しコマンドを繰り返し発行する場合、コマンド発行を省略できる(省略された場合読み出しコマンドが発行されたものとみなす)という設定があります。これを使うことで合計4サイクルを削り、1スロット=64クロックの制約を満たすようにしています。

演算パイプラインが稼働している時のSPI Flashコントローラの動作はここまでに書いた通りですが、SPI FlashコントローラはSPI FlashをSPIモードからQPIモードに切り替えたり、逆にQPIモードからSPIモードに切り替える設定を行う機能も持っています。電源投入直後のSPI FlashはSPIモードで起動しているため、演算パイプラインを動かす前にQPIモードに切り替える処理が必要になります。また、ESP32からSPI Flashへの書き込みを行う場合はSPIモードで通信を行うため、この時はQPIモードからSPIモードに切り替える処理が必要になります。これらの処理はSPIスレーブ・コマンドデコーダ経由でESP32から書き込まれる、設定レジスタの特定のbitへの書き込みをトリガとして実行されます。

線形補間器

SPI Flashコントローラの次は線形補間器です。サンプルカウンタの節で説明した通り、出力される音の音程を自由に変更するためには、サンプルの読み出し速度を細かい刻みで変更できることが必要です。しかし、サンプルの読み出し速度を変えていった場合、カウント値が小数値となってしまう問題が発生します。上の図のように、例えばサンプルを1.5倍の速度で読み出す場合、0→1.5→3とカウント値が遷移していきます。しかし、サンプルを格納しているSPI Flashには、1.5というカウント値に対応したサンプルは格納されていません。この問題に対応する処理として、線形補間処理を行うのが線形補間器です。

先に書いたように、1.5というカウント値に対応したサンプルはSPI Flash上に存在しません。最も簡単な解決策としては、1.5に近い、カウント値1か2のサンプルを読んで代用してしまう方法が考えられます。もちろんこれでもそれなりに音は出るのですが、代用元のサンプルと、本来カウント値1.5の場所にあるべきサンプルの値にはずれがあるため、常に正しいサンプルを読み出せる等倍速再生時に比べると音質が悪化してしまいます。そこで、線形補間処理を行い、カウント値1のサンプルと、カウント値2のサンプルから、カウント値1.5のサンプルの値を近似します。線形補間もあくまで近似ではありますが、音質改善にかなりの効果があります。

線形補間処理はRyuzのブログさんの記事にある、1クロックで1bit分の精度を上げる回路を実装しています。1スロットに64クロック使うことができるので、回路規模削減の観点から線形補間回路は複数個用意せず、1個の線形補間回路を16クロック動かして16bit分の精度を上げる処理を行っています。

音量制御

音量制御は読んで字のごとく、音量を制御するブロックです。現状では最低限の実装として、発音終了フラグが立っている場合と、ループ再生を行わないモードで、サンプル列の末尾まで再生が終わってしまった場合に、音量を0に落とす処理のみを行っています。

今後は音の強弱カーブを生成するEG(Envelope Generator)を実装し、ピアノのような音量の強弱を再現できるようにする予定です。

アキュムレータ

演算パイプラインの最後の処理ブロックがアキュムレータです。アキュムレータは「累算器」とも訳される処理ブロックで、入力値を順次足し合わせて累計値を求める機能を持ちます。アキュムレータの手前までの処理ブロックは、音源の各スロットに対してそれぞれ処理を行っていましたが、アキュムレータでは全16スロット分のここまでの処理結果を加算し、その累計値を出力サンプルとして、I2Sマスタに渡しています。

まとめ

本連載記事ではESP32とFPGAを搭載したオーディオ基板を作成し、その基板上に実装したPCM音源の構成について紹介しました。音量制御ブロックの機能増強や、SPI Flashに書き込んでおくサンプル列の管理ソフトウェアの開発など、まだまだやりたいことはいろいろありますが、ここまで実装すると、下のTweetにあるようにまあまあそれらしい音を出すことができます。

連載記事は完結しましたが、引き続きJLCPCBとコラボした記事は継続掲載予定です。次回は自作キーボードの製作記を紹介する予定です。お楽しみに!

  1. ちょっと直感的ではないかもしれませんが、64クロック × 16音 = 1024クロック後に次のサンプルのための処理が実行できればよい、と考えると分かりやすいかと思います。
  2. 詳細は説明しませんが、SPI,QPIモードの他ににDIO(Dual I/O)、QSPI(Quad SPI)と呼ばれるモードをサポートしている場合もあります。
公開日:2022/08/31