今回は、PicoのPWMをDA変換に使って、ゲームの効果音などに使える音を出してみます。
PicoでもPWMで矩形波を出して、ArduinoのTone()関数のようなブザー音を出すことはできます。しかし、この方法は音量が調節できず、波形も矩形波のみです。
PWMを使って、矩形波以外の音を出す方法として次のようなやり方があります。
耳に聞こえないほど高い周波数(固定値)の矩形波を生成し、デューティ比を音声信号で変調します。矩形波自体は1か0の2値ですが、この矩形波をローパスフィルタに通すと、変調に使った音声信号を取り出すことができます。
デューティ比を変調した矩形波をローパスフィルタに通すとなぜ変調に使った信号を取り出せるかというと、直感的には、PWMでLEDの明るさを制御するのと同じと考えると分かりやすいと思います。LEDは実際にはオンとオフだけで点滅しているのですが、「残像の効果」で中間的な明るさで点灯しているように見えます。この「残像の効果」に相当するのがローパスフィルタというわけです。
今回の記事で使うのはこの方法です。ハードウェアとして外付けのローパスフィルタが必要になりますが、これは以下のようにコンデンサと抵抗1つずつで作れる回路です。コンデンサや抵抗の値は2倍くらい違っても大丈夫です。ローパスフィルタの出力はヘッドフォンやアンプに接続できます。VGA demo baseをお持ちの場合は、PWM Audio出力が使用できます。
実際のところは、キャリアとして使っている矩形波は人の耳では聴こえない周波数ですから、ローパスフィルタを入れずにヘッドホンやイヤホンを直結してもまあ音は出ます。なお圧電ブザーは、蚊の鳴くような音量しか出ません。
GPIOはGP27を使います。これはVGA demo baseの設計に合わせています。左右のチャンネルを使う場合はGP28も使用しますが、ステレオ対応ではなく、左右とも同じ音が出ます。
次にソフトウェアですが、今回作ったのは簡易的なシンセサイザです。矩形波・鋸波・三角波とノイズが複数チャネル出力でき、周波数と音量が設定できます。ファミコンの効果音のようなものを作るのに手ごろな感じのものを目指しました。VGA版ブロック崩しでも使用しています。
最初に使い方の例を挙げておきます。 呼び出す側は以下のコードのように波形(psg_type)・周波数(psg_freq)・音量(psg_vol)を指定します。第一引数はオシレータの番号です。
実際に鳴らすとこんな音が出ます。
同時に複数の音を出すこともできます。以下の例はノイズも含め4音を同時に出しています。
コード全体は以下のリポジトリにアップロードしてあります。
以下、中身の説明も簡単に書いておきます。
まず、出力する音声の量子化ビット数とサンプリングレートですが、今回は10ビット・約122KHzとなっています。
PWMを使ったDA変換は、基本的には出力したいアナログ量を矩形波のデューティ比として指定するだけです。
従って、デューティ比の指定に用いるビット数がそのまま量子化ビット数になります。サンプリングレートは矩形波の周波数です。
これらは独立には決められません。「デューティ比の段階の数」と「サンプリング周波数」の積がPicoのPWMのクロックになります。標準でクロックは125MHzですので、例えば量子化ビット数が10ビット(=1024段階)なら、サンプリングレートは
125,000,000 / 1024 = 122,070 Hz
となります。
これはPWMのハードウェアが下図のような仕組みになっているためです。PWMのカウンタは値が1クロックごとに増えていき、ある指定値を境に、カウンタがそれより小さい値ならH、それ以上の値ならLをGPIOが出力します。最大値に達するとカウンタはリセットされます。このリセットの時間間隔がサンプリングレートで、カウンタの最大値が量子化の最大値になります。
RP2040では、カウンタの最大値は16bitの範囲で指定できます。16bitで指定できる最大の値、65535を指定したとすると、PWM出力の周波数は125MHz / 65536 = 1907Hzになります。これは音声を出力するには周波数が低すぎますね。
今回のコードでは、量子化ビット数を10ビットとしたので、周波数は122.07KHzです。ちなみにPWMを使ったデジタルアンプなどでは、250KHz~1.5MHzくらいが使われているようです。
なお、デューティ比の最大値は、必ずしも2のべき乗にする必要はありませんが、「16ビット44.1KHz」などあらかじめ決まった量子化ビット数&サンプリング周波数の音声を再生するには、いずれにせよサンプリングレート変換ないし量子化のやりなおしは必要になります。pico-extrasには、pwm-audioという高機能なライブラリがあり、サンプリングレート変換やInterpolatorを使ったディザリングなどの機能を備えています。
次に、シンセサイザの波形生成です。
波形としては矩形波、鋸波、三角波を用意しました。真面目にやるにはエイリアスノイズ対策をしたウエーブテーブルを用意すべきですが、今回は音質軽視(笑)なのでそのようなことはせず、以下のような定義で波形を生成します。横軸がφ、縦軸がyです。
φは0から始まり、時間とともに増加し、1.0になったら0に戻ります。φに対応するyの値をPWMのデューティ比に換算して出力します。
φはサンプリング周波数の周期で増加しますが、その増分は鳴らしたい音の周波数で決まります。以下の図のstepが増分ですが、stepを小さくすれば低い周波数、大きくすれば高い周波数の音が出ます。
具体的には、stepは
鳴らす音の周波数/サンプリング周波数
で決まります。
このほか、波形としてはノイズがありますが、ノイズに使う乱数はRP2040のハードウェアrandom number generator(RP2040データシートの2.17.5節)を使っています。これは1ビットのランダムなビットを生成できるものです。ちなみに標準ライブラリのrand()は、IRQハンドラの中で使用すると動作がおかしくなるようです。
最後に実装です。
上記を実装するには、PWMのカウンタが最大値に達する都度、割り込みをかけて次のパルスのデューティ比の値を指定します。割り込みハンドラのコードは以下のようになっています。
psg_next()は、φをstepだけ先に進めます。
psg_value(i)は、i番目のオシレータのyの値を返します。
今回は以上です。同じやり方で、FM音源やウエーブテーブル音源なども簡単に作れると思います。
コメント
興味をもって2日間取り組みましたが、ビルドでつまづきました・・・。
何をどうやっても、エラーが大量に出て失敗。
急ぎませんので、リポジトリにある3ファイル(1つは、Readmeなんで実質2つですか)をどうやったらmakeできるか、ご教示いただけますと幸いです。
ちなみに、
https://www.raspberrypi.com/news/how-to-blink-an-led-with-raspberry-pi-pico-in-c/
に沿って、root/picoというフォルダ直下にpwm-soundというフォルダを作り、pico_sdk_import.cmakeもコピペしてから取り組んでいますが、だめでした。
何卒よろしくおねがいします!
たびたび申し訳ございません。blinkなどのごく最初のサンプルプログラムは、無事にビルドできております。
そんなレベルでお問い合わせして申し訳ないのですが、、、よろしくお願いします。
こんにちは、以下のようなファイル構成にしているということでしょうか?
pwm-sound/
├── CMakeLists.txt
├── pico_sdk_import.cmake
└── pwmsound.c
でしたら、そうではなくて以下のような構成にしてください。
any-name/
├── pico_sdk_import.cmake
└── pwm-sound
├── CMakeLists.txt
└── pwmsound.c
これでREADMEの通りにコマンドを打っていただくと、たぶんビルドできると思います。
下記忘れてました、Ubuntu 22.04.1 LTS 64bit環境です。
pico直下にpwm-sound、そこにbuildフォルダを新設して「cmake -B build」はできたのかなと思うのですが、その次の「make -C build」でエラーが発生しているようです(この構造自体が、手探りなのですが)
CMake Error: Error required internal CMake variable not set, cmake may not be built correctly.
Missing variable is:
CMAKE_ASM_COMPILE_OBJECT
こうゆうのが多数吐かれて「.uf2」ファイルが出てきません。
もしご存知のところがありましたら、よろしくお願いします!
もともとこのリポジトリはpicoの下にpico_test_projects、その下にpwm-soundフォルダを置く構成にしていますので、(名前は何でもいいですが)pico_test_projects相当のフォルダを作り、そこにpico_sdk_import.cmakeを置かないとビルドできません。
さっそくのコメント、誠にありがとうございます!!!!
承知いたしました、今週末にもまた挑戦してみます。
ブログ大変、参考になります。これからもよろしくお願い申し上げます!!