Raspberry Pi setup
I've used the Raspberry Pi Imager utility to flash the Raspberry Pi SD cards.
The tool allows you to set an initial wifi, user, password, and host name. I used Raspberry Pi OS Lite (64-bit)
as a starting point. This is a minimal distribution, which allows us to install only the packages we need.
The following scheme for names, hostnames and passwords can be used for a consistent setup:
- Username: nodeA
- Hostname: nodeA
- Password: knotenA
Note: Only use this password scheme if the nodes are not connected to a public network. However, after you copied the ssh key (next step) you can disable ssh login via password, as described here. You could do this temporarily, since the Pis require an internet connection during the ansible setup.
The letter A is then replaced for each node by incrementing the letter. After the image is flashed, we copy our public ssh key to each Raspberry Pi using ssh-copy-id
:
ssh-copy-id nodeA@nodeA.local
After this, the setup process is automated using Ansible. The configuration files are in this repository's ansible subfolder.
Ansible
Config
Currently, these are only used to save some time running commands. ansible.cfg
only contains two options:
[defaults]
INVENTORY = inventory
[ssh_connection]
pipelining = True
The first line sets the default "inventory" file, which contains the username@host for all Raspberry Pis which should be setup using Ansible.
The second option pipelining = True
enables pipelining via ssh:
Pipelining, if supported by the connection plugin, reduces the number of network operations required to execute a module on the remote server, by executing many Ansible modules without actual file transfer. It can result in a very significant performance improvement when enabled. However this conflicts with privilege escalation (become). For example, when using ‘sudo:’ operations you must first disable ‘requiretty’ in /etc/sudoers on all managed hosts, which is why it is disabled by default. This setting will be disabled if ANSIBLE_KEEP_REMOTE_FILES is enabled.
Note that I don't remember explicitly disabling requiretty
on the Pis, so it might be the default. However, if there are problems with privilege you can try disabling this option.
General Setup
Software Overview
The setup.yml
file contains a general setup for the Pis, making sure that the hardware is set up correctly and everything is up to date. The following software is installed:
- lightdm, lxsession: Lightweight display manager lxde
- supercollider, sc3-plugins supercollider
- jackd2 Jack audio
- python3-pip, git Requirements for p2psc
- xinput I'm not quite sure whether this is still necessary..
LXDE
The following command enables automatic login (so no password is required) after boot:
raspi-config nonint do_boot_behaviour B4
As you may notice, the screen rotation is wrong, so we need to configure LXDE to flip the screen. This requires a reboot (which is done automatically), after which the LXDE configuration files are created.
To rotate the screen, a script is run after every boot which executes some xrandr commands to rotate the screen and touchscreen
#!/bin/bash
DISPLAY=:0 xrandr -o inverted
DISPLAY=:0 xinput --set-prop "generic ft5x06 (79)" "Coordinate Transformation Matrix" -1 0 1 0 -1 1 0 0 1
Keyboard layout
Is set to German by default, since the keyboards at TU are all German.
Jack Audio
The following is required to allow jack audio real-time scheduling, see here:
- name: Add user to audio group and enable jack limits config
hosts: p2psc
tags: jack
tasks:
- shell: usermod -a -G audio {{ ansible_user_id }}
become: true
- copy: remote_src=True src=/etc/security/limits.d/audio.conf.disabled dest=/etc/security/limits.d/audio.conf
become: true
Additionally, a script is created that automatically starts jack audio in a terminal after boot:
/usr/bin/jackd -P75 -dalsa -dhw:1 -p512 -n3 -s -r44100 2>&1 &
P2PSC
Clones the p2psc repository (~/p2psc
) and creates a script to automatically run p2psc in a terminal window after boot. There is also a 10s delay, to wait for a DHCP lease if a server is present:
#!/bin/bash
echo "Waiting for DHCP/AutoIP init..."
sleep 10;
PYTHONPATH=~/p2psc/ python3 ~/p2psc/p2psc/main.py -v
This is required since p2psc automatically detects the IP address. If p2psc is started immediately after boot, it will use an AUTO-IP address, which will lead to issues in a DHCP enabled network.
Additionally, the setup creates a p2psc configuration file with the "name" set to each host's host name:
{
"name": "{{ ansible_user_id }}",
"zeroconf": true,
"ip": null,
"port": 3760
}
Supercollider
Since we want to use the p2psc supercollider library, a config file for supercollider is created:
~/.config/SuperCollider/sclang_conf.yaml
includePaths:
- ~/p2psc/libs/sclang
excludePaths:
[]
postInlineWarnings: false
Jacktrip Setup
Note that this has not been tested for a while and was only a proof of concept. So take it with a grain of salt ;)
jacktrip
When I created this script, the jacktrip
provided in the Ubuntu repositories was missing some essential features, which is why I created an extra step to clone and compile a more recent version of jacktrip. This might not be necessary anymore!
jack-matchmaker
Creates a systemd service that runs jack-matchmaker in the background. This allows us to automatically connect certain jack clients with others. Since we can update the configuration while this script is running, we can dynamically change the connections it makes. This allows us to map jack and jacktrip channels in a sensible manner.
Supercollider
We also create a supercollider startup file, which sets the number of SC channels to 12 by default:
~/.config/SuperCollider/startup.scd
s.options.numInputBusChannels = 12;
s.options.numOutputBusChannels = 12;
Note: That the number of channels must match or exceed the number of nodes in the network + Output channels!
Connection Setup
Note that this requires some manual intervention since the jack to jacktrip port mapping is not implemented dynamically. This is the current default mapping of nodes to supercollider ports:
nodeA:receive_1
SuperCollider:in_3
SuperCollider:out_3
nodeA:send_1
nodeB:receive_1
SuperCollider:in_4
SuperCollider:out_4
nodeB:send_1
nodeC:receive_1
SuperCollider:in_5
SuperCollider:out_5
nodeC:send_1
nodeD:receive_1
SuperCollider:in_6
SuperCollider:out_6
nodeD:send_1
nodeE:receive_1
SuperCollider:in_7
SuperCollider:out_7
nodeE:send_1
nodeF:receive_1
SuperCollider:in_8
SuperCollider:out_8
nodeF:send_1
nodeG:receive_1
SuperCollider:in_9
SuperCollider:out_9
nodeG:send_1
nodeH:receive_1
SuperCollider:in_10
SuperCollider:out_10
nodeH:send_1
The connection establishment is "semi-automatic" and uses the Ansible inventory to determine which connections need to be made. In a fixed setup, the script used here could be run after boot and automatically create all jackrip connections.
Since this was more of a proof-of-concept, this is a bit hacky, but it worked if I remember correctly ;) First, we create a list of other hosts for each node. This might a bit tricky to grasp, but we only want to create connections in one direction (which then are bidirectional). This means, not every node connects to every other node. This script creates a subset of nodes for each node to achieve this:
Example hosts [A,B,C,D]:
+ A connects to [B,C,D]
+ B connects to [C,D]
+ C connects to [D]
+ D connects to [] (none)
Which results in a fully connected graph
ownIndex={{ play_hosts.index(inventory_hostname) }}
index={{ index }}
echo "$ownIndex $index" >> /home/{{ ansible_user_id }}/test
if [ "$ownIndex" -lt "$index" ]; then
item={{ item }}
remote_host_addr=${item#*@}
remote_host=${remote_host_addr%%.*}
echo -n "$remote_host " >> /home/{{ ansible_user_id }}/.jacktrip_connections ;
fi
This file is then used in a script to create a jacktrip hub instance and establish all connections:
#!/bin/bash
# start jacktrip server in hub (-S) mode for incoming connections
# and disable default connections (-D)
jacktrip -S -D > /dev/null 2>&1 &
# wait 5 seconds to make sure all hubs are started
sleep 5;
# Go through list of hosts and establish connections
hosts=$(cat /home/{{ ansible_user_id }}/.jacktrip_connections)
port=4464
for h in $hosts; do
# create a 1-channel connection to each host (-n1)
jacktrip -C ${h}.local -J $h -K {{ ansible_user_id }} -n1 -D -B${port} > /dev/null 2>&1 &
((port=port+1))
done
Running Ansible
From within the ansible subfolder, run the following command:
ansible-playbook setup.yml
Note: Make sure that the
inventory
file in the ansible subfolder contains a line for each node you want to set up (e.g. nodeA@nodeA.local || \<user>@\<hostname>.local)
Running Jacktrip
Note that this is untested but should work!
Run the jacktrip connect script created during the jacktrip_setup:
ansible p2psc -m shell -a "nohup ~/scripts/jt_connect.sh &"
Kill all jacktrip instances
ansible p2psc -m shell -a "killall jacktrip&"