用樹莓派 + Tube-Pi 當藍芽接收器播放音樂

自從發現自己多了一塊樹莓派的擴接板 Tube Pi 後,我就開使用它做一些無聊的事,
Audio Visualizer - 替音頻加上可視化特效利用樹莓派 + Tube-Pi 播放 Kiss Radio 廣播

最近我就在想,樹莓派 + Tube Pi 能不能當成藍芽接收器,讓它接收手機的音樂,再由 Tube Pi 輸出?最好是 headless 的方式。
結論是:可以的。

不過我在網路上找了很多資料,試了很多種方式,不知什麼原因,都無法成功。今天不死心,又胡搞瞎搞了一番,結果居然可以了。
雖然有點碰運氣,不過大致上的步驟我會把他記錄如下。

因為實在是修改了太多次,沒辦法完整記錄,有些地方可能無關緊要,不過我還是把他寫出來。

修改 /boot/config.txt

# 刪除下面這行,直接刪除或是前面加個 # 註解掉
# dtparam=audio=on

# 新增下面這行
dtoverlay=hifiberry-dac

重新開機。

安裝套件

sudo apt update 
sudo apt install bluez pulseaudio-module-bluetooth python-gobject python-gobject-2 bluez-tools udev
sudo apt install libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev

更新 bluez 程式

原生的 bluez 的版本是 5.43,這一版似乎問題很多,所以我們要自行把他更新成最新版。

git clone git://git.kernel.org/pub/scm/bluetooth/bluez.git
cd bluez
git checkout 5.50
./configure --prefix=/usr --mandir=/usr/share/man --sysconfdir=/etc --localstatedir=/var --enable-experimental
make -j4
sudo make install

讓使用者 pi 擁有使用 bluetoothctl 的權限。

sudo adduser pi bluetooth

修改 Dbus 設定檔。

sudo cp /etc/dbus-1/system.d/bluetooth.conf /etc/dbus-1/system.d/bluetooth.conf.bak
sudo nano /etc/dbus-1/system.d/bluetooth.conf

# 在 <policy user="root"> 底下加入
<allow send_interface="org.bluez.ThermometerWatcher1"/>
<allow send_interface="org.bluez.HeartRateWatcher1"/>
<allow send_interface="org.bluez.CyclingSpeedWatcher1"/>


# 在 <!-- allow users of bluetooth group to communicate --> 底下加入
<policy group="bluetooth">
    <allow send_destination="org.bluez"/>
</policy>

安裝 bluealsa

$ sudo apt install bluealsa

重新開機。

沒問題的話,應該可以看到 bluetoothctl 的版本是預期的 5.50。

pi@raspberrypi:~ $ bluetoothctl -v
bluetoothctl: 5.50
pi@raspberrypi:~ $

修改設定檔

pi 加入 pulseaudio 群組內。

$ sudo usermod -a -G lp pi

新增 /etc/bluetooth/audio.conf 檔案,並新增:

$ sudo nano /etc/bluetooth/audio.conf

# 加入
[General]
Enable = Source,Sink,Media,Socket

編輯 /etc/bluetooth/main.conf,加入:

# 在 [General] 下加入
Enable = Source,Sink,Media
Class = 0x00041C
DiscoverableTimeout = 0
PairableTimeout = 0

0x00041C 代表藍芽可以支援 A2DP 協定。

修改 /etc/pulse/daemon.conf

resample-method = trivial

關於 resample-method 還有其他種選擇。

resample-method= The resampling algorithm to use.
Use one of src-sinc-best-quality, src-sinc-medium-quality, src-sinc-fastest, src-zero-order-hold, src-linear, trivial, speex-float-N, speex-fixed-N, ffmpeg. See the documentation of libsamplerate for an explanation for the different src- methods. The method trivial is the most basic algorithm implemented. If you’re tight on CPU consider using this. On the other hand it has the worst quality of them all. The Speex resamplers take an integer quality setting in the range 0…9 (bad…good). They exist in two flavours: fixed and float. The former uses fixed point numbers, the latter relies on floating point numbers. On most desktop CPUs the float point resmampler is a lot faster, and it also offers slightly better quality. See the output of dump-resample-methods for a complete list of all available resamplers. Defaults to speex-float-3. The --resample-method command line option takes precedence. Note that some modules overwrite or allow overwriting of the resampler to use.

執行 pulseaudio -D。在這裡,我把他設成開機啟動。

新增 /etc/init.d/pulseaudio.sh

$ sudo nano /etc/init.d/pulseaudio.sh

# 加入以下內容
#! /bin/sh
#
# pulseaudio initscript
#
### BEGIN INIT INFO
# Provides:          pulseaudio.sh
# Required-Start:    $local_fs $remote_fs
# Required-Stop:     $remote_fs
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts pulseaudio at startup
# Description:       starts pulseaudio at startup
### END INIT INFO
# This is needed for bluetooth audio
pulseaudio -D

# 存檔跳出

$ sudo chmod 755 /etc/init.d/pulseaudio.sh
$ sudo update-rc.d pulseaudio.sh defaults

編輯 /etc/udev/rules.d/99-input.rules 檔案:

SUBSYSTEM="input", GROUP="input", MODE="0660"
KERNEL=="input[0-9]*", RUN+="/usr/lib/udev/bluetooth"

新增 /usr/lib/udev/bluetooth 檔案並將檔案權限改為 777 :

$ sudo mkdir /usr/lib/udev && cd /usr/lib/udev
$ sudo nano bluetooth && sudo chmod bluetooth

# 加入以下內容
#!/bin/bash
# This script is called by udev when you link a bluetooth device with your computer
# It's called to add or remove the device from pulseaudio
#
#

# Output to this file
LOGFILE="/var/log/bluetooth_dev"

# Name of the local sink in this computer
# You can get it by calling : pactl list short sinks
# AUDIOSINK="alsa_output.platform-bcm2835_AUD0.0.analog-stereo"
AUDIOSINK="alsa_output.0.analog-stereo.monitor"
# User used to execute pulseaudio, an active session must be open to avoid errors
USER="pi"

# Audio Output for raspberry-pi
# 0=auto, 1=headphones, 2=hdmi. 
AUDIO_OUTPUT=1

# If on, this computer is not discovearable when an audio device is connected
# 0=off, 1=on
ENABLE_BT_DISCOVER=1

echo "For output see $LOGFILE"

## This function add the pulseaudio loopback interface from source to sink
## The source is set by the bluetooth mac address using XX_XX_XX_XX_XX_XX format.
## param: XX_XX_XX_XX_XX_XX
## return 0 on success
add_from_mac(){
  if [ -z "$1" ] # zero params
    then
        echo "Mac not found" >> $LOGFILE
    else
        mac=$1 # Mac is parameter-1

        # Setting source name
        bluez_dev=bluez_source.$mac
        echo "bluez source: $mac"  >> $LOGFILE

        # This script is called early, we just wait to be sure that pulseaudio discovered the device
        sleep 1
        # Very that the source is present
        CONFIRM=`sudo -u pi pactl list short | grep $bluez_dev`
        if [ ! -z "$CONFIRM" ]
        then
            echo "Adding the loopback interface:  $bluez_dev"  >> $LOGFILE
            echo "sudo -u $USER pactl load-module module-loopback source=$bluez_dev sink=$AUDIOSINK rate=44100 adjust_time=0"  >> $LOGFILE

            # This command route audio from bluetooth source to the local sink..
            # it's the main goal of this script
            sudo -u $USER pactl load-module module-loopback source=$bluez_dev sink=$AUDIOSINK rate=44100 adjust_time=0  >> $LOGFILE
            return $?
        else
            echo "Unable to find a bluetooth device compatible with pulsaudio using the following device: $bluez_dev" >> $LOGFILE
            return -1
        fi
    fi
}

## This function set volume to maximum and choose the right output
## return 0 on success
volume_max(){
    # Set the audio OUTPUT on raspberry pi
    # amixer cset numid=3 <n> 
    # where n is 0=auto, 1=headphones, 2=hdmi. 
    amixer cset numid=3 $AUDIO_OUTPUT  >> $LOGFILE

    # Set volume level to 100 percent
    amixer set Master 100%   >> $LOGFILE
    pacmd set-sink-volume 0 65537   >> $LOGFILE
    return $?
}

## This function will detect the bluetooth mac address from input device and configure it.
## Lots of devices are seen as input devices. But Mac OS X is not detected as input
## return 0 on success
detect_mac_from_input(){
    ERRORCODE=-1

    echo "Detecting mac from input devices" >> $LOGFILE
    for dev in $(find /sys/devices/virtual/input/ -name input*)
    do
        if [ -f "$dev/name" ]
        then
            mac=$(cat "$dev/name" | sed 's/:/_/g')
            add_from_mac $mac

            # Endfor if the command is successfull
            ERRORCODE=$?
            if [ $ERRORCODE -eq 0]; then
                return 0
            fi
        fi
    done
    # Error
    return $ERRORCODE
}
## This function will detect the bt mac address from dev-path and configure it.
## Devpath is set by udev on device link
## return 0 on success
detect_mac_from_devpath(){
    ERRORCODE=-1
    if [ ! -z "$DEVPATH" ]; then
        echo "Detecting mac from DEVPATH"  >> $LOGFILE
        for dev in $(find /sys$DEVPATH -name address)
        do
            mac=$(cat "$dev" | sed 's/:/_/g')
            add_from_mac $mac

            # Endfor if the command is successfull
            ERRORCODE=$?
            if [ $ERRORCODE -eq 0]; then
                return 0
            fi

        done
        return $ERRORCODE;
    else
        echo "DEVPATH not set, wrong bluetooth device? " >> $LOGFILE
        return -2
    fi
    return $ERRORCODE
}


## Detecting if an action is set
if [ -z "$ACTION" ]; then
    echo "The script must be called from udev." >> $LOGFILE
    exit -1;
fi
## Getting the action
ACTION=$(expr "$ACTION" : "\([a-zA-Z]\+\).*")

# Switch case
case "$ACTION" in
"add")

    # Turn off bluetooth discovery before connecting existing BT device to audio
    if [ $ENABLE_BT_DISCOVER -eq 1]; then
        echo "Stet computer as hidden" >> $LOGFILE
        hciconfig hci0 noscan
    fi

    # Turn volume to max
    volume_max

    # Detect BT Mac Address from input devices
    detect_mac_from_input
    OK=$?

    # Detect BT Mac address from device path on a bluetooth event
    if [ $OK != 0 ]; then
        if [ "$SUBSYSTEM" == "bluetooth" ]; then
            detect_mac_from_devpath
            OK=$?
        fi
    fi

    # Check if the add was successfull, otherwise display all available sources
    if [ $OK != 0 ]; then
        echo "Your bluetooth device is not detected !" >> $LOGFILE
        echo "Available sources are:" >> $LOGFILE
        sudo -u $USER pactl list short sources >> $LOGFILE
    else
        echo "Device successfully added " >> $LOGFILE
    fi
    ;;

"remove")
    # Turn on bluetooth discovery if device disconnects
    if [ $ENABLE_BT_DISCOVER -eq 1]; then
        echo "Set computer as visible" >> $LOGFILE
        sudo hciconfig hci0 piscan
    fi
    echo "Removed" >> $LOGFILE
    ;;

#   
*)
    echo "Unsuported action $action" >> $LOGFILE
    ;;
esac
echo "--" >> $LOGFILE

裡面有個變數 AUDIOSINK 必須要改成自己用的,可以用 pactl list short sinks 指令查看。

pi@raspberrypi:/usr/lib/udev $ pactl list short sinks
0       alsa_output.platform-soc_sound.analog-stereo    module-alsa-card.c      s16le 2ch 44100Hz       RUNNING
pi@raspberrypi:/usr/lib/udev $

像我的就是 pactl list short sinks

使用 bluetoothctl 控制藍芽設備

接著我們要使用 bluetoothctl 來跟設備連線,依序要輸入的指令為:

  • power on
  • kbd agent on
  • kbd default-agent
  • kbd discoverable on
  • kbd pairable on
  • kbd trust xx:xx:xx:xx:xx:xx

接著我打開我 iPhone 的藍芽開關,會看到一個設備名稱為 raspberrypi 的設備,點選它進行連線,然後手機跟 rpi 都會出現驗證碼,
選確認就可以了。

pi@raspberrypi:/usr/lib/udev $ bluetoothctl
Agent registered
[bluetooth]# power on
Changing power on succeeded
[bluetooth]# agent on
Agent is already registered
[bluetooth]# default-agent
Default agent request successful
[bluetooth]# discoverable on
Changing discoverable on succeeded
[CHG] Controller B8:27:EB:49:EA:A3 Discoverable: yes
[bluetooth]# pairable on
Changing pairable on succeeded
[CHG] Device xx:xx:xx:xx:xx:xx Connected: yes
Request confirmation
[agent] Confirm passkey 931286 (yes/no): y
Request confirmation
[agent] Confirm passkey 090994 (yes/no): yes
[CHG] Device xx:xx:xx:xx:xx:xx Connected: no
[CHG] Device xx:xx:xx:xx:xx:xx Connected: yes
Request confirmation
[agent] Confirm passkey 018954 (yes/no): yes
[CHG] Controller B8:27:EB:49:EA:A3 Discoverable: no
[iPhone]# trust xx:xx:xx:xx:xx:xx
Changing xx:xx:xx:xx:xx:xx trust succeeded
[iPhone]# exit

當然,可以在 bluetoothctl 內用 show 指令看一下連接點的資訊。

[iPhone]# show
Controller B8:27:EB:49:EA:A3 (public)
        Name: raspberrypi
        Alias: raspberrypi
        Class: 0x004c0000
        Powered: yes
        Discoverable: no
        Pairable: yes
        UUID: Headset AG                (00001112-0000-1000-8000-00805f9b34fb)
        UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
        UUID: A/V Remote Control        (0000110e-0000-1000-8000-00805f9b34fb)
        UUID: Generic Access Profile    (00001800-0000-1000-8000-00805f9b34fb)
        UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
        UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb)
        UUID: Audio Source              (0000110a-0000-1000-8000-00805f9b34fb)
        UUID: Audio Sink                (0000110b-0000-1000-8000-00805f9b34fb)
        UUID: Handsfree Audio Gateway   (0000111f-0000-1000-8000-00805f9b34fb)
        Modalias: usb:v1D6Bp0246d0532
        Discovering: no
[iPhone]# exit

總算,東搞西搞總算可以連線樹莓派 + Tube Pi 發聲成功了。

我最近都把喜歡的歌放到 Telegram 的自建頻道內,這樣即使關閉螢幕或跳到其他視窗還是可以繼續播歌,也可以在鎖定畫面中控制歌曲哦。

至於藍芽連線的音質… 就見仁見智了,有機會的話,我想試試看走 I2S 連接 Tube Pi 輸出。
因為我發現,rpi 好像不能當成 USB 喇叭,也就是我用 PC 透過 USB 接到 rpi,讓 rpi 經過 Tube-Pi 輸出音樂。

What I mean is whether the rpi can be configured to act as a “USB SPEAKER”. In other words, that I can connect a Windows10 computer to the rpi through a USB cable (male-male) and that the W10 sees the rpi as a usb speaker […]

No, that is not possible, except perhaps with the zero, because USB relationships are not symmetrical; they are master/slave and the pi ports (except on the zero) are, like most of those on normal computers, master only (I realize that is a duplicate, but you can click through or read both Q&As if you like), whereas a USB speaker is a slave device. You cannot effectively attach a slave to a slave or a master to a master.

In fact: Beware that it is possible to cause permanent physical damage to one or both computers by connecting them using a male-male cable in the manner you describe. There are special adapter cables that can be used, but they tend to be expensive and have spotty support on the pi, so probably not a route worth exploring.

參考資料