Raspberry PiでAirMac Expressの代替を目指す (AirPlayの導入まで)

# 背景
自宅では長らくAirMac Expressを愛用してきました。最初は部屋間にLANケーブルを這わせるのはどうしても嫌、という妻の要望を叶える為、WDSモードで部屋同士のネットワークを無線LANで繋げるために購入しましたが、その必要も無くなった後は、AirPlayのレシーバとして重宝していました。

ただ、「諸行無常の響きあり」とあるように、どんなものにも別れの時はあります。最初3台購入していたAirMac Expressも、購入して10年程経ってから、一つ、また一つ壊れていき、今ではついに残り一台となってしまいました。

無線LANのアクセスポイントとしての代替品は世の中にたくさんありますが、AirPlayのレシーバとしての代替品は寡聞にして知りません。いや、ある事はあるようなのですが、私のやりたい事は既存のスピーカと繋げて、Appleご謹製のRemoteで大元のiTunesを操作して、いつでも好きな音楽を枕元で聞くというものなので、そういう目的に見合う物がどうやら世の中には無いようなのです。

長くなりましたが、この文章は、残ったただ一つのAirMac Expressがお亡くなりになる前に代替品をなんとかして準備した手順の備忘録になります。

# 前提条件
* Raspberry Pi
* MicroSDカード
* HDMI接続のモニター
* USB接続のキーボード
* まぁまぁのLinuxの知識

# おおまかな手順
何故か家にRaspberry Pi 3Bが一つ余っていたので、これを使ってAirMac Expressの代替品を作成する事にしました。ネットで検索すると、Raspberry PiをAirPlayのレシーバにしようとされた方々が多数いらっしゃったので、特に苦労する事も無いだろうと思ったのがその理由です。

* Raspberry Piのセットアップ
* Raspberry Pi OSの初期設定
* Raspberry Pi OSの追加設定
* Webminの導入
* AirPlayレシーバの設定
    * apt経由での導入
    * Dockerコンテナとしての導入
        * Dockerの導入
        * Portainerの導入

# Raspberry Piのセットアップ
知らない間にRaspbianがRaspberry Pi OSと名前を変えていました。Raspberry Pi OSはOSイメージをダウンロードしてMicro SDカードに書き込む必要がありますが、この作業を超絶簡単に行ってくれるのがRaspberry Pi Imagerです。これは[Raspberry Piの公式ページ](https://www.raspberrypi.org/software/)からダウンロードできました。作業する環境に合わせて、Windows/Mac/Linuxのどれかを選んでからダウンロードします。

もちろんMicroSDカードも必要です。今回は何故か家で余っていた16GBのMicro SDカードを使用する事にしました。

Raspberry Pi Imagerを立ち上げるとこのような画面が表示されます。Operating SystemからインストールするOSの種類を、Storageで書き込み先を選択します。

![Raspberry Pi Imager](https://www.raspberrypi.org/homepage-9df4b/static/md-82e922d180736055661b2b9df176700c.png "Raspberry Pi Imager")

今回は出来るだけ余計な物をインストールしたくなかったので、Raspberry Pi OS Liteを選びました。Writeを押すと必要なイメージをダウンロードして指定したSDカードに書き込んでくれます。便利な世の中になったものです。

全ての作業が終了したら、もうSDカードを抜いてもいいよ、というメッセージが表示されるので、書き込みが終わったSDカードをRaspberry Piに差し込んで、ついでにモニター・キーボードも接続して電源ケーブルを接続します。何やら色々表示された後、ログインプロンプトが表示されるので、とりあえずデフォルトのユーザであるpiでログインします。パスワードはraspberryです。

# Raspberry Pi OSの初期設定
Raspberry Pi OSを使うに当たって最小限必要と思われる設定を行っていきます。必要最小限の設定は`raspi-config`で行う事が出来ます。

    $ sudo raspi-config

## 日本語環境
まずは日本語環境に変更します。UTF-8をlocaleに追加。ただ、コマンドラインで作業する事もあるので、標準はCにしておきます。また、タイムゾーンは東京にします。今回使用したキーボードが109キーボードなので、それに一番近そうな設定にします。

### Locale
1. `Localisation Options`を選択
1. `Locale`を選択
1. `ja_JP.UTF-8`を追加
1.  `Default locale`では`C.UTF-8`を選択

### タイムゾーン
1. `Localisation Options`を選択
1. `Timezone`を選択
1. `Asia`を選択
1. `Tokyo`を選択

### キーボード
1. `Localisation Options`を選択
1. `Keyboard`を選択
1. `Keyboard model`では`Generic 105-Key PC (intl.)`を選択
1. `Keyboard layout`では`Other`を、続く`Country of origin`では`Japanese`を選択
1. もう一度`Keyboard layout`に戻るので、今回は`Japanese`を選択
1. AltGrに割り当てるキーには`The default for the keyboard layout`を選択
1. Composeキーは`No Compose key`を選択

### 無線LAN
1. `Localisation Options`を選択
1. `WLAN Country`を選択
1. `JP`を選択

## sshを有効化
後ほどWebminを入れてwebベースで設定を行えるようにしますが、それでもコンソールで作業する場面も出てくると思うので、sshを有効化しておきます。

1. `Interface Options`を選択
1. `SSH`を選択
1. `Yes`で有効化する

## WiFiの有効化
WiFiでネットワークに接続する場合、下記のようにWiFiを有効化する必要があります。ただしこのままですと少し難があるので、[後ほど説明するよう](#無線LANの追加設定)にパスフレーズのハッシュ化を行った方が良いと思います。

1. `System Options`を選択
1. `Wireless Lan`を選択
1. SSIDとパスフレーズを入力

## ホスト名の変更
ホスト名の変更は以下のようにおこないます。

1. `System Options`を選択
1. `Hostname`を選択
1. 変更したいホスト名を入力

## 画面サイズ
これはだいぶ余計かもですが、今回準備したモニターは持ち運び可能な小型モニターなので、高解像度のままだととても見にくいため、低解像度にしました。基本的にはリモートで作業を行う事になるので、高解像度にする必要も無いですし。

1. `Display Options`を選択
1. `Resolution`を選択
1. `CEA Mode 1`を選択して解像度を640x480にしておく。

最後に`Finish`を選択すると再起動するかを聞かれるので、`Yes`で再起動します。特にホスト名を変更した場合再起動する必要があるので、もし途中でESCキーを押した等で`raspi-config`を抜け出した場合は自力で再起動して下さい。

# Raspberry Pi OSの追加設定
## ファイアウォールの設定
`apt`等でネットワークを使用する前に、まずはファイアウォールの設定を行っておきます。今の時代、たとえ個人で内向けに運用しているサービスでも攻撃を受ける可能性が十分にあります。[^1]

[^1]: 特にちょうど今Qlockerが話題になっていて他人事では無いです。

ファイアウォールの設定には通常`iptables`を使用しますが、正直難しいです。そこで、今回ファイアウォール設定を簡単に行える`ufw`を導入してみます。コンソール上から下記のような手順で`ufw`のインストールから有効化までをおこないます。

    $ sudo apt install ufw
    $ sudo ufw enable

次に基本外から内への通信を遮断します。

    $ sudo ufw default DENY

このあと最低限開いておきたいポートを指定します。今回sshを有効化しているので、とりあえずsshのポートだけ開こうと思います。ただし、万が一のため、同じLAN内からの接続のみを許可し、短時間で複数回接続を試みてきた通信を一時拒否するようにします。もし、同一LANが192.168.1.0/24であれば下記のようにします。

    $ sudo ufw limit from 192.168.1.0/24 to any port ssh

ここから先はsshを使用してリモートで作業も可能です。

## 無線LANの追加設定
`raspi-config`でWiFiの設定を行った場合、WiFiの接続設定ファイルにパスフレーズが平文で書き込まれてしまうので、あまり心情的に宜しくはありません。WiFiの接続設定は`/etc/wpa_supplicant/wpa_supplicant.conf`に保存され、またパスフレーズのハッシュ化には`wpa_passphrase`が使用できます。

そこで気になる方は、下記のような手順でパスフレーズをハッシュ化してみて下さい。

まず下記のコマンドを実行して、ハッシュ化されたパスフレーズを含んだ設定を`wpa_supplicant.conf`に保存します。

    $ sudo sh -c "wpa_passphrase <SSID> <パスフレーズ> >> /etc/wpa_supplicant/wpa_supplicant.conf"

こうする事で下記のような行が`wpa_supplicant.conf`に追加されているはずです。

    network={
            ssid="<SSID>"
            #psk="<平文のパスフレーズ>"
            psk=<ハッシュ化されたパスフレーズ>
    }

上記から平文のパスフレーズの行を削除してください。また、`raspi-config`によって書き込まれた下記のような行も上記の行の前にあると思うので、丸ごと削除してください。

    network={
            ssid="<SSID>"
            psk="<平文のパスフレーズ>"
    }

## SWAP領域の停止
Raspberry Piは基本SDカードで運用するので、頻繁な書き込みにはあまり強く無いと思います。そのため、SWAP領域を無効化して少しでも寿命を伸ばそうと思います。

    $ sudo swapoff --all
    $ sudo systemctl stop dphys-swapfile
    $ sudo systemctl disable dphys-swapfile

## パッケージとファームウェアのアップグレード
これでいろいろ作業するための最低限の設定が終わりましたので、一度
パッケージとファームウェアのアップグレードを行っておきます。

    $ sudo apt update
    $ sudo apt full-upgrade
    $ sudo apt-get autoremove
    $ sudo apt-get autoclean
    $ sudo reboot

# Webminの導入
ここまでで基本的な設定は全て終了しましたが、今後リモートで色々と設定作業を行う際、`ssh`で接続してというよりはWebベースで設定が行えると便利です。そんな環境を提供してくれるのがWebminです。Webminの導入も昔に比べるとだいぶ楽になりました。

まず、Webminが提供されているリポジトリのソース元を`apt`の`sources.list`に追加します。

    $ sudo vi /etc/apt/sources.list

で`sources.list`を開いて、下記のような行を追加します。

    ## webmin
    deb https://download.webmin.com/download/repository sarge contrib

次に、上記リポジトリのパッケージを認証するためのキーを追加しまします。

    $ wget https://download.webmin.com/jcameron-key.asc
    $ sudo apt-key add jcameron-key.asc
    $ rm jcameron-key.asc

また、上記リポジトリにはhttps経由で接続するので、https経由でaptが使用できるよう`apt-transport-https`もインストールします。

    $ sudo apt install apt-transport-https

最後にWebminをインストールします。

    $ sudo apt update
    $ sudo apt install webmin

これでWebminに接続してWebベースで各種設定が行えるようになっています。Webminにはデフォルトでは下記のように10000番ポートを指定して接続します。

    https://<接続先IP>:10000

そのため下記のように10000番ポートを開いておきます。

    sudo ufw allow from 192.168.1.0/24 to any port 10000

なお、接続するポート番号はもちろん変更出来ます。その際には上記の`ufw`への指定も変更したポート番号に応じて変更して下さい。

## Webmin上で新規ユーザ設定
せっかくWebminを導入してみたので、デフォルトの作業ユーザであるpiに代わるユーザを作成してみます。

なお、Webminで使用しているSSL証明書はいわゆるオレオレ証明書なので、接続した際に下記のように警告される筈です。まぁ、無視しても良いのですが、もし気になるようでしたらLet's EncryptからSSL証明書を入手しても良いかも知れません。

![スクリーンショット 2021-07-10 14.12.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132564/fc13020d-19f2-00a0-625e-54bf0aa7192f.png)

Webmin上ではSystemから選択できるUsers and Groupsでユーザ情報の管理が出来ます。`Create a new user`ボタンを選択する事で新規ユーザを作成出来ます。新規ユーザ名とパスワードを入力し、シェルは`/bin/bash`を選んでみます。所属グループはpiと同じ物を選択します。`Create`ボタンでユーザ作成を終了し、新規ユーザでのログインに支障が無い事が確認出来たら、piユーザを無効化しておきます。

# shairport-syncの導入
さて、そろそろ本題の、AirPlayのレシーバの設定をおこないます。ここからは大分試行錯誤が入ります。

## aptを使用しての導入
AirPlayのレシーバとして使用するには、shairport-syncというソフトウェアを導入すると良いらしいです。shairport-syncはDebianのパッケージとしても提供されているので、早速`apt`を使ってインストールしてみました。

    $ sudo apt install shairport-sync

これで、RemoteのAirPlay一覧にRaspberry Piのホスト名が表示されますが、このままでは使用出来ません。

shairport-syncは外からの接続要求やデータのやりとりに特定のポートを使用しており、この設定は`/etc/shairport-sync.con`に記述します。デフォルトでは以下のように、TCPの5000番と、UDPの6001-6101番を使用するようです。

    general = 
    {
    //      port = 5000; // Listen for service requests on this port
    //      udp_port_base = 6001; // start allocating UDP ports from this port number when needed
    //      udp_port_range = 100; // look for free ports in this number of places, starting at the UDP port base. Allow at least 10, though only three are needed in a steady state.
    }

shairport-syncが使用するポートを開くため、ufwで以下のように指定します。

    $ sudo ufw allow from 192.168.1.0/24 to any port 5000 proto tcp
    $ sudo ufw allow from 192.168.1.0/24 to any port 6001:6101 proto udp

これでAirPlayの接続先として選べます。なお、このままですと、人によっては音が出ずに悩むかも知れません。Raspberry Piは音声の出力先は2種類あり、一つは本体についているイヤフォンジャックから、もう一つはHDMI経由での出力になります。で、どうも標準ではHDMI経由の出力になっているようです。

    $ amixer
    Simple mixer control 'HDMI',0
      Capabilities: pvolume pvolume-joined pswitch pswitch-joined
      Playback channels: Mono
      Limits: Playback -10239 - 400
      Mono: Playback 0 [96%] [0.00dB] [on]

これを変えるには、例えば`raspi-config`を立ち上げて、

1. `System Options`を選択
1. `Audio`を選択
1. `1 Headphones`を選択して`OK`を押す

とする事で標準の音声出力先をイヤフォンジャックに変更出来ます。変更後、例えば

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

とすると、どちらから音が出ているか直接確認出来ます。

ただ、この方法は`shairport-sync`には反映されません。`shairport-sync`では出力先は`/etc/shairport-sync.conf`に記述する必要があります。

    alsa =
    {
    //      output_device = "default"; // the name of the alsa output device. Use "alsamixer" or "aplay" to find out the names of devices, mixers, etc.
    }

で、上記`output_device`と`mixer_device`の記述方法ですが、ちゃんと書かれた文書が見つからなかったのですが、

    $ man shairport-sync

によると、記述例として

    alsa = {
    output_device = "hw:0";
    mixer_control_name = "PCM";
    };

とあるので、多分、`"hw:<カード番号>"`のように指定すればいいのではと推測しています。カード番号は`aplay -l`で分かります。

    $ aplay -l
    **** List of PLAYBACK Hardware Devices ****
    card 0: b1 [bcm2835 HDMI 1], device 0: bcm2835 HDMI 1 [bcm2835 HDMI 1]
      Subdevices: 4/4
      Subdevice #0: subdevice #0
      Subdevice #1: subdevice #1
      Subdevice #2: subdevice #2
      Subdevice #3: subdevice #3
    card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
      Subdevices: 4/4
      Subdevice #0: subdevice #0
      Subdevice #1: subdevice #1
      Subdevice #2: subdevice #2
      Subdevice #3: subdevice #3

これによるとイヤフォンジャックのカード番号は1なので、

    alsa = 
    {
            output_device = "hw:1";
    }

として、

    $ sudo systemctl restart shairport-sync

でサービスを再起動させます。これで、イヤフォンジャックから音楽が流れるようになっている筈です。

## Dockerコンテナとしての導入
これでうまく行ったかと思ったら、そうは問屋が卸してくれませんでした。標準ではスピーカー名がホスト名になるので味気がありません。そこでスピーカー名を変えようと

    general =
    {
            name = "AirPlay";
    }

としてサービスを再起動させると、何故かエラーで落ちてしまいます。

    $ sudo systemctl restart shairport-sync
    Job for shairport-sync.service failed because a fatal signal was delivered to the control process.
    See "systemctl status shairport-sync.service" and "journalctl -xe" for details.

理由がさっぱりわからなかったのですが、[このサイト](https://github.com/mikebrady/shairport-sync/issues/922)でも同様のエラーが報告されていて、作者による返事では理由がいまいちはっきりせず、とりあえず3.3.5以上にしてくれ、というものでした。`apt`でとってきたのは3.2.2でした。

    $ shairport-sync -V
    3.2.2-OpenSSL-Avahi-ALSA-pa-dummy-stdout-pipe-soxr-convolution-metadata-sysconfdir:/etc

3.3.5以上にあげるには自分で最新版のソースコードを持ってきてbuildするか、Debianのbackportsとやらから持ってくるといいらしいのですが、前者は面倒だし、後者はいまいち不信感があるのであまりやりたくないです。どうしたものかと悩んでいたら、shairport-syncのDockerイメージがあることを発見。Dockerも導入しようかと思っていたところなので、渡に舟と、Dockerコンテナとしてshairport-syncを導入する事にしました。

その前にもったいないけど、まずインストール済みのshairport-syncと関連するパッケージを削除しました。

    $ sudo apt purge shairport-sync
    $ sudo apt autoremove

### Dockerの導入
[Dockerのサイト](https://docs.docker.com/engine/install/debian/)によると、Raspberry Pi OSへはDebianで通常推奨しているインストール方法は使用出来ないらしい。

    Raspbian users cannot use this method!

    For Raspbian, installing using the repository is not yet supported. You must instead use the convenience script.

そこで、[ここ](https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script)に書かれている手順に沿って導入してみました。

まず、念の為、既存のDockerがらみのパッケージを全て削除して欲しいらしい。多分そんなものは一つも入っていない無い筈だけど、念の為下記のコマンドを実行。

    sudo apt-get purge docker docker-engine docker.io runc

次にconvenienceなスクリプトをダウンロードします。

    curl -fsSL https://get.docker.com -o get-docker.sh

ダウンロード後スクリプトを実行すると、勝手に必要なファイルのダウンロードとインストール作業を行ってくれます。

    sudo sh get-docker.sh

### Portainerの導入

追加でWebベースでDokerコンテナの管理が出来るようPortainerも導入します。PortainerはDockerイメージがあるので、

    sudo docker pull portainer/portainer-ce:linux-arm

としてDockerイメージを入手しておいて、

    sudo docker run -d -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:linux-arm

というように起動させます。この例ではポート9000番を使ってPortainerに接続するので、下記のように9000番を開けておきます。

    sudo ufw allow from 192.168.1.0/24 to any port 9000

これで以下のようなURLでPortainerに接続できます。

    https://<接続先IP>:9000

最初に接続した際に、管理者ユーザの作成が必要になります。これはPortainerだけで使用されるユーザのようです。

### shairport-syncコンテナ
shairport-syncのDockerイメージは`mikebrady/shairport-sync:latest`です。

Portinerの`Containers`タブを選択して、コンテナ名を入力し、Dockerイメージに`mikebrady/shairport-sync:latest`を指定して`Deploy the container`ボタンを押すと、Dockerイメージのダウンロードからコンテナ起動までを行ってくれます。

他に指定すべきオプションは[こちらの説明](https://hub.docker.com/r/mikebrady/shairport-sync)をみると、

* NetworkはHost
* Restart policyはUnless stopped
* Deviceに`/dev/snd`
* その他、shairport-syncとALSAに渡す引数をCommandに指定する。
    * `-a AirPlay`でスピーカー名を指定
    * `-- -d hw:1`でイヤフォンジャックを出力先に指定

これで好みのスピーカ名で接続できるようになっています。

# 今後の予定
本当はAirMac Expressの本来の目的であるWiFiアクセスポイントとしての設定まで行いたかったのだけど、自宅ではブリッジモードでWiFiのアクセスポイントを作成したいので`bridge-utils`をインストールしてゴニョゴニョしましたが、結論として現在WiFi側とLAN側をブリッジさせる事は出来なくなっているらしい。とりあえずRaspberry Piに搭載されているWiFiを使用してブリッジは出来ないっぽい。

    $ sudo brctl addif br0 wlan0
    can't add wlan0 to bridge br0: Operation not supported

解決策としては、[ここ](https://serverfault.com/questions/152363/bridging-wlan0-to-eth0)にあるように、無線LAN側を4addr(WDS)モードにするか、もしくは外付けUSBのWiFiアダプターを用いると良いようだ。ただ、前者は理由が完全に理解出来ていないし(WDSモードってイーサコンバータとして使う場合に使用するのではなかったのかな？)、後者は、余っている筈のWiFiアダプターが行方不明になってしまったので、とりあえず今後の課題としました。

また、イヤフォンジャック経由ですと音が悪いとのことでUSB-DACを使用すると良いという[話](https://101010.fun/iot/raspberry-pi-airplay-server.html)もあるので、こちらも試してみようと思う。

# その他参考にしたサイト
* [RaspberryPiにDockerを構築する](https://qiita.com/k_ken/items/0f2d6af2618618982723)
* [Installing Portainer to the Raspberry Pi](https://pimylifeup.com/raspberry-pi-portainer/)
* [Shairport Sync](https://github.com/mikebrady/shairport-sync)