Raspberry Pi PicoとCircuitPythonで和音楽器を作る

この記事ははJLCPCBの提供でお送りします。

JLCPCBとは

jlcpcb.jp (↑こちらは日本語版のログインページで、お得なクーポンも配布されています。)

JLCPCBとは、プリント基板製造などで有名な香港の企業です。

日本からでもWebページでポチポチするだけでKiCADなどで作成した基板データの製造を依頼できます。

値段もかなりお手頃で、ホビー電子工作ユーザーの間では広く利用されています。

この記事の作例はJLCPCBに基板を発注して作成した、以前の基板を更に活用した内容です。

inajob.hatenablog.jp

これは何?

私が作っているRakuChordは、もともとArduino UNOなどで用いられているATmega328をメインCPUとしたものでした。 しかし昨今半導体の高騰や新しい組み込み向けのCPUの登場により、ATmega328以外の選択肢も模索したいという気持ちが高まってきました。

ということでこの記事ではRaspberry Pi Picoに搭載されていることでおなじみのRP2040を使って和音楽器を作る方法について紹介します。

Raspberry Pi Picoでのプログラミング

今回利用するのはRP2040-Zeroという開発ボードで、厳密にはRaspberry Pi Picoとは異なります。 しかし回路の構成はほぼRaspberry Pi Picoと同じなので、Raspberry Pi Picoでもほぼ同じように和音楽器を作ることができるはずです。

Raspberry Pi Picoに搭載されているRP2040は、ATmega328などの8bit CPUと比べるとかなりパワフルなマイコンです。 C/C++での開発はもちろんできますがMicroPythonやCircuitPythonなどのインタプリタ言語も動作します。

今回は和音楽器の作成にあたり、CircuitPythonを使うことを選択しました。

C/C++と違い、インタプリタ型のCircuitPythonでの開発は、少し変更して実行してみるというトライ&エラーがやりやすく、高速に開発することができると感じました。 最終的にパフォーマンスの要件などでC/C++を使うとしても、プロトタイプとしてCircuitPythonを使って動作検証を行うのはかなりアリだと思います。

CircuitPythonで和音を鳴らす

CircuitPythonはインタプリタ方式のプログラミング言語で、C/C++と比べると明らかに実行速度が遅いです。 そのため、音の生成、特に和音の生成などをCircuitPythonの層で実装することは現実的ではありません。

しかしCircuitPythonには標準の組み込みのモジュールとしてsynthioという和音をサポートして音源ライブラリが搭載されており、これを利用することで、気軽なインタプリタ方式のプログラミング言語で、高速動作が要求される和音の生成を実現することができます。

synthio

RP2040で動作するPythonの実装としては有名なのはMicroPythonとCircuitPythonがあります。これらはそれぞれ微妙に違った特性を持っていますが、今回CircuitPythonを選択したのは、CircuitPythonには組み込みライブラリとしてsynthioがあったからです。

このライブラリは音を鳴らすためのもので、今回やりたい和音の生成もサポートしています。

docs.circuitpython.org

配線

synthioはDAC出力とPWM出力をサポートしています。今回は追加部品が不要なPWM出力を利用します。 このために出力ピンは適切なRCフィルタを構成のが定石のようですが、ちょっと試す分には、深く考えずスピーカやオーディオアンプを直接繋いでも動作を確認できます。 参考元の資料では1kΩの抵抗と100nFのコンデンサでRCフィルタを作成していました。

単音を鳴らしてみる

以下のようなシンプルなソースコードでファの音を0.5秒毎にON/OFFできます。

import board, time
import synthio

import audiopwmio
audio = audiopwmio.PWMAudioOut(board.GP10)

synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)

while True:
    synth.press(65) # midi note 65 = F4
    time.sleep(0.5)
    synth.release(65)
    time.sleep(0.5)

from GitHub - todbot/circuitpython-synthio-tricks: tips, tricks, and examples of using CircuitPython synthio

和音を鳴らしてみる

和音を鳴らすのも単音と同じようにできます。

import board, time
import audiopwmio
import synthio

audio = audiopwmio.PWMAudioOut(board.GP10)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)

while True:
  synth.press( (65,69,72) ) # midi notes 65,69,72  = F4, A4, C5
  time.sleep(0.5)
  synth.release( (65,69,72) )
  time.sleep(0.5)

from github.com

キー操作に対応して音を鳴らしてみる

ここまでの例は無限ループでsleepさせて音を鳴らしていましたが、ここをキーの入力判定に書き換えればキー操作に対応して音を鳴らすことが出来る、いわゆる「楽器」を作ることが出来ます

この例ではRaspberry Pi PicoのGPIO2番にタクトスイッチつながっており、スイッチを押下することでGNDとショートするような回路を想定しています。

import board, time
import audiopwmio
import synthio

audio = audiopwmio.PWMAudioOut(board.GP10)
synth = synthio.Synthesizer(sample_rate=22050)
audio.play(synth)

p1 = digitalio.DigitalInOut(board.GP2)
p1.direction = digitalio.Direction.INPUT
p1.pull = digitalio.Pull.UP
isPlaying= False

while True:
  if p1.value == False and isPlaying== False:
    synth.press( (65,69,72) ) # midi notes 65,69,72  = F4, A4, C5
    isPlaying= True
  else:
    if isPlaying== True:
      synth.release( (65,69,72) )
      isPlaying= False

和音を鳴らす以外の処理が複雑になると音が途切れてしまう問題の対処

さて、ここまでで電子楽器を作るための準備が整いました。 しかし、楽器を作りこんでいくにしたがって、音を鳴らす以外の処理が増えてきます。前述のキーの入力検出に始まり、画面の制御なども実装していくと、だんだん生成される音にノイズが混じるようになってきます。

これはsynthioの音波の生成がほかの処理により遅れてしまい、音の再生スピードに追い付かなくなることから発生するようです。

根本的には、音波の生成以外の処理を減らせば、この問題は起きませんが、もっと処理を詰め込むことは出来ないでしょうか?

こんな時に役立つのがsynthioのバッファリング機能です。

synthioでは、生成する音波をバッファにためておく機能があり、これを使うことで、少しほかの処理が複雑になっても、バッファにためておいたデータの分だけ時間に余裕が出来ます。

ただ、バッファを大きくすればするほど、音の生成にラグが生じるので、バッファの大きさには注意が必要です。楽器として使いやすくするうえではバッファをなるべく小さくして、キーを入力したらなるべく早く音が鳴ってくれる方がが扱いやすいので、ここは操作性とのトレードオフとなります。

import board, time
import audiopwmio
import audiomixer
import synthio

audio = audiopwmio.PWMAudioOut(board.GP10)

# ここからがバッファリングの処理
mixer = audiomixer.Mixer(sample_rate=22050, buffer_size=2048) # ここの2048を大きくするとバッファを大きくできる
synth = synthio.Synthesizer(sample_rate=22050)
mixer.voice[0].play(synth)
mixer.voice[0].level = 0.25 # 音量の調整もできる
# ここまでバッファリングの処理

p1 = digitalio.DigitalInOut(board.GP2)
p1.direction = digitalio.Direction.INPUT
p1.pull = digitalio.Pull.UP
isPlaying= False

while True:
  if p1.value == False and isPlaying== False:
    synth.press( (65,69,72) ) # midi notes 65,69,72  = F4, A4, C5
    isPlaying= True
  else:
    if isPlaying== True:
      synth.release( (65,69,72) )
      isPlaying= False

OLEDの制御

さて、ここまでで和音楽器として実用的なものが作れるようになりましたが、私の作ろうとしている楽器には128x64のI2C接続のOLEDが搭載されており、キーの入力に合わせてOLEDの表示も変更する必要がありました。

CircuitPythonは組み込みのライブラリではOLED制御をするためのものが含まれていないのでOLEDを制御するためのライブラリを導入します。

OLED制御のライブラリはCircuitPython Library Bundleという標準的なライブラリ集に含まれているので、これをダウンロードします。

Libraries

この中に含まれるファイルをRaspberry Pi Picoのファイルシステムにコピーします。 adafruit_framebuf.mpy, adafruit_ssd1306.mpylib以下に、font5x8.binをルートに配置します。

ここまで準備すれば、以下のようなプログラムでOLEDに文字を表示できます。

import board
import busio
import adafruit_ssd1306

i2c = busio.I2C(board.GP11, board.GP10)
oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c)
oled.text("Hello World!",20,5,1)
oled.show()

OLEDの表示と和音生成の両立

しかし上記の方法でOLEDを制御しつつ音を鳴らそうとすると、画面表示にCPU処理を使いすぎてしまい、かなり大きなバッファを設けないとノイズが発生してしまう問題が起きました。

しかし、バッファを大きくすると前述のようにラグが大きくなり、実用に耐えない楽器となってしまうという問題があります。

色々調べているうちに、OLEDの画面転送のオプションを変更し、1つの画面をいくつかに分割して更新することで、バッファを小さく保ったままノイズのない音を生成することが出来ました。

これは、OLEDの画面転送を分割することで、その隙間にsynthioの音波生成の処理を実行することが出来るようになるからだと考えています。

副作用として画面の更新が少し遅くなりますが、楽器を操作するうえで気になるほどではありませんでした。

import board
import busio
import adafruit_ssd1306

i2c = busio.I2C(board.GP11, board.GP10)

# page_addressing=True により画面更新を分割する
oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, page_addressing=True)
# なぜか手元のOLEDだと表示位置がずれるので微調整
oled.page_column_start[1] = oled.page_column_start[1] -4
oled.text("Hello World!",20,5,1)

# page_addressingの設定を変更したのでこのshowの中身の動作が変わり、画面更新を分割して実行する
oled.show()

ATmega328用の基板を流用してRP2040の動作の確認をする

ここまでで、ソフトウェア的にRakuChord的なものをRP2040で実行できる目途が立ちました。

ここからは、ハードウェア的にRakuChord的な構成を作り、動作検証をしていきたいので、このために新たに基板を作る必要がありそうです、、、が、、

そういえば以前表面実装部品版RakuChordの改良版を作った - inajob's blogの記事で作成したRakuChordは、キー入力のためのピンを引き出して設計していたので、この基板を流用して、RP2040の開発ボードと無理やりくっつけることで、ハードウェア的な動作確認もできるのでは?と考えました。

その結果がこちら!

動画はこちら

www.youtube.com

21個のキーの入力を受け付けるために3*7のマトリクスで処理しているので、全部で10本の信号線と電源の線をつなげる必要があり、こんな風にモジャモジャになりました。アンプやスピーカーもつなげました。

これで、RP2040をコアとしたRakuChordの試作機が完成しました。

こんなこともあろうかと、キー入力のパッドを引き出しておいてよかったです。

で、実際にRakuChord的なソフトウェアをCircuitPythonで作って、触ってみたところ、、特に問題なく動作しそうでした。

さて、次はこの構成を踏まえてRP2040をコアとしたRakuChordの基板を発注するぞ!という気持ちです。(それはまた別の話と言うことで・・)

まとめ

今までATmega328を使っていたRakuChordのCPUをRP2040かつCircuitPythonに変更できるかを色々検証しました。

C/C++に比べるとパフォーマンスの低いCircuitPythonでも、工夫すれば音の生成とOLEDの描画が出来そうなことがわかりました。

CircuitPythonでの開発は非常に手軽なので、簡単な電子楽器を作ろうと考えている方にはとてもお勧めの手法です。

また、過去に作成したATmega328用のボードの「こんなこともあろうかと」用意しておいた拡張端子をうまく利用することで、新たに基板を設計する前に、動作確認することが出来ました。

参考

Synthioの活用法については以下のリポジトリが参考になりました

github.com