diff --git a/playbooks/deploy_audio_pi.yml b/playbooks/deploy_audio_pi.yml new file mode 100644 index 0000000..4d6e22d --- /dev/null +++ b/playbooks/deploy_audio_pi.yml @@ -0,0 +1,8 @@ +--- +- name: Configure audio.z9 as a bluetooth audio sink and AirPlay server + become: true + hosts: audio + roles: + - hifiberry + - bluetooth_audio_sink + - shairport_sync diff --git a/playbooks/files/configs/audio/shairport-sync/shairport-sync.conf b/playbooks/files/configs/audio/shairport-sync/shairport-sync.conf new file mode 100644 index 0000000..83cb38a --- /dev/null +++ b/playbooks/files/configs/audio/shairport-sync/shairport-sync.conf @@ -0,0 +1,12 @@ +general = +{ + name = "Audio Pi"; + port = 5000; + udp_port_base = 6001; + udp_port_range = 10; + interpolation = "soxr"; + output_backend = "alsa"; + mdns_backend = "avahi"; + volume_range_db = 40; + volume_max_db = -18.0; +}; diff --git a/playbooks/roles/add_apt_repository/meta/argument_specs.yml b/playbooks/roles/add_apt_repository/meta/argument_specs.yml index 06d1d49..b728e7c 100644 --- a/playbooks/roles/add_apt_repository/meta/argument_specs.yml +++ b/playbooks/roles/add_apt_repository/meta/argument_specs.yml @@ -9,11 +9,11 @@ argument_specs: type: bool add_apt_repository__keyring_url: description: URL to the repository's keyring - required: true + required: false type: str add_apt_repository__keyring_path: description: Path where to store the keyring - required: true + required: false type: str add_apt_repository__repo: description: The apt source line diff --git a/playbooks/roles/add_apt_repository/tasks/main.yml b/playbooks/roles/add_apt_repository/tasks/main.yml index be697cf..abd2e46 100644 --- a/playbooks/roles/add_apt_repository/tasks/main.yml +++ b/playbooks/roles/add_apt_repository/tasks/main.yml @@ -13,6 +13,7 @@ name: apt-transport-https when: add_apt_repository__https_repo - name: Add repository signing key to keychain + when: add_apt_repository__keyring_url is defined and add_apt_repository__keyring_path is defined ansible.builtin.apt_key: url: "{{ add_apt_repository__keyring_url }}" keyring: "{{ add_apt_repository__keyring_path }}" @@ -21,3 +22,4 @@ ansible.builtin.apt_repository: repo: "{{ add_apt_repository__repo }}" filename: "{{ add_apt_repository__filename }}" + update_cache: true diff --git a/playbooks/roles/bluetooth_audio_sink/files/override.conf b/playbooks/roles/bluetooth_audio_sink/files/override.conf new file mode 100644 index 0000000..de07b82 --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/files/override.conf @@ -0,0 +1,3 @@ +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin audiosink --noclear %I 38400 linux diff --git a/playbooks/roles/bluetooth_audio_sink/files/speaker-agent.service b/playbooks/roles/bluetooth_audio_sink/files/speaker-agent.service new file mode 100644 index 0000000..e665f19 --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/files/speaker-agent.service @@ -0,0 +1,8 @@ +[Unit] +Description=Bluetooth speaker agent + +[Service] +ExecStart=python3 /opt/speaker-agent/speaker-agent.py + +[Install] +WantedBy=default.target diff --git a/playbooks/roles/bluetooth_audio_sink/handlers/main.yml b/playbooks/roles/bluetooth_audio_sink/handlers/main.yml new file mode 100644 index 0000000..5b734ad --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/handlers/main.yml @@ -0,0 +1,14 @@ +--- +- name: Restart bluetooth service + ansible.builtin.systemd: + service: bluetooth.service + state: restarted +- name: Restart speaker-agent service + become_user: audiosink + ansible.builtin.systemd: + service: speaker-agent.service + state: restarted + scope: user + daemon_reload: true +- name: Reboot host + ansible.builtin.reboot: diff --git a/playbooks/roles/bluetooth_audio_sink/meta/argument_specs.yml b/playbooks/roles/bluetooth_audio_sink/meta/argument_specs.yml new file mode 100644 index 0000000..3b66203 --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/meta/argument_specs.yml @@ -0,0 +1,8 @@ +--- +argument_specs: + main: + options: + bluetooth_audio_sink__name: + description: Name the bluetooth device should have + type: str + required: true diff --git a/playbooks/roles/bluetooth_audio_sink/meta/main.yml b/playbooks/roles/bluetooth_audio_sink/meta/main.yml new file mode 100644 index 0000000..2c3bd99 --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: distribution_check + vars: + distribution_check__supported_distributions: + - name: Debian + versions: + - "11" diff --git a/playbooks/roles/bluetooth_audio_sink/tasks/main.yml b/playbooks/roles/bluetooth_audio_sink/tasks/main.yml new file mode 100644 index 0000000..e0b4573 --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/tasks/main.yml @@ -0,0 +1,142 @@ +--- +- name: Ensure acl is installed + ansible.builtin.apt: + name: acl +- name: Ensure machine-info file is deployed + ansible.builtin.template: + src: machine-info.j2 + dest: /etc/machine-info + mode: 0644 + owner: root + group: root + notify: Restart bluetooth service +- name: Ensure bluetooth config is deployed + ansible.builtin.template: + src: main.conf.j2 + dest: /etc/bluetooth/main.conf + mode: 0644 + owner: root + group: root + notify: Restart bluetooth service +- name: Ensure bluetooth service is enabled and started + ansible.builtin.systemd: + service: bluetooth.service + state: started + enabled: true +- name: Ensure audiosink user exists + ansible.builtin.user: + name: audiosink + groups: + - audio +- name: Ensure user services are started without needing to login + ansible.builtin.command: + cmd: loginctl enable-linger audiosink + changed_when: false +- name: Detect if on Raspberry Pi # noqa ignore-errors + ansible.builtin.include_role: + name: raspberry_pi_check + ignore_errors: true +- name: Ensure debian archive keyring is installed # noqa no-handler + when: raspberry_pi_check__cpuinfo.found + ansible.builtin.apt: + deb: https://ftp.debian.org/debian/pool/main/d/debian-archive-keyring/debian-archive-keyring_2021.1.1_all.deb +- name: Ensure debian backports repository is enabled + ansible.builtin.include_role: + name: add_apt_repository + vars: + add_apt_repository__https_repo: false + add_apt_repository__repo: "deb http://deb.debian.org/debian {{ ansible_facts.lsb.codename }}-backports main contrib non-free" + add_apt_repository__filename: "{{ ansible_facts.lsb.codename }}-backports.list" +- name: Ensure pipewire, wireplumber and libspa-0.2-bluetooth are installed + ansible.builtin.apt: + name: + - pipewire + - wireplumber + - libspa-0.2-bluetooth + default_release: "{{ ansible_facts.lsb.codename }}-backports" +- name: Ensure pipewire user service is enabled and started + become_user: audiosink + ansible.builtin.systemd: + name: pipewire.service + state: started + enabled: true + scope: user +- name: Ensure pipewire-pulse user service is enabled and started + become_user: audiosink + ansible.builtin.systemd: + name: pipewire-pulse.service + state: started + enabled: true + scope: user +- name: Ensure wireplumber user service is enabled and started + become_user: audiosink + ansible.builtin.systemd: + name: wireplumber.service + state: started + enabled: true + scope: user +- name: Ensure speaker-agent dependencies are installed + ansible.builtin.apt: + name: + - git + - python3-dbus +- name: Ensure speaker-agent repository is cloned + ansible.builtin.git: + repo: https://github.com/fdanis-oss/pw_wp_bluetooth_rpi_speaker.git + dest: /opt/speaker-agent + version: 9a939a23865ea020816017e06fd2290f301c35f9 +- name: Ensure .config directory exists + become_user: audiosink + ansible.builtin.file: + path: /home/audiosink/.config + state: directory + mode: 0700 + owner: audiosink + group: audiosink +- name: Ensure .config/systemd directory exists + become_user: audiosink + ansible.builtin.file: + path: /home/audiosink/.config/systemd + state: directory + mode: 0755 + owner: audiosink + group: audiosink +- name: Ensure .config/systemd/user directory exists + become_user: audiosink + ansible.builtin.file: + path: /home/audiosink/.config/systemd/user + state: directory + mode: 0755 + owner: audiosink + group: audiosink +- name: Ensure speaker-agent service file is deployed + become_user: audiosink + ansible.builtin.copy: + src: speaker-agent.service + dest: /home/audiosink/.config/systemd/user/speaker-agent.service + mode: 0644 + owner: audiosink + group: audiosink + notify: Restart speaker-agent service +- name: Ensure speaker-agent service is enabled and started + become_user: audiosink + ansible.builtin.systemd: + name: speaker-agent.service + state: started + enabled: true + scope: user +# This part is janky, but I don't know how to make the bluetooth service, wireplumber and pipewire +# work with each other without being logged in. +- name: Ensure getty@tty1 override file is deployed + ansible.builtin.copy: + src: override.conf + dest: /etc/systemd/system/getty@tty1.service.d/override.conf + mode: 0644 + owner: root + group: root + notify: Reboot host +- name: Ensure autologin for audiosink user is enabled + ansible.builtin.systemd: + service: getty@tty1.service + enabled: true + notify: Reboot host diff --git a/playbooks/roles/bluetooth_audio_sink/templates/machine-info.j2 b/playbooks/roles/bluetooth_audio_sink/templates/machine-info.j2 new file mode 100644 index 0000000..88ebdfa --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/templates/machine-info.j2 @@ -0,0 +1 @@ +PRETTY_HOSTNAME=Audio Pi diff --git a/playbooks/roles/bluetooth_audio_sink/templates/main.conf.j2 b/playbooks/roles/bluetooth_audio_sink/templates/main.conf.j2 new file mode 100644 index 0000000..0afe8d0 --- /dev/null +++ b/playbooks/roles/bluetooth_audio_sink/templates/main.conf.j2 @@ -0,0 +1,211 @@ +[General] + +# Defaults to 'BlueZ X.YZ', if Name is not set here and plugin 'hostname' is not loaded. +# The plugin 'hostname' is loaded by default and overides the Name set here so +# consider modifying /etc/machine-info with variable PRETTY_HOSTNAME= instead. +Name = {{ bluetooth_audio_sink__name }} + +# Default device class. Only the major and minor device class bits are +# considered. Defaults to '0x000000'. +Class = 0x200414 # Audio (Major Service Class), Audio/Video (Major Device Class), Loudspeaker (Minor Device Class) + +# How long to stay in discoverable mode before going back to non-discoverable +# The value is in seconds. Default is 180, i.e. 3 minutes. +# 0 = disable timer, i.e. stay discoverable forever +#DiscoverableTimeout = 0 +DiscoverableTimeout = 0 + +# Always allow pairing even if there are no agent registered +# Possible values: true, false +# Default: false +#AlwaysPairable = false +AlwaysPairable = true + +# How long to stay in pairable mode before going back to non-discoverable +# The value is in seconds. Default is 0. +# 0 = disable timer, i.e. stay pairable forever +#PairableTimeout = 0 +PairableTimeout = 0 + +# Use vendor id source (assigner), vendor, product and version information for +# DID profile support. The values are separated by ":" and assigner, VID, PID +# and version. +# Possible vendor id source values: bluetooth, usb (defaults to usb) +#DeviceID = bluetooth:1234:5678:abcd + +# Do reverse service discovery for previously unknown devices that connect to +# us. For BR/EDR this option is really only needed for qualification since the +# BITE tester doesn't like us doing reverse SDP for some test cases, for LE +# this disables the GATT client functionally so it can be used in system which +# can only operate as peripheral. +# Defaults to 'true'. +#ReverseServiceDiscovery = true + +# Enable name resolving after inquiry. Set it to 'false' if you don't need +# remote devices name and want shorter discovery cycle. Defaults to 'true'. +#NameResolving = true + +# Enable runtime persistency of debug link keys. Default is false which +# makes debug link keys valid only for the duration of the connection +# that they were created for. +#DebugKeys = false + +# Restricts all controllers to the specified transport. Default value +# is "dual", i.e. both BR/EDR and LE enabled (when supported by the HW). +# Possible values: "dual", "bredr", "le" +#ControllerMode = dual + +# Enables Multi Profile Specification support. This allows to specify if +# system supports only Multiple Profiles Single Device (MPSD) configuration +# or both Multiple Profiles Single Device (MPSD) and Multiple Profiles Multiple +# Devices (MPMD) configurations. +# Possible values: "off", "single", "multiple" +#MultiProfile = off + +# Permanently enables the Fast Connectable setting for adapters that +# support it. When enabled other devices can connect faster to us, +# however the tradeoff is increased power consumptions. This feature +# will fully work only on kernel version 4.1 and newer. Defaults to +# 'false'. +#FastConnectable = false + +# Default privacy setting. +# Enables use of private address. +# Possible values: "off", "device", "network" +# "network" option not supported currently +# Defaults to "off" +# Privacy = off + +# Specify the policy to the JUST-WORKS repairing initiated by peer +# Possible values: "never", "confirm", "always" +# Defaults to "never" +#JustWorksRepairing = never +JustWorksRepairing = always + +# How long to keep temporary devices around +# The value is in seconds. Default is 30. +# 0 = disable timer, i.e. never keep temporary devices +#TemporaryTimeout = 30 + +# Enables the device to issue an SDP request to update known services when +# profile is connected. Defaults to true. +#RefreshDiscovery = true + +Enable=Source,Sink,Media + +[Controller] +# The following values are used to load default adapter parameters. BlueZ loads +# the values into the kernel before the adapter is powered if the kernel +# supports the MGMT_LOAD_DEFAULT_PARAMETERS command. If a value isn't provided, +# the kernel will be initialized to it's default value. The actual value will +# vary based on the kernel version and thus aren't provided here. +# The Bluetooth Core Specification should be consulted for the meaning and valid +# domain of each of these values. + +# BR/EDR Page scan activity configuration +#BRPageScanType= +#BRPageScanInterval= +#BRPageScanWindow= + +# BR/EDR Inquiry scan activity configuration +#BRInquiryScanType= +#BRInquiryScanInterval= +#BRInquiryScanWindow= + +# BR/EDR Link supervision timeout +#BRLinkSupervisionTimeout= + +# BR/EDR Page Timeout +#BRPageTimeout= + +# BR/EDR Sniff Intervals +#BRMinSniffInterval= +#BRMaxSniffInterval= + +# LE advertisement interval (used for legacy advertisement interface only) +#LEMinAdvertisementInterval= +#LEMaxAdvertisementInterval= +#LEMultiAdvertisementRotationInterval= + +# LE scanning parameters used for passive scanning supporting auto connect +# scenarios +#LEScanIntervalAutoConnect= +#LEScanWindowAutoConnect= + +# LE scanning parameters used for passive scanning supporting wake from suspend +# scenarios +#LEScanIntervalSuspend= +#LEScanWindowSuspend= + +# LE scanning parameters used for active scanning supporting discovery +# proceedure +#LEScanIntervalDiscovery= +#LEScanWindowDiscovery= + +# LE scanning parameters used for passive scanning supporting the advertisement +# monitor Apis +#LEScanIntervalAdvMonitor= +#LEScanWindowAdvMonitor= + +# LE scanning parameters used for connection establishment. +#LEScanIntervalConnect= +#LEScanWindowConnect= + +# LE default connection parameters. These values are superceeded by any +# specific values provided via the Load Connection Parameters interface +#LEMinConnectionInterval= +#LEMaxConnectionInterval= +#LEConnectionLatency= +#LEConnectionSupervisionTimeout= +#LEAutoconnecttimeout= + +[GATT] +# GATT attribute cache. +# Possible values: +# always: Always cache attributes even for devices not paired, this is +# recommended as it is best for interoperability, with more consistent +# reconnection times and enables proper tracking of notifications for all +# devices. +# yes: Only cache attributes of paired devices. +# no: Never cache attributes +# Default: always +#Cache = always + +# Minimum required Encryption Key Size for accessing secured characteristics. +# Possible values: 0 and 7-16. 0 means don't care. +# Defaults to 0 +#KeySize = 0 + +# Exchange MTU size. +# Possible values: 23-517 +# Defaults to 517 +#ExchangeMTU = 517 + +# Number of ATT channels +# Possible values: 1-5 (1 disables EATT) +# Default to 3 +#Channels = 3 + +[Policy] +# +# The ReconnectUUIDs defines the set of remote services that should try +# to be reconnected to in case of a link loss (link supervision +# timeout). The policy plugin should contain a sane set of values by +# default, but this list can be overridden here. By setting the list to +# empty the reconnection feature gets disabled. +#ReconnectUUIDs=00001112-0000-1000-8000-00805f9b34fb,0000111f-0000-1000-8000-00805f9b34fb,0000110a-0000-1000-8000-00805f9b34fb + +# ReconnectAttempts define the number of attempts to reconnect after a link +# lost. Setting the value to 0 disables reconnecting feature. +#ReconnectAttempts=7 + +# ReconnectIntervals define the set of intervals in seconds to use in between +# attempts. +# If the number of attempts defined in ReconnectAttempts is bigger than the +# set of intervals the last interval is repeated until the last attempt. +#ReconnectIntervals=1,2,4,8,16,32,64 + +# AutoEnable defines option to enable all controllers when they are found. +# This includes adapters present on start as well as adapters that are plugged +# in later on. Defaults to 'false'. +AutoEnable=true diff --git a/playbooks/roles/hifiberry/handlers/main.yml b/playbooks/roles/hifiberry/handlers/main.yml new file mode 100644 index 0000000..427a90a --- /dev/null +++ b/playbooks/roles/hifiberry/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: Restart system + ansible.builtin.reboot: diff --git a/playbooks/roles/hifiberry/meta/argument_specs.yml b/playbooks/roles/hifiberry/meta/argument_specs.yml new file mode 100644 index 0000000..5972bd6 --- /dev/null +++ b/playbooks/roles/hifiberry/meta/argument_specs.yml @@ -0,0 +1,17 @@ +--- +argument_specs: + main: + options: + hifiberry__device: + description: Which HiFiBerry board to enable + type: str + choices: + - hifiberry-dac + - hifiberry-dacplus + - hifiberry-dacplushd + - hifiberry-dacplusadc + - hifiberry-dacplusadcpro + - hifiberry-digi + - hifiberry-digi-pro + - hifiberry-amp + required: true diff --git a/playbooks/roles/hifiberry/meta/main.yml b/playbooks/roles/hifiberry/meta/main.yml new file mode 100644 index 0000000..6995b78 --- /dev/null +++ b/playbooks/roles/hifiberry/meta/main.yml @@ -0,0 +1,9 @@ +--- +dependencies: + - role: distribution_check + vars: + distribution_check__supported_distributions: + - name: Debian + versions: + - "11" + - role: raspberry_pi_check diff --git a/playbooks/roles/hifiberry/tasks/main.yml b/playbooks/roles/hifiberry/tasks/main.yml new file mode 100644 index 0000000..42128a4 --- /dev/null +++ b/playbooks/roles/hifiberry/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Remove dtparam=audio=on + ansible.builtin.lineinfile: + line: dtparam=audio=on + dest: /boot/config.txt + state: absent + notify: Restart system +- name: Set audio=off for dtoverlay=vc4-kms-v3d + ansible.builtin.lineinfile: + regexp: ^dtoverlay=vc4-kms-v3d + line: dtoverlay=vc4-kms-v3d,audio=off + dest: /boot/config.txt + notify: Restart system +- name: Add hifiberry dtoverlay + ansible.builtin.lineinfile: + line: "dtoverlay={{ hifiberry__device }}" + dest: /boot/config.txt + notify: Restart system diff --git a/playbooks/roles/raspberry_pi_check/tasks/main.yml b/playbooks/roles/raspberry_pi_check/tasks/main.yml new file mode 100644 index 0000000..925fc67 --- /dev/null +++ b/playbooks/roles/raspberry_pi_check/tasks/main.yml @@ -0,0 +1,12 @@ +- name: Detect if on Raspberry Pi + ansible.builtin.lineinfile: + path: /proc/cpuinfo + regexp: ".*Raspberry Pi.*" + state: absent + check_mode: true + register: raspberry_pi_check__cpuinfo + changed_when: false +- name: Fail when not on Raspberry Pi # noqa no-handler + when: not raspberry_pi_check__cpuinfo.found + ansible.builtin.fail: + msg: You are not running on Raspberry Pi hardware! diff --git a/playbooks/roles/shairport_sync/handlers/main.yml b/playbooks/roles/shairport_sync/handlers/main.yml new file mode 100644 index 0000000..b908074 --- /dev/null +++ b/playbooks/roles/shairport_sync/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart shairport-sync service + ansible.builtin.systemd: + service: shairport-sync + state: restarted diff --git a/playbooks/roles/shairport_sync/meta/argument_specs.yml b/playbooks/roles/shairport_sync/meta/argument_specs.yml new file mode 100644 index 0000000..55570ac --- /dev/null +++ b/playbooks/roles/shairport_sync/meta/argument_specs.yml @@ -0,0 +1,8 @@ +--- +argument_specs: + main: + options: + shairport_sync__config: + description: shairport-sync config content + type: str + required: true diff --git a/playbooks/roles/shairport_sync/meta/main.yml b/playbooks/roles/shairport_sync/meta/main.yml new file mode 100644 index 0000000..2c3bd99 --- /dev/null +++ b/playbooks/roles/shairport_sync/meta/main.yml @@ -0,0 +1,8 @@ +--- +dependencies: + - role: distribution_check + vars: + distribution_check__supported_distributions: + - name: Debian + versions: + - "11" diff --git a/playbooks/roles/shairport_sync/tasks/main.yml b/playbooks/roles/shairport_sync/tasks/main.yml new file mode 100644 index 0000000..8aa9906 --- /dev/null +++ b/playbooks/roles/shairport_sync/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Ensure shairport-sync is installed + ansible.builtin.apt: + name: shairport-sync +- name: Ensure shairport-sync config is deployed + ansible.builtin.copy: + content: "{{ shairport_sync__config }}" + dest: /etc/shairport-sync.conf + mode: 0644 + owner: root + group: root + notify: Restart shairport-sync service +- name: Ensure shairport-sync service is enabled and started + ansible.builtin.systemd: + service: shairport-sync + state: started + enabled: true