PicoRubyで作るオーディオビジュアライザー
ドワンゴ Advent Calendar 2025 3日目の記事です。
PicoRuby Overflow会議およびながらRuby会議01で展示した電子工作作品です。
物語
突然のPicoRubyの流行
私はRubyを昔から触っていて、以前から色々なRubyコミュニティに参加していました。
大阪に住んでいることもあって今年の夏に関西Ruby会議08のスタッフもやったりしていたんですが、その中で関西Ruby会議08の応募で惜しくも落選してしまったCFPの中にPicoRuby関連のものが多数あるという話が出ていました。そして主催だったydahが「PicoRubyだけでもう1個会議作れるんちゃう?」とか言い出したところから物語が始まります。
PicoRuby Overflow会議
私は工業高校出身ということもあり、昔は電子工作もやっていてよく日本橋(※浪速区)でパーツを買ったりユニバーサル基板に芸術的()なはんだ付けをしたりしていました。10年ほど前にソフトウェアエンジニアになってからはこては握らなくなっていたんですが、RaspberryPiを使って家電制御やペットのカメの水槽管理を自動化したりしていて、ある程度技術は追っていました。
PicoRuby Overflow会議もスタッフとして手伝う予定で参加していて、ぼんやりCFPの集まり具合を眺めていたんですが、作品展示の応募枠が直前でも空きがあると言う話になり「昔作ったやつのリベイクぐらいならサクッと作れるんじゃないか?」と思って登壇者としても応募しました。
作品概要
ソフトウェアエンジニアをやっておいてなんですが、私はデジタル回路よりアナログ回路のほうが好きで、特に音声信号処理を趣味にしていたので、その方向で作ることにしました。
昔に一度コイルとコンデンサで組んだ10並列RLCバンドパスフィルタ回路を組んだことがあり、そのときのボードが残っていたのでこれをもとに改良しようと考えました。

ビジュアライズについてはいにしえのWindowsMediaPlayerについていたスペアナ表示あたりが見栄えしていいかなと思ったのでそんな感じで考えていました。
問題はRLCバンドパスフィルタ回路が十数年前のものなので一部壊れていて動かなかったので新造するひつようがありました。
また、ここにどうやってPicoRubyを組み合わせるかも悩みました。RaspberryPi Picoには3端子だけアナログ入力ができるピンがありましたが、スペアナの中間とするには圧倒的に足りず、かといって他のアナログ端子が多いボードでは現状PicoRubyが動かないので、なんとかRaspberryPi Picoで組む必要がありました。
結局数の暴力で解決することにしました。何個かブレッドボードで仮組みした結果、スペアナに必要な分解能は9回路あれば十分と結論づけたので、ラズパイPico3台に分散させて処理させることにしました。
結果としてこれは他の問題もいくつか解決してくれたので良い選択でした。そもそもラズパイPicoの処理能力が低く、アナログ入力からLED制御信号をいい感じにビジュアライズする処理を組み込むと3入力分ぐらいしか時間猶予がありませんでした。また、試しに9回路分のオペアンプとLEDを同時に駆動しようとするとラズパイPicoの駆動電力が限界らしく、オペアンプの安定性とLED光量に影響が出ていました。これも3個づつに分割し、さらにLED制御用電力を4個目のラズパイPicoから引くことで分散させて解決できました。
あとはLEDを光らせるだけなのですが、ラズパイPicoのデジタル出力ピンが3.3Vという点が今度は困りました。赤色LEDならば2Vちょっとぐらいで光るのですが、青と緑はちょうど3.3Vぐらいが駆動電圧で光らせるのが困難でした。そこでラズパイPicoの5V端子から別途駆動電力を取り、デジタルピンとトランジスタの駆動回路で制御する方式にしました。パーツが増えるのが面倒ですがまあしょうがないですね。
というわけでこんな感じに回路を組み合わせればオーディオビジュアライザーを作れそうという結論になりました。
- 音声入力の信号を増幅
- スペクトラムアナライザー回路で周波数ごとに分解
- RaspberryPi Picoのアナログ入力で信号強度を計測
- ラズパイ上のPicoRubyスクリプトで信号強度からPWM信号を生成し出力
- LED制御ボードでPWM信号からLEDをドライブして点灯
- アクリルボードが光って嬉しい
あとはこれをどうやってPicoRubyOverfllow会議のCFPにするかでしたが、スペクトラムアナライザーのことをアナログコンピューターと言い張ることで魅力的なタイトルに昇華させました。コンピュータとは単純な回路を組み合わせて想像しがたいほど高度な処理能力を手に入れたものなんです(早口)。・・・今年一番の謳い文句を作ったと自負しています。

作ったもの
というわけで最終形態はこんな感じになりました。
ブロック図
ブロック詳細
PreAmp
音声信号を解析機へ回す前に増幅する回路です。
最初は音声信号をそのまま9列に入れていたんですが、オーディオインターフェースの出力不足で信号がうまく取れなかったのでアンプを挟みました。後段のスペアナ3列を1アンプで賄えそうだったので入力を3個のオペアンプで受けてそれぞれをさらに後段3列に渡しました。幸いオーディオインタフェースの出力はオペアンプの直接入力なら3個は賄えそうでした。
また、この段階で単電源オペアンプのためにAudioSig+をプルアップ抵抗で挟んで1.1Vを印加しています。後段も単電源駆動なのと、ラズパイPicoも0V~3.3Vしか取れないのでプルアップのまま通すことにしました。

注:とりあえず動くことが目的だったので手元のパーツで組める超絶雑な回路になってしまいました。マネして壊しても責任は取れません。ちゃんとした増幅回路のサンプルを見てください。
スペクトラムアナライザー
この作品の肝の部分です。
昔のようにRLCバンドパスフィルタでも良かったんですが、RLCだと信号が純粋に減衰することと、精度が悪くボンヤリと動いているなあぐらいの動作しかしないのが不満点でした。
そこでどこかで見たことがあったオペアンプでできるバンドパスフィルタ(BPF)回路を作ることにしました。オペアンプ式の利点はオペアンプ自体が信号増幅もしてくれるのでラズパイPicoのアナログ入力端子で扱いやすい電圧帯に調整しやすいという利点もありました。
オペアンプ式BPFはオペアンプ数が1,2,3個の物があるのですが、今回はオペアンプ2個でできる回路(Dual Amplifer Band Pass Filter,DABPF)にしました。
ちょうどシリコンハウス(日本橋にある電子パーツ専門店)で2回路入りのオペアンプが安く売っていたのと、オペアンプは1石に2回路か4回路のものしかないのでちょうど1回路で1石使えてわかりやすかったからです。
DABPFはQ値(Quality Factor、尖鋭度≒通過周波数幅)がR1とR2R3で決まるので計算が楽で、中心周波数もR2R3とC1C2で決まるので考える変数が格段に少なくて便利です。1段増幅よりはS/N比もよく、設定の自由度も十分高いので今回の回路にはうってつけでした。信号増幅度が2倍で固定されるのが少し不便ですが、今回はそれほど不都合もなかったので問題ありませんでした。
回路の理解や計算、設計はこのあたりのサイトを参考にし、こんな感じの回路図になりました。
- http://www.g-munu.t.u-tokyo.ac.jp/local_manual/bpf/bpf.html
- https://edycube.blog.fc2.com/blog-entry-1000.html
- https://ameblo.jp/tantans36/entry-12830342662.html

スペクトラムアナライザーの回路図1列分(実際はAudioSig1に3列分のR1が接続されている)
回路の周波数決定についてはコンデンサの静電容量だけで受け持つことにしました。
最初は抵抗値のほうが細かく制御できるのでそちらにしようと考えていたのですが、ブレッドボードに実装しているときに抵抗を置いていたケースをぶちまけてどれがどれかわからなくなったので管理は無理だと悟りました。
結果、R2~R5は10kΩ固定、R1は10kにQ値を掛け算したものとし、C1とC2を同値にして狙ったあたりの周波数に近い静電容量のものを選定しました。静電容量は調べた限り(10~20)/周波数(uF)あたりにするとよいらしいと出てきたので間を取って15/fとしました。
この方針は圧倒的に部品選定の手間が省けて実装不可が格段に減ったのでこれは正解でした。
選定した数値は以下の表のとおりです。コンデンサは日本橋にあるシリコンハウスとデジットで売ってる素子で丸め込んだのできちっとした値にはなりませんでしたが、まあ大体あってればええやろ。
各パーツの数値
| ターゲット 周波数 | 15/f | C1 C2 | R2~R5 | Q値 | R1 | カットオフ周波数(下) | 中心 周波数 | カットオフ周波数(上) |
|---|---|---|---|---|---|---|---|---|
| 80 | 0.1875 | 0.18u | 10k | 3 | 30k | 74 | 88 | 103 |
| 160 | 0.0937 | 0.10u | 10k | 3 | 30k | 133 | 159 | 186 |
| 250 | 0.0600 | 0.068u | 10k | 3 | 30k | 195 | 234 | 273 |
| 500 | 0.0300 | 0.027u | 10k | 2 | 20k | 442 | 589 | 737 |
| 1000 | 0.0150 | 0.015u | 10k | 2 | 20k | 796 | 1061 | 1326 |
| 2000 | 0.0075 | 6800p | 10k | 2 | 20k | 1755 | 2341 | 2926 |
| 4000 | 0.0037 | 3900p | 10k | 2 | 20k | 3061 | 4081 | 5101 |
| 8000 | 0.0018 | 1800p | 10k | 2 | 20k | 6631 | 8842 | 11052 |
| 15000 | 0.0010 | 1000p | 10k | 2 | 20k | 11937 | 15915 | 19894 |
厳密には素子ごとの誤差も考えるべきですが、5%以下のものを使っているのと、趣味工作なのでそのあたりは妥協しました。だいたい動きゃいいんだよ。
計算式
テーブルの各値の導出式です。
参考にしたWebページにも記載されていますが、こちらにも転記しておきます。
\displaylines{
Q = \frac{R_1}{\sqrt{R_2 \times R_3}} \\
中心周波数 f_0 = \frac{1}{2 \pi C_1 \sqrt{R_2R_3}} \\
通過周波数 \Delta f = \frac{f_0}{2Q}
}
RaspberryPi Pico + PicoRuby
アナログ入力とLED制御の中継段としての役割ですが、それ以外にもいい感じのビジュアライズ調整も行っています。柔軟な動作はソフトウェア制御の利点ですね。
スクリプト全文はGitHubにあげてあります。
ラズパイPicoのアナログ端子は上で言った通り3ピンしかないので同じコードを3台に入れて動かしてあります。PicoRubyは /home/app.rb というファイルがあると電源投入後に直接それを実行するので多数同時運用でもPC制御なしに動かすことができて便利です。
やっていることは基本的にピークtoピーク検出で、その場その場の信号強度の差とかも調整しています。基本的にこのスクリプトが全てで、ある程度詳細にコメントを残しているので解説はいらないかなと思ったんですが、少し補足したいところもあるので一部コードを追っていきます。
ウォッチドッグ
# ウォッチドッグLED用設定
# 1秒で1サイクル。これを変えると点滅スピードが変わる
wd_res = 1.0 / loop_sleep_time
wd_i = 0
# ウォッチドッグLED(基盤実装LED)
l=GPIO.new(25,2)
# ~~~
loop do
# ~~~
# ウォッチドッグLEDの点滅
# 何らかのエラーで止まったらLEDが点滅しなくなる
# 変数wd_resとsleepの時間から1秒で1サイクルするように調整してある
# wd_resの変数の値を変えると点滅時間が変わる
if wd_i < wd_res/2
l.write(1)
else
l.write(0)
end
wd_i = wd_i + 1
wd_i = 0 if wd_i >= wd_res
end
ウォッチドッグ、案外便利です。
やっぱマイコンってなんだかんだ突然止まるんですよね。
点滅が止まったら死んでるのがわかるので重宝します。ほぼ必須設定。
基盤実装LEDのピン番号はモデルごとに違うので都度調べてください。
PWM制御
f=100000
duty=10000
# PWM出力端子(LED制御用)
p6=PWM.new(6, frequency: f, duty: duty)
p7=PWM.new(7, frequency: f, duty: duty)
p8=PWM.new(8, frequency: f, duty: duty)
PWM制御用のサイクル周波数とサイクル内でのON/OFF比・・・だと思うんです。
いくら調べても値を変えてもうまく動かなくて、何となくこの値にしたらPWM制御のちらつきが気にならないレベルになったのでこの値にしてます。識者求む。
ビジュアライズ調整その1
# 動作中のpeak to peakの信号差の最大値を保存して最大点灯の目安にする
m26 = x26 if m26 < x26
m27 = x27 if m27 < x27
m28 = x28 if m28 < x28
# peak to peakの信号差の最小値を保存(これは多分ノイズ成分)こっちは完全消灯の目安
b26 = x26 if b26 > x26
b27 = x27 if b27 > x27
b28 = x28 if b28 > x28
# ノイズ成分を除去し、直近の最大信号差からどれだけ低いかを抽出。これが点灯レベルの基本の値となる
x26 = (x26 - b26)/m26
x27 = (x27 - b27)/m27
x28 = (x28 - b28)/m28
ピークtoピーク検出部分です。
最初はアナログ入力値をそのままPWMに突っ込んでたんですが、よく考えたら音の山のどこを取ったかわからないことに気づいたので0.01*20=0.2秒間の山の差を取るようにしました。ちょっと考えたら当然ですね・・・
というわけで結構いい感じに音が大きいとよく光るようになりました。
ビジュアライズ調整その2
# duty比として渡すために0~100に拡大する
# あえて-10~110に拡大して最大/最小付近で点灯レベルが0%/100%に張り付くようにする
duty26 = duty26 * 120 -10
duty27 = duty27 * 120 -10
duty28 = duty28 * 120 -10
最初0-100でやっていたら音量が大きいなあと思っていても実際には90%ぐらいで止まっていて100%点灯がほとんど起きていなかったのに気づいたので入れた処置。
ホントはBカーブじゃなくてちゃんと対数カーブにすればいいような気もするけど、計算が面倒なので伸ばしてプッツンしたほうが動作が早いしコーディングも早かったのでそうしました。だいたいキレイに見えればいいんだよ。
ビジュアライズ調整その3
# ちょっとづつ過去の最大値を減衰させて曲ごと/メロ-サビ間の音量差を吸収する
# だいたいこの調整で連続再生の切れ目でリセットされる
m26 = m26 - 0.1 if m26 > 0.5
m27 = m27 - 0.1 if m26 > 0.5
m28 = m28 - 0.1 if m26 > 0.5
連続稼働させていると音圧が高い曲のあとに音圧がカスの曲が来ると光らないことに気づきました。かといって曲の切れ目なんか検出するの面倒だし。。。せや、常に減らしとけばピークtoピークが高いうちは常に更新されるしピークtoピークが低くなったら減衰してちょうどいい感じになるやん、ということに思い至ったのでそう実装しました。ついでにサビとメロで音量が違う曲とかも対応できたのでよかったです。この設定値で追従時間がだいたい10秒程度になったのでヤマ感で設定した割には案外うまくいってうれしい。
LEDドライブボード
これについては特に言うこともなく一般的なLED光らせるだけの回路です。
今回青色LED1色ですが、なぜ1色かというと3色制御するのが面倒だった(どこで制御するんだ?ラズパイ辛さにコード伸ばすのか?制御トランジスタも3倍いるぞ。面倒くさい・・・)のと、高輝度青色がデジットで100個入りが500円という激安価格で売ってたのでそれが決め手でした。

展示
PicoRuby Overflow会議
実はOverflow会議には間に合ってなくて、スペアナの実装をミスって動かなかったんですよね。
急遽昔のRLCの回路で動かしたんですが、一部が壊れていてうまく動かなかったです。急いで新しいことをするとよくないというごく当然のことを学びました。
ちなみにバグの原因はR1の抵抗値が20~30kΩにするべきところを10倍の200k~300kΩで作っていたからでした。Q値が20とオーディオビジュアライザーにするにはとんでもなく高い値になっていて、通過周波数帯域が数Hzしかありませんでした。寝ぼけながら抵抗をピックアップしてはいけない(戒め)
ながらRuby会議01
Overflowの1ヶ月後に岐阜で開催されたながらRuby会議ではリベンジで完成させて無事展示できました(冒頭のツイート)
最初は一般参加だけするつもりだったんですが、主催のころちゃんさんから展示しないんですか?!と圧を受けたので急遽完成させて展示にこぎつけました。
今度はちゃんと家で動作確認してそのまま持っていったので完全に動作しました。
時間に余裕があったので新しいアクリルボードにながらRuby会議のロゴを彫り込んで持っていって、展示後にころちゃんさんに記念品としてプレゼントしました。
完走した感想
7月頭から3週間後のPicoRuby Overflowに間に合わせるためにデスマって間に合わなかったのは残念でしたが、9月のながらRuby会議で完成できたのでよかったです。
割とまだ電子工作の腕も残ってるんだなと再認識できたのでこれからもちまちまと回路制作やマイコン制御をやっていこうと思います。PicoRubyに貢献できるほどの能力があるかはわかりませんが。。。
全体としてはいい夏の思い出になったなと思っています。最高の夏にしようぜ
宣伝
今回の制作でPicoRubyの使い方に詳しくなったので連作記事を書いています(最近滞っていますが、、、夏以降万博に時間を全部吸われた)
そろそろ続きを書こう(決意)

