#!/bin/sh
#
# ALSA initscript
#

set -e

# Be a no-op unless alsa-base is installed
[ -d /lib/alsa ] || exit 0

PATH=/usr/local/sbin:/sbin:/usr/sbin:/usr/local/bin:/bin:/usr/bin

# Default values of variables in /etc/default/alsa
alsactl_store_on_shutdown="always autosave"
runlevels_save='[2-5]'
force_unload_modules_before_suspend=""

# Other settings
MYNAME=/etc/init.d/alsa

[ -f /etc/default/alsa ] && . /etc/default/alsa

bugout()
{
	echo "${MYNAME}: Programming error" >&2
	exit 123
}

# $* MESSAGE
# Doesn't print a newline!
warn()
{
	echo -n "${MYNAME}: Warning: $* " >&2
}

# $* MESSAGE
warn_nl()
{
	echo "${MYNAME}: Warning: $* " >&2
}

# $1 PROGRAM
executable() {
	# If which is not available then we must be running before
	# /usr is mounted, on a system that has which in /usr/bin/.
	# Conclude that $1 is not executable
	[ -x /usr/bin/which ] || [ -x /bin/which ] || return 1
	which "$1" >/dev/null 2>&1
}

# $1 PROGRAM
executable_or_warn()
{
	executable "$1" || { warn "No $1 program available." ; return 1 ; }
}

# $1 CARD | "all"
restore_levels()
{
	[ -f /var/lib/alsa/asound.state ] || return 1
	executable_or_warn alsactl || return 1
	CARD="$1"
	[ "$1" = all ] && CARD=""
	# Assume that if alsactl prints a message on stderr
	# then it failed somehow.  This works around the fact
	# that alsactl doesn't return nonzero status when it
	# can't restore settings for the card
	if MSG="$(alsactl restore $CARD 2>&1 >/dev/null)" && [ ! "$MSG" ] ; then
		return 0
	else
		warn "'alsactl restore${CARD:+ $CARD}' failed with error message '$MSG'."
		return 1
	fi
}

# $1 CARD | "all"
store_levels()
{
	executable_or_warn alsactl || return 1
	CARD="$1"
	[ "$1" = all ] && CARD=""
	if MSG="$(alsactl store $CARD 2>&1)" ; then
		sleep 1
		return 0
	else
		warn "'alsactl store${CARD:+ $CARD}' failed with error message '$MSG'."
		return 1
	fi
}

# $1 CARD | "all" | "all-autosaving-once"
# In case of "all-autosaving-once" check autosave-once flag
store_levels_if_allowed()
{
	SMLIA_RETURNSTATUS=0
	if \
		[ "$alsactl_store_on_shutdown" = "always autosave" ] \
		|| { [ "$1" = "all-autosaving-once" ] && [ -f /var/lib/alsa/autosave-once ] ; }
	then
		if 
			runlevel | grep -E "^$runlevels_save " > /dev/null 2>&1 \
			|| runlevel | grep -E " $runlevels_save\$" > /dev/null 2>&1
		then
			case "$1" in
			all-autosaving-once)
				if store_levels all ; then
					[ -f /var/lib/alsa/autosave-once ] && rm -f /var/lib/alsa/autosave-once
				else
					SMLIA_RETURNSTATUS=1
				fi
				;;
			*)
				store_levels "$1" || SMLIA_RETURNSTATUS=1
				;;
			esac
		fi
	fi
	return $SMLIA_RETURNSTATUS
}

echo_card_indices()
{
	if [ -f /proc/asound/cards ] ; then
		sed -n -e's/^\([0-7]\)[[:space:]].*/\1/p' /proc/asound/cards
	fi
}

filter_amixer_output()
{
	sed \
		-e '/Unable to find simple control/d' \
		-e '/Unknown playback setup/d' \
		-e '/^$/d'
}

# The following functions try to set many controls.
# No card has all the controls and so some of the attempts are bound to fail.
# Because of this, the functions can't return useful status values.

# $1 CONTROL
# $2 LEVEL
# $CARDOPT
unmute_and_set_level()
{
	{ [ "$2" ] && [ "$CARDOPT" ] ; } || bugout
	amixer $CARDOPT -q set "$1" "$2" unmute 2>&1 | filter_amixer_output || :
	return 0
}

# $1 CONTROL
# $CARDOPT
mute_and_zero_level()
{
	{ [ "$1" ] && [ "$CARDOPT" ] ; } || bugout
	amixer $CARDOPT -q set "$1" "0%" mute 2>&1 | filter_amixer_output || :
	return 0
}

# $1 CONTROL
# $2 "on" | "off"
# $CARDOPT
switch_control()
{
	{ [ "$2" ] && [ "$CARDOPT" ] ; } || bugout
	amixer $CARDOPT -q set "$1" "$2" 2>&1 | filter_amixer_output || :
	return 0
}

# $1 CARD
sanify_levels_on_card()
{
	# This code is based on code from the alsaconf program
	executable_or_warn amixer || return 1
	CARDOPT="-c $1"
	unmute_and_set_level "Master" "75%"
	unmute_and_set_level "PCM" "90%"
	unmute_and_set_level "Synth" "90%"
	unmute_and_set_level "CD" "90%"
	# Mute mic
	mute_and_zero_level "Mic"
	# ESS 1969 chipset has 2 PCM channels
	unmute_and_set_level "PCM,1" "90%"
	# Trident/YMFPCI/emu10k1
	unmute_and_set_level "Wave" "100%"
	unmute_and_set_level "Music" "100%"
	unmute_and_set_level "AC97" "100%"
	# CS4237B chipset
	unmute_and_set_level "Master Digital" "75%"
	# Envy24 chips with analog outs
	unmute_and_set_level "DAC" "90%"
	unmute_and_set_level "DAC,0" "90%"
	unmute_and_set_level "DAC,1" "90%"
	# Some notebooks use headphone instead of master
	unmute_and_set_level Headphone "75%"
	unmute_and_set_level Playback "100%"
	# Digital switches
	switch_control "Audigy Analog/Digital Output Jack" on
	# ... and "SB Live Analog/Digital Output Jack" ?
	return 0
}

# $1 CARD | "all"
sanify_levels()
{
	TTSDML_RETURNSTATUS=0
	case "$1" in
		all)
			for CARD in $(echo_card_indices) ; do
				sanify_levels_on_card "$CARD" || TTSDML_RETURNSTATUS=1
			done
			;;
		*)
			sanify_levels_on_card "$1" || TTSDML_RETURNSTATUS=1
			;;
	esac
	return $TTSDML_RETURNSTATUS
}

# $1 CARD
mute_and_zero_levels_on_card()
{
	executable_or_warn amixer || return 1
	CARDOPT="-c $1"
	for CTL in \
		Master \
		PCM \
		Synth \
		CD \
		Line \
		Mic \
		"PCM,1" \
		Wave \
		Music \
		AC97 \
		"Master Digital" \
		DAC \
		"DAC,0" \
		"DAC,1" \
		Headphone \
		Playback
	do
		mute_and_zero_level "$CTL"
	done
	for CTL in \
		"Audigy Analog/Digital Output Jack" \
		"SB Live Analog/Digital Output Jack"
	do
		switch_control "$CTL" off
	done
	return 0
}

# $1 CARD | "all"
mute_and_zero_levels()
{
	TTZML_RETURNSTATUS=0
	case "$1" in
	all)
		for CARD in $(echo_card_indices) ; do
			mute_and_zero_levels_on_card "$CARD" || TTZML_RETURNSTATUS=1
		done
		;;
	*)
		mute_and_zero_levels_on_card "$1" || TTZML_RETURNSTATUS=1
		;;
	esac
	return $TTZML_RETURNSTATUS
}


# $1 [CARD | "" | "all"]
start()
{
	START_RETURNSTATUS=0
	case "$1" in
	""|all)
		echo -n "Setting up ALSA..."
		[ -d /proc/asound ] || { echo "done (not loaded)." ; return $START_RETURNSTATUS ; }
		if ! restore_levels all >/dev/null 2>&1 ; then
			sanify_levels all || START_RETURNSTATUS=1
			restore_levels all || :
		fi
		;;
	*)
		echo -n "Setting up ALSA card ${1}..."
		[ -d "/proc/asound/card$1" ] || [ -d "/proc/asound/$1" ] || { echo "done (not loaded)." ; return $START_RETURNSTATUS ; }
		if ! restore_levels "$1" ; then
			sanify_levels "$1" || START_RETURNSTATUS=1
		fi
		;;
	esac
	case "$START_RETURNSTATUS" in
		0) echo "done." ;;
		*) echo "failed." ;;
	esac
	return $START_RETURNSTATUS
}

# $1 [CARD | "" | "all" ]
stop()
{
	STOP_RETURNSTATUS=0
	case "$1" in
	"")
		echo -n "Shutting down ALSA..."
		[ -d /proc/asound ] || { echo "done (not loaded)." ; return $STOP_RETURNSTATUS ; }
		store_levels_if_allowed "all-autosaving-once" || STOP_RETURNSTATUS=1
		mute_and_zero_levels all || STOP_RETURNSTATUS=1
		;;
	all)
		echo -n "Shutting down ALSA..."
		[ -d /proc/asound ] || { echo "done (not loaded)." ; return $STOP_RETURNSTATUS ; }
		store_levels_if_allowed all || STOP_RETURNSTATUS=1
		mute_and_zero_levels all || STOP_RETURNSTATUS=1
		;;
	*)
		echo -n "Shutting down ALSA card ${1}..."
		[ -d "/proc/asound/card$1" ] || [ -d "/proc/asound/$1" ] || { echo "done (not loaded)." ; return $STOP_RETURNSTATUS ; }
		store_levels_if_allowed "$1" || STOP_RETURNSTATUS=1
		mute_and_zero_levels "$1" || STOP_RETURNSTATUS=1
		;;
	esac
	case "$STOP_RETURNSTATUS" in
		0) echo "done." ;;
		*) echo "failed." ;;
	esac
	return $STOP_RETURNSTATUS
}

echo_procs_using_sound()
{
	echo $( \
		lsof +D /dev -F rt \
		| awk '/^p/ {pid=$1} /^t/ {type=$1} /^r0x(74|e)..$/ && type == "tCHR" {print pid}' \
		| cut -c 2- \
		| uniq \
	)
}

# $* [PID]...
echo_with_command_names()
{
	[ "$1" ] || return 0
	echo $( \
		ps --no-headers -o "%p %c" "$@" \
		| sed -e 's/\([0-9][0-9]*\) \(.*\)/\1(\2)/' \
	)
}

kill_procs_using_sound()
{
	procs_using_sound="$(echo_procs_using_sound)"
	if [ "$procs_using_sound" ] ; then
		echo -n "Terminating processes:"
		for attempt in 1 2 3 4 ; do
			echo -n " ${procs_using_sound}"
			kill $procs_using_sound || :
			sleep 1
			procs_using_sound="$(echo_procs_using_sound)"
			[ "$procs_using_sound" ] || break
		done
		# Either no more procs using sound or attempts ran out
		if [ "$procs_using_sound" ] ; then
			echo -n " (with SIGKILL:) ${procs_using_sound}"
			kill -9 $procs_using_sound || :
			sleep 1
		fi
		procs_using_sound="$(echo_procs_using_sound)"
		if [ "$procs_using_sound" ] ; then
			echo " (failed: processes still using sound devices: $(echo_with_command_names $procs_using_sound))."
			return 1
		fi
		echo "."
	fi
	return 0
}

# $* MODULE-NAME [MODULE-NAME]... | "all"
unload_modules()
{
	procs_using_sound="$(echo_procs_using_sound)"
	if [ "$procs_using_sound" ] ; then
		warn_nl "Processes using sound devices: $(echo_with_command_names $procs_using_sound)."
	fi
	if [ -d /var/run/alsa ] ; then
		:> /var/run/alsa/modules-removed
	else
		warn_nl "Directory /var/run/alsa is not present."
	fi
	echo -n "Unloading ALSA sound driver modules:"
	[ -d /proc/asound ] || { echo " (none loaded)." ; return 0 ; }
	echo_snd_modules_loaded()
	{
		lsmod \
		| sed -n -e 's/^\(snd[-_][^[:space:]]*\)[[:space:]].*/\1/p' \
		| sed -e 's/_/-/g'
	}
	for FSMBS in $* ; do
		MODULES_TO_REMOVE=""
		SND_MODULES_LOADED="$(echo_snd_modules_loaded)"
		case "$FSMBS" in
			all) MODULES_TO_REMOVE="$SND_MODULES_LOADED" ;;
			snd_*|snd-*)
				FSMBS="$(echo "$FSMBS" | sed -e 's/_/-/g')"
				for M in $SND_MODULES_LOADED ; do
					if [ "$FSMBS" = "$M" ] ; then
						MODULES_TO_REMOVE="$FSMBS"
						break
					fi
				done
				;;
		esac
		[ "$MODULES_TO_REMOVE" ] || continue
		if [ -d /var/run/alsa ] ; then
			echo "$MODULES_TO_REMOVE" >> /var/run/alsa/modules-removed
		fi
		for M in $MODULES_TO_REMOVE ; do
			echo -n " ${M}"
			modprobe -r "$M" >/dev/null 2>&1 || :
		done
	done
	if [ -f /var/run/alsa/modules-removed ] ; then
		MODULES_STILL_LOADED="$(echo_snd_modules_loaded | grep -F -f /var/run/alsa/modules-removed)"
		MODULES_STILL_LOADED="$(echo $MODULES_STILL_LOADED)"
	else
		MODULES_STILL_LOADED=""
	fi
	if [ "$MODULES_STILL_LOADED" ] ; then
		echo " (failed: modules still loaded: ${MODULES_STILL_LOADED})."
		return 1
	else
		echo "."
		return 0
	fi
}

# $* MODULE-NAME [MODULE-NAME]... | "all"
force_unload_modules()
{
	kill_procs_using_sound || :
	unload_modules "$@" || return 1
	return 0
}

load_unloaded_modules()
{
	LUM_RETURNSTATUS=0
	MODULES_TO_LOAD=""
	[ -d /var/run/alsa ] || warn "Directory /var/run/alsa is not present."
	echo -n "Loading ALSA sound driver modules:"
	[ -f /var/run/alsa/modules-removed ] && MODULES_TO_LOAD="$(echo $(cat /var/run/alsa/modules-removed))"
	[ "$MODULES_TO_LOAD" ] || { echo " (none to reload)." ; return $LUM_RETURNSTATUS ; }
	echo -n " $MODULES_TO_LOAD"
	for MDL in $MODULES_TO_LOAD ; do
		modprobe $MDL || LUM_RETURNSTATUS=1
	done
	case "$LUM_RETURNSTATUS" in
		0) echo "." ;;
		*) echo " (failed)." ;;
	esac
	return $LUM_RETURNSTATUS
}

case "$1" in
	start)
		start $2 || exit $?
		;;
	stop)
		stop $2 || exit $?
		;;
	restart)
		EXITSTATUS=0
		stop $2 || EXITSTATUS=1
		start $2 || EXITSTATUS=1
		exit $EXITSTATUS
		;;
	unload)
		unload_modules all || exit $?
		;;
	reload)
		EXITSTATUS=0
		unload_modules all || EXITSTATUS=1
		load_unloaded_modules || EXITSTATUS=1
		exit $EXITSTATUS
		;;
	force-unload)
		force_unload_modules all || exit $?
		;;
	force-reload|force-restart)
		# force-restart alias will be removed after sarge
		EXITSTATUS=0
		force_unload_modules all || EXITSTATUS=1
		load_unloaded_modules || EXITSTATUS=1
		exit $EXITSTATUS
		;;
	suspend)
		case "$force_unload_modules_before_suspend" in
			""|false) : ;;
			true) force_unload_modules all || exit $? ;;
			*) force_unload_modules $force_unload_modules_before_suspend || exit $? ;;
		esac
		;;
	resume)
		case "$force_unload_modules_before_suspend" in
			""|false) : ;;
			*) load_unloaded_modules || exit $? ;;
		esac
		;;
	*)
		echo "Usage: /etc/init.d/alsa {start [CARD]|stop [CARD]|restart [CARD]|unload|reload|force-unload|force-reload|suspend|resume}" >&2
		exit 3
		;;
esac
