#!/bin/bash
#
# start-qjackctl: Start the JACK Control Panel with suitable settings.
# 
# This is a big, ugly, brute force script.  It operates on the basis
# of seeing what actually works (i.e. starting jackd and watching the
# results) rather than what should work (i.e. looking at the soundcard
# parameters and going for something plausible).
#
# Michael McIntyre & Chris Cannam, Fervent Software Ltd 2004-2007

myname=`basename $0`

getopt -T 2>/dev/null
if [ "$?" -ne 4 ]; then
    echo "$myname: enhanced getopt required"
    exit 1
fi

usage() {
    echo
    echo "$myname: Start the JACK Control Panel with suitable settings."
    echo
    echo "Usage:"
    echo
    echo " -h, --help: print out usage and exit"
    echo " -k, --kill: if the panel is already running, terminate it instead of exiting"
    echo " -q, --quiet: no progress dialog and no error if JACK can't be started"
    echo " -L, --lowlatency: try for lower latencies than the conservative defaults"
    echo " -n, --dummy: do everything except for actually starting the control panel"
    echo " -Q <name>, --qjackctl <name>: run <name> instead of /usr/bin/qjackctl"
    echo " -j <name>, --jackd <name>: use JACK server <name> instead of jackd"
    echo " -b <name>, --backend <name>: use JACK backend <name> (e.g. alsa, freebob)"
    echo " -d <name>, --device <name>: use output device <name> (e.g. hw:0)"
    echo " -r <rate>, --samplerate <rate>: use samplerate <rate> Hz (e.g. 48000)"
    echo " -3, --3periods: try nperiods=3 first, before the usual default of 2"
    echo " -N <name>, --name <name>: use <name> as pretty name for pcm (requires -d)"
    echo
    echo "Exit status: 0 = JACK OK, control panel started"
    echo "             1 = JACK cannot start; audio unavailable"
    echo
    echo "If the JACK server, backend and device are unspecified, this program"
    echo "will attempt to find any settings that work."
    echo 
    exit 2
}

options=`getopt -n "$myname" -o hkqLn3Q:j:b:d:r:N: -l help,kill,quiet,dummy,3periods,lowlatency,qjackctl:,jackd:,backend:,device:,rate:,name: -- "$@"`
[ "$?" -ne 0 ] && exit 1

kill_existing=""
quiet=""
dummy=""
lowlatency=""
three=""
qjackctl=/usr/bin/qjackctl
jackd=""
backend=alsa
device=""
rate=""
prettyname=""

eval set -- "$options"

while true; do
    case "$1" in
	-h|--help)       usage;;
	-k|--kill)       kill_existing=1; shift;;
	-q|--quiet)      quiet=1; shift;;
	-n|--dummy)      dummy=1; shift;;
	-L|--lowlatency) lowlatency=1; shift;;
	-Q|--qjackctl)   qjackctl="$2"; shift 2;;
	-j|--jackd)      jackd="$2"; shift 2;;
	-b|--backend)    backend="$2"; shift 2;;
	-d|--device)     device="$2"; shift 2;;
	-r|--samplerate) rate="$2"; shift 2;;
	-3|--3periods)   three=1; shift;;
	-N|--name)       prettyname="$2"; shift 2;;
	--)              shift; break;;
	*) echo "Internal error in command line parser: arg \"$1\" read"; exit 1;;
    esac
done

if [ -n "$prettyname" -a -z "$device" ]; then
    echo "$myname: Can't specify --name without --device"
    exit 1
fi

for x in "$qjackctl" "$jackd"; do
    if [ -n "$x" -a ! -x "$x" ]; then
	echo "$myname: $x not found or not executable"
	exit 1
    fi
done

[ -z "$qjackctl" ] && qjackctl=qjackctl

clear_defunct_shm() {
    ipcs -mp | \
      fgrep " `id -un` " | \
      while read ID NAME CPID LPID; do 
        if [ ! -d "/proc/$CPID" ] && [ ! -d "/proc/$LPID" ]; then 
          ipcrm -m "$ID" 
        fi 
      done
}

terminate_defunct() {
    killall -9 jackstart jackd jackdmp $jackd 2>/dev/null
    clear_defunct_shm
}

qjackctl_pid=`pidof "$qjackctl"`
if [ -n "$qjackctl_pid" ]; then
    if [ -n "$kill_existing" ]; then
	terminate_defunct
	killall "$qjackctl"
    else
	echo "The JACK Control Panel is already running."
	[ -n "$quiet" ] || kdialog --title "Error" --error "The JACK Control Panel is already running."
	exit 1
    fi
fi

trap "rm -f /tmp/$$_jacktest /tmp/$$_db /tmp/$$_qjackctlrc /tmp/$$_qjackctlrc_preset /tmp/$$_method" 0

[ -n "$quiet" ] || dcop=`kdialog --title "JACK Control Panel" --icon "/usr/share/pixmaps/qjackctl.xpm" --progressbar "Starting JACK audio server..." 110`;

#[ -f /etc/sysconfig/fervent ] && . /etc/sysconfig/fervent

# First ensure jackd is ready and waiting (i.e. loaded from CD, if
# running from CD) -- otherwise our delay may not be adequate to get
# it started up the first time around
"$jackd" -d alsa -_NO_SUCH_OPTION_ >/dev/null 2>&1
terminate_defunct
sleep 1

# test CPU speed to adjust timeout
DELAY=1
GHZ=0
if [ -f "/proc/cpuinfo" ]; then
    mhzlist=`cat /proc/cpuinfo | grep MHz | awk '{print $4}' | cut -d \. -f 1`
    proc=0
    for m in $mhzlist; do
	v=""
	if [ -x "/usr/bin/cpufreq-info" ]; then
	    v=`/usr/bin/cpufreq-info -c$proc -f`
	fi
	if [ -n "$v" ]; then
	    v=$(($v / 1000000))
	else
	    v=$(($m / 1000))
        fi
        GHZ=$(($GHZ + $v))
    done
fi
if [ "$GHZ" == "0" ]; then
    DELAY=2
else
    DELAY=1
fi

echo "GHZ=$GHZ, DELAY=$DELAY"

last_method=1
[ -z "$jackd" ] && last_method=3

reset_method() {
    echo 0 > /tmp/$$_method
}
reset_method
[ -z "$jackd" ] && jack-select -j jackd

get_method() {
    if [ -z "$jackd" ]; then
	case "`cat /tmp/$$_method`" in 
	    0 ) echo "jackdmp -R" ;;
	    1 ) echo "jackd -R" ;;
	    2 ) echo "jackdmp" ;;
	    3 ) echo "jackd" ;;
	esac
    else
	case "`cat /tmp/$$_method`" in 
	    0 ) echo "$jackd -R" ;;
	    1 ) echo "$jackd" ;;
	esac
    fi
}

incr_method() {
    __method=`expr "\`cat /tmp/$$_method\`" + 1 `
    if [ "$__method" -gt "$last_method" ]; then return 1; fi
    echo $__method > /tmp/$$_method
    return 0
}

# Return codes:
RC_GOOD=0
RC_GENERAL_FAILURE=1
RC_RUN_FAILED=2
RC_BAD_RATE=3
RC_BAD_PCM=4
RC_BUSY=5
RC_NO_RT_MODE=6
RC_ADEQUATE=7

try_jack () # backend, category, number, subnumber, rate, frames, periods
{
    #!!! TODO: Update this for the errors from freebob

    _backend=$1; shift
    _category=$1; shift
    _number=$1; shift
    _subnumber=$1; shift
    _rate=$1; shift
    _frames=$1; shift
    _periods=$1; shift

    _method=`get_method`
    _command=${_method% *}

    _device=$_category:$_number
    [ -n "$_subnumber" ] && _device=$_device,$_subnumber

    echo "$myname: trying $_method -d$_backend -d$_device -r$_rate -p$_frames -n$_periods"
    if ( $_method -v -d"$_backend" -d"$_device" -r"$_rate" -p"$_frames" -n"$_periods" > /tmp/$$_jacktest 2>&1 & ); then
	( sleep $DELAY ; echo 'TIMED_OUT' >> /tmp/$$_jacktest ) &
	count=0
	sleep 0.1
	while ! egrep -q '(waiting|unregistering|JackThreadedDriver::Start|unknown driver|relocation error|TIMED_OUT)' /tmp/$$_jacktest ; do
	    sleep 0.1
	    count=$((count+1))
	    if [ "$count" -eq "$((DELAY*10))" ]; then
		echo "$myname: Normal timeout not detected -- too much output?"
		terminate_defunct
		return $RC_GENERAL_FAILURE
	    fi
        done

	if grep -q 'does not match requested rate' /tmp/$$_jacktest; then
	    echo "$myname: Sample rate $_rate not supported natively by card"
	    terminate_defunct
	    return $RC_BAD_RATE
	fi
	if grep -q 'Assertion .* failed' /tmp/$$_jacktest; then
	    echo "$myname: Uh-oh: $_category:$_number not a PCM device, or ALSA install problem?"
	    terminate_defunct
	    return $RC_BAD_PCM
	fi
	if grep -q 'cannot configure capture channel' /tmp/$$_jacktest; then
	    echo "$myname: $_category:$_number apparently not capable of duplex operation"
	    terminate_defunct
	    return $RC_BAD_PCM
	fi
	if grep -q 'already in use' /tmp/$$_jacktest; then
	    echo "$myname: jackd or another ALSA client already running?"
	    terminate_defunct
	    return $RC_BUSY
	fi
	if grep -q 'unknown driver' /tmp/$$_jacktest; then
	    echo "$myname: No such driver!"
	    terminate_defunct
	    return $RC_RUN_FAILED
	fi
	if grep -q 'relocation error' /tmp/$$_jacktest; then
	    echo "$myname: Run-time linkage error!"
	    terminate_defunct
	    return $RC_RUN_FAILED
	fi
	if grep -q 'cannot use real-time scheduling' /tmp/$$_jacktest || \
	   grep -q 'a suitable kernel would have printed' /tmp/$$_jacktest; then
	    echo "$myname: real-time mode not supported"
	    terminate_defunct
	    return $RC_NO_RT_MODE
	fi
	if pidof "$_command" ; then
	    count=0
	    while ! fgrep -q TIMED_OUT /tmp/$$_jacktest ; do
		sleep 0.1
		count=$((count+1))
		[ "$count" -eq 10 ] && break;
	    done
	    if [ "`grep 'xrun_recovery' /tmp/$$_jacktest | wc -l`" -gt 1 ]; then
		echo "$myname: Generating xruns already -- bad sign"
		terminate_defunct
		return $RC_GENERAL_FAILURE
	    fi
	    # we know it works now, but we'll restart it with qjackctl later
	    echo "$myname: looks good"
	    if grep -q 'trying .* instead' /tmp/$$_jacktest ||
	       grep -q 'will try a ' /tmp/$$_jacktest ||
	       grep -q 'final selected sample format .* 16bit' /tmp/$$_jacktest; then
		echo "$myname: (but imperfect sample width)"
		terminate_defunct
		return $RC_ADEQUATE
	    elif [ "$CATEGORY" = "plughw" ]; then
		echo "$myname: (but plughw not ideal)"
		terminate_defunct
		return $RC_ADEQUATE
	    else
		terminate_defunct
		return $RC_GOOD
	    fi
	else
	    echo "$myname: JACK exited, some problem here"
	    terminate_defunct
	    return $RC_GENERAL_FAILURE
	fi
    else
	echo "$myname: failed to run $_command executable"
	terminate_defunct
    	return $RC_RUN_FAILED
    fi
}

echo "$myname: Probing for working JACK settings..."

if [ -n "$device" ]; then
    devices=$device
else
    devices=` \
    for card in /proc/asound/card* ; do 
        if test -f $card/pcm0p/info -a -f $card/pcm0c/info ; then 
	    n=${card#*card}
#	    echo -n "hw:$n plughw:$n "
	    echo -n "hw:$n "
        fi 
    done `
fi

echo "$myname: Devices are:"
for d in $devices; do
    n=${d#*w:}
    echo "$myname: $d: `cat /proc/asound/card$n/id`"
done

ANY_DEVICE=""
BEST_DEVICE=""

cat /dev/null > /tmp/$$_db

if [ -n "$rate" ]; then
    for known in 192000 176400 96000 88200 48000 44100 24000 22050 12000 11025; do
	if [ "$rate" -gt "$known" ]; then
	    rates="$rate $known"
	    break
	elif [ "$rate" -eq "$known" ]; then
	    rates="$rate"
	    break
	fi
    done
else
    rates="48000 44100"
fi

if [ -n "$lowlatency" ] ; then
    frames="256 512 1024"
else
    frames="1024 2048 512"
fi

total=`echo $devices | awk '{ print NF; }'`
total=$(($total * 10))
dcount=0
icount=0
echo "total $total"
[ -n "$quiet" ] || dcop "$dcop" setProgress 11;

HW_GOOD=""

for DEVICE in $devices; do

    GOOD=""
    BAD=""
    IMPERFECT=""
    MULTI=""
    TRY_ERR=0
    TRY_RATES="$rates"
    TRY_FRAMES="$frames"
    CATEGORY=${DEVICE%:*}
    NUMBER=${DEVICE#*:}
    SUBNUMBER=${NUMBER#*,}
    [ "$SUBNUMBER" = "$NUMBER" ] && SUBNUMBER=""
    NUMBER=${NUMBER%,*}

    reset_method

    if [ "$CATEGORY" = "plughw" -a -n "$HW_GOOD" ]; then
	dcount=$((dcount+10))
	icount=$dcount
	progress=$(($icount*100/$total+10))
	echo "$icount $progress"
	[ -n "$quiet" ] || dcop "$dcop" setProgress $progress;
	continue
    fi

    for FRAMES in $TRY_FRAMES; do
	TRY_PERIODS=2
	if [ -n "$three" ]; then TRY_PERIODS="3 2"
	elif [ "$backend" = "freebob" ]; then TRY_PERIODS="3 2 4"
	elif [ "$FRAMES" -eq "512" ]; then TRY_PERIODS="2 3 4";
	elif [ "$CATEGORY" = "plughw" ]; then TRY_PERIODS="4 3 2";
	fi
	for PERIODS in $TRY_PERIODS; do
	    for RATE in $TRY_RATES; do
		while true; do
		    if [ "$backend" = "freebob" ]; then
		    	echo br short | firecontrol "$NUMBER" >/dev/null 2>&1
	            fi
		    if try_jack "$backend" "$CATEGORY" "$NUMBER" "$SUBNUMBER" "$RATE" "$FRAMES" "$PERIODS"; then
			GOOD=true
			break
		    else
			RC=$?
			if [ $RC = $RC_ADEQUATE ]; then # not ideal sample width
			    GOOD=true
			    IMPERFECT=true
			    break
			elif [ $RC = $RC_BAD_RATE ]; then # rate no good
			    RATES=`echo $RATES | sed "s/$RATE//"`
			    echo "$myname: Rates now $RATES"
			    [ -n "$RATES" ] || BAD=true
			elif [ $RC = $RC_BAD_PCM ]; then # no such PCM device
			    BAD=true
			    echo "$myname: No PCM"
			elif [ $RC = $RC_RUN_FAILED ]; then
			    echo "$myname: Failed to run JACK!"
			    incr_method && continue
			    BAD=true
			elif [ $RC = $RC_NO_RT_MODE -o $RC = $RC_GENERAL_FAILURE ]; then
			    incr_method && continue
			    BAD=true
			else
			    echo "$myname: Some other error:"
#			grep -i error /tmp/$$_jacktest
			    tail /tmp/$$_jacktest
			fi
		    fi # ; test -n "$GOOD" -o -n "$BAD" && break
		    break
		done ; test -n "$GOOD" -o -n "$BAD" && break
	    done ; test -n "$GOOD" -o -n "$BAD" && break
	    icount=$(($icount+1))
	    progress=$(($icount*100/$total+10))
echo "$icount $progress"
	    [ -n "$quiet" ] || dcop "$dcop" setProgress $progress;
	done ; test -n "$GOOD" -o -n "$BAD" && break
    done

    if [ -n "$GOOD" ]; then
	if [ -z "$ANY_DEVICE" ]; then
	    ANY_DEVICE=$DEVICE
	fi
	if expr `echo /proc/asound/card$NUMBER/pcm*p | wc -l` '>' 1 >/dev/null; then
	    MULTI=true
	elif grep -qi multi /proc/asound/card$NUMBER/pcm0p/info; then
	    MULTI=true
	fi
	if [ -z "$BEST_DEVICE" ]; then
	    if [ -n "$MULTI" -a -z "$IMPERFECT" ]; then
		BEST_DEVICE=$DEVICE
	    fi
	fi
	if [ -z "$HW_GOOD" ]; then # haven't already written out a good hw:n
	    echo "$DEVICE $RATE $FRAMES $PERIODS" >> /tmp/$$_db
	    if [ "$CATEGORY" = "hw" ]; then
		HW_GOOD=1
	    fi
        fi
    fi

    dcount=$((dcount+10))
    icount=$dcount
    progress=$(($icount*100/$total+10))
    echo "$icount $progress"
    [ -n "$quiet" ] || dcop "$dcop" setProgress $progress;
done

[ -n "$quiet" ] || dcop "$dcop" setProgress 110;

if [ -z "$ANY_DEVICE" ]; then
    [ -n "$quiet" ] || dcop "$dcop" close;
    cat << EOF
    Our apologies.  JACK cannot be started automatically on this hardware.
EOF
    if [ -z "$quiet" ]; then
        kdialog --title Sorry --error " Failed to initialise audio system. \nNo audio hardware capable of running the JACK audio system was found. \nEither no compatible sound hardware is installed, or autodetection or driver initialisation failed. \nPlease consult the Studio to Go! documentation for information about supported audio hardware, or contact Fervent Software for support."
    else
    	kdialog --title Sorry --error " Failed to start the JACK audio server. \nEither the soundcard autodetection or driver initialisation failed, or the selected sound hardware is not compatible. \nPlease consult the Studio to Go! documentation for information about supported audio hardware, or contact Fervent Software for support."
    fi
    exit 1
fi

if [ -z "$BEST_DEVICE" ]; then
    BEST_DEVICE=$ANY_DEVICE
fi

echo "We like $BEST_DEVICE best"

# Prime the presets file and the non-presets file which is to
# contain the remainder of the existing qjackctl setup
if [ -f $HOME/.qt/qjackctlrc ]; then
    if grep -q '\[Presets\]' $HOME/.qt/qjackctlrc; then 
        # Presets in existing file: copy to new file, excluding autodetected ones
	echo '[Presets]' > /tmp/$$_qjackctlrc_preset
	grep '^Preset' $HOME/.qt/qjackctlrc | grep -v Autodetect >> /tmp/$$_qjackctlrc_preset
    else
        # No presets in existing file, prime the new file only
	echo '[Presets]' > /tmp/$$_qjackctlrc_preset
    fi
    echo 'DefPreset=(default)' >> /tmp/$$_qjackctlrc_preset
    grep -v Autodetect $HOME/.qt/qjackctlrc | fgrep -v '[Aliases' | grep -v Preset | sed -n -e '/\[Splitter/q' -e p  > /tmp/$$_qjackctlrc
    if ! grep -q '\[Settings\]' /tmp/$$_qjackctlrc; then
	echo '[Settings]' >> /tmp/$$_qjackctlrc
    fi
else
    echo '[Settings]' > /tmp/$$_qjackctlrc
    echo '[Presets]' > /tmp/$$_qjackctlrc_preset
    echo 'DefPreset=(default)' >> /tmp/$$_qjackctlrc_preset
fi

BEST_NUMBER=${BEST_DEVICE#*:}; BEST_NUMBER=${BEST_NUMBER%,*}
BEST_NAME="`cat /proc/asound/card$BEST_NUMBER/id`_Autodetect_$BEST_NUMBER"

cat /tmp/$$_db
COMMAND=`get_method | sed 's/ .*$//'`

NOMEMLOCK=false
if [ "`jack-select -t`" = "jackdmp" ] ; then NOMEMLOCK=true; fi

CHANNELS=0
if [ "$backend" = "freebob" ]; then CHANNELS=64; fi

cat /tmp/$$_db | \
    ( while read DEVICE RATE FRAMES PERIODS; do
	NUMBER=${DEVICE#*:}; NUMBER=${NUMBER%,*}
	NAME="`cat /proc/asound/card$NUMBER/id`_Autodetect_$NUMBER"
	METHOD=`cat /tmp/$$_method`
	if [ "$METHOD" -eq "0" ]; then REALTIME=true;
	else REALTIME=false; fi
	cat >> /tmp/$$_qjackctlrc <<EOF
$NAME/Audio=0
$NAME/Chan=0
$NAME/Dither=0
$NAME/Driver=$backend
$NAME/Frames=$FRAMES
$NAME/HWMeter=false
$NAME/HWMon=false
$NAME/IgnoreHW=false
$NAME/InChannels=$CHANNELS
$NAME/InDevice=\0
$NAME/Interface=$DEVICE
$NAME/Monitor=false
$NAME/NoMemLock=$NOMEMLOCK
$NAME/OutChannels=$CHANNELS
$NAME/OutDevice=\0
$NAME/Periods=$PERIODS
$NAME/PortMax=128
$NAME/Priority=0
$NAME/Realtime=$REALTIME
$NAME/SampleRate=$RATE
$NAME/Server=$COMMAND
$NAME/Shorts=false
$NAME/SoftMode=false
$NAME/StartDelay=2
$NAME/Timeout=2000
$NAME/UnlockMem=true
$NAME/Verbose=false
EOF
	done )

if [ -n "$prettyname" ]; then
    # Requires that the stub qjackctlrc has aliases enabled but
    # none set; we don't deal with that part here.  We shouldn't
    # have prettyname set if we have more than one device
    cat /tmp/$$_db | \
    (   read DEVICE RATE FRAMES PERIODS
	NUMBER=${DEVICE#*:}; NUMBER=${NUMBER%,*}
	NAME="`cat /proc/asound/card$NUMBER/id`_Autodetect_$NUMBER"
	cat >> /tmp/$$_qjackctlrc <<EOF

[Aliases]
$NAME/Jack/Inputs/Client1/Alias=$prettyname (${backend}_pcm)
$NAME/Jack/Inputs/Client1/Name=${backend}_pcm
$NAME/Jack/Outputs/Client1/Alias=$prettyname (${backend}_pcm)
$NAME/Jack/Outputs/Client1/Name=${backend}_pcm
EOF
    )
fi

if grep -q Autodetect /tmp/$$_qjackctlrc_preset; then
    cat /tmp/$$_qjackctlrc_preset >> /tmp/$$_qjackctlrc
else
    cat /tmp/$$_qjackctlrc_preset | \
	sed "s/DefPreset=.*/DefPreset=$BEST_NAME/" >> /tmp/$$_qjackctlrc
    NPRESETS=`grep Preset[0-9] /tmp/$$_qjackctlrc_preset | \
	tail -1 | \
	sed 's/^Preset//' | \
	sed 's/=.*//'`
    cat /tmp/$$_db | \
	( while read DEVICE RATE FRAMES PERIODS; do
	    NUMBER=${DEVICE#*:}; NUMBER=${NUMBER%,*}
	    NAME="`cat /proc/asound/card$NUMBER/id`_Autodetect_$NUMBER"
	    NPRESETS=`expr $NPRESETS + 1`
	    echo "Preset$NPRESETS=$NAME" >> /tmp/$$_qjackctlrc
	    done )
fi

[ -n "$quiet" ] || dcop "$dcop" close;

if [ "$backend" = "freebob" ]; then
    echo br short | firecontrol "$NUMBER"
fi

test -f $HOME/.qt/qjackctlrc && mv $HOME/.qt/qjackctlrc $HOME/.qt/qjackctlrc.bak
mv /tmp/$$_qjackctlrc $HOME/.qt/qjackctlrc
( sleep $DELAY; rm -f /tmp/$$_jacktest ) &
rm -f /tmp/$$_qjackctlrc_preset
rm -f /tmp/$$_db
rm -f /tmp/$$_method
[ -z "$jackd" ] && jack-select -j "`basename $COMMAND`"

echo Updated qjackctlrc, running qjackctl...
[ -z "$dummy" ] && ( setsid "$qjackctl" -s & )

exit 0
