みなさん、こんにちは。
前回の記事では、Raspberry Pi OS Trixie で従来の wpa_supplicant を使い続ける方法をご紹介しました。
OSのメジャーアップデート、特に Bookworm から Trixie への移行は、IoTエンジニアにとって「これまで動いていたものが、なぜか動かなくなる」という試練の連続です。実はネットワーク設定以外にも、もう一つ大きな、そして非常に根の深い「つまづきポイント」がありました。
それが 「オーディオ周り(音声録音)」 です。
これまで Bullseye 環境で快調に動いていた PyAudio による録音処理が、Trixie にした途端にピタリと止まってしまう……。今回は、この「録音ハングアップ問題」の正体と、現場で編み出した解決策を記録しておきます。
異変 – Trixie 上で PyAudio が「黙る」
私の開発環境では、PyAudio を使って PulseAudio 経由で音声録音を行う処理を書いていました。Bullseye では何の問題もなかったコードです。
ところが Trixie に乗り換えた途端、録音処理がブロックされたまま返ってこない という現象が多発しました。
ログを確認しても「録音開始」の記録はある。しかし、その後の処理が一切流れない。タイムアウトすら発生せず、プロセスはただ静かに、永遠に待ち続けている……。この「原因不明の沈黙」こそが、今回のトラブルの始まりでした。
原因 – Trixie のデフォルト「PipeWire」の挙動
なぜ止まったのか? その答えは、Raspberry Pi OS の基盤となっている Debian の変更にありました。
Bookworm 以降、そして Trixie では、デフォルトの音声システムとして PipeWire が採用されています。
PipeWire には PulseAudio 互換レイヤー(pipewire-pulse)があるため、一見すると従来のコードがそのまま動くように見えます。しかし、ここに大きな罠が隠れていました。それが 「ソースの SUSPENDED(サスペンド)状態」 です。
魔の「SUSPENDED」ステータス
PipeWire + pipewire-pulse の組み合わせでは、リソース節約のためにアクティブなクライアントがいない入力ソースを自動的にサスペンド状態にします。
問題は、PyAudio(内部では PortAudio → PulseAudio と連携)がこのサスペンド状態のソースに接続しに行ったときです。本来なら接続と同時に「起きて」データを流すべきなのですが、PipeWire 環境下ではデータが一切流れてこない(ストリームが開始されない) という事象が起きていたのです。
PyAudio の read() は同期的なブロッキング処理です。データが来なければ、プログラムはそこでフリーズしてしまいます。これがハングアップの正体でした。
解決策 – PipeWire を捨て、ALSA + PulseAudio に回帰する
最新の PipeWire は有望な技術ですが、今回の「ヘッドレス環境で安定してサービスを常駐させる」という目的においては、実績のある PulseAudio を直接制御する ほうが確実であると判断しました。
音声システムの再構成(OS設定)
まずは、PipeWire の互換レイヤーを削除し、PulseAudio をシステムに常駐させる設定を行います。
# PulseAudio のインストール
sudo apt-get install pulseaudio pulseaudio-utils alsa-plugins-pulseaudio
# PipeWire の PulseAudio 互換レイヤーを削除
sudo apt remove pipewire-pulse
# PipeWire サービスを無効化し、干渉を防ぐ
systemctl --user stop pipewire-pulse.socket pipewire-pulse.service
systemctl --user disable pipewire-pulse.socket pipewire-pulse.service
systemctl --user mask pipewire-pulse.socket pipewire-pulse.service
# ログインしていなくても PulseAudio が常時稼働するよう「linger」を有効化
# ※ <ユーザー名> は自分の環境に合わせて書き換えてください
sudo loginctl enable-linger <ユーザー名>
systemctl --user unmask pulseaudio.service pulseaudio.socket
systemctl --user enable --now pulseaudio.service pulseaudio.socket
ここで重要なのが loginctl enable-linger です。これにより、SSHセッションを切っても、あるいはシステム起動直後のログイン前であっても、ユーザー権限で PulseAudio がバックグラウンドで動き続けてくれます。
実装の変更 – PyAudio から arecord(ALSA直接アクセス)へ
音声システムを整えても、PyAudio にはデバイスインデックスの不安定さなどの懸念が残ります。そこで、録音処理そのものを ALSA に直接アクセスする arecord コマンド を叩く方式に全面刷新しました。
旧実装(PyAudio)の問題
# PyAudio は PortAudio を通じて PulseAudio と通信するため、
# SUSPENDED 問題の影響をモロに受け、ここで止まっていました。
frames = stream.read(chunk)
新実装(arecord + subprocess)
arecord を使えば ALSA を直接叩くため、PulseAudio のサスペンド問題に左右されません。また、subprocess の timeout 機能を使い、万が一のハングアップにも備えています。
import subprocess
def get_alsa_card_for_default_source():
# PulseAudio のデフォルトソースから ALSA のカード番号を特定する
default_source = subprocess.run(
["pactl", "get-default-source"],
capture_output=True, text=True
).stdout.strip()
sources_output = subprocess.run(
["pactl", "list", "sources"],
capture_output=True, text=True
).stdout
in_target = False
for line in sources_output.split('\n'):
if default_source and default_source in line:
in_target = True
if in_target and 'alsa.card = "' in line:
card = line.split('"')[1]
return int(card)
return 1 # 見つからない場合のフォールバック
# デバイス番号を動的に取得して録音
alsa_card = get_alsa_card_for_default_source()
alsa_device = f"plughw:{alsa_card},0"
result = subprocess.run(
["arecord", "-D", alsa_device, "-f", "S16_LE", "-r", "16000", "-c", "1", "-t", "raw", "-d", "5"],
capture_output=True,
timeout=15 # 録音時間+αでタイムアウトを設定
)
raw_data = result.stdout
あえて pactl でカード番号を取得しているのは、USBマイクの抜き差しなどでデバイス番号が変わるのを防ぐためです。「管理は PulseAudio、実務(録音)は ALSA」という、いいとこ取りのハイブリッド構成にしました。
まとめ – Trixie 移行後のオーディオ構成
| 項目 | Bullseye 時代 | Trixie 対応後 |
| デフォルト音声系 | PulseAudio | PulseAudio (PipeWire除去) |
| 録音ライブラリ | PyAudio (PortAudio経由) | arecord (ALSA直接アクセス) |
| デバイス指定 | device_index (固定) | pactl による動的取得 |
| 堅牢性 | なし | subprocess の timeout 指定 |
Trixie への移行は、単なるOSのアップデートではなく、音声サブシステムのアーキテクチャそのものが入れ替わる大きな変化でした。
「最新の PipeWire で頑張る」という選択肢もありましたが、現場のシステムに求められるのは何よりも 「止まらないこと」 です。もし、Trixie + ヘッドレス環境で PyAudio のハングアップに悩んでいる方がいたら、この「あえて戻す」という選択肢も検討してみてください。
本日も最後までお読みいただきありがとうございました。
それでは、よい Raspberry Pi ライフを!



