#!/usr/bin/env bash
# This is a simple GUI to make a new sound input device which will join audio from microphone and sound output
# This script was written for ALT Linux, but should work on any GNU/Linux or *BSD system with Xorg or XWayland, PulseAudio and YAD (Yet Another Dialog).
# Author: Mikhail Novosyolov <mikhailnov@dumalogiya.ru>, 2018
# License: GPLv3

# Bash scripts localization guide:
# https://www.opennet.ru/docs/RUS/bash_scripting_guide/a15021.html
#export TEXTDOMAINDIR="/usr/share/locale"
export TEXTDOMAIN=pulsejoin

# We export all variables and functions because we fork shell via GUI buttons
#set -a # TODO: probably may remove all export's (?)

export virtual_sink1="pa_joined_sink1"
# mktemp -d --suffix=_pulsejoin is for GNU mktemp, mktemp -d -t pulsejoin_ is for BSD mktemp
tmp_dir="${tmp_dir:-$(mktemp -d --suffix=_pulsejoin || mktemp -d -t pulsejoin_ || mktemp -d)}"
if [ -n "$tmp_dir" ]
# $tmp_dir maybe empty e.g. if mktemp is not available
	then export tmp_dir
	else export tmp_dir="/tmp/pulsejoin_tmp_dir/"
fi
mkdir -p "${tmp_dir}"
export pa_modules_list_file="${tmp_dir}/pa-modules.list"
export pavucontrol_config="${HOME}/.config/pavucontrol.ini"

echo_help(){
	echo ""
}
export -f echo_help

gt(){
	gettext -s "$*"
}
export -f gt

yad_error(){
	echo "$*"
	yad --error --width=450 --text="$*"
}
export -f yad_error

yad_info(){
	echo "$*"
	yad --info --width=450 --text="$*"
}
export -f yad_info

if [ ! -x "$(command -v pactl)" ]
	then
		yad_error "$(gt pactl utily has not been found. Please install package pulseaudio / pulseaudio-utils / pulseaudio-daemon. Cannot continue working!)"
		exit 1
fi 

pa_bug_workaround(){
# See https://github.com/wwmm/pulseeffects/issues/99 why it's needed
	pulseaudio_version="$(pulseaudio --version | awk '{print $NF}' | awk -F '.' '{print $1}')"
	export pulseaudio_version
	if env LANG=c pacmd list-modules | grep -q switch-on-connect && [ "$pulseaudio_version" -lt 12 ]; then
		export PA_BUG_WORKAROUND='1'
		pactl unload-module module-switch-on-connect || return 1
	fi
}
export -f pa_bug_workaround

get_pa_default_devices(){
	pa_default_sink="$(env LANG=POSIX LC_ALL=POSIX pactl info | grep -i "^default sink:" | awk -F ": " '{print $2}')"
	pa_default_source="$(env LANG=POSIX LC_ALL=POSIX pactl info | grep -i "^default source:" | awk -F ": " '{print $2}')"
	if [ -z "$pa_default_sink" ] || [ -z "$pa_default_source" ]; then
		yad_error "$(gt Unable to get PulseAudio default source and/or sink. Please report a bug and attach output of pactl info.)"
	fi
	export pa_default_sink pa_default_source
}
export -f get_pa_default_devices

pa_remove_devices(){
	if [ -f "$pa_modules_list_file" ]; then
		( set -e
		while read -r line
		do
			pactl unload-module "$line"
		# 's/ /\n/g' does not work with BSD sed, https://stackoverflow.com/a/19883696
		done < <(sed -e 's/ /\'$'\n/g' -e '/^$/d' "${pa_modules_list_file}")
		rm -fv "${pa_modules_list_file}"
		set +e )
	fi
}
export -f pa_remove_devices

pa_make_devices(){
	trap "pa_remove_devices && return 1" ERR
	# pactl returns the number of the loaded module
	PA_MODULE_NULLSINK_N1="$(pactl load-module module-null-sink sink_name="${virtual_sink1}" sink_properties=device.description="$(gt Sound_from_microphone+speakers)")"
	PA_MODULE_LOOPBACK_N1="$(pactl load-module module-loopback source="${pa_default_source}" sink="${virtual_sink1}")"
	PA_MODULE_LOOPBACK_N2="$(pactl load-module module-loopback source="${pa_default_sink}.monitor" sink="${virtual_sink1}")"
	export PA_MODULE_NULLSINK_N1 PA_MODULE_LOOPBACK_N1 PA_MODULE_LOOPBACK_N2
	# check if PulseAudio modules have really been loaded
	if [ -z "$PA_MODULE_NULLSINK_N1" ] || [ -z "$PA_MODULE_LOOPBACK_N1" ] || [ -z "$PA_MODULE_LOOPBACK_N2" ]; then return 1; fi
	pactl set-sink-volume "${virtual_sink1}" 100%
	echo "PulseAudio modules were loaded: $PA_MODULE_NULLSINK_N1 $PA_MODULE_LOOPBACK_N1 $PA_MODULE_LOOPBACK_N2"
	for i in "$PA_MODULE_LOOPBACK_N1" "$PA_MODULE_LOOPBACK_N2" "$PA_MODULE_NULLSINK_N1"
	do
		echo "$i" >> "${pa_modules_list_file}"
	done
}
export -f pa_make_devices

pa_workaround_cleanup(){
	if [ "$PA_BUG_WORKAROUND" = '1' ]; then
		pactl load-module module-switch-on-connect
	fi
}
export -f pa_workaround_cleanup

cleanup(){
	pa_remove_devices && \
	pa_workaround_cleanup && \
	find "$tmp_dir" -maxdepth 0 -empty -exec rm -fr {} \;
	# We have to create ${pavucontrol_config}.pulsejoin.bak to make sed command
	# compatible with both GNU and BSD sed (https://stackoverflow.com/a/22084103).
	# We don't restore original config to prevent races with e.g. a running pavucontrol.
	if [ -f "${pavucontrol_config}.pulsejoin.bak" ] && [ -f "${pavucontrol_config}" ]
		then rm -f "${pavucontrol_config}.pulsejoin.bak"
	fi
}
export -f cleanup
trap cleanup EXIT

pavucontrol_check(){
	# check if pavucontrol was openned by the current user
	if pgrep --uid="$UID" pavucontrol >/dev/null
		then
			export PAVUCONTROL_WAS_RUNNING='1'
		else
			PAVUCONTROL_WAS_RUNNING='0'
	fi
}
export -f pavucontrol_check

pavucontrol_tab(){
# pavucontrol --tab 2 for "Recording" tab
# 4 — "Input devices"
	if [ -z "$PAVUCONTROL_APP" ]; then
		echo "Pavucontrol(-qt) is not available"
		return 2
	fi
	pkill pavucontrol || :
	if [ -f "${pavucontrol_config}" ]; then
		sed -i.pulsejoin.bak -e 's/sinkType=1/sinkType=0/g' -e 's/sinkType=2/sinkType=0/g' -e 's/sinkType=3/sinkType=0/g' "${pavucontrol_config}"
	fi
	"$PAVUCONTROL_APP" --tab "$1" &
}
export -f pavucontrol_tab

do_all_auto_application(){
	( set -e
	if [ "$1" = 'pulsejoin_choose_application' ]
		then
			run_command="$(yad --form --text "$(gt Enter command to run)" --entry)"
		else
			run_command="$*"
	fi
	pa_remove_devices
	if ! pa_make_devices; then
		yad_error "$(gt Error creating virtual devices in PulseAudio. Try restarting PulseJoin or choose \"Restart PulseAudio\" in the PulseJoin window.)"
		return 1
	fi
	env PULSE_SOURCE="${virtual_sink1}.monitor" PULSE_SINK="${pa_default_sink}" $run_command
	pa_remove_devices
	set +e )
}
export -f do_all_auto_application

do_mk_virt_source(){
	# TODO: yad_error code is doubled here and in do_all_auto_application, because I want pa_make_devices to not depend from GUI
	if ! pa_make_devices; then
		yad_error "$(gt 'Error creating virtual devices in PulseAudio. Try restarting PulseJoin or choose \"Restart PulseAudio\" in the PulseJoin window.')"
		return 1
	fi
	pavucontrol_tab 3
}
export -f do_mk_virt_source

do_rm_virt_source(){
	if ! pa_remove_devices; then
		yad_error "$(gt 'Error removing virtual devices in PulseAudio. Try to choose \"Restart PulseAudio\" in the PulseJoin window.')"
		return 1
	fi
	pavucontrol_tab 3
}
export -f do_rm_virt_source

do_pa_restart_yad_info(){
	yad_info "$(gt PulseAudio server has been restarted successfully. You may need to restart applications that input or output sound.)"
	# erase the list of previously loaded PulseAudio modules because all of them have been unloaded during PA restart
	[ -f "$pa_modules_list_file" ] && rm -f "$pa_modules_list_file"
}
export -f do_pa_restart_yad_info

do_pa_restart_yad_error(){
	yad_error "$(gt An error occured while restarting PulseAudio audio server. It is not working at the moment. Try restarting it again.)"
}
export -f do_pa_restart_yad_error

do_pa_restart(){
	counter_pa_kill='0'
	while true
	do
		# error if pulseaudio --kill returns not zero (error) immediately
		pgrep pulseaudio >/dev/null && \
		if ! pulseaudio --kill; then
			do_pa_restart_yad_error && return 1
		fi
		sleep 2
		counter_pa_kill="$((counter_pa_kill+1))"
		if pgrep pulseaudio >/dev/null
			then
				# we must break even do_pa_restart_yad_error returns not zero
				if [ "$counter_pa_kill" -gt 5 ]; then
					do_pa_restart_yad_error && return 1
				fi
			else
				break
		fi
	done
	
	# error if pulseaudio --start returns not zero (error) immediately
	if ! pulseaudio --start; then
		do_pa_restart_yad_error && return 1
	fi
	# PulseAudio is started with delay, check if it has really been started
	counter_pa_start='0'
	while [[ "$STOP" != 1 ]]
	do
		[ "$counter_pa_start" -gt 5 ] && do_pa_restart_yad_error && STOP=1 && break
		if [[ "$STOP" != 1 ]] && ! pulseaudio --check
			then
				# is has not started yet
				sleep 2 && counter_pa_start="$((counter_pa_start+1))"
			else
				# if pulseaudio has been started successfully
				# we need $STOP variable to exit from nested loop and its parent nested loop
				do_pa_restart_yad_info
				STOP=1
		fi
	done
}
export -f do_pa_restart

yad_main_dialog(){
# http://wiki.puppyrus.org/programming/yad
# http://smokey01.com/yad/
# https://www.thelinuxrain.com/articles/the-buttons-of-yad
# :LBL must have an empty value: https://groups.google.com/forum/#!topic/yad-common/qilGuiVZxFc
# "bash -c xxx" forks, "@bash -c xxx" blocks YAD UI while the fork is still working
# gtk3-icon-browser for icons
	yad --form \
		--width=500 \
		--center \
		--title="PulseJoin" \
		--text="$(gt 'The <b>PulseJoin</b> script allows to <b>record sound from microphone and speakers at the same time in programs which cannot record from multiple sources at the same time</b>.')" \
		--field="$(gt Please choose what to do.):LBL" "" \
		--field=" :LBL" "" \
		--field="$(gt 'It is possible <i>do everything automatically</i>')::LBL" "" \
		--field="$(gt '1) create a virtual microphone, which will combine sound from the default input device (microphone) and default output device (speakers) (sound which is sent to speakers/headphones)'):LBL" "" \
		--field="$(gt '2) open Audacity or another application to record sound from this newely created virtual device'):LBL" ""\
		--field="$(gt '3) remove this virtual device after closing the application'):LBL" ""\
		--field="$(gt Do everything automatically with running Audacity)!audacity:FBTN" "bash -x -c 'do_all_auto_application audacity'" \
		--field="$(gt Do everything automatically with running SimpleScreenRecorder)!simplescreenrecorder:FBTN" "bash -x -c 'do_all_auto_application simplescreenrecorder'" \
		--field="$(gt Do everything automatically with running another application)!edit-find:FBTN" "bash -x -c 'do_all_auto_application pulsejoin_choose_application'" \
		--field=" :LBL" "" \
		--field="$(gt You may perform each action seperately using buttons bellow)::LBL" "" \
		--field="$(gt Make a virtual microphone)!audio-input-microphone:FBTN" "bash -x -c do_mk_virt_source" \
		--field="$(gt Remove the virtual microphone)!media-playback-stop:FBTN" "bash -x -c do_rm_virt_source" \
		--field=" :LBL" "" \
		--field="$(gt All input and output devices may be reset by restarting the PulseAudio audio server)::LBL" "" \
		--field="$(gt Restart PulseAudio)!edit-clear-all:FBTN" "@bash -x -c do_pa_restart"
		
		# Unfortunately, SimpleScreenRecorder does not use envs PULSE_SOURCE or PULSE_SINK
		# https://github.com/MaartenBaert/ssr/issues/669
}

main(){
	if [ -x "$(command -v pavucontrol)" ]; then
		PAVUCONTROL_APP="$(command -v pavucontrol)"
		export PAVUCONTROL_APP
	elif [ -x "$(command -v pavucontrol-qt)" ]; then
		PAVUCONTROL_APP="$(command -v pavucontrol-qt)"
		export PAVUCONTROL_APP
	fi
	echo "Pavucontrol App: $PAVUCONTROL_APP"

	echo "Temp dir: $tmp_dir"
	if [ ! -w "$tmp_dir" ]; then
		yad_error "$(gt Temporary directory) $tmp_dir $(gt 'is not available for writing. The function \"Remove the virtual microphone\" will not work.')."
	fi

	pa_bug_workaround
	get_pa_default_devices
	yad_main_dialog
}
	
while [ -n "$1" ]
do
	case "$1" in
		-x|--debug ) set -x ;;
		-h|--help ) echo_help; exit;;
		--pa_make_devices ) pa_make_devices; exit;;
	esac
	shift
done

main
