#!/bin/dash
# Specify the sink and source for aloopd
# This file always writes to $HOME/.aloopdrc, never to /etc/aloopd.conf
#
# Copyright (C) James Budiono 2021, 2022, 2023
# License: GPL Version 3 or later
#

### configuration
APPTITLE="ALSA Loop Server Configuration"
APPTITLE_MAIN="$APPTITLE"
APPTITLE_LOCALSINK="Computer >--> Speaker"
APPTITLE_LOCALSOURCE="Mic >--> Computer"
APPTITLE_NETSOURCE="Computer >--> Network"
APPTITLE_NETSINK="Network >--> Computer"

OUTFILE=$HOME/.aloopdrc
RESTART_DELAY=${RESTART_DELAY:-10} # in seconds, usually it's enough
ASOUNDRC=${ASOUNDRC:-$HOME/.asoundrc} # it can easily be /etc/asound.conf for system-wide settings

DEFAULT_TRX_PORT=1350
DEFAULT_LATENCY=500
AVAIL_LATENCIES="
	50 \"50ms - better audio/video sync\"
	500 \"500ms - lighter CPU load\"
	other \"Other - specify yourself\"
"

ALOOPD_DEVICES="
	aloopd \"aloopd - exclusive output (better latency)\"
	aloopdshared \"aloopdshared - shared output (can be used for recording mixout) \"
"

NETSINK_DEVICES="
	default \"Default computer speaker\"
	aloopd \"Default computer mic input\"
"

########## UI helpers #########

# $1-info
msgbox() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --msgbox "$1" 0 0 10000
	else
		dialog --backtitle "$APPTITLE" --msgbox "$1" 0 0 > /dev/stderr
	fi
}

# $1-info $2-timeout (in ms)
infobox() {
	local timeout=${2:-10000}
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --infobox "$1" 0 0 $timeout
	else
		dialog --backtitle "$APPTITLE" --no-cancel --pause "$1" 12 60 $(( timeout/1000 )) > /dev/stderr
	fi
}

# $1-text, $2-choices, output: stdout
choices() {
	if [ $DISPLAY ]; then
		eval Xdialog --title \"$APPTITLE\" --stdout --no-tags --menubox \""$1"\" 20 100 5 $2
	else 
		eval dialog --backtitle \"$APPTITLE\" --stdout --no-tags --menu \""$1"\" 0 0 0 $2
	fi
}

# $1-text, $2-choices, output: stdout
multi_choices() {
	if [ $DISPLAY ]; then
		eval Xdialog --title \"$APPTITLE\" --stdout --no-tags --separator \" \" --checklist \""$1"\" 20 100 5 $2
	else
		eval dialog --title \"$APPTITLE\" --stdout --no-tags --separator \" \" --checklist \""$1"\" 0 0 0 $2
	fi
}

# $1-text
yesno() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --yesno "$1" 0 0
	else
		dialog --title "$APPTITLE" --yesno "$1" 0 0
	fi
}

# $1-text
noyes() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --defaultno --yesno "$1" 0 0
	else
		dialog --title "$APPTITLE" --defaultno --yesno "$1" 0 0
	fi
}

# $1-prompt, $2-current value
inputbox() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --stdout --inputbox "$1" 0 0 "$2"
	else
		dialog --title "$APPTITLE" --stdout --inputbox "$1" 0 0 "$2"		
	fi
}

# $1-prompt
fselect() {
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --backtitle "$1" --stdout --no-buttons --fselect "$PWD" 0 0
	else
		dialog --title "$APPTITLE" --backtitle "$1" --stdout --fselect "$PWD" 10 70
	fi
}

# $1-prompt
tailbox() {
	local tmpfile=/tmp/bt-status.$$
	if [ $DISPLAY ]; then
		Xdialog --title "$APPTITLE" --no-cancel --backtitle "$1" --tailbox - 0 0
	else
		cat > $tmpfile
		dialog --title "$APPTITLE" --backtitle "$1" --textbox $tmpfile 0 0
		rm $tmpfile
	fi
}

# $1-text
splash() {
	if [ "$XPID" ]; then
		kill $XPID
		XPID=
	fi

	if [ "$1" ]; then	
		if [ $DISPLAY ]; then
			Xdialog --title "$APPTITLE" --no-buttons --infobox "$1" 0 0 1000000 &
			XPID=$!
		else
			dialog --backtitle "$APPTITLE" --infobox "$1" 0 0 > /dev/stderr
		fi
	fi
}

# $1-text
die() {
	infobox "$1"
	exit 1
}

die_unchanged() {
	die "Operation cancelled. Nothing was changed."
}

# $1-option, $* options
has_option() {
	local p inp=$1
	shift
	for p; do
		case $p in
			$inp) return 0
		esac	
	done
	return 1
}


########## Task helpers ############

# out: PLAYBACK_DEVICES RECORD_DEVICES BT_DEVICES
detect_hardware() {
	PLAYBACK_DEVICES=$(LANG=C aplay -l | awk '$1~/card/ && $0!~/Loopback/ { print "\"" $0 "\" " "\"" $0 "\"" }')
	RECORD_DEVICES=$(LANG=C arecord -l | awk '$1~/card/ && $0!~/Loopback/ { print "\"" $0 "\" " "\"" $0 "\"" }')

	if pidof jackd > /dev/null; then
		PLAYBACK_DEVICES="$PLAYBACK_DEVICES jack \"ALSA -> JACK bridge\""
		RECORD_DEVICES="$RECORD_DEVICES jack \"JACK bridge -> ALSA\""
	fi

	if pidof bluetoothd > /dev/null && BT_DEVICES=$(bt-device -l);
	then
		BT_DEVICES="$(echo "$BT_DEVICES" | sed '1d; s|.*(\(.*\))|"\1" "&"|')"
		PLAYBACK_DEVICES="$PLAYBACK_DEVICES bluetooth \"Bluetooth devices\""
		RECORD_DEVICES="$RECORD_DEVICES bluetooth \"Bluetooth devices\""
	fi
}

# see if trx package is installed
has_trx() {
	# hopefully this is reliable enough ... 
	ls /var/log/packages/trx-* > /dev/null 2>&1
}

# $1-extra text out: LATENCY (in ms)
get_latency() {
	LATENCY=$(choices "Choose latency $1" "$AVAIL_LATENCIES") || die_unchanged
	if [ "$LATENCY" = other ]; then
		LATENCY=$(inputbox "Specify the latency in milliseconds.

If the latency is too big you will notice audio/video out of sync.
If the latency is too small you will either hear choppy sound, or nothing at all.

500 (500ms = 0.5 second) helps with slower machine and slower devices (e.g. Bluetooth)
50 (50ms = 0.05 second) provides unnoticeable delay
Use larger values iff you use 'aloopdshared' as output

" "$DEFAULT_LATENCY") || die_unchanged
	fi
}

# get trx ip and port
# $1-send/recv, out: TRX_IP and TRX_PORT
get_trx_ip_port() {
	local msgip= msgport= 
	case $1 in
		send)
			msgip="Specify remote machine IP address to send the audio to (cannot be blank):"
			msgport="Specify remote machine port number (default is $DEFAULT_TRX_PORT):"
			;;
			
		recv)
			msgip="Specify listening IP address (for multicast - can be left empty otherwise):"
			msgport="Specify listening port number (default is $DEFAULT_TRX_PORT):"
			;;
	esac

	while true; do
		TRX_IP=$(inputbox "$msgip" ) || die_unchanged
		case $1 in
			send)
				if [ -z "$TRX_IP" ]; then
					msgbox "Remote machine IP cannot be blank!"
					continue
				fi
		esac
		[ "$TRX_IP" ] && TRX_IP="-h $TRX_IP"

		TRX_PORT=$(inputbox "$msgport" "$DEFAULT_TRX_PORT") || die_unchanged		
		if [ "$TRX_PORT" = "$DEFAULT_TRX_PORT" ]; then
			TRX_PORT= # if same as default port, leave it blank
		fi
		[ "$TRX_PORT" ] && TRX_PORT="-p $TRX_PORT"
		break
	done
}

# $1-commen $2-job
JOBNUM=1 # start at 1
set_job() {
	local comment="$1" job="$2"
	#echo JOB$JOBNUM=\'$job\'
	#echo JOBCOMMENT$JOBNUM=\'$comment\'
	eval JOB$JOBNUM='$job'
	eval JOBCOMMENT$JOBNUM='$comment'
	JOBNUM=$(( JOBNUM + 1 ))
}

# $1-card, $2-type (Playback/Capture, if blank, get all mixers)
get_mixers() {
	amixer -D $1 controls | awk -F, "/MIXER/ && \$0~/$2/ {gsub(/'/,\"'\\\\''\",\$3); print \"-m \\\"\" \$3 \"\\\"\"}"
}

########## Configuration tasks ############

# out: JOBx BT_DEV
localsink() {
	local SOURCE= SINK= LATENCY= MIXERS=
	APPTITLE="$APPTITLE_LOCALSINK"

	msgbox "Configuring $APPTITLE"

	SOURCE=$(choices "Choose audio source to use" "$ALOOPD_DEVICES") || die_unchanged
	detect_hardware # detect hardware just before it is used	
	SINK=$(choices "Choose output device" "$PLAYBACK_DEVICES") || die_unchanged
	case "$SINK" in
		bluetooth)
			BT_DEV=$(choices "Choose bluetooth devices for output" "$BT_DEVICES") || die_unchanged
			SINK="bluealsa_raw:SRV=org.bluealsa,DEV=$BT_DEV,PROFILE=A2DP,DELAY=0"
			
			# this is the correct way to do it but it doesn't work for bluealsa at the moment
			# perhaps one day it will
			#if yesno "Redirect mixer controls from bluealsa?"; then
			#	# we can't get mixer if we aren't connected
			#	bt-device -c "$BT_DEV"
			#	MIXERS=$(get_mixers bluealsa Playback)
			#fi

			# instead, replace the .asoundrc
			sed -i -e '/^ctl.!default.*bluealsa/d' $ASOUNDRC
			echo "ctl.!default bluealsa" >> $ASOUNDRC
			;;
		jack)
			#noyes "Apply sample converter for $SINK for better compatibility?" && # always if we don't use it
			SINK="plug:$SINK"
			;;
		*)
			SINK=$(echo $SINK | sed 's/card \([0-9]*\):.*device \([0-9]*\):.*/hw:\1,\2/')
			sed -i -e '/^ctl.!default.*bluealsa/d' $ASOUNDRC
			yesno "Redirect mixer controls from $SINK?" && MIXERS=$(get_mixers ${SINK%%,*} Playback)
			noyes "Apply sample converter for $SINK for better compatibility?" && SINK="plug$SINK"
			;;
	esac
	get_latency
	set_job "localsink: computer -> speaker/bt" \
		"alsaloop -t $((LATENCY * 1000)) -T -1 -S none -C $SOURCE -P $SINK $MIXERS -d"

	APPTITLE="$APPTITLE_MAIN"
}

# out: JOBx
localsource() {
	local SOURCE= SINK= LATENCY= MIXER=
	APPTITLE="$APPTITLE_LOCALSOURCE"

	msgbox "Configuring $APPTITLE"

	detect_hardware # detect hardware just before it is used
	SOURCE=$(choices "Choose mic source" "$RECORD_DEVICES") || die_unchanged
	case "$SOURCE" in
		bluetooth)
			BT_DEV=$(choices "Choose bluetooth devices for output" "$BT_DEVICES") || die_unchanged
			;;
		jack)
			#noyes "Apply sample converter for $SOURCE for better compatibility?" && # always fail if we don't use it
			SOURCE="plug:$SOURCE"
			;;			
		*)
			SOURCE=$(echo $SOURCE | sed 's/card \([0-9]*\):.*device \([0-9]*\):.*/hw:\1,\2/')
			# redirecting mixer here doesn't work. it only causes stuttering when recording.
			# remove it for now. maybe one day it will be fixed.
			#yesno "Redirect mixer controls from $SOURCE?" && MIXERS=$(get_mixers ${SOURCE%%,*} Capture)
			noyes "Apply sample converter for $SOURCE for better compatibility?" && SOURCE="plug$SOURCE"
			;;
	esac
	SINK="aloopd" # no choice, always aloopd
	get_latency

	# job setup
	case "$SOURCE" in
		bluetooth)
			set_job "localsource: bt -> computer" \
				"start-stop-daemon -S -b -x bluealsa-aplay -- --profile-sco --pcm-buffer-time=$((LATENCY * 1000)) -D $SINK $BT_DEV"			
			;;
		*)
			set_job "localsource: mic -> computer" \
				"alsaloop -t $((LATENCY * 1000)) -T -1 -S none -C $SOURCE -P $SINK $MIXERS -d"
			;;
	esac

	
	APPTITLE="$APPTITLE_MAIN"
}

# out: JOBx
netsource() {
	local SOURCE= TRX_IP= TRX_PORT=
	APPTITLE="$APPTITLE_NETSOURCE"	
	if ! has_trx; then
		msgbox "Install trx package first."
		APPTITLE="$APPTITLE_MAIN"
		return
	fi

	msgbox "Configuring $APPTITLE"

	detect_hardware # detect hardware just before it is used
	SOURCE=$(choices "Choose audio/mic source" "$ALOOPD_DEVICES $RECORD_DEVICES") || die_unchanged
	case "$SOURCE" in
		aloopd*) ;; # do nothing
		bluetooth)
			BT_DEV=$(choices "Choose bluetooth devices for output" "$BT_DEVICES") || die_unchanged
			;;
		*)
			SOURCE=$(echo $SOURCE | sed 's/card \([0-9]*\):.*device \([0-9]*\):.*/hw:\1,\2/')
			noyes "Apply sample converter for $SOURCE for better compatibility?" && SOURCE="plug$SOURCE"
			;;
	esac
	get_trx_ip_port send

	# job setup
	case "$SOURCE" in
		bluetooth)
			# pump and then pull - replace sink and source
			# we have to use 22-bluealsa-aplay.conf helper (two loopbacks are used)
			SINK="bluealsa-aplay:PROFILE=sco" 
			SOURCE="bluetooth_sco_capture"
			get_latency "for bluetooth"			
			set_job "netsource - bt pull: bt -> computer" \
				"start-stop-daemon -S -b -x bluealsa-aplay -- --profile-sco --pcm-buffer-time=$((LATENCY * 1000)) -D $SINK $BT_DEV"
			;;
	esac
	get_latency "for network"
	set_job "netsource: computer -> network" \
		"tx -d $SOURCE -m $LATENCY $TRX_IP $TRX_PORT -D /dev/null"

	APPTITLE="$APPTITLE_MAIN"
}

# out: JOBx BT_DEV
netsink() {
	local SOURCE= SINK= LATENCY=
	APPTITLE="$APPTITLE_NETSINK"
	if ! has_trx; then
		msgbox "Install trx package first."
		APPTITLE="$APPTITLE_MAIN"
		return
	fi

	msgbox "Configuring $APPTITLE"

	detect_hardware # detect hardware just before it is used
	SINK=$(choices "Choose output device" "$NETSINK_DEVICES $PLAYBACK_DEVICES") || die_unchanged
	case "$SINK" in
		aloopd*|default) ;; # do nothing
		bluetooth)
			BT_DEV=$(choices "Choose bluetooth devices for output" "$BT_DEVICES") || die_unchanged
			SINK="bluealsa_raw:SRV=org.bluealsa,DEV=$BT_DEV,PROFILE=A2DP,DELAY=0"					
			;;
		*)
			SINK=$(echo $SINK | sed 's/card \([0-9]*\):.*device \([0-9]*\):.*/hw:\1,\2/')
			noyes "Apply sample converter for $SINK for better compatibility?" && SINK="plug$SINK"
			;;
	esac
	get_trx_ip_port recv
	get_latency
	set_job "netsink: network -> computer/bt" \
		"rx -d $SINK -m $LATENCY -j $LATENCY $TRX_IP $TRX_PORT -D /dev/null"

	APPTITLE="$APPTITLE_MAIN"
}

# output all the jobs to the config file
output_config_file() {
	# write output
	{
		cat << EOF
# For template/examples/notes, see /etc/aloopd.conf

RESTART_DELAY='$RESTART_DELAY'
MAXJOBS='$((JOBNUM - 1))'

# Bluetooth device to connect
BT_DEV='$BT_DEV'

EOF
		# output jobs and comments
		for p in $(seq 1 $(( JOBNUM - 1)) ); do
			echo "# $(eval echo \$JOBCOMMENT$p)"
			echo JOB$p=\'"$(eval echo \$JOB$p)"\'
			echo
		done
	
	} > $OUTFILE
}



########### main ############

AVAIL_TASKS="
	localsink   \"Computer >--> Local Speaker\" on
	localsource \"Local Mic >--> Computer\" off
	netsource   \"Computer/Mic >--> Network\" off
	netsink     \"Network >--> Computer/Mic\" off
"

TASKS=$(multi_choices "Choose what you want to configure" "$AVAIL_TASKS") || die_unchanged
[ -z "$TASKS" ] && die_unchanged

for TASK in $TASKS; do
	case $TASK in
		localsink)   localsink ;;
		localsource) localsource ;;
		netsource)   netsource ;;
		netsink)     netsink ;;
	esac
done

# Done, but don't write output until we stopped the daemon 
if yesno "Almost done!

Do you want to start/restart aloopd service?
"; then
	if service aloopd status | grep -q running; then
		splash "Stopping aloopd ..."
		service aloopd stop
		splash "Waiting $RESTART_DELAY seconds to release ALSA resources ..."
		sleep $RESTART_DELAY
		splash ""		
	fi
	output_config_file
	service aloopd start
	infobox "aloopd (re-)started.
Config file is written to $OUTFILE."
	
else
	output_config_file
	infobox "Config file is written to $OUTFILE."
fi
