From 0608ebea8f29f96f1b1e66b25e7823a7cced5b1d Mon Sep 17 00:00:00 2001 From: Jafner <40403594+Jafner@users.noreply.github.com> Date: Sun, 24 Oct 2021 17:33:03 -0700 Subject: [PATCH] Init --- README.md | 59 +++++++ pa_xtouch_control.sh | 364 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 423 insertions(+) create mode 100644 pa_xtouch_control.sh diff --git a/README.md b/README.md index b98a960..740c302 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ # pa-xtouch-control Control PulseAudio and X with a Behringer X-Touch Mini + + # Dependencies + This program was written to work within the stock Pop!_OS desktop environment (running X and PulseAudio) with the `xdotool` package installed to enable binding the currently-focused application to a column. + +I am open to supporting other window systems if there is sufficient interest. + +# Installation as a foreground script + +Download the script (via `curl` or `wget`) and `chmod +x pa-xtouch-control` to make it executable. Then run it with `./pa-xtouch-control` and test your Behringer X-Touch Mini to make sure everything is working. + +# Installation as a daemon + TODO + +# Modifying the script + +I've done my best to make the script readable and modifyable. Here is a summary of how the script is laid out: + +## Define functions +After some comments, we begin defining functions, which all follow the format + +```bash +function_name() { + # some code here +} +``` + +Not all of these are used. Some exist for the purpose of making support for other modes (mackie vs. standard mode) easier. + +If you know any bash (terminal commands), you can define your own functions by copying and altering the functions I've written. + +These functions are not exactly optimized, but they should be readable. + +## Run the initialization, then main function. +In this script, I initialize the script by setting the col_N_app_pid values (where N is 1-8) to -1, which ensures they will not accidentally affect applications before they are intentionally set. + +The main function watches the signals coming in from the "X-TOUCH MINI" device and checks them against its processing logic. I started using Mackie mode, which works differently from Standard mode. Then I switched to developing for Standard mode (only layer A supported for now) because it handles the lighting automatically, instead of requiring sending signals back to the device. + +## Binding an application +When you press a knob on the X-Touch Mini, the script binds the currently-focused-window's process ID (PID) to that column of the controller (the knob, top button, and bottom button). + +Note: PulseAudio does not directly interact with processes, but rather audio streams, which have their own numbering system within PulseAudio. These "Stream Index" values are not "sticky", so refreshing a Firefox YouTube video might cause the stream index to change for that audio stream. + +## Changing volume +After binding an application to a column, you can adjust the volume of that application with the rotary encoder. In standard mode, each time you move the encoder it will send a new volume level to the application (between 0% and 127%). In Mackie mode, your changes will be processed as relative (+N% or -N%) to the current volume with no upper limit. +## (Un)Muting an application +After binding an application to a column, you can toggle muting the volume of that application with the top button below the encoder. In standard mode, you can press the button (causing it to light up) to mute the bound application. In Mackie mode, each time you press the button it will toggle the mute of the application. + +## Media Keys +By default (Mackie mode or standard mode), the keys marked with the media key icons for FF, RW, Play/Pause, and Stop work as media keys by sending instructions to the X server to control media. + +# Expanding beyond the default configuration +I am more than happy to approve good pull request to add functionality to this script. If you have a different controller with different MIDI signals, I am happy to support your device (as much as reasonable). + +## Adding devices +For most MIDI devices, this simply requires copy-pasting the main function to handle the Note on or Control change signals from your controller. I cannot test these PRs, but if they look reasonable, I will approve them. + +## Adding functions +If you would like to add a function, please submit a pull request! So long as your pull request adds a function that supports an existing environment with new functionality, I will almost certainly approve it. +If it adds support for a new environment (device, audio server, or X server), I will probably still approve it. diff --git a/pa_xtouch_control.sh b/pa_xtouch_control.sh new file mode 100644 index 0000000..070f363 --- /dev/null +++ b/pa_xtouch_control.sh @@ -0,0 +1,364 @@ +#!/bin/bash +# +# Assumes a stock PopOS installation with xdotool +# +# X-Touch Mini Mappings (Mackie Mode) +# ===================== +# Col#, Knob press, knob turn, top button, bottom button +# 1, 32, 16, 89, 87 +# 2, 33, 17, 90, 88 +# 3, 34, 18, 40, 91 +# 4, 35, 19, 41, 92 +# 5, 36, 20, 42, 86 +# 6, 37, 21, 43, 93 +# 7, 38, 22, 44, 94 +# 8, 39, 23, 45, 95 +# +# Fader = 8 +# --------------------- +# Misc. Info +# Knobs and buttons are all channel 0 +# Fader is channel 8 +# Button presses (including knobs) are notes at velocity 127 +# Knob turns counter-clockwise are a value of 64 + n, +# where n is a small number reflecting the amount it was turned within the last polling period +# Knob turns clockwise are a value of 0 + n, +# where n is a small number reflecting the amount it was turned within the last polling period + +# Default column apps +# When you (re)start the script, it looks for the pids of the binaries specified for each column and sets the pid for each column +# These are overridden for the session by the bind_application function + +initialize(){ + echo "Initializing" + echo "Checking for xdotool" + if ! hash xdotool &> /dev/null; then + echo "xdotool could not be found, exiting" + exit + else + echo "xdotool found" + fi + col_1_app_pid=-1 + col_2_app_pid=-1 + col_3_app_pid=-1 + col_4_app_pid=-1 + col_5_app_pid=-1 + col_6_app_pid=-1 + col_7_app_pid=-1 + col_8_app_pid=-1 + assign_profile_1 + print_col_app_ids +} + +assign_profile_1() { + echo "Setting profile 1" +} + +assign_profile_2() { + echo "Setting profile 2" +} + +print_col_app_ids() { + echo "Col 1: $col_1_app_pid" + echo "Col 2: $col_2_app_pid" + echo "Col 3: $col_3_app_pid" + echo "Col 4: $col_4_app_pid" + echo "Col 5: $col_5_app_pid" + echo "Col 6: $col_6_app_pid" + echo "Col 7: $col_7_app_pid" + echo "Col 8: $col_8_app_pid" +} + +change_volume_mackie() { + # take the pid of an app + # set the volume of all streams for that app + + # get the volume change amount + if (( $2 >= 64 )); then + vol_change="-$(expr $2 - 64)" + else + vol_change="+$2" + fi + + # take the pid and change the volume for each of its streams + app_pid=$1 + + all_sink_inputs="$(pacmd list-sink-inputs)" + all_sink_inputs="$(paste \ + <(printf '%s' "$all_sink_inputs" | grep 'application.process.id' | cut -d'"' -f 2) \ + <(printf '%s' "$all_sink_inputs" | grep 'index: ' | rev | cut -d' ' -f 1 | rev))" + + echo "$all_sink_inputs" | while read line ; do + pid=$(echo "$line" | cut -f1) + if [[ "$pid" == "$1" ]]; then + stream_id="$(echo "$line" | cut -f2)" + pactl set-sink-input-volume $stream_id $vol_change% 2> /dev/null + fi + done +} + +change_volume_standard() { + # take the pid of an app + # set the volume of all streams for that app + + # get the new volume value + + new_vol=$2 + + # take the pid and change the volume for each of its streams + app_pid=$1 + + all_sink_inputs="$(pacmd list-sink-inputs)" + all_sink_inputs="$(paste \ + <(printf '%s' "$all_sink_inputs" | grep 'application.process.id' | cut -d'"' -f 2) \ + <(printf '%s' "$all_sink_inputs" | grep 'index: ' | rev | cut -d' ' -f 1 | rev))" + + echo "$all_sink_inputs" | while read line ; do + pid=$(echo "$line" | cut -f1) + if [[ "$pid" == "$1" ]]; then + stream_id="$(echo "$line" | cut -f2)" + pactl set-sink-input-volume $stream_id $new_vol% 2> /dev/null + fi + done +} + +toggle_mute() { + # take the pid of an app + # toggle mute all streams for that app + + app_pid=$1 + + all_sink_inputs="$(pacmd list-sink-inputs)" + all_sink_inputs="$(paste \ + <(printf '%s' "$all_sink_inputs" | grep 'application.process.id' | cut -d'"' -f 2) \ + <(printf '%s' "$all_sink_inputs" | grep 'index: ' | rev | cut -d' ' -f 1 | rev))" + + echo "$all_sink_inputs" | while read line ; do + pid=$(echo "$line" | cut -f1) + if [[ "$pid" == "$1" ]]; then + stream_id="$(echo "$line" | cut -f2)" + pactl set-sink-input-mute $stream_id toggle + fi + done +} + +mute_on() { + # take the pid of an app + # mute all streams for that app + + app_pid=$1 + + all_sink_inputs="$(pacmd list-sink-inputs)" + all_sink_inputs="$(paste \ + <(printf '%s' "$all_sink_inputs" | grep 'application.process.id' | cut -d'"' -f 2) \ + <(printf '%s' "$all_sink_inputs" | grep 'index: ' | rev | cut -d' ' -f 1 | rev))" + + echo "$all_sink_inputs" | while read line ; do + pid=$(echo "$line" | cut -f1) + if [[ "$pid" == "$1" ]]; then + stream_id="$(echo "$line" | cut -f2)" + pactl set-sink-input-mute $stream_id on + fi + done +} + +mute_off() { + # take the pid of an app + # unmute all streams for that app + + app_pid=$1 + + all_sink_inputs="$(pacmd list-sink-inputs)" + all_sink_inputs="$(paste \ + <(printf '%s' "$all_sink_inputs" | grep 'application.process.id' | cut -d'"' -f 2) \ + <(printf '%s' "$all_sink_inputs" | grep 'index: ' | rev | cut -d' ' -f 1 | rev))" + + echo "$all_sink_inputs" | while read line ; do + pid=$(echo "$line" | cut -f1) + if [[ "$pid" == "$1" ]]; then + stream_id="$(echo "$line" | cut -f2)" + pactl set-sink-input-mute $stream_id off + fi + done +} + +get_stream_index_from_pid(){ + all_sink_inputs="$(pacmd list-sink-inputs)" + all_sink_inputs="$(paste \ + <(printf '%s' "$all_sink_inputs" | grep 'application.process.id' | cut -d'"' -f 2) \ + <(printf '%s' "$all_sink_inputs" | grep 'index: ' | rev | cut -d' ' -f 1 | rev))" + + stream_ids="" + echo "$all_sink_inputs" | while read line ; do + pid=$(echo "$line" | cut -f1) + if [[ "$pid" == "$1" ]]; then + echo "$line" | cut -f2 + fi + done +} + +get_pid_from_binary(){ + # todo + echo "Not yet implemented" +} + +bind_application() { + window_pid="$(xdotool getactivewindow getwindowpid)" + window_name="$(xdotool getactivewindow getwindowname)" + col_id=$1 + #echo "window_pid=$window_pid" + #echo "window_name=$window_name" + #echo "col_id=$col_id" + + case "$col_id" in + "1" ) col_1_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "2" ) col_2_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "3" ) col_3_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "4" ) col_4_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "5" ) col_5_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "6" ) col_6_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "7" ) col_7_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + "8" ) col_8_app_pid=$window_pid && notify-send "Set knob $col_id to $window_name" ;; + esac + +} + +media_play_pause() { + xdotool key XF86AudioPlay +} +media_prev() { + xdotool key XF86AudioPrev +} +media_next() { + xdotool key XF86AudioNext +} +media_stop() { + xdotool key XF86AudioStop +} + +main_mackie(){ + aseqdump -p "X-TOUCH MINI" | \ + while IFS=" ," read src ev1 ev2 ch label1 data1 label2 data2 rest; do + #echo "$ev1 $ev2 $data1 $data2" + case "$ev1 $ev2 $data1 $data2" in + # column 1 + "Note on 32"* ) bind_application 1 ;; # knob press + "Note on 89"* ) toggle_mute $col_1_app_pid ;; # top button + "Note on 87"* ) print_col_app_ids ;; # bottom button + "Control change 16"* ) change_volume $col_1_app_pid $data2 ;; # knob turn + + # column 2 + "Note on 33"* ) bind_application 2 ;; # knob press + "Note on 90"* ) toggle_mute $col_2_app_pid ;; # top button + "Note on 88"* ) ;; # bottom button + "Control change 17"* ) change_volume $col_2_app_pid $data2 ;; # knob turn + + # column 3 + "Note on 34"* ) bind_application 3 ;; # knob press + "Note on 40"* ) toggle_mute $col_3_app_pid ;; # top button + "Note on 91"* ) media_prev ;; + "Control change 18"* ) change_volume $col_3_app_pid $data2 ;; # knob turn + + # column 4 + "Note on 35"* ) bind_application 4 ;; # knob press + "Note on 41"* ) toggle_mute $col_4_app_pid ;; # top button + "Note on 92"* ) media_next ;; + "Control change 19"* ) change_volume $col_4_app_pid $data2 ;; # knob turn + + # column 5 + "Note on 36"* ) bind_application 5 ;; # knob press + "Note on 42"* ) toggle_mute $col_5_app_pid ;; # top button + "Note on 86"* ) ;; + "Control change 20"* ) change_volume $col_5_app_pid $data2 ;; # knob turn + + # column 6 + "Note on 37"* ) bind_application 6 ;; # knob press + "Note on 43"* ) toggle_mute $col_6_app_pid ;; # top button + "Note on 93"* ) media_stop ;; + "Control change 21"* ) change_volume $col_6_app_pid $data2 ;; # knob turn + + # column 7 + "Note on 38"* ) bind_application 7 ;; # knob press + "Note on 44"* ) toggle_mute $col_7_app_pid ;; # top button + "Note on 94"* ) media_play_pause ;; + "Control change 22"* ) change_volume $col_7_app_pid $data2 ;; # knob turn + + # column 8 + "Note on 39"* ) bind_application 8 ;; # knob press + "Note on 45"* ) toggle_mute $col_8_app_pid ;; # top button + "Note on 95"* ) ;; + "Control change 23"* ) change_volume $col_8_app_pid $data2 ;; # knob turn + + # layer a and b buttons + "Note on 84"* ) assign_profile_1 ;; + "Note on 85"* ) assign_profile_2 ;; + esac + done +} + +main_standard(){ + aseqdump -p "X-TOUCH MINI" | \ + while IFS=" ," read src ev1 ev2 ch label1 data1 label2 data2 rest; do + #echo "$ev1 $ev2 $data1 $data2" + case "$ev1 $ev2 $data1 $data2" in + # column 1 + "Control change 9 127" ) bind_application 1 ;; # knob press + "Control change 17 127" ) mute_on $col_1_app_pid ;; # top button on + "Control change 17 0" ) mute_off $col_1_app_pid ;; # top button off + "Control change 25 127" ) print_col_app_ids ;; # bottom button + "Control change 1 "* ) change_volume_standard $col_1_app_pid $data2 ;; # knob turn + + # column 2 + "Control change 10 127" ) bind_application 2 && echo "bind_application 2";; # knob press + "Control change 18 127" ) mute_on $col_2_app_pid;; # top button on + "Control change 18 0" ) mute_off $col_2_app_pid ;; # top button off + "Control change 26 127" ) ;; # bottom button + "Control change 2 "* ) change_volume_standard $col_2_app_pid $data2 ;; # knob turn + + # column 3 + "Control change 11 127" ) bind_application 3 ;; # knob press + "Control change 19 127" ) mute_on $col_3_app_pid ;; # top button on + "Control change 19 0" ) mute_off $col_3_app_pid ;; # top button off + "Control change 27 127" ) media_prev ;; + "Control change 3 "* ) change_volume_standard $col_3_app_pid $data2 ;; # knob turn + + # column 4 + "Control change 12 127" ) bind_application 4 ;; # knob press + "Control change 20 127" ) mute_on $col_4_app_pid ;; # top button on + "Control change 20 0" ) mute_off $col_4_app_pid ;; # top button off + "Control change 28 127" ) media_next ;; + "Control change 4 "* ) change_volume_standard $col_4_app_pid $data2 ;; # knob turn + + # column 5 + "Control change 13 127" ) bind_application 5 ;; # knob press + "Control change 21 127" ) mute_on $col_5_app_pid ;; # top button on + "Control change 21 0" ) mute_off $col_5_app_pid ;; # top button off + "Control change 29 127" ) ;; + "Control change 5 "* ) change_volume_standard $col_5_app_pid $data2 ;; # knob turn + + # column 6 + "Control change 14 127" ) bind_application 6 ;; # knob press + "Control change 22 127" ) mute_on $col_6_app_pid ;; # top button on + "Control change 22 0" ) mute_off $col_6_app_pid ;; # top button off + "Control change 30 127" ) media_stop ;; + "Control change 6 "* ) change_volume_standard $col_6_app_pid $data2 ;; # knob turn + + # column 7 + "Control change 15 127" ) bind_application 7 ;; # knob press + "Control change 23 127" ) mute_on $col_7_app_pid ;; # top button on + "Control change 23 0" ) mute_off $col_7_app_pid ;; # top button off + "Control change 31 127" ) media_play_pause ;; + "Control change 7 "* ) change_volume_standard $col_7_app_pid $data2 ;; # knob turn + + # column 8 + "Control change 16 127" ) bind_application 8 ;; # knob press + "Control change 24 127" ) mute_on $col_8_app_pid ;; # top button on + "Control change 24 0" ) mute_off $col_8_app_pid ;; # top button off + "Control change 32 127" ) ;; + "Control change 8 "* ) change_volume_standard $col_8_app_pid $data2 ;; # knob turn + esac + done +} + +initialize +main_standard