この記事について
この記事は Akatsuki Games Advent Calendar 2023 の4日目の記事です。
昨日の記事はちょう さんの 「負荷に強い(かもしれない)サーバーエンジニアが開発時に心掛けていること」でした。
サーバの負荷はぼくもきになるジャンルなのでとても興味深い記事でした。
発端
先日友人から「お年玉の金額をその場でランダムで決めたい」って相談受けたのが発端
要求仕様としてはこんなかんじ
・0~9999円の数字がランダムで出る
・確率はすべて均等
・出来上がったら郵送
というところを踏まえて話しながら結果的に仕様はこんなかんじ
・Raspberry pi picoを使用
・小形で比較的安価なカラー液晶を使用
・適当なガワ(ケース)にはいっている
・モバイルバッテリーなどのUSB電源で動作
・抽選開始押しボタンを別途用意
・音が鳴る
パーツ
Raspberry Pi Pico
今回WiFiは使わないのと材料費すこしでも安くしたいのでWじゃない方。
フラッシュストレージにファイルとして音声ファイルなどを書き込める点も採用理由。
おかげでSDカードスロットのハードウェアと読み出しルーチンの実装が省けるのでよき。700円くらい。
https://www.marutsu.co.jp/pc/i/2194960/
Raspberry Pi Pico用 1.14インチ 液晶モジュール 240×135
型番的にはこれ
WAVESHARE-19340
ピンヘッダさすだけの簡単確実接続。
フレキシブルケーブルとかあると輸送中に壊れたりしやすそうなのと、
ワンチャン好評で令和最新版電子サイコロとして量産するときも楽ということでこれにした。
あとカラー液晶としてはそこそこ安いのも理由。
タクトスイッチ2つと十字キーがついているけど今回は直接使用せず。1340円。
https://www.marutsu.co.jp/pc/i/2229754/
超小型D級アンプキット
最初PWMの音声出力を直接スピーカーにつないでいたが、さすがに音が小さすぎるので追加。
外付けのアンプスピーカにまかせればいいかとあきらめかけたけど
ちょうど固定ゲインの小型のアンプキットがあったので採用。しかも安い。300円。
https://akizukidenshi.com/catalog/g/gK-08161/
リード線
押しボタンなどなにかと無理やり配線する用
100円ショップのプッシュライト
見た目が昔懐かしのへぇボタンに似ているアレ。
中身のスイッチがトグルスイッチなのでプッシュスイッチに取り換えて使用。いまどきめずらしいちゃんと100円。
プッシュスイッチ
友人の親戚のキッズたちに乱暴に押されるかもしれないのでホームセンター電工用のちょっと頑丈なやつにした
100円ショップの置き時計(ケースとして)
最初そのまま液晶を転用できないか検討したけど、型番など不明なのでロジックアナライザとかで解析しないといけないので時間的にあきらめ。いろいろ取り外してガワだけつかうことに。ふりかえってみるとタッパウェアとかでもよかったんじゃないか説。高性能なので意外と高い。500円。
https://jp.daisonet.com/collections/electricity0220/products/4550480131825
Type-C – 3.5mmイヤフォンマイクジャック変換ケーブル
ばらしてイヤフォンジャックとしてL,R,GNDだけ使う。
直でスピーカーつないでもいけど任意の有線スピーカが使えるように。100円。
https://ec.cando-web.co.jp/item/4962242489325
有線スピーカー
100円ショップで最近はなかなか売ってないので意外と貴重。
ほとんどBluetoothになってしまった。500円とかでBluetoothスピーカー売ってるのもすごい。なんか隔世の感がある。
表面覆う用ウレタン
ホームセンターでいい感じのやつを選定。B4で300円くらい。
その他手元にあったいろいろ
部屋の防音につかった吸音ウレタンとか
プロトタイプ
最初は数字が4桁ランダムででるものを作成
まずはピンヘッダをつける。ブレッドボードにさしてはんだ付けすると楽
公式手順にそってPico本体にあるボタンをおしながらPCとつないでファームウェアインストール
正常に更新できたらいったんPCとの接続を外したのちに
液晶をPinを確認しながら接続する
GPIO8 ~ 13は液晶制御で使っているのでここは使わないようにする。
IDEのThonnyをインストール
ひとまずMicroPythonのサンプルコードをうごかしてみる
ちゃんとAボタンとの入力もとれてる
しかしライブラリのドキュメントを読むとこのままだと小さい文字しか出せないとのこと
そこでしらべてみたところこのような感じでいったん普通に描画した後指定の色のピクセルだけ拾って拡大した文字データを作成している方がいらっしゃった。
ただ、このままだとたまたま描画したい文字の色がもともと書かれていた場合文字が崩れてしまうので、背景色を指定することにして描画領域をその色で塗りつぶしてから拡大処理を行うようにした。
かわりに背景透過で描画するのはできなくなったけれど今回の目的としてはヨシなのでひとまずこれで。
def write_text(self,text,x,y,size,color,background):
''' Method to write Text on OLED/LCD Displays
with a variable font size
Args:
text: the string of chars to be displayed
x: x co-ordinate of starting position
y: y co-ordinate of starting position
size: font size of text
color: color of text to be displayed
'''
# background = self.pixel(x,y)
info = []
# 拡大処理を行うまえに作業領域をbackgroundの色で塗りつぶしておく
self.fill_rect(x,y,8*len(text),8,background)
# Creating reference charaters to read their values
self.text(text,x,y,color)
for i in range(x,x+(8*len(text))):
for j in range(y,y+8):
# Fetching amd saving details of pixels, such as
# x co-ordinate, y co-ordinate, and color of the pixel
px_color = self.pixel(i,j)
info.append((i,j,px_color)) if px_color == color else None
# Clearing the reference characters from the screen
# self.text(text,x,y,background)
self.fill_rect(x,y,8*size*len(text),8*size,background)
# Writing the custom-sized font characters on screen
for px_info in info:
self.fill_rect(size*px_info[0] - (size-1)*x , size*px_info[1] - (size-1)*y, size, size, px_info[2])
拡大表示できた。
プッシュライト型の押しボタン式にする
材料の説明にもあったように、もとはプッシュライトなので一回押したらONのままもう一度押すとOFFという挙動をする(あたりまえ)
今回の用途としては押してるあいだだけONになってほしいので、いったんばらしてプッシュスイッチに取り換える。
ちょっと不格好だけどもともとある壁掛け用の穴から配線を引き出してこれをAボタンのピンとグランドにつなぐ(プルアップしているので)
回路図としてはこんな感じ
単純な音声を鳴らす
最初はビープ音を音階で鳴らしたりしてまずは音がPWMで聞こえるかどうか確認。
このように接続してPWM出力で単純な波形の音声が鳴ることが確認できた。
PCMの音声を鳴らす
抽選されたお年玉金額を読み上げたいのでPCMで音声を鳴らしたい。
今回はPicoAudioPWMというライブラリを使用して音声を鳴らす。
単純にならすだけれあればサンプリング周波数8kHz 16bitのwavをPicoに転送したうえで次のように書くだけで鳴るのはありがたい。
from wavePlayer import wavePlayer
player = wavePlayer(Pin(SPK_L), Pin(SPK_R), Pin(SPK_VGND))
player.play('/sounds/sample.wav')
player.stop()
が、しかしメインスレッドで鳴らしてしまうと音声再生中はグラフィックの書き換えができない。
そこで_threadモジュールをつかって別スレッドで再生するようにした。
ところどころprintしているのはデバッグ用に正しくwavファイルが読めているか確認のためのデバッグ出力。
from wavePlayer import wavePlayer
import wave
import _thread
led_onboard = Pin(25, Pin.OUT) # sound indicator
player = wavePlayer(Pin(SPK_L), Pin(SPK_R), Pin(SPK_VGND))
lock = _thread.allocate_lock()
def play_wav(filename):
print("play_wav "+filename)
f = wave.open(filename,'rb')
print("{0:<45}".format('File Path'), 'Frame Rate Width Chans Frames')
print("{0:<50}".format(filename), "{0:>5}".format(f.getframerate()), "{0:>5}".format(f.getsampwidth()), "{0:>6}".format(f.getnchannels()), "{0:>6}".format(f.getnframes()) )
lockP = lock.acquire(1, 2.0)
if not lockP:
print("_play_wav can not get the lock. "+filename)
return
_thread.start_new_thread(_play_wav, (filename,))
def _play_wav(filename):
time.sleep(0.2)
led_onboard.value(1)
player.play(filename)
player.stop()
led_onboard.value(0)
time.sleep(0.2)
lock.release()
D級アンプをかませる
このままだとかなり音量が小さいので秋月電子の「TPA2006使用 超小型D級アンプキット」を使用して音声信号を増幅する
ローパスフィルタをかませる
そのまま増幅するとノイズ(特に高周波)がやばいのでローパスフィルタをwavePlayerのライブラリのコメントにあるとおりにつくる
2K
PIO2 -/\/\/-----+----- headphone left
|
=== 0.1uF
|
PIO4 -----------+----- headphone ground
|
=== 0.1uF
2k |
PIO3 -/\/\/-----+----- headphone right
音声素材をつくる
自分の声で「0~9、十、百、千」と収録して単語ごとにwavファイルにする。もちろんそのままだとおっさんの声でよみあげられてしまうので当然ボイチェンにかける。はづかしいけどややかわゆくしゃべるのがコツ。
これを最終的にコンプレッサーをかけてからノーマライズしてボリューム調整したものを8kHz 16bit monoで出力。
できたファイルをThornny上で右クリックしてPicoにアップロード
3.5mmヘッドフォンコネクタをつける
100円ショップのUSB Type C -> ヘッドフォンマイクコネクタ変換ケーブルをばらす
左からR,L,MIC,AC(GND)が見える
今回はモノラル出力なのでRとLをショートさせアンプからのOUT+と接続、OUT-をAC(GND)と接続する。MICについてはどこにも接続しないままにしておく。
ケースに入れる
100円ショップで買ったデジタル置時計を分解して中のパーツをとりだしてケーブルなどを通す穴をあける
電池ボックスも不要なので削ってスペースを確保する
正面もウレタンで覆って完成
動作の様子
https://youtube.com/shorts/Ybx9tYJs2fo?feature=share
ご注意
既製品を改造するときは十分注意して自己責任でお願いします。
参考
https://github.com/danjperron/PicoAudioPWM/tree/main
https://docs.micropython.org/en/latest/library/_thread.html#module-_thread
https://www.waveshare.com/wiki/Pico-LCD-1.14
http://www.yas-s.com/index.php?key=jo3nzeaja-51
https://serverarekore.blogspot.com/2021/07/raspberry-pi-picotpa2006dwav.html