format_list_bulleted

Unattended Linux Audio Server

dark_mode
Unattended Linux Audio Server
Allow pairing Bluetooth without confirmation or service authorization.
alsa
bluetooth
pipewire
visibility -- public -- group --

Bluetooth Setup

HCI over UART

For RealTek UART-linked Bluetooth device.

stty -F /dev/ttyS1 115200

rfkill block bluetooth

sleep 1

rfkill unblock bluetooth

rtk_hciattach -n -s 115200 /dev/ttyS1 rtk_h5

BlueZ

# /etc/bluetooth/main.conf

[General]
Class = 0x000100
DiscoverableTimeout = 0
AlwaysPairable = true
PairableTimeout = 0
ReverseServiceDiscovery = true
NameResolving = true
ControllerMode = bredr
MultiProfile = multiple
FastConnectable = true
JustWorksRepairing = always

[GATT]
Cache = no

[Policy]
ReconnectAttempts=7
bluetoothctl discoverable yes
bluetoothctl pairable yes

BlueZ Agent for trusting everybody

BlueZ D-Bus interface behavior: it accepts on succeed and rejects on error raised.

Script from StackOverflow answer:

#!/usr/bin/env python

import dbus
import dbus.mainloop.glib
import dbus.service
from gi.repository import GLib

AGENT_PATH = "/org/bluez/AuthorizeServiceAgent"
AGENT_INTERFACE = 'org.bluez.Agent1'
CAPABILITY = "NoInputNoOutput"

class Agent(dbus.service.Object):
    @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="")
    def AuthorizeService(self, device, uuid):
        pass

if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    bus = dbus.SystemBus()
    agent = Agent(bus, AGENT_PATH)
    obj = bus.get_object("org.bluez", "/org/bluez")

    manager = dbus.Interface(obj, "org.bluez.AgentManager1")
    manager.RegisterAgent(AGENT_PATH, CAPABILITY)
    manager.RequestDefaultAgent(AGENT_PATH)

    GLib.MainLoop().run()

Wireplumber

Bluetooth configuration - Wireplumber

Add this configuration so that BlueZ can be accessed by Wireplumber running under any user session.

# /usr/share/wireplumber/wireplumber.conf.d/bluez.conf

wireplumber.profiles = {
    main = {
        monitor.bluez.seat-monitoring = disabled
    }
}

Audio Server

Pulseaudio

Install pulseaudio-bluetooth to support BlueZ.

# /etc/pulse/default.pa

# Accept remote audio connection on protocol Pulse.
load-module module-native-protocol-tcp auth-ip-acl=0.0.0.0/0

Pipewire

/etc/pipewire/pipewire-pulse.conf.d/pulse.conf

pulse.properties = {
    server.address = [
        "unix:native"
        "tcp:4713"
    ]

    pulse.default.format = S16
    pulse.default.position = [ FL FR ]
}

Down-mix stereo to mono

In some cases like …

  • speech broadcasting or
  • listening to podcasts

… you want all the content mixed into mono channel and playback them on all speakers.

Pulseaudio

# /etc/pulse/default.pa

# Mix stereo channels into mono and play on both speakers.
load-module module-remap-sink sink_name=mono master=@DEFAULT_SINK@ channels=2 channel_map=mono,mono remix=yes

# Playback to mono.
set-default-sink mono

PipeWire

# /etc/pipewire/pipewire.conf.d/mono.conf

context.modules = [{
    name = libpipewire-module-combine-stream
    args = {
        combine.mode = sink
        node.name = "mono_sink"
        node.description = "Mono Sink"
        combine.props = {
            audio.position = [ MONO ]
        }
        stream.rules = [{
            matches = [{
                media.class = "Audio/Sink"
            }]
            actions = {
                create-stream = {
                    combine.audio.position = [ MONO ]
                    audio.position = [ FL FR ]
                }
            }
        }]
    }
}]

Start services

systemctl enable-linger pipewire-user

systemctl enable --now bluetooth
machinectl shell [email protected]

systemctl enable --user --now pipewire pipewire-pulse wireplumber

Client

Pipewire

# ~/.config/pipewire/pipewire.conf.d/pulse.conf

context.modules = [{
    name = libpipewire-module-pulse-tunnel
    args = {
        tunnel.mode = sink
        reconnect.interval.ms = 5000
        pulse.server.address = "tcp:192.168.1.1:4713"
        pulse.latency = 500
        audio.rate = 44100
        audio.channels = 2
        audio.position = [ FL FR ]
        audio.format = S16
        stream.props = {
            node.name = "pipewired"
            node.description = "Pipewire Server"
        }
    }
}]