2
0

Raspberry Pi Pico W で WAVファイルをストリーミング再生する

Last updated at Posted at 2024-05-26

1. 背景

WAVファイルは、MP3ファイルに比べるとファイル容量が大きく、数秒の短い小さいファイルを除き、Raspberry Pi Picoの2MBしかないフラッシュメモリーには置けません。
大きいWAVデータを再生したい場合は、SDカードに置く方法がありますが。。。

そんな中、WAVデータをSDカードにコピーする作業をしているときに、ストリーミング再生 を思い付きました。

Pico W は Wifi が使えます。HTTPでアクセスできるファイルサーバにWAVデータを置いておけば、いちいちSDカードにコピーする手間が省けますし、インターネットで公開されているWAVデータも再生することもがきます。

ただし、フラッシュメモリーの空き容量が少ないため、ダウンロード方式は使えません。あくまでストリーミングで再生する必要があります。

最終的に MicroPython で実現できたので、ここにまとめておきます。

2. おさらい

Pico で WAVファイルを再生する方法は、次のとおり。

・ CircuitPython の場合

CircuitPython
wav = audiocore.WaveFile("sample.wav")
audio = audioio.AudioOut(board.A0)
audio.play(wav)

CircuitPythonのライブラリが使えるため、実質3行のコードでWAVファイルを再生できます。

当初、audiocore.WaveFileメソッドの第一引数BinaryIOsocketを渡す方法を模索し試行錯誤したのですが、結局、よく分からずで諦めました。
(今でもこのアプローチで実現できるのでは と思っています)


・ 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"が始まる位置
wave_file.py
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でも応用できると思います。

pcm5102_dac.png
sock_stream_play.py
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. 考察

  1. 2KBずつ受信としていますが、家WiFiだと ホストによらず 800バイトずつ受信していました。どこかをいじればTCP/IPのパケット長を変更できるかも知れません
     

  2. ぶつぶつ音が飛ぶようなら、非同期IOとダブルバッファでの実装を考えていましたが、同期IOでも再生が十分間に合ったため、いまのコードに落ち着きました
     

  3. 4時間のWAVデータは、正確には 4時間2分46秒です。PicoWでストリーミング再生した時間を測ると、4時間3分0秒で差はわずか14秒です。
    4時間の再生時間の中での差としては、無視できる程度と言えるでしょう
     

  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カードからの再生は なんの問題もありませんでした。

以上

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0