🎧

LDAC対応なUSB to BluetoothオーディオトランスミッターをRapberry piで作った話

2024/07/03に公開

はじめに

BTA30 ProのようにPCの音をBluetoothヘッドホンなどに送信するデバイスが欲しかったものの、LDACまで対応しているものが少なさそうだったので、手元のRaspberry pi zero 2whで作ってみたという話です。
WindowsとUbuntuの両方で動作を確認しています。

モチベーション

  • WindowsとUbuntuのデュアルブート環境を使用しているため、再起動のたびにヘッドホンをつなぎなおす必要があり面倒
  • LDAC対応のワイヤレスヘッドホンであるAnkerのSoundcore Space Q45を使っているので、LDACで音を出したい
  • Windowsは標準ではLDACに対応しておらず、対応させるには有料のAlternative A2DP Driverなどを使わないといけないが、PCごとに購入する必要があり使いたくない

以上のような理由から、USB-DACで有線ヘッドホンをつなぐようなノリで、ワイヤレスヘッドホンをつなぐUSB機器を探していたのですが、LDAC対応のものとなると選択肢が皆無で、見つかったBTA30 Proはアナログ変換にも対応しているなど今回の目的には余分な機能が多く、その分高価になっています。そこで、自作できないか調査したところ、Raspberry pi zero を USB to Bluetooth トランスミッターにする備忘録等のようにRaspberry piで自作していらっしゃる方がおり、ここを参考にさせていただき、自分でも作ってみようと思いました。

手法

LinuxのUSB Gadget、PulseAudio、BlueZを組み合わせています。そのため、Raspberry piに限らず、Bluetoothの使えるLinux PCであれば同じことができると思います。

USB Gadget

USB Gadgetは、Linuxカーネルに含まれる機能で、デバイスをUSB周辺機器として動作させることを可能にします。これにより、LinuxデバイスがUSBキーボード、マウス、ネットワークアダプター、ストレージデバイスなどとして振る舞うことができます。
今回は、そのうちのg_audioというドライバーを用いてオーディオデバイスとして機能するようにします。

PulseAudio

PulseAudioは、Linuxや他のUnix系オペレーティングシステム向けのサウンドサーバーで、オーディオ管理を統合し、様々なアプリケーションからのオーディオストリームを効率的に処理・ルーティングするためのミドルウェアです。これを用いて、PCからUSBで流れてきた音声をBluetoothでヘッドホンにループバックすることで、PCの音声がヘッドホンから流れるようになります。

BlueZ

BlueZは、Linux向けのBluetoothプロトコルスタックで、Bluetoothデバイスの管理と通信を行うためのソフトウェアフレームワークです。BlueZは、Bluetoothのコアプロトコル、上位プロトコル、およびアプリケーション用のライブラリとツールを提供し、Bluetoothデバイスとの通信を容易にします。これを用いてBluetoothヘッドホンに接続します。

作成手順

今回はRaspberry Pi Zero 2 Wを使用したので、それ以外のデバイスを用いる方は適宜読み替えてください。

Raspberry Pi Zero 2 Wのセットアップ

https://www.raspberrypi.com/software/ を参考にRaspberry Pi Imagerなどを用いてmicro SDにイメージを焼いて使います。最初はスワップが100MBしかなく、sudo apt upgradeだけで固まるので、Raspberry PIにてSWAPファイルのリサイズなどを参考に、先にスワップを増やしましょう。

追記:以下の記事にまとめました。
https://zenn.dev/shryt/articles/5bbe75969012c0

USB Gadgetの設定

USB Gadgetのg_audioは以下のようなコマンドで有効化することができます。

$ sudo modprobe g_audio c_srate=44100 c_ssize=2 p_srate=44100 p_ssize=2

逆に無効化するときは以下のコマンドです。

$ sudo modprobe -r g_audio

ただし、g_audioを有効化するにはdwc2というドライバーが必要となるようなので、その設定をします。
まず、/boot/firmware/config.txtに以下を追記しておきます。編集にはsudoが必要なことにご注意ください。

dtoverlay=dwc2

また、起動時にdwc2を自動的に読み込むため、/boot/firmware/cmdline.txtの末尾に以下を追加します。こちらも編集にはsudoが必要なことにご注意ください。

modules-load=dwc2

以上でdwc2の設定は完了です。一度再起動すればdwc2が読み込まれます。
g_audioについては、後ほどスクリプト上で有効化するため、今は有効化する必要はありません。

PulseAudioのインストール

以下のコマンドでPulseAudioをインストールします。最近のLinuxではPipeWireが標準になっていそうですが、今回はPulseAudioを使うため、PipeWireが入っていたら削除します。

$ sudo apt install pulseaudio
$ sudo apt purge pipewire # pipewireが入っている場合 

また、リサンプリングの設定をするため、/etc/pulse/daemon.confを以下のように編集します。

resample-method = soxr-vhq
avoid-resampling = yes

Bluetoothの設定

基本Raspbian を LDAC と AAC に対応させる備忘録と一緒です。

以下のコマンドでBluetooth周りの必要なものをインストールします。

$ sudo apt install libpulse-dev libbluetooth-dev libdbus-1-dev libsbc-dev libavcodec-dev libavutil-dev dh-autoreconf git cmake

ldacBTのビルド・インストール

LDACで通信できるようにするため、ldacBTというライブラリを導入します。以下のコマンドを適当なディレクトリ(筆者の場合は~/develop)で実行してください。

$ git clone https://github.com/EHfive/ldacBT.git
$ cd ldacBT
$ git submodule update --init
$ cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local -DINSTALL_LIBDIR=/usr/local/lib -DLDAC_SOFT_FLOAT=OFF
$ cmake --build build
$ sudo cmake --install build

pulseaudio-modules-btのビルド・インストール

PulseAudioからBluetoothのオーディオデバイスにアクセスできるようにするため、pulseaudio-modules-btというライブラリを使用します。以下のコマンドを適当なディレクトリ(筆者の場合は~/develop)で実行してください。

$ MODDIR=`pkg-config --variable=modlibexecdir libpulse`
$ sudo find $MODDIR -regex ".*\(bluez5\|bluetooth\).*\.so" -exec cp {} {}.bak \;
$ git clone https://github.com/EHfive/pulseaudio-modules-bt.git
$ cd pulseaudio-modules-bt/
$ git submodule update --init
$ git -C pa/ checkout v`pkg-config libpulse --modversion|sed 's/[^0-9.]*\([0-9.]*\).*/\1/'`
$ cmake -S . -B build -DCODEC_APTX_FF=ON -DCODEC_APTX_HD_FF=ON -DCODEC_AAC_FDK=ON -DCODEC_LDAC=ON
$ cmake --build build
$ sudo cmake --install build

共有ライブラリの依存関係のキャッシュ再生成

ここまでインストールしてきたライブラリを正しく読み込めるよう、共有ライブラリの依存関係のキャッシュを再生成します。

$ sudo ldconfig
$ ldconfig -p | grep local
libldacBT_enc.so.2 (libc6,hard-float) => /usr/local/lib/libldacBT_enc.so.2
libldacBT_enc.so (libc6,hard-float) => /usr/local/lib/libldacBT_enc.so
libldacBT_abr.so.2 (libc6,hard-float) => /usr/local/lib/libldacBT_abr.so.2
libldacBT_abr.so (libc6,hard-float) => /usr/local/lib/libldacBT_abr.so

ユーザーグループの設定

一般ユーザーからbluetoothとpulse-audioにアクセスできるように、以下のコマンドを実行します。<username>を適宜お使いのユーザー名に置き換えてください。

$ sudo adduser <username> bluetooth
$ sudo adduser <username> audio
$ sudo adduser <username> pulse-access

また、ここまでで一度念のため再起動しておきます。

$ sudo reboot

サービスの設定

Bluetoothのサービスにおいて、SIMアクセスプロファイルを無効にするため、/lib/systemd/system/bluetooth.serviceのExecStartの行を以下のように編集します。編集にはsudoが必要なことにご注意ください。

ExecStart=/usr/lib/bluetooth/bluetoothd -E --noplugin=sap

また、PulseAudioのサービスを有効化し、常時起動するようにしておきます。

$ systemctl --user enable pulseaudio
$ systemctl --user start pulseaudio

確認

pulseaudio-modules-btが読み込まれているかどうかは以下のコマンドで確認できます。

$ pacmd list-modules | grep blue
name: <module-bluetooth-policy>
        module.description = "Policy module to make using bluetooth devices out-of-the-box easier"
name: <module-bluetooth-discover>
name: <module-bluez5-discover>

読み込まれていなければ、共有ライブラリの依存関係のキャッシュ再生成を再度行ってみてください。
読み込まれていたら、実際にワイヤレスヘッドホンにつないでみます。以下のコマンドでbluetoothctlを立ち上げます。

$ bluetoothctl

続いて、Bluetoothデバイスをスキャンして、ヘッドホンを見つけ、つなぎます。

[bluetooth]# scan on
[NEW] Device AA:AA:AA:AA:AA:AA soundcore Space Q45
[bluetooth]# pair AA:AA:AA:AA:AA:AA
[soundcore Space Q45]# trust AA:AA:AA:AA:AA:AA
[soundcore Space Q45]# connect AA:AA:AA:AA:AA:AA
[soundcore Space Q45]# exit

これでヘッドホンがつながったはずです。上手くつながらない場合はPulseAudioが起動しているかどうか確認してください。
実際に音を鳴らしてみます。

$ wget https://www.kozco.com/tech/LRMonoPhase4.wav
$ paplay LRMonoPhase4.wav

無事にワイヤレスヘッドホンから音声が鳴ればBlueToothの設定は終了です。

スクリプトの作成

基本Raspberry pi zero を USB to Bluetooth トランスミッターにする備忘録と一緒です。
Bluetoothの接続とg_audioの読み込みを自動化するため、スクリプトを作成します。
まずBluetoothに接続するためのスクリプトです。/usr/local/bin以下に適当な名前で作成して下さい。筆者は/usr/local/bin/connect.space-Q45としました。

#!/bin/sh
bluetoothctl << EOL
power on
connect AA:AA:AA:AA:AA:AA
quit
EOF

つづいて、g_audioを読み込むスクリプトです。筆者は/usr/local/bin/on.usb_dacとしました。

#!/bin/bash
sudo modprobe -r g_audio
ASINK_LN=`pacmd list-sinks|grep -n "* index"|cut -d":" -f1`
SINK_NAME=`pacmd list-sinks|head -n $(($ASINK_LN +1))|tail -n 1|awk -F'[<>]' '{print $2}'`
SINK_DP=`pacmd list-sinks|tail -n +$ASINK_LN|grep "sample spec"|head -n 1|awk -F'[^0-9]+' '{print $2}'`
SINK_SR=`pacmd list-sinks|tail -n +$ASINK_LN|grep "sample spec"|head -n 1|awk -F'[^0-9]+' '{print $4}'`
sudo modprobe g_audio c_srate=$SINK_SR c_ssize=$(($SINK_DP /8)) p_srate=$SINK_SR p_ssize=$(($SINK_DP /8))
sleep 3
ASOURCE_LN=`pacmd list-sources|grep -n "* index"|cut -d":" -f1`
SOURCE_NAME=`pacmd list-sources|head -n $(($ASOURCE_LN +1))|tail -n 1|awk -F'[<>]' '{print $2}'`
echo "SINK NAME:" $SINK_NAME
echo "SINK AUDIO DEPTH:" $SINK_DP
echo "SINK SAMPLING RATE:" $SINK_SR
echo "SOURCE NAME:" $SOURCE_NAME
pactl load-module module-loopback source=$SOURCE_NAME sink=$SINK_NAME

最後に、上記のスクリプトを組み合わせるスクリプトです。筆者は/usr/local/bin/ssとしました。

#!/bin/sh
CN=0
while true; do
  /usr/local/bin/connect.space-Q45
  sleep 2
  CF=`pacmd list-sinks|grep a2dp.sink`
  if [ -n "$CF" ]; then
    break
  elif [ $CN -ge 33 ]; then
    sudo modprobe g_ether
    exit 0
  fi
  CN=`expr $CN + 1`
done
/usr/local/bin/on.usb_dac
while true; do
  CF=`pacmd list-sinks|grep a2dp.sink`
  if [ -z "$CF" ]; then
    sudo poweroff
    break
  fi
  sleep 15
done

いずれも、実行できるよう権限を与えます。

$ sudo chmod +x /usr/local/bin/connect.space-Q45
$ sudo chmod +x /usr/local/bin/on.usb_dac
$ sudo chmod +x /usr/local/bin/ss

ユーザーの自動ログイン

$ sudo raspi-config

から設定画面に入り、
1 System Options Configure system settings
↳S5 Boot / Auto Login Select boot into desktop or to command line
↳B2 Console Autologin Text console, automatically logged in as '<username>' user
にて設定できます。
そして、~/.bashrcに以下を追記することで起動時にssを実行するように設定します。

if [ -f /proc/$PPID/cmdline ]; then
  if [ "$(command cut -d : -f1 < "/proc/$PPID/cmdline")" != "sshd" ] && [[ $- == *i* ]]; then
    /usr/local/bin/ss 
  fi
fi

設定は以上です。一度電源を切り、PCに接続することで起動し、ヘッドホンに繋がることを確認してみましょう。PCとは電源用じゃない方のUSBポートで接続する必要があります。

おわりに

Raspbian を LDAC と AAC に対応させる備忘録Raspberry pi zero を USB to Bluetooth トランスミッターにする備忘録を参考に、というかほぼそのまままとめたような記事になりました。
最近のLinuxではPipeWireが標準になっていることからPipeWireに対応させることを目指します。

Discussion