昔、オーディオアンプとかについていた、サウンドレベルメータを作ってみようと思います。近頃では、スマホ等の音楽再生ソフトウェアの視覚効果などとして、表示されているケースもあります。良く使用しているSoundEngine Freeと言うソフトウェアにも、下図の様なレベルメータが表示されています。
※表示の都合上、反時計回りに90°回転しました。
一番高い(上図左端)レベルが0dB、そこから右に行くにつれてレベルが下がって行きます。ちょっと見難いですが、表示されている数値は負数です。一番右端では、−90dBですね。2チャンネル(ステレオデータ)のメータとなっています。
今回はこれをArduinoで制作します。音声入力は、サウンドセンサ(マイク)のアナログ出力を使い、モノラル(1チャンネル)のレベルメータとします。
そもそも、サウンドレベルって、どういう量なのでしょう??いろいろと調べて行くうちに行き着いたのは、「音圧レベル」という言葉でした。単位はデシベル[dB]。
音圧p[μPa](マイクロパスカル、気圧など圧力の単位)とした時、基準となる音圧p0[μPa]とすると、音圧レベルLは、以下式で定義されます。
L=10×log10(p/p0)2=20×log10(p/p0) [dB]
基準値p0は、1kHzの音に対する人間の可聴可能な最小値、p0=20μPaに設定されています。pは、瞬間値ではなくて、一定時間の実効値(Root Mean Square、RMS)が用いられるそうです。
でもこれって、「近くの飛行場の騒音が○○[dB]」みたいな時に使う、絶対的な音の大きさを表す時の指標ですよね?前節に示したレベルメータは、最大値が0dB、すなわちp/p0=1に相当します。最小可聴値p0が最大値って、おかしいですよね。
結論から言うと、音とデシベル[dB]の関係って、なかなか複雑で良く分からない\(1^O^;)/降参。
調べても、アンプなどについている「サウンドレベル」の表示として、何が正しいのか?は、解らなかったけど、このページでは、以下の通りとします。
「20×log10(RMS)」辺りを抑えてあるので、「当たり🎯」じゃないとしても、そんなに外してはいないはず。
普通に考えると、LEDを何個か並べておいて、測定したレベルに応じて端から順に光らせたら良いのですよね?サウンドレベルの高さ(大きさ)に従って、緑 → 黄色 → 赤など、LEDの色を変えておくと、よりそれらしく見えるでしょう。
けど、LED+電流制限抵抗を何個も用意するのは、ブレッドボードと言えども、面倒です。LED 1本にArduinoのデジタル出力1本を割り当ててしまうのも、無駄なので、シフトレジスタとか使い始めると、これまた回路が複雑化して行きます。
LEDがある程度固まって使えて、シフトレジスタっぽいICが組み合わさったモジュール、と言うと、8×8 LEDが有りましたね(下図)。
これを使いましょう(使うのは、もう何度目かなので、詳しい使い方については「逆引き索引」から過去記事をあたってください)。
例えば、下のドット(行)から順に、サウンドレベルが高くなるにつれて上のドット(行)が光っていくようにすると、0〜8まで、9レベルの表示が可能です。何より、LEDの発光回路を何個もブレッドボード上に構築しなくていいし、最大8×8=64個のLEDを制御可能なので、表示を工夫すると、表示レベルの段階数を増加することも可能です。
SPIで制御可能なLEDドライバ(MAX7219)も搭載されているので、便利便利(1^−^)v。
8×8 LEDのお陰でだいぶ簡単になって、回路と言うほどではないけれど、以下図の様になりました。
……結線するだけ。ネットリスト(接続表)でも十分でした……。まあ、グラフィカルに結線が見えた方が、解り易いですから。
ソフトウェアの設計に移ります。
計算自体は難しくないので、AD変換器の制御について考察しましょう。ArduinoのコアライブラリからanalogRead()関数を使うと、アナログ信号をAD変換器で変換して、デジタル値として読み出すことが可能です。ATmega328のAD変換器は、「#4-4 デジタルオシロスコープ」の時に少し突っ込んで検討したので、そちらも復習してみてください。
analogRead()関数では、AD変換器の変換クロックを、設定可能な一番遅いクロックであるfcpu/128に設定してあります。Arduino UNOなどATmega328を16MHzで動作させる機体では、変換時間は約104μsです。しかも、analogRead()関数を使ってしまうと、変換が終わるまでの104μs間処理がブロックされてしまい、制御が戻ってきません。
UNOの様な非力なMCUを搭載した機体で、104μsもの時間をブロックされるのは致命的です。そこで、ATmega328のAD変換器を直接制御して、1)変換時間を高速化、2)変換中にも他の演算処理などを実行できる、様にしてみます。
以下、ATmega328に搭載された10ビットAD変換器の、新しく設定するレジスタ値です。
レジスタ名 | アドレス | 設定値 | 説明 |
ADCSRA | 0x007a | 0xd5 |
AD変換器有効、変換開始、自動トリガ無効、フラグクリア、 割り込み無効、クロック選択:fcpu/32 |
ADMUX | 0x007c | 0x40 |
リファレンス電圧:AVcc (=5V) データレジスタ:右詰め 入力端子:ADC0 |
変換クロックは、定格内でもう一段速いクロック(fcpu/16)を選択可能ですが、可聴域20〜20k[Hz]がターゲットですので、fcpu/32(サンプリングレート換算:約38.5kHz)で十分でしょう。このスピードでも、UNOの処理速度では活かしきれないかもしれません。
※クロック選択と、サンプリングレートの関係については、「#4-4 デジタルオシロスコープ」で表にまとめてありますのでご覧ください。変換クロックを200kHz以上に高速化すると、変換精度が落ちる(±2LSB → ±4.5LSB)のですが、AD変換した結果を録音してWAVファイルにするわけでは無いので問題なしとします。
後は、analogRead()関数の様に、AD変換実行中にブロックされない様、AD変換をトリガするadc_start()関数と、変換結果を読み出すadc_read()関数を作っておきます。以下、該当コードを示し、その下に解説を加えます。
まずは、adc_start()関数の解説です(↓)。
int adc_start (
int adch)
{
// start adc channel adch
// return: ErrNo / ErrAdcIlle
10ビットAD変換器の変換を開始する関数です。変換対象のチャンネル番号adchを引数に取ります。
// parameter
if (adch<0 || 7<adch)
{
// ERROR:
return (ErrAdcIlle);
}
変換チャンネルadchが0〜7の範囲に無かったらエラーとしています。でも、実のところ、UNOに搭載されているATmega328P-PU(28DIP)は、アナログ入力端子が6本しか端子に引き出されていませんので、adch=6,7のパラメータが渡されると、本当はエラーにすべきです。
実は、ATmega328P-AU(32TQFP、Pro Mini等)や、ATmega328P-MU(32MLF、Nano等)であれば、8本使用可能です。確か、何とかっていうプリデファインがあったから、#ifdefで切り替えることは可能だったけど、面倒なのでそこまでしてませんm(1__)m。
※28DIPから増えた4本(32TQFP,32MLF)の内、2本はVCC,GNDで、残りの2本がアナログ入力端子です。
// wait adc conversion complete, polling while ADSC=1
while (ADCSRA&0x40)
;
もし、前にトリガしたAD変換が終了していなかった場合は、終了するまでここでポーリングします。普通は、ここで「待ち」に入ることは無いはずですが、プログラムの組み方次第では、前の変換が終了するまでブロックされます。
// adc setting
// AVcc, right, adch
// enable, start, !auto(single), flag clear, !interrupt, fcpu/32
ADMUX=0x40 + adch;
ADCSRA=0xd5;
return (ErrNo);
}
新しく、adchチャンネルのAD変換をスタートして、そのまま戻ります。プログラムは次の処理に移ることができますが、AD変換器自体は約26μsかけてAD変換を実行しています。
続いて、adc_read()関数を解説して行きます(↓)。
int adc_read (void)
{
// read adc data
// return: adc data
int dat;
AD変換結果を読み出す関数です。読み出した10ビットデジタル値を返します。
// wait adc conversion complete, polling while ADSC=1
while (ADCSRA&0x40)
;
もし、AD変換が終了していなかった場合には、AD変換終了までポーリングします。AD変換にかかる時間は約26μsですので、16MHzのUNOにとってはあっという間です。ちょっと複雑な演算を実行したら、26μsくらいは過ぎてしまいます。よって、普通、ここで「待ち」に入ることは無いはずです。
と、言うより、ここでブロックされない様に、うまくプログラムを組みます。
// read adc data
noInterrupts ();
dat=ADC;
interrupts ();
return (dat);
}
AD変換器のデータレジスタは、8ビットの下位データレジスタADCLと上位データレジスタADCHに分かれていますので、割り込みを禁止して、一気に読み出す必要があります。読み出したデータを関数の返り値として終了します。
adc_start()関数とadc_read()関数の具体的な使い方は、以下の通りです。
dat=adc_read ();
adc_start (ana_LEVEL);
// レベルメータの処理
:
予めスタートしておいたAD変換結果を読み取り(adc_read()関数)、すぐに次のAD変換をスタート(adc_start()関数)します。その後でサウンドレベルメータの処理(20×log10(RMS)の計算や表示など)を実施しますので、その間に約26μsの変換時間は過ぎています。これをloop()関数内で実行するので、次にloop()関数が実行されて最初のadc_read()関数に到達した時には、AD変換が終了していて、すぐに変換データを読み出せる状態になっていることが期待できます。
完成したプログラムをリンクしておきます。
完成プログラム:lev_meter.ino
コンパイルしてUNOに書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。
コマンド仕様:
8×8 LEDのどの方向を上と呼び、どの方向を右と呼ぶのか?って話ですが、後で実験中の写真をご覧に入れますので、そこでご確認ください。お使いになる8×8 LEDの実装によっては、同じMAX7219を使ったモジュールであっても、天地・左右の向きが異なっているかもしれないので、随時修正してお使いください。
以下、実験中の様子です。
プログラムを転送したら、もうそのままで、サウンドレベルメータとして機能していますので、マイクに向けて音楽でも、何でも、何らかの音を流してみてください。8×8 LEDが上下に動いて、サウンドレベルを表示するはずです。
【pat】コマンドで表示パタンを変更してみたり、VP-Pが大きすぎたり小さすぎたりした場合には、【vpp】コマンドで最大値を調整してみたりしてください。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pat,1】
OK: pattern #1
【vpp,1000】
OK: Vp-p=996[mV] 204[LSB]
【pat,2】
OK: pattern #2
【vpp,5000】
OK: Vp-p=4995[mV] 1023[LSB]
当方で実験した際の動画をご覧ください。
音とデシベルの関係については結局よく解らなかったし、所謂「サウンドレベルメータ」と言う物が、どういう測定値を表示しているのか?も、わからずじまいでしたが、一応、見た目はそれらしく動いてくれました。後はお詳しい皆様にお任せいたしますm(1__)m。