1. 背景
WAVファイルは、MP3ファイルに比べるとファイル容量が大きく、数秒の短い小さいファイルを除き、Raspberry Pi Picoの2MBしかないフラッシュメモリーには置けません。
大きいWAVデータを再生したい場合は、SDカードに置く方法がありますが。。。
そんな中、WAVデータをSDカードにコピーする作業をしているときに、ストリーミング再生 を思い付きました。
Pico W は Wifi が使えます。HTTPでアクセスできるファイルサーバにWAVデータを置いておけば、いちいちSDカードにコピーする手間が省けますし、インターネットで公開されているWAVデータも再生することもがきます。
ただし、フラッシュメモリーの空き容量が少ないため、ダウンロード方式は使えません。あくまでストリーミングで再生する必要があります。
最終的に MicroPython で実現できたので、ここにまとめておきます。
2. おさらい
Pico で WAVファイルを再生する方法は、次のとおり。
・ CircuitPython の場合
wav = audiocore.WaveFile("sample.wav")
audio = audioio.AudioOut(board.A0)
audio.play(wav)
CircuitPythonのライブラリが使えるため、実質3行のコードでWAVファイルを再生できます。
当初、
audiocore.WaveFile
メソッドの第一引数BinaryIO
にsocket
を渡す方法を模索し試行錯誤したのですが、結局、よく分からずで諦めました。
(今でもこのアプローチで実現できるのでは と思っています)
・ MicroPython の場合
↓ こちらのサイトで紹介されているコードにて、実現できます。
How to get i2s playback with Raspberry Pi Pico
2. WAVファイルヘッダーを読み取る
前述の How to get i2s playback with Raspberry Pi Pico で紹介されているコードをベースに、WAVデータを読み込んだバッファからヘッダー情報を得るように改修しました。
WaveFileHeader.from_buffer()
主なヘッダー情報;
- チャンネル数 ex) 1:モノラル、2:ステレオ
- サンプリング周波数 ex) 44.1KHz、22.05KHz
- ビットレート ex) 16bits、8bits
- オフセット:WAVファイルヘッダー"RIFF"が始まる位置
import struct
class WaveFileHeader:
@staticmethod
def from_buffer(file_buffer):
offset = file_buffer.find(b"RIFF")
if offset < 0:
raise ValueError("No RIFF header.")
ident = file_buffer[offset: offset+4]
if ident != b"RIFF":
raise ValueError(f"Expected RIFF header, got {ident}.")
file_size = struct.unpack("I", file_buffer[offset+4: offset+8])
file_type = file_buffer[offset+8: offset+16]
if file_type[:7] != b"WAVEfmt":
raise ValueError(f"Not WAVE type. Got {file_type}")
fmt = "IHHIIHHHI"
fmt_size, wave_fmt, channels, sample_rate, byte_rate, block_align, bits_per_sample, extra, data_size = \
struct.unpack(fmt, file_buffer[offset+16: offset+16 + struct.calcsize(fmt)])
header = WaveFileHeader()
header.wave_fmt = wave_fmt
header.channels = channels
header.sample_rate = sample_rate
header.bits_per_sample = bits_per_sample
header.data_size = data_size
header.offset = offset
return header
def __init__(self):
self.data_size = 0xFFFF
self.wave_fmt = 1 # PCM
self.channels = 1
self.sample_rate = 22_050
self.bits_per_sample = 16
self.offset = 0
def print(self, filename=None):
if filename: print(f"File Name: {filename}")
print(f"Data size: {self.data_size:,} bytes")
print(f"Wave format: {self.wave_fmt}")
print(f"Bits per sample: {self.bits_per_sample}")
print(f"Sample rate: {self.sample_rate:,} Hz")
print(f"Channels: {self.channels}")
print(f"Offset: {self.offset}")
4. ストリーミング再生するコード
4.1 sock_stream_play
これも前述の How to get i2s playback with Raspberry Pi Pico のコードを参考にさせてもらいました。
↓ i2s DACを使用していますが、PWM Audioでも応用できると思います。

from machine import I2S, Pin
import socket
import ucontextlib
import wave_file
# I2S setup
i2s_sck_pin = Pin(0) #BCK
i2s_word_select= Pin(1) #LCK
i2s_data_out = Pin(2) #DIN
i2s_id = 0
BUFFER_LENGTH_IN_BYTES = 40000
@ucontextlib.contextmanager
def managed_i2s(wav_header):
i2s = I2S(
i2s_id,
sck=i2s_sck_pin,
ws=i2s_word_select,
sd=i2s_data_out,
mode=I2S.TX,
bits=wav_header.bits_per_sample,
format=(I2S.MONO if wav_header.channels == 1 else I2S.STEREO),
rate=wav_header.sample_rate,
ibuf=BUFFER_LENGTH_IN_BYTES,
)
try:
yield i2s
finally:
i2s.deinit()
@ucontextlib.contextmanager
def sock_stream(url):
_, _, host, path = url.split('/', 3)
filename = path.split('/')[-1]
addr_info = socket.getaddrinfo(host, 80, 0, socket.SOCK_STREAM)
addr = addr_info[0][-1]
sock = socket.socket()
sock.connect(addr)
http_req = bytes(f'GET /{path} HTTP/1.0\r\nHost: {host}\r\n\r\n', 'utf8')
sock.send(http_req)
try:
yield sock
finally:
sock.close()
def play(url, sample_buf=2048):
with sock_stream(url) as sock:
data = sock.recv(sample_buf)
if data.find(b'200') < 0:
raise ValueError(f"http request error. {data[:32]}")
wav_header = wave_file.WaveFileHeader.from_buffer(data)
wav_header.print()
data = data[wav_header.offset:]
with managed_i2s(wav_header) as i2s:
while data:
_ = i2s.write(data)
data = sock.read(sample_buf)
- Http の Getリクエストを組み立て、socket を使用して目的のホストに送信し、レスポンスを2KBずつ受信する
- 最初のレスポンスデータに WAVファイルヘッダーが含まれているので、取得したWAVヘッダー情報をもとにi2sオーディオを生成
- eofになるまで、recv(soket)し write(i2s)する
試行錯誤の末に、このコードに辿り着きました。
4.2 使用例
Wifiにつながっている前提のため、このコードに先立ってWifiに接続しておくこと。
from sock_stream_play import play
play('https://www.ne.jp/asahi/music/myuu/wave/musicbox.wav') # 1.4MB、8秒
#play('https://www.ne.jp/asahi/music/myuu/wave/strings.wav') # 5.2MB、30秒
#play('https://server.local/sounds/bgm1.wav') # 0.9GB、1.5時間
#play('https://server.local/sounds/bgm2.wav') # 2.4GB、4.0時間
4.0時間のWAVデータ(容量2.4GB、44.1KHz、16bits)も 4時間ノンストップで再生できました。ときどきノイズが出るのは通信の乱れの影響と思いますが、ネットワークアクセスが安定していれば普通に再生します。
5. 考察
-
2KBずつ受信としていますが、家WiFiだと ホストによらず 800バイトずつ受信していました。どこかをいじればTCP/IPのパケット長を変更できるかも知れません
-
ぶつぶつ音が飛ぶようなら、非同期IOとダブルバッファでの実装を考えていましたが、同期IOでも再生が十分間に合ったため、いまのコードに落ち着きました
-
4時間のWAVデータは、正確には 4時間2分46秒です。PicoWでストリーミング再生した時間を測ると、4時間3分0秒で差はわずか14秒です。
4時間の再生時間の中での差としては、無視できる程度と言えるでしょう
-
ESP32でも同じコードで動くはずです(i2sのGPIOを変えるだけ)
6. フリー音源
↓こちらで公開されているWAVファイルを、何度も利用させていただきました。
ありがとうございます。
7. おわりに
Raspberry Pi Pico W の半値程度のESP32モジュールを使って、ストリーミング再生機を作ろうか思案中。
ダイソーの300円アンプ付きスピーカに内蔵してしまうのも面白い。
ESP32の内蔵DACで実装すれば 安価に製作できる。おそらくモノラル再生となってしまうが、どうせBGM用途のため 問題は無い。
その場合は、44.1KHzの高いサンプリングレートは不要で、16KHz程度でよいと思う。
2024.5.27 追記
さっそく、ESP32で同じコードで試したのですが、ノイズだらけでとても聴けたもんではありませんでした。おそらく、socketの受信が追いついて無いのだと思います。・・・残念
なお、SDカードからの再生は なんの問題もありませんでした。