#!/bin/bash
# Control Snapcast and MPD using MIDI controller "SubZero MiniControl"
# Snapcast can be controlled using a JSON-RPC API over HTTP.
# For example: set client's volume and mute clients.
# curl is used to send requests and receive responses.
# jq is used to filter JSON data and make it more readable
# MPD can be controlled by MPC.
# aseqdump (ALSA sequencer dump) is used to read the MIDI controller data.
# Like: number of the controller (fader, knob, button) and controller value
# SubZero MiniControl Layout
#
# Channels 1-4
# Buttons, faders, knobs and their controller numbers
# +------+------+------+------+
# | K 14 | K 15 | K 16 | K 17 | ids index = controller no - 14
# +------+------+------+------+
# | F 3 | F 4 | F 5 | F 6 | ids index = controller no - 3
# +------+------+------+------+
# | B 23 | B 24 | B 25 | B 26 | ids index = controller no - 23
# +------+------+------+------+
# | 1 | 2 | 3 | 4 |
# +------+------+------+------+
# ids[0] ids[1] ids[2] ids[3]
#
# K = knob / F = fader / B = button
# 1-4: channel number printed on the SubZero MiniControl
# ids[]: Snapclient IDs assigned to channels
#
# Media buttons and their controller numbers
# +-----------+-------------+---------+---------+---------+-----------+
# | Repeat 49 | Previous 47 | Next 48 | Stop 46 | Play 45 | Record 44 |
# +-----------+-------------+---------+---------+---------+-----------+
# Name of the MIDI controller
controller="USB MIDI Controller"
# Name of the server where the Snapserver is running
server="nas"
# Put the Snapclient IDs in an array
# So they can be easily assigned to controllers of adjacent channels
# To find out the IDs of your Snapclients, run this script with the -c option
ids[0]="34:17:eb:cc:89:f9" # Living room (channel 1)
ids[1]="f0:79:59:5a:ae:05" # Hobby room (channel 2)
ids[2]="18:31:bf:56:dc:d4" # Kitchen (channel 3)
ids[3]="e0:d4:e8:77:dc:97" # Laptop (channel 4)
# Show properties of all Snapclients (like the IDs)
function show_config
{
json2curl '{"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"}' | jq .result.server.groups[].clients[]
}
# Set Snapclient volume
# Arguments: $1: Snapclient ID, $2: fader value
function set_volume()
{
volume=$((100 * $2 / 127))
echo "Set volume $1"
json2curl '{"id":"1","jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"'$1'","volume":{"percent":'$volume'}}}'
}
# Toggle mute Snapclient
# Arguments: $1: Snapclient ID, $2: button value
function toggle_mute()
{
# Only take action if button is pressed
if [ $2 -eq 127 ]
then
muted=$(json2curl '{"id":1,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"'$1'"}}' | jq .result.client.config.volume.muted)
if [ "$muted" = "true" ]
then
muted=false
else
muted=true
fi
echo "Toggle mute $1"
json2curl '{"id":"1","jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"'$1'","volume":{"muted":'$muted'}}}'
fi
}
# Set Snapclient latency
# Arguments: $1: Snapclient ID, $2: knob value
function set_latency()
{
echo "Set latency $1"
json2curl '{"id":1,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"'$1'","latency":'$2'}}'
}
# Control MPC
# Arguments: $1: mpd command, $2: button value
function control_mpc()
{
# Only take action if button is pressed
if [ $2 -eq 127 ]
then
echo "Control mpc: $1"
mpc -h $server $1
fi
}
# Send JSON request to Snapserver
# Argument: $1: JSON request
# curl option -s is used to suppress headers
function json2curl()
{
curl -s --data $1 $server:1780/jsonrpc
}
# Main code
# Show config (-c option)
# Use the -c option to find out the Snapclient IDs
if [ "$1" = "-c" ]
then
show_config
exit
fi
# Test MIDI controller (-t option)
if [ "$1" = "-t" ]
then
aseqdump -p "$controller"
exit
fi
# aseqdump (ALSA sequencer dump) shows MIDI data:
# Waiting for data. Press Ctrl+C to end.
# Source Event Ch Data
# 20:0 Control change 0, controller 3, value 39
# 20:0 Control change 0, controller 3, value 40
# 20:0 Control change 0, controller 3, value 41
# 20:0 Control change 0, controller 23, value 127
# 20:0 Control change 0, controller 23, value 0
# ...
# Read these lines into variables:
# src ev1 ev2 ch label1 ctrl_no label2 ctrl_value
# ctrl_no is number of controller (fader, knob, button)
# ctrl_value is value of controller
# fader/knob value: 0 - 127
# button value pressed: 127, value released: 0
# There are two delimiters in the output of aseqdump: comma and space
# So set IFS (internal field separator) to comma and space (" ," or ", ")
# IFS must be specified before each read!
aseqdump -p "$controller" |
{
# Ignore first two ouput lines of aseqdump (info and header)
read
read
while IFS=" ," read src ev1 ev2 ch label1 ctrl_no label2 ctrl_value rest
do
case $ctrl_no in
# Control Snapclients
[3-6]) # Fader 1-4 (set volume)
id=${ids[$ctrl_no-3]}
set_volume $id $ctrl_value ;;
1[4-7]) # Knob 1-4 (set latency)
id=${ids[$ctrl_no-14]}
set_latency $id $ctrl_value ;;
2[3-6]) # Button 1-4 (mute, unmute)
id=${ids[$ctrl_no-23]}
toggle_mute $id $ctrl_value ;;
# Control MPD with MPC
45) # Play button (play, pause)
control_mpc toggle $ctrl_value ;;
46) # Stop button (stop)
control_mpc stop $ctrl_value ;;
47) # Previous button (previous song)
control_mpc prev $ctrl_value ;;
48) # Next button (next song)
control_mpc next $ctrl_value ;;
49) # Repeat button (toggle random play)
control_mpc random $ctrl_value ;;
44) # Record button (add all songs to the queue)
control_mpc clear $ctrl_value
control_mpc "add /" $ctrl_value ;;
esac
done
}