LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第15回LXCの構築・活用 [3] ― コンテナ内でサウンドを取り扱う

この連載のLXCの構築・活用のシリーズが、今回で3回目を迎えました。目的に合わせたパッケージ構成で、システムコンテナを作成・構築することに、だいぶ慣れてきたかと思います。皆さまは、作成・構築したコンテナをどのように利用されているでしょうか。

LXCでコンテナを使うときは、たいていサーバ用途のアプリケーションを導入して運用することが多く、これまでコンテナ内でサウンドを使う手法は、あまり知られていませんでした。しかし、コンテナ上でサウンドを使うアプリケーションの利用や評価を行ったり、多数のコンテナを起動して、利用者向けにリモートコンソール環境やリモートデスクトップ環境を提供するなど、サウンドを必要とする場面も考えられ、このような使い方に興味をお持ちの方も多いのではないかと思います。今回は、コンテナ内でサウンドを取り扱うにはどうすれば良いのかを紹介していきます。

Linuxにおける主なサウンドシステム・サウンドサーバ

まず、Linuxにおける主なサウンドシステム・サウンドサーバについて、簡単に整理しておきましょう。

Open Sound System(OSS)
UNIXベースのプラットフォームで使うことができるサウンドシステムで、ハードウェアを直接制御するOSSドライバを含んでいます。
Advanced Linux Sound Architecture(ALSA)
OSSを置き換えるために開発されたLinuxにおける高機能なサウンドシステムで、現在はALSAが主流となっています。ALSAドライバとOSSエミュレーション機能を含んでいます。
Enlightened Sound Daemon(ESD、EsounD)
GNOMEで使われていたサウンドサーバで、実行中の各種アプリケーションが出力するサウンドを取りまとめて、ローカルデバイスやネットワーク先のデバイスに送り出します。
PulseAudio
ESDの置き換えとして、クロスプラットフォームでネットワークに対応したサウンドサーバで、現在はPulseAudioが主流となっています。ESDエミュレーション機能を含んでいます。

これらのサウンドシステム・サウンドサーバの関係は、図1のようになっています。基本的に、OSS APIを使用したアプリケーションは/dev/dsp/dev/audioなど(OSS互換デバイスファイル)を通じてデバイスにアクセスし、ALSA APIを使用したアプリケーションは/dev/snd/*(ALSAデバイスファイル)を通じてデバイスにアクセスします。Plamo Linuxでは、aplayコマンドなどのALSA APIを使用したアプリケーションやesdplayコマンドなどのESD APIを使用したアプリケーションは、paplayコマンドなどのPulseAudio APIを使用したアプリケーションとともに、PulseAudioを経由して処理されるように設定されており、またOSS APIを使用したアプリケーションについても、padspコマンド(ラッパープログラム)使って、PulseAudioで一元的に管理することができます。

図1 サウンドシステム・サウンドサーバの関係
図1 サウンドシステム・サウンドサーバの関係

OSSを経由してサウンドを出力する

まず、ホスト上で音を出すことを考えてみましょう。OSS互換オーディオデバイスファイルを経由してサウンドを出力する仕組みはとても簡単です。Sunオーディオファイルであれば、以下のように/dev/audioにリダイレクトすることにより、音を出すことができます。

taro@host:~$ cat pipipipi.au > /dev/audio

一般的なWAVファイルのような、Sunオーディオファイル以外の音声ファイルについては、/dev/dspを使います。このとき、音声ファイルをそのままリダイレクトするのではなく、/dev/dspをオープンしてから音声ファイル形式を設定し、そこに音声データを流し込むことにより、音を出ことができます。

ここで、16ビット/ステレオ/44.1kHzの音声ファイルを再生するサンプルプログラムdsptest.cを紹介します。わかりやすさ優先で、ファイル名は決め打ち、エラー処理は必要最小限に止めた簡単なプログラムです。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/soundcard.h>
#include <sys/ioctl.h>
#include <unistd.h>

static unsigned char buf[2048];

int main(void)
{
    int fdi, fdo, arg, len;

    fdi = open("geragerapo-no-uta.wav", O_RDONLY);
    if ((fdo = open("/dev/dsp", O_WRONLY)) == -1) {
        perror("/dev/dsp");
        close(fdi);
        return 1;
    }
    arg = AFMT_S16_LE, ioctl(fdo, SNDCTL_DSP_SETFMT, &arg);
    arg = 2, ioctl(fdo, SNDCTL_DSP_CHANNELS, &arg);
    arg = 44100, ioctl(fdo, SNDCTL_DSP_SPEED, &arg);
    lseek(fdi, 44, SEEK_SET);
    while ((len = read(fdi, buf, 2048)) > 0)
        write(fdo, buf, len);
    close(fdi), close(fdo);
    return 0;
}

以下のようにコンパイルして実行することにより、音を出すことができます。このプログラムは後で使いますので、残しておいてください。

taro@host:~$ make dsptest
cc     dsptest.c   -o dsptest
taro@host:~$ ./dsptest

コンテナ内でサウンドを出力する(OSS)

それでは、いよいよコンテナ内で音を出すことを試してみましょう。あらかじめ、taroユーザがroot権限でdefaultバリアントのPlamoコンテナct01を作成し、そのコンテナ内で作成したhanakoユーザが音を出すことを試みます。

hanako@ct01:~$ cat pipipipi.au > /dev/audio
-bash: /dev/audio: 許可がありません
hanako@ct01:~$ ./dsptest
/dev/dsp: No such file or directory

コンテナ内に/dev/audio/dev/dspが存在しないため、音が出ませんね。そこで、ホストにいるtaroユーザが以下のコマンドを実行して、OSS互換オーディオデバイスファイルを作成します。

taro@host:~$ sudo lxc-attach -n ct01 -e -- \
> mknod -m 660 /dev/mixer c 14 0
taro@host:~$ sudo lxc-attach -n ct01 -e -- \
> mknod -m 660 /dev/sequencer c 14 1
taro@host:~$ sudo lxc-attach -n ct01 -e -- \
> mknod -m 660 /dev/dsp c 14 3
taro@host:~$ sudo lxc-attach -n ct01 -e -- \
> mknod -m 660 /dev/audio c 14 4
taro@host:~$ sudo lxc-attach -n ct01 -e -- \
> mknod -m 660 /dev/sequencer2 c 14 8
taro@host:~$ sudo lxc-attach -n ct01 -- \
> chmod +t /dev/{mixer,sequencer{,2},dsp,audio}
taro@host:~$ sudo lxc-attach -n ct01 -- \
> chgrp audio /dev/{mixer,sequencer{,2},dsp,audio}

また、オーディオデバイスにアクセスできるように、以下の設定をコンテナの設定ファイル/var/lib/lxc/ct01/configに追加して、コンテナを再起動します。

# Audio configuration
lxc.cgroup.devices.allow = c 14:* rwm

これらの設定により、音が出るかどうか再び試みます。

hanako@ct01:~$ cat pipipipi.au > /dev/audio
hanako@ct01:~$ ./dsptest

これで音が出るようになりました。

コンテナ内でサウンドを出力する(ALSA)

次に、ALSA APIを使用したaplayコマンドを使って音を出してみましょう。実行したところ、以下のエラーが出てしまいました。

hanako@ct01:~$ aplay pipipipi.au
ALSA lib confmisc.c:768:(parse_card) cannot find card '0'
ALSA lib conf.c:4246:(_snd_config_evaluate) function snd_func_card_driver returned error: \
そのようなファイルやディレクトリはありません
ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings
ALSA lib conf.c:4246:(_snd_config_evaluate) function snd_func_concat returned error: \
そのようなファイルやディレクトリはありません
ALSA lib confmisc.c:1251:(snd_func_refer) error evaluating name
ALSA lib conf.c:4246:(_snd_config_evaluate) function snd_func_refer returned error: \
そのようなファイルやディレクトリはありません
ALSA lib conf.c:4725:(snd_config_expand) Evaluate error: \
そのようなファイルやディレクトリはありません
ALSA lib pcm.c:2217:(snd_pcm_open_noupdate) Unknown PCM default
aplay: main:696: audio open error: \
そのようなファイルやディレクトリはありません

はい、コンテナ内に/dev/snd/*が存在しないためですね。ホストにいるtaroユーザが以下のコマンドを実行して、/dev/sndディレクトリを作成します。

taro@host:~$ sudo lxc-attach -n ct01 -- mkdir -m 755 /dev/snd

また、/dev/sndをバインドマウントする処理を含めた以下の設定を、コンテナの設定ファイル/var/lib/lxc/ct01/configに追加して、コンテナを再起動します。

# Audio configuration
lxc.mount.entry = /dev/snd dev/snd none bind 0 0
lxc.cgroup.devices.allow = c 14:* rwm
lxc.cgroup.devices.allow = c 116:* rwm

これらの設定により、音が出るかどうか再び試みます。

hanako@ct01:~$ aplay pipipipi.au
再生中 Sparc オーディオ 'pipipipi.au' : Mu-Law, レート 8000 Hz, モノラル
hanako@ct01:~$ aplay geragerapo-no-uta.wav
再生中 WAVE 'geragerapo-no-uta.wav' : Signed 16 bit Little Endian, レート 44100 Hz, ステレオ

これで音が出るようになりました。

非特権コンテナ内でサウンドを出力する

一般ユーザ権限で利用するコンテナ(非特権コンテナ)で音を出すための設定は、ここまでの節で説明してきたroot権限で利用するコンテナで音を出すための設定とほぼ同じです。ただし、デバイスファイルを作成する代わりにバインドマウントを行い、デバイスファイルのパーミッションを変更する必要があります。

あらかじめ、taroユーザが一般ユーザ権限でdefaultバリアントのPlamoコンテナct01(非特権コンテナ)を作成し、そのコンテナ内でhanakoユーザを作成しておきます。非特権コンテナでは、キャラクタデバイスやブロックデバイスを作成するmknodは実行できないので、代わりにOSS互換オーディオデバイスファイルをバインドマウントする処理を含めた以下の設定を、コンテナの設定ファイル/var/lib/lxc/ct01/configに追加して、コンテナを再起動します。

# Audio configuration
lxc.mount.entry = /dev/mixer dev/mixer none bind,create=file 0 0
lxc.mount.entry = /dev/sequencer dev/sequencer none bind,create=file 0 0
lxc.mount.entry = /dev/dsp dev/dsp none bind,create=file 0 0
lxc.mount.entry = /dev/audio dev/audio none bind,create=file 0 0
lxc.mount.entry = /dev/sequencer2 dev/sequencer2 none bind,create=file 0 0
lxc.mount.entry = /dev/snd dev/snd none bind 0 0
lxc.cgroup.devices.allow = c 14:* rwm
lxc.cgroup.devices.allow = c 116:* rwm

また、コンテナ内のhanakoユーザは、ホストOSのaudioグループ権限を持っていないので、ホスト上でオーディオデバイスファイルのパーミッションを変更して、コンテナ内のhanakoユーザが、オーディオデバイスファイルにアクセスできるようにする必要があります。

taro@host:/dev$ sudo find . -group audio -exec chmod o+rw {} \;

これらの設定により、音が出るようになります。

hanako@ct01:~$ cat pipipipi.au > /dev/audio
hanako@ct01:~$ ./dsptest
hanako@ct01:~$ aplay pipipipi.au
再生中 Sparc オーディオ 'pipipipi.au' : Mu-Law, レート 8000 Hz, モノラル
hanako@ct01:~$ aplay geragerapo-no-uta.wav
再生中 WAVE 'geragerapo-no-uta.wav' : Signed 16 bit Little Endian, レート 44100 Hz, ステレオ

これで、root権限で利用するコンテナ、一般ユーザ権限で利用するコンテナ(非特権コンテナ)ともに、コンテナ内から音を出すことができました。

サウンドサーバを使ってコンテナ内のサウンドを出力する

ここまでの節で説明した方法は、ホストOSで管理しているサウンドデバイスを、コンテナが使いたいときに、早いもの勝ちで利用する仕組みです。すなわち、あるアプリケーションがサウンドデバイスを使っている間、他のアプリケーションはサウンドデバイスを使用できなくなります。ホストにいるtaroユーザとコンテナ内のhanakoユーザが、あらかじめサウンドデバイスを使う割り当て時間を決めておき、ルールを守って仲良くシェアしているうちは良いのですが、多数のコンテナを起動して、多くの利用者が同時に使うような状況では、かなり難しい運用であることが想像できると思います。

そこで、ネットワークに対応したサウンドサーバ(PulseAudio)を使って、コンテナ内のサウンドを出力することを考えてみましょう。PulseAudioは、defaultバリアントのPlamoコンテナに含まれていないので、pulseaudioパッケージを追加するか、PulseAudioを含むバリアントのPlamoコンテナを用意する必要があります。ここでは、手っ取り早くlargeバリアントのPlamoコンテナを作成して、PulseAudioを使った方法を試してみましょう。

あらかじめ、以下のコマンドでtaroユーザが一般ユーザ権限でlargeバリアントのPlamoコンテナct02(非特権コンテナ)を作成し、そのコンテナ内でhanakoユーザを作成しておきます。

taro@host:~$ lxc-create -n ct02 -t download -- -d plamo -r 5.x -a amd64 \
> --variant=large --server=repository.plamolinux.org --keyid=0xC0B578C84772EC0D

なお、lxc-1.0.6以前の安定版で作成したPlamoコンテナでは、/dev/shmディレクトリが存在せず、PulseAudioが必要とするshm_open()システムコールが使えない状態になっているので、lxc-1.0.7以降の安定版にアップデートするか、LXCの修正パッチを参考に、お手元の/usr/share/lxc/templates/lxc-plamo/usr/share/lxc/config/plamo.common.confを修正してから、コンテナを作成するようにしてください。

※)
執筆時点では、lxc-1.0.7はまだリリースされていません。

さて、準備ができたようなので、PulseAudioの設定を行いましょう。

ホストOSと同一セグメント192.168.1.0/24にあるコンテナからの音声データの受け付けを許可するため、ホストにいるtaroユーザは、/etc/pulse/default.pa~/.config/pulse/default.paにコピーしてから、以下の行を編集します。

load-module module-esound-protocol-tcp auth-ip-acl=192.168.1.0/24
load-module module-native-protocol-tcp auth-ip-acl=192.168.1.0/24

ESDとPulseAudioのサウンドサーバが同居する場合、ESDの機能はPulseAudioがエミュレートするため、module-native-protocol-tcpのモジュールだけでなく、module-esound-protocol-tcpのモジュールもロードしておく必要があります。

また、ホストOSのIPアドレス192.168.1.2に音声データを送信するため、コンテナ内のhanakoユーザは、/etc/pulse/client.conf~/.config/pulse/client.confにコピーしてから、以下の行を編集します。

default-server = 192.168.1.2

このように設定すると、図2のようにコンテナからホストのPulseAudioサウンドサーバが透過的に見え、コンテナ内のアプリケーションにとって、あたかもホスト上でサウンドデバイスにアクセスしているような状態になります。

図2 PulseAudioサウンドサーバの共有
図2 PulseAudioサウンドサーバの共有

それでは、実際に動かしてみましょう。

以下は、PulseAudio APIを使用したアプリケーションとして、paplayコマンドを実行する例です。

hanako@ct02:~$ paplay -v geragerapo-no-uta.wav
サンプル仕様 '再生' と チャンネルマップ 's16le 2ch 44100Hz' で \
 front-left,front-right ストリームを開いています。
接続が確立
ストリームは正常に作成完了
バッファメトリックス: maxlength=4194304, tlength=352800, prebuf=349276, minreq=3528
サンプル仕様 's16le 2ch 44100Hz' 、チャンネルマップ 'front-left,front-right' を使用。
デバイス alsa_output.pci-0000_00_1b.0.analog-stereo に接続 (0, not suspended)
ストリーム開始
ストリームアンダーラン
排出したストリームを再生
サーバーへの排出接続

以下は、ALSA APIを使用したアプリケーションとして、aplayコマンドを実行する例です。PulseAudioを経由して処理されています。

hanako@ct02:~$ aplay geragerapo-no-uta.wav
再生中 WAVE 'geragerapo-no-uta.wav' : Signed 16 bit Little Endian, レート 44100 Hz, ステレオ

以下は、ESD APIを使用したアプリケーションとして、esdplayコマンドを実行する例です。ホストOSのIPアドレス192.168.1.2に音声データを送信するため、あらかじめESPEAKERの環境変数を設定しておきます。

hanako@ct02:~$ export ESPEAKER=192.168.1.2
hanako@ct02:~$ esdplay geragerapo-no-uta.wav

以下は、OSS APIを使用したアプリケーションとして、あらかじめ作成しておいたdsptestコマンドを実行する例です。dsptestコマンドを、padspコマンド(ラッパープログラム)の引数として実行することにより、PulseAudioを経由して処理されます。

hanako@ct02:~$ padsp ./dsptest

このように、どのサウンドAPIを使用したアプリケーションでも、PulseAudioで一元的に管理して、コンテナ内でサウンドを出力できることが、おわかりいただけたかと思います。

また、多数のコンテナを起動して、利用者向けにリモートコンソール環境やリモートデスクトップ環境を提供するような適用例では、それぞれの利用者の端末側に音声データを送るように設定すれば良いでしょう。

まとめ

今回は、コンテナ内でサウンドの取り扱いについて紹介しました。これにより、LXCを使ったコンテナの用途の幅が、大きく広がったことと思います。

コンテナ内でサウンドを出力する方法として、前半ではホストOSで管理しているサウンドデバイスを共有する方法を紹介し、後半ではサウンドサーバ(PulseAudio)を使う方法を紹介しました。PulseAudioが含まれないdefaultバリアントやminiバリアントのPlamoコンテナでは、前半で紹介した方法を使い、PulseAudioが含まれるlargeバリアントやfullバリアントのPlamoコンテナでは、後半で紹介した方法を使うことになります。

前々回から3回にわたって連載したLXCの構築・活用のシリーズが、今回で一区切りついたので、次回からは加藤さんに一旦バトンを戻しまして、ユーザネームスペースの利用について紹介する予定です。

おすすめ記事

記事・ニュース一覧