#!/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 }