DACなしでPi Picoで音声ファイルを再生する
Raspberry Picoは、組み込み開発で今や私のお気に入りのチップになりつつあります。よく作り込まれた頑丈なハードウェアで、スマートさと情熱をもって設計されたように見える機能が満載です(GPIOを駆動するステートマシンは、まさに強力な機能です!)。主な弱点だった接続性の欠如も、Wバリアントによって解決されました。データシートは非常に優秀で、チップのあらゆる側面が文書化されています。さらに、MicroPython(私はかなり使っています)のサポートも充実しており、C SDKの開発環境も悪くありません。今日の流行に合わせた、cmakeビルドシステムがさらにMakefileを生成する仕組みや、あれこれを定義するファイル(使用するライブラリ、デバッグ出力など)といった、役に立たない複雑さで満ちているとしてもです。いや、それどころではありません。W版と非W版の違いを除けば、機能が固定されたハードウェア向けに、小さなデバイス用の小さなプログラムをコンパイルするだけなのに、これほど複雑なのです。今日のソフトウェアがどれほどひどいかという愚痴はこのくらいにしておきますが、そのことは覚えておくべきでしょう。
このようなMCUでやりたいことの一つといえば、音を生成することです。最もわかりやすい方法は、チップに内蔵されたPWM機能を使うことです。GPIOを、次のように、指定した周波数で0と1を交互に切り替えるよう設定できます。
from machine import Pin, PWM
pwm = PWM(Pin(1))
pwm.freq(400)
pwm.duty_u16(1000)PicoのGNDと1番ピンに圧電ブザーを接続したとすると、周波数400Hzの矩形波音が聞こえます。ところで、矩形波ほど聞くに堪えない音はそうありません。もう少しましにできるかもしれません。ここでは、正弦波の生成など途中の手順は省略して、いきなりWAVファイルの再生に進みます。これができるとわかれば、正弦波やノイズ、そうした波形のエンベロープなど、ほかの波形も簡単に生成できるようになります。
ここで、PicoがピンをHighかLowにしか切り替えられないのに、WAVファイルを再生する複雑な波形をどうやって生成するのか、と疑問に思うでしょう。矩形波ではない適切な波形は異なるレベルで構成されるため、DACが必要になるはずです。しかし幸いなことに、DACはまったく必要ありません。Picoのピンを1本使うだけで、すべて実現できます。
複雑な音声生成の仕組み
ここで背景を詳しく説明するつもりはありません。ただ、単純な矩形波、つまり出力を最小レベルと最大レベルの間で交互に切り替えるだけの波形を生成したくないなら、次のような中間段階が必要になる、ということだけ知っておいてください。
S0: #
S1: ####
S2: ######
S3: #######
S4: ########このように続きます。S0が最初のサンプル、S1が2番目のサンプル、という具合です。
各サンプルの長さはサンプリング周波数によって決まります。サンプリング周波数とは、音声波形を再生するとき、または録音するときに、1秒間に何回変化させるか、あるいはサンプリングするかを表すものです。つまり、複雑な音を再生するには、Picoのピンから異なる電圧を出力できなければなりません。
PicoでPWMだけを使ってこれを実現する方法があります。非常に高い周波数の矩形波を使い、生成したい電圧ごとにデューティ比を変えるのです。まず、出力周波数を非常に高く設定します。
pwm.freq(100000)次に、S0のサンプルを生成したければ、デューティ比(値は0から65535の間です)を小さく設定します。S1のサンプルなら、より大きな値を使います。このように続けて、次のように設定できます。
pwm.duty_u16(3000) # S0
pwm.duty_u16(12000) # S1
pwm.duty_u16(18000) # S2
pwm.duty_u16(21000) # S3
pwm.duty_u16(24000) # S4デューティ比とは、ピンが1に設定されている時間と、0に設定されている時間の割合です。デューティ比65535は、ピンが100%の時間Highであることを意味します。0%なら、ずっとLowです。これらすべてを、設定した交互切り替えの周波数を保ったまま行います。オシロスコープで見ているように拡大すると、S2とS3のサンプル生成中に何が起きているかがわかります。
S2:
######################
#
#
#
#
######################
#
#
#
#一方、S3は次のようになります。
######################
######################
#
#
#
######################
######################
#
#
#ピンが上下する周波数は同じですが、S3ではHighの時間が長くなっています。そのため平均電圧が高くなります。これによって、波形を近似できるわけです。
WAVファイルを変換して再生する
WAVファイルを再生するには、MicroPythonで読み取りやすいraw形式に変換する必要があります。SoundCloudから「Oh no!」という音声のWAVファイルをダウンロードしました。変換は次のようになります。
ffmpeg -i ohno.wav -ar 24000 -acodec pcm_u8 -f u8 output.rawここでは、ファイルを8ビット音声に変換しています(サンプルごとに256段階の出力レベル)。とはいえ、今回のPWMのトリックでは異なるレベルをそれほどうまく近似できませんし、リソースにも制約があります。16ビットでも試せますが、私の場合はこの設定で十分な結果が得られました。
次に、mpremoteを使ってoutput.rawファイルをデバイスへアップロードします。
mpremote cp output.raw :では、「play.py」という名前のファイルを、好きな名前で構いませんので作成し、次の内容を書き込みます。
from machine import Pin, PWM
pwm = PWM(Pin(1))
pwm.freq(100000)
f = open("output.raw","rb")
buf = bytearray(4096)
while f.readinto(buf) > 0:
for sample in buf:
pwm.duty_u16(sample<<8)
x=1
x=1
x=1
x=1
x=1
f.close()ここで行っているのは、1回の反復につき4096サンプルずつファイルを読み込み、サンプル値に応じてPWMのデューティ比を次々に設定することで「再生」することだけです。問題は、PCMファイルには1秒あたり24000サンプルあることです(ffmpegのコマンドラインを見てください)。これがMicroPythonの処理速度と一致すると、どうすれば確信できるでしょうか。実際には完全には一致しないため、正しいと思える音程に近づくよう、少し遅延させるために「x=1」という文を追加しました。
そうそう、sample<<8が何をしているのか気になるかもしれません。これは、8ビットのサンプルを、PWMのデューティ比の設定に必要な16ビット精度いっぱいまでスケールし直しているだけです。
この方法の欠点は、再生中ずっとプログラムが専有されることです。まだ試していませんが、MicroPythonはスレッドをサポートしているので、音声再生をスレッドで行うのが解決策になるかもしれません。
おまけ:正弦波の音を生成する
# Sin wave
wave=[]
wave_samples = 40
pwm.freq(100000)
for i in range(wave_samples):
x = i/wave_samples*3.14*2
dc = int((1+math.sin(x))*65000)
wave.append(dc)
print(wave)
for i in range(1000):
for dc in wave: pwm.duty_u16(dc)記事をランダムに読む