Zynqによるオーディオ処理 - アルペジエータ効果

Taylor Roorda(記)、最終校正2019年6月10日

はじめに

ミュージシャンとして、またエンジニアとして、機材を自作することは私の長い間の目標でした。少し前にSparkFunのProto Pedalキットを見つけて、最終的に始めるには良い機会かもしれないと思いました。どのようなエフェクトにするかを決めるのに時間がかかりました。私はすでに、標準的なドライブ、リバーブ、ディレイ効果をカバーする基本的な機器は一式持っているので、何か違うことをやってみたかったのです。エフェクトをデジタルで実装することで、後からコードで簡単に変更できる自由度があることもわかっていました。やがて、Earthquaker DevicesのArpanoidというギター用に設計されたアルペジエータペダルのことを思い出しました。Arpanoidの音の可能性に興味を持った私は、アルペジエータペダルのDIYに挑戦することにしました。

このコンセプトを実現するために、目標の処理プラットフォームとしてXilinxのZynqを選択した理由は2つあります。1つ目は、Digi-Key でペダルのフォーム ファクタに収まる小型で低コストのモジュールを見つけたからです。2つ目は、XilinxのHLS (High Level Synthesis:高レベル合成)開発システムを試してみたかったからです。HLSをあまりご存知でない人のために説明すると、これは C、C++、またはSystemC のコードからHDLコードを作成する合成ツールです。私は、できるだけ多くのアルゴリズムをファブリックで処理したかったので、HLSのアプローチは、開発時間を節約する良い方法であると思いました。

この記事を書いている時点では、デジタル部分のみが完成しており、アナログハードウェアはまだ実装されていませんが、とにかく面白い部分はすべてプログラミングにあるので、この記事ではその点をカバーします。ハードウェアの主な検討内容は、記事の最後に概要を記載しています。

アルゴリズムの概要

下図は、アルペジエーションアルゴリズムの実装に必要なブロックを示したものです。

図1. アルペジエーションアルゴリズムの概要

オーディオデータはI2Sでデザインに入り、2つのパスに分けられます。1つのパスは入力のエンベロープを計算し、もう1つのパスはアルペジエーションシーケンスを計算します。コアアルゴリズムは、FPGAファブリックに完全に実装されています。ARMプロセッサは、制御入力を読み込んで、対応する値をファブリックレジスタに書き込むだけです。ARMが実際に扱うオーディオデータはありません。

I2Sインターフェースを除き、すべてのカスタムIPブロックはXilinxのHLSツールでインプリメントされました。すべての計算は、Xilinxのap_fixed.hライブラリを使用した、固定小数点演算を使用して実行されました。各ブロックのヘッダファイルには、固定小数点数の幅と2進小数点位置が記述されたtypedefが含まれていることに注意してください。

データパスに関与するすべてのブロックは、基本的なAXI Streamプロトコルで通信します。制御レジスタは、ARMプロセッサからAXI Liteインターフェースを使用して書き込まれます。AXIプロトコルの詳細については、XilinxのUG761 AXI User’s Guideを参照してください。

左から右へ、各ブロックの詳細を以下に説明します。また、各セクションには、関連するソースコードと HLS 合成結果の要約が含まれます。使用したHLS最適化ディレクティブは、それぞれのセクションに記載されます。作成された各IPブロックに使用されたテストベンチは、最後にメインプロジェクトファイルに含まれます。

I2Sインターフェース

image
ペダルの主な入出力はアナログなので、オーディオコーデックとのやりとりが必要です。I2Sは4つの信号を使用します。

  • BCLK(Bit clock):BCLKの立ち下がりエッジで各ビットが変化します。通常、サンプリング周波数の倍数です。
  • LRCLK(Left/right clock):転送されているサンプルが左(ロジック0)か右(ロジック1)かを示します。このクロックはサンプリング周波数で動作します
  • SDIN(Serial data in):ADCからの入力サンプル。データはLRCLK遷移後、BCLKの2番目の立ち下がりエッジで有効になります。データはMSBファーストで転送されます。
  • SDOUT(Serial data out):DACへの出力サンプルです。タイミングはSDINと同じルールに従います。

もちろん、メーカーごとに命名規則が異なることもあります。

この例ではI2Sインターフェースをマスターとして使用していますが、必要に応じてスレーブとして動作するように簡単に設定することができます。1回のサンプル転送はBCLKの32サイクル続きますが、実際に使用されるビット数はパラメータで指定します。それ以外のビットは捨てられます。この例では24 ビットのサンプルを想定しています。

フルサンプルがシフトインされると、サンプルは32ビットに符号拡張され、AXI Streamを介してブロックの外に転送されます。このデザインでは、左右のチャンネルがそれぞれ独自のストリーミングインターフェースを持ちますが、必要に応じて同じインターフェースにインターリーブすることができます。一般的なギター信号はモノラルなので、いずれにせよ左チャンネルしか使用しません。図2は、I2Sインターフェースの入力転送の例です。


図2. I2Sの転送

ソースコード

i2s_axis (Top Level).txt(9.9 KB)
i2s_interface.vhd(4.1 KB)
i2s_recv_axis.txt(11.8 KB)
i2s_send_axis.txt(5.4 KB)

ピッチ検出
image

アルゴリズムの中で最も重要かつ厄介な部分である正確なピッチ検出は、間違いなくアルペジエータの核となるものです。ピッチ検出には自己相関、ケプストラム分析、FFTなどさまざまな手法が評価されましたが、最終的にはYINの手法が最も良い結果を残しました。YINアルゴリズムは、要するに重み付けされた差分関数です。ある関数をあるウィンドウでシフトしたものと比較することで、信号の基本(またはルート)周波数を抽出することができます。このアルゴリズムの詳細については、CheveigneとKawaharaによるオリジナルの論文を読むことをお勧めします。

YINブロックのコアとなる機能は、論文にある式6です。

image

その結果を式8に従って正規化します。

image

そして、設定された閾値より小さいτの最小値を求めることで、ルートを決定します。このアプリケーションでは、0.5より小さい最初のτ値をルートとします。

そして、τは、この関係によって、周波数に直接変換することができます。

image

標準チューニングのギターは、E線開放部の82.41Hz(E2)から、1弦24フレットの1300Hz(E6)付近まで、幅広い周波数帯域を持っています。この広い範囲に対応するために、多くのτ値を計算する必要があります。また、標準チューニング以外のチューニングも考慮する必要があります。低周波をカバーするために、τ値は900、つまり49Hzが選ばれています。高周波数帯は、τ値25、つまり1764Hzを選択しました。幸いなことに、25から900以外の値は無視できることになりますが、それでも875ポイントの計算が必要となります。

τ = 900まで計算できるようにするためには、この関数は少なくともその2倍のサンプル数を必要とします。そのため、YINブロックは計算を開始する前に1800個の正規化されたサンプルのバッファを埋めます。この処理は非常に遅く、44.1kHzで約40msかかります。入力をオーバーサンプリングすることで時間を短縮することができますが、効果はLFO成分(後で詳しく説明します)に依存するため、この待ち時間は許容範囲内です。

バッファが満たされると、上に示した関数が計算されます。差分関数を実行するi_loopは、HLSで部分的にアンロールされ、エリアを犠牲にして並列性と性能を高めています。アンロールファクタは、エリアとレイテンシーの間で妥当な5が選ばれました。τの各値は以前の値に依存するため、大きなtau_loopは残念ながらアンロールすることができません。

τの各値は、選択した閾値0.5以下かどうか、また、0.5以下の場合は局所最小値かどうかが確認されます。これらの条件を満たす最初の値がルートとして選択され、DDSコンパイラが要求する位相増分形式に一致するようにスケーリングされます。

ソースコード

yin_cpp.h (651 Bytes)
yin.cpp (2.5 KB)

合成の結果

image

シーケンスの生成

image

シーケンサはアルペジエータアルゴリズムの2番目の主要ブロックです。これは、コントロール入力が登場する最初のブロックです。各制御入力は、XADCまたはGPIOを介してARMプロセッサによって読み取られ、データ処理IPブロック用にフォーマットされます。シーケンサへの入力をまとめると、次のようになります。

  • Step(7:0) - シーケンスの音数、1~15
  • Direction(7:0) - シーケンスの方向を設定 1 = 上、-1 = 下、0 = 上&下
  • Mode(7:0) - シーケンスのオクターブや調性を設定(範囲は1~8)
    • モード 1 - メジャー、-1オクターブからルートまで
    • モード 2 - メジャー、ルートから+1オクターブまで
    • モード 3 - メジャー、-1オクターブから+1オクターブまで
    • モード 4 - 予約
    • モード 5 - マイナー、-1オクターブからルートまで
    • モード 6 - マイナー、ルートから+1オクターブまで
    • モード 7 - マイナー、-1オクターブから+1オクターブまで
    • モード 8 - 予約

まず、シーケンサはモードが変更されたかどうかを確認します。もしそうなら、ルートから1オクターブ下から1オクターブ上まで、2オクターブにわたって15の半音階の計算をします。音楽理論では半音階は数学的には2の(1/12)乗の倍数であるとされています。2の(1/12)乗から2の(11/12)乗までの各係数は、定数としてメモリに保存されます。

次に、シーケンサはLFOの立ち上がりエッジをチェックします。もし立ち上がりエッジがあれば、3つのアクションが実行されます。最初に、現在の音符が音階インデックスに従って設定されます。第2に、現在の旋律の最初の5つのハーモニックスの位相増分が計算され、保存されます。第3に、スケールインデックスが方向に基づいて更新され、必要に応じて折り返されます。

最後に、次の5クロックサイクルに渡って、前節で計算されたハーモニックスがDDSコンパイラに送出されます。DDSブロックからのエラーを避けるため、出力データにはフレームの終わりを示すTLAST信号が必要です。しかし、最終的なブロック図では、tlast_ap_vldがDDSブロックのTLAST入力に接続されていることにお気づきでしょう。これは実は間違いだったのですが、結果的に私に有利に働きました。以下のソースコードでは、出力変数tlastは5つのサンプルのうち最後のサンプルで設定され、クリアされることはありません。しかし、tlastのデータ有効信号は、最後の出力サンプルの間に1クロックサイクルだけアサートされるという望ましい動作をしています。

ソースコード

sequencer.h(296 Bytes)
sequencer.cpp(6.5 KB)

合成の結果

image image

ノートの合成

image

シーケンサの出力には、演奏する必要のある音のルート周波数が含まれています。しかし、純粋な正弦波や矩形波はそれほど音楽的ではありません。よりリアルな音を作るために、先端を切り詰めた正弦波のみのフーリエ級数でギター信号を近似しています。この実装では5つの項が使用されています。係数は、録音されたギターサンプルのFFTから、最初の5つのハーモニックスの相対的なピーク値を選びました。

DDSコンパイラはフーリエ級数の正弦項を生成します。出力はシーケンサによって提供されたルート周波数の最初の5つのハーモニックスに対応する5つのインターリーブされたチャンネルです。インターリーブされたサンプルはノートシンセブロックに流れ込み、そこで機能を実行します。

ここで、係数は以下の表1に従って与えられます。

a1 a2 a3 a4 a5
0.53803 0.90710 1.00000 0.86922 0.55631

表1. 合成の係数値

y(t)の最大値は約3.2であるため、24ビットの範囲内に収まるようにスケーリングファクタ¼を使用しています。

ソースコード

synth_cpp.h(358 Bytes)
synth.cpp(863 Bytes)

合成の結果

image image

ADSRエンベロープ

image

ADSRは、「Attack Decay Sustain Release」の頭文字を取ったものです。楽器やシンセサイザが奏でる音のエンベロープを表現するのに使われます。ADSRエンベロープを使用すると、鋭いスタートとストップのエッジがなく、よりリアルなサウンドのノートが作成されます。ADSRブロックは、1つのコントロールインプットを持っています。

  • Rate(31:0) - LFOの1周期あたりのクロックサイクル数

エンベロープを適用する前に、入力データは最大サンプル値でスケーリングされ、結果が-1から1の範囲に収まるようになっています。入力と係数の両方がその範囲にある場合、最終出力もその範囲にあります。そのため、他のブロックが通常32ビットの整数出力であるのに対し、結果は整数1ビット、小数31ビットの固定小数点値として蓄積されます。

ADSRエンベロープは、LFOの周期のおおよそのパーセンテージでスケーリングされたシンプルな線形セクションで構成され、LFOの立ち上がりエッジごとにリセットされます。四捨五入の関係でパーセンテージの合計がフルウィンドウに満たない場合、残りのサンプルにゼロを掛けます。図3は、1秒間のADSRエンベロープの例です。

図3. ADSRエンベロープ

セクションの内訳は以下の通りです。

  • Attack:振幅が増加し、周期の15%にわたって0 → 1となる。
  • Decay:振幅が減少し、周期の15%にわたって1 → 0.8となる。
  • Sustain:振幅は一定で、 周期の30%にわたって0.8→0.8のままである。
  • Release:振幅は減少し、 周期の40%にわたって0.8 → 0.05となる。

合成ブロックからADSRステージに入るデータは、サンプリングレートが20MHzです。出力のサンプリング周波数は44.1kHzです。そのため、データは出力段に送られる前に454倍にダウンサンプリングされる必要があります。

ソースコード

adsr.h(283 Bytes)
adsr.cpp(2.7 KB)

合成の結果

image image

エンベロープ検出

データパスの最終段は乗算器であり、合成されたシーケンスと入力信号のエンベロープの積を生成します。ドライな入力信号はデジタル処理されないので、これによって出力振幅が入力に追従するようになります。

入力信号のエンベロープを求めるために、まず信号を85倍にダウンサンプリングします。サンプリングレートを下げて、データの絶対値をカットオフ周波数10Hzの3次バターワースLPFに通します。フィルタの出力は、次にCICフィルタによって同じ係数85で補間されます。得られたエンベロープは合成された波形と掛け合わされ、最後にI2Sインターフェースに戻され変換されます。

以下のコードと合成結果は、エンベロープフィルタ部分のみです。ダウンサンプラーのコードと利用方法は簡単で、メインプロジェクトのソースに含まれています。

ソースコード

envelope_filter.cpp(767 Bytes)

合成の結果

image image

出力乗算器

image

最終ステージは、合成されたデータのストリームを受け取り、入力信号の計算されたエンベロープで乗算する、単純なAXIストリーム乗算器です。各入力はAXIストリームFIFOによってバッファリングされ、乗算器は両方のラインにデータが存在するときのみ動作するようになっています。出力乗算器はHLSで作成されましたが、コードは些細なものであり、ここには含まれていませんが、フルプロジェクトで見ることができます。

制御

image

各制御入力(Mode、Direction、Steps、Rate) は、ARMからファブリックに供給されます。ModeDirectionはスイッチから来るGPIO入力から、StepsRateはXADCによって読み込まれるポテンショメータの分圧から読み込まれます。XADCは将来的に4つの外部電圧を使用できるように設定されていますが、ここではそのうちの2つだけが必要です。Rateは、AXIレジスタの1番目に書き込まれる32ビット値です。Mode、Direction、Stepsは、8ビット値を32ビットワードに結合したもので、2番目のAXIレジスタに書き込まれます。

また、RateはAXIを介してLFOタイマに書き込まれます。タイマのpwm出力は、LFO信号を必要とするファブリック内のさまざまなブロックに供給されます。LFOタイマの出力を外部LEDにルーティングして、現在のテンポを視覚化することができます。

ARM上で動作するコードの初期サンプルは以下の通りです。
ARM Pedal Control.txt(4.7 KB)

ハードウェアに関する考察

プロジェクトの大部分はデジタルの世界に存在しますが、現実の世界とのインターフェースとして、外部からのアナログ部分が必要です。

冒頭で述べたように、この設計のハードウェア部分はこの原稿を書いている時点では実装されていませんが、これらはソフトウェアのデジタル部分の設計で念頭に置いた部品と回路です。

Digi-Keyから入手可能なパーツ

Sparkfun Proto Pedal Kit
Zynq “Soft Propeller” Module
AK4556VT Codec
OPA134 Op Amp

プロトペダル

image

このプロジェクトの原型となったSparkfunのProto Pedalキットは、ギターのシグナルチェーンとペダルのエレクトロニクスをつなぐインターフェースとして機能しています。メインのプロトタイピングエリアに加え、真のバイパススイッチング用3PDT、9Vバッテリおよび1/4インチ楽器ケーブル用のコネクタを搭載しています。迅速なプロトタイピングソリューションとして、このキットはこれから始める方に最適です。

Zynqモジュール

image

プロトタイピング用のZynqボードとして選ばれたのは、Trenz ElectronicのTE0722です。このボードは、この記事の執筆時点でDigi-Keyが提供する最も低価格のZynqボードで、40ピンの18 x 51mmパッケージにZynqシステムをフル搭載しています。ただし、このモジュールは外部DDR RAMを搭載していませんが、内蔵RAMはこのアプリケーションには十分すぎるほどであることに注意してください。

入力バッファ

ペダルへの信号減衰を防ぐため、入力インピーダンスを高く保つ必要があります。このため、単純なオペアンプのバッファを使用します。12MΩと2MΩの抵抗による分圧で1.5VのDCオフセットを作り、シングルエンディッドコーデックのレンジの中央に信号を配置します。トランジスタやJFETの入力バッファがあれば、音にも好ましいのですが、これはデモ用として動作します。

図4. 入力バッファ回路

コーデック

24bitコーデックは、デジタルとアナログの世界の架け橋となるものです。AKMのAK4556が選ばれたのは、20ピンのTSSOPパッケージがかなり試作しやすかったことが主な理由です。メーカーの資料によると、回路は比較的シンプルで、いくつか外付けの受動部品が必要なだけです。

このコーデックはシングルエンドであり、DCオフセットが必要であることに注意してください。アルペジエーションシステムは、符号付き入力を受け取るように設定されていますが、最終の実装段階では調整が必要です。

このアプリケーションでは、コーデックがI2S スレーブであり、BCLKとLRCKがZynqから提供される入力であると想定しています。コーデックのシステムクロックであるMCLKは常に供給される必要があります。以下の設定を前提としています。

  • fs = 44.1 kHz
  • MCLK = 512fs = 22.5792 MHz
  • BCLK = 64fs = 2.8224 MHz

図5. コーデック回路

出力ミキサ

一方、シンセサイザ信号とドライ信号は、最終的にペダルから送り出される前に合成される必要があります。それぞれの信号には、ミックスを調整するためのレベルポットがあり、2つの信号は単純なオペアンプによる加算器で加算されます。

図6. 出力ミキサ回路

ダウンロード

ソースコード、テストベンチ、サンプルデータ、WAVオーディオサンプルを含むプロジェクトの完全なディレクトリです。一部のファイルパスは変更する必要があるかもしれませんが(特にHLSテストベンチ)、必要なファイルはすべて揃っているはずです。

Pedal Project Source

結論

アルペジエーションは計算が複雑なアルゴリズムであり、FPGAプラットフォームと相性がいいです。可能な限り演算を並列パスに展開することで、従来のプロセッサ実装と比較してレイテンシを大幅に削減することができます。エンベロープ検出とアルペジエーションの機能についても、データストリームを並列パスに分割することで、すべてを順次実行するよりもはるかに効率的です。FPGAのエリアが限られていても、計算全体を比較的短時間で実行することができます。さらにエリアを増やせば、レイテンシをさらに短縮できるでしょう。

複雑な設計の結果として、シミュレーションに非常に長い時間がかかります。そのため、一度に何時間も何日も待つことなくシステムの徹底的なテストを行うことは困難です。私が妥当な時間でシミュレーションできたのは、せいぜい1秒程度のデータでした。このため、シミュレーションでは捕捉できなかった、処理が期待通りに動作しないバグがあるかもしれません。また、タイミング制約もまだデザインが最終的に確定していないため、実装段階で誤差が生じる可能性があります。

この設計には、他にも多くの改善点があると思われます。まず、ピッチ検出に必要なサンプル数を減らすことができれば、レイテンシを大幅に削減することができます。エンベロープ検出ステージも少し初歩的で、ゲインを調整することでより入力に追従することができます。最終的には、シーケンスのタイミングをコントロールするためのタップテンポオプションが理想的でしょう。

最終的に、このプロジェクトは将来、他のオーディオ処理に取り組むための強固な基盤を作ることになりました。I2Sインターフェースは、ほぼすべてのコーデックとのインターフェースに使用でき、XilinxのDDSとCICコンパイラは、あらゆる種類の変調タイプの効果に役立ちます。




オリジナル・ソース(English)