みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

ざっくりとALSAとPulseAudioの関係(の続きのようなもの)

はじめに

以前に書いた2015-04-04 ざっくりとALSAとPulseAudioの関係が未だにアクセスがあるようなのでちょっと振り返ってみる。

基本的な理解としては、

  1. 最終的に音を鳴らすのはデバイス
  2. バイスを叩くためにデバイスドライバ(カーネルランド)
  3. サウンド関連でカーネルランドとユーザーランド橋渡しをする部分を取りまとめているのがALSA
  4. 各アプリケーション間でサウンド関連のリソースを調停するのがPulseAudioなどのサウンドサーバ

これは変わっていない。

先述の記事では次のように書いた。

それを回避するためにPulseAudioなどのサウンドサーバはALSAのデバイスのふりをして、ALSAAPIを直接叩いているアプリケーションのサウンドリソースを横取りしてしまう。 そうして、管理下に置いたあと本物のALSAに音を流す。

ここで言う本物のALSAとは、カーネルランドにいるALSAのドライバフレームワークを主に示している。

実験環境

確認にはラズベリーパイ3 Model Bを使用。 OSはYocto Projectで作成したものを使用。(warrior/2.7)

ホストPCの環境はUbuntu 18.04。

ほんとに横取りしているのか

これを簡単に確認するにはPulseAudioが動く環境でaplayを実行してみるのがわかりやすい。

Yocto環境の設定

aplayとPulseAudioが動く環境を作るためにlocal.confに次の行を追加する。

# userland ALSA
IMAGE_INSTALL_append = " alsa-utils"

# pulseaudio
IMAGE_INSTALL_append = " pulseaudio-server"

# Enable onboard audio
RPI_EXTRA_CONFIG = ' \n \
# Enable audio (loads snd_bcm2835) \n \
dtparam=audio=on \n \
'

RPI_EXTRA_CONFIGでは、オンボードサウンドバイスを有効化している。

bitbakeでcore-image-baseを作成し、そのイメージでラズベリーパイを起動する。

横取りの確認

sshなどを使用して、ラズベリーパイのターミナルを2つ立ち上げる。

片方でpulseaudioを起動する。

$ pulseaudio --verbose

そしてもう片方でaplayを実行する。

$ aplay /usr/share/sounds/alsa/Front_Left.wav

PulseAudioのターミナルの表示が次のようになり、aplayを実行することでPulseAudioが動作していることがわかる。

I: [pulseaudio] main.c: Daemon startup complete.
I: [pulseaudio] module-suspend-on-idle.c: Sink alsa_output.platform-soc_audio.analog-mono idle for too long, suspending ...
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Device suspended...
I: [pulseaudio] client.c: Created 0 "Native client (UNIX socket client)"
I: [pulseaudio] protocol-native.c: Got credentials: uid=0 gid=0 success=1
I: [pulseaudio] sink-input.c: Trying to change sample rate
I: [pulseaudio] alsa-sink.c: Updating rate for device hw:0, new rate is 48000
I: [pulseaudio] source.c: Changed sampling rate successfully
I: [pulseaudio] sink.c: Changed format successfully
I: [pulseaudio] sink-input.c: Rate changed to 48000 Hz
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Trying resume...
I: [alsa-sink-bcm2835 ALSA] alsa-util.c: Cannot disable ALSA period wakeups
I: [alsa-sink-bcm2835 ALSA] alsa-util.c: ALSA period wakeups were not disabled
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Time scheduling watermark is 36.75ms
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Resumed successfully...
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Starting playback.
I: [pulseaudio] sink-input.c: Created input 0 "ALSA Playback" on alsa_output.platform-soc_audio.analog-mono with sample spec s16le 1ch 48000Hz and channel map mono
I: [pulseaudio] sink-input.c:     media.name = "ALSA Playback"
I: [pulseaudio] sink-input.c:     application.name = "ALSA plug-in [aplay]"
I: [pulseaudio] sink-input.c:     native-protocol.peer = "UNIX socket client"
I: [pulseaudio] sink-input.c:     native-protocol.version = "32"
I: [pulseaudio] sink-input.c:     application.process.id = "321"
I: [pulseaudio] sink-input.c:     application.process.user = "root"
I: [pulseaudio] sink-input.c:     application.process.host = "raspberrypi3"
I: [pulseaudio] sink-input.c:     application.process.binary = "aplay"
I: [pulseaudio] sink-input.c:     application.language = "C"
I: [pulseaudio] sink-input.c:     application.process.machine_id = "e08cd0ccf41446ecb446dbd2a097d362"
I: [pulseaudio] sink-input.c:     application.process.session_id = "c1"
I: [pulseaudio] sink-input.c:     module-stream-restore.id = "sink-input-by-application-name:ALSA plug-in [aplay]"
I: [pulseaudio] protocol-native.c: Requested tlength=500.00 ms, minreq=125.00 ms
I: [pulseaudio] protocol-native.c: Final latency 625.00 ms = 250.00 ms + 2*125.00 ms + 125.00 ms
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Tried rewind, but was apparently not possible.
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Tried rewind, but was apparently not possible.
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Tried rewind, but was apparently not possible.
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Tried rewind, but was apparently not possible.
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Tried rewind, but was apparently not possible.
I: [pulseaudio] sink-input.c: Freeing input 0 "ALSA Playback"
I: [pulseaudio] client.c: Freed 0 "ALSA plug-in [aplay]"
I: [pulseaudio] protocol-native.c: Connection died.
I: [pulseaudio] module-suspend-on-idle.c: Sink alsa_output.platform-soc_audio.analog-mono idle for too long, suspending ...
I: [alsa-sink-bcm2835 ALSA] alsa-sink.c: Device suspended...

PulseAudioはフォアグラウンドで実行している時、一定時間クライアントから接続がないと終了してしまうので注意が必要。

callgrindで見てみる

実際のところ、どのようになっているのかをcallgrindで見てみる。

$ valgrind --tool=callgrind aplay /usr/share/sounds/alsa/Front_Left.wav

で作成したコールグラフをホストPCのkcachegrindで開く。

画面左上のリストからaplayを選択し、左下のリストでmainを選択する。

画面右下の方にあるCall Graphのタブをクリックしコールグラフを開く。

するとaplayのメイン関数に注目したコールグラフが表示される。

しかし肝心な部分が<cycle 3>としてまとめられてしまっていて細かいことを読み取ることができない。

f:id:mickey_happygolucky:20190830124314p:plain
aplayのコールグラフ

ツールバーにあるCycle Detectionをクリックし、cycle検出機能をオフにすると、<cycle 3>の内容が展開される。

f:id:mickey_happygolucky:20190830124323p:plain
<cycle 3>の展開

main'2からsnd_pcm_openが呼び出されていることがわかる。

コールグラフ中の関数をダブルクリックすると、注目する関数を切り替えることができる。その時に注目している関数がどのライブラリ(ELF Object)に属しているかは画面左上のリストの選択状態でわかる。

このようにして関数を調べていくと、ALSA APIのインターフェースであるlibasound.so.2.0.0ioplug_priv_transfer_areas関数からPulseAudioのモジュールであるlibasound_module_pcm_pulse.sopulse_start関数が呼び出されていることがわかった。

f:id:mickey_happygolucky:20190830124337p:plain
libasoundからPulseAudioの呼び出し

つまり、アプリケーションからALSAAPIを呼び出すと内部からpulseaudioのモジュールが呼び出されているということになる。

まとめ

aplayやcallgrindをつかってユーザー空間でのALSA APIの挙動を調べたところ、PulseAudioがALSAの処理を横取りしていることが確認できた。

f:id:mickey_happygolucky:20150404105017p:plain
横取りの図式

gstreamerでalsasinkを使う場合などでもcallgrindでみると同じようになっていることが確認できる。

PulseAudioからカーネルALSAフレームワークへの流れはまた機会があれば。