#!/bin/sh
#
# ALSA initscript
#

set -e

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

. /lib/lsb/init-functions

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

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

# $* message
warn()
{
	log_warning_msg "${MYNAME}: Warning: $*"
}

# $1 program
# This function should only be used to test for the executability of
# programs that are no-ops when executed with the "--help" option.
# See #218530 for a discussion of this issue.
executable()
{
	eval $1 --help > /dev/null 2>&1 || case "$?" in (126|127) return 1 ;; esac
	return 0
}

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


# $1 card
restore_mixer_levels()
{
	[ "$1" ] || bug
	[ -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
store_mixer_levels()
{
	[ "$1" ] || bug
	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
store_mixer_levels_if_allowed()
{
	[ "$1" ] || bug
	RETURNSTATUS=0
	if \
		[ "$alsactl_store_on_shutdown" = "always autosave" ] \
		|| { [ -f /var/lib/alsa/autosave-once ] && [ "$1" = "all-default" ] ; }
	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-default)
				if store_mixer_levels all ; then
					[ -f /var/lib/alsa/autosave-once ] && rm -f /var/lib/alsa/autosave-once
				else
					RETURNSTATUS=1
				fi
				;;
			*)
				store_mixer_levels "$1" || RETURNSTATUS=1
				;;
			esac
		fi
	fi
	return $RETURNSTATUS
}

echo_card_indices()
{
	if [ -f /proc/asound/cards ] ; then
		sed -e 's/[[:space:]].*$//' -e '/^$/d' /proc/asound/cards
	fi
}

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

# The following "try_to_do_stuff_with_amixer" 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 name
# $2 level
# $CARDOPT
try_to_set_mixer_on()
{
	{ [ "$2" ] && [ "$CARDOPT" ] && executable amixer ; } || bug
	amixer $CARDOPT -q set "$1" "$2" unmute 2>&1 | filter_amixer_output || :
	return 0
}

# $1 control name
# $CARDOPT
try_to_set_mixer_off()
{
	{ [ "$1" ] && [ "$CARDOPT" ] && executable amixer ; } || bug
	amixer $CARDOPT -q set "$1" "0%" mute 2>&1 | filter_amixer_output || :
	return 0
}

# $1 card
try_to_set_default_mixer_levels_on_card()
{
	# This code is based on code from the alsaconf program
	[ "$1" ] || bug
	executable_or_warn amixer || return 1
	CARDOPT="-c $1"
	try_to_set_mixer_on "Master" "70%"
	try_to_set_mixer_on "PCM" "70%"
	try_to_set_mixer_on "Synth" "70%"
	try_to_set_mixer_on "CD" "70%"
	# Mute mic
	try_to_set_mixer_off "Mic"
	# ESS 1969 chipset has 2 PCM channels
	try_to_set_mixer_on "PCM,1" "90%"
	# Trident/YMFPCI/emu10k1
	try_to_set_mixer_on "Wave" "100%"
	try_to_set_mixer_on "Music" "100%"
	try_to_set_mixer_on "AC97" "100%"
	# CS4237B chipset
	try_to_set_mixer_on "Master Digital" "75%"
	# Envy24 chips with analog outs
	try_to_set_mixer_on "DAC" "90%"
	try_to_set_mixer_on "DAC,0" "90%"
	try_to_set_mixer_on "DAC,1" "90%"
	# Some notebooks use headphone instead of master
	try_to_set_mixer_on Headphone "75%"
	try_to_set_mixer_on Playback "100%"
	# Turn off digital switches
	try_to_set_mixer_off "SB Live Analog/Digital Output Jack"
	try_to_set_mixer_off "Audigy Analog/Digital Output Jack"
	return 0
}

# $1 card
try_to_set_default_mixer_levels()
{
	[ "$1" ] || bug
	RETURNSTATUS=0
	case "$1" in
		all)
			for CARD in $(echo_card_indices) ; do
				try_to_set_default_mixer_levels_on_card "$CARD" || RETURNSTATUS=1
			done
			;;
		*)
			try_to_set_default_mixer_levels_on_card "$1" || RETURNSTATUS=1
			;;
	esac
	return $RETURNSTATUS
}

# $1 card
try_to_zero_mixer_levels_on_card()
{
	[ "$1" ] || bug
	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 \
		"SB Live Analog/Digital Output Jack" \
		"Audigy Analog/Digital Output Jack"
	do
		try_to_set_mixer_off "$CTL"
	done
	return 0
}

# $1 card
try_to_zero_mixer_levels()
{
	[ "$1" ] || bug
	RETURNSTATUS=0
	case "$1" in
	all)
		for CARD in $(echo_card_indices) ; do
			try_to_zero_mixer_levels_on_card "$CARD" || RETURNSTATUS=1
		done
		;;
	*)
		try_to_zero_mixer_levels_on_card "$1" || RETURNSTATUS=1
		;;
	esac
	return $RETURNSTATUS
}


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

# [$1 card]
stop()
{
	RETURNSTATUS=0
	case "$1" in
	"")
		log_begin_msg "Shutting down ALSA..."
                if [ -d /proc/asound ]; then
                    store_mixer_levels_if_allowed "all-default" || RETURNSTATUS=1
                    try_to_zero_mixer_levels all || RETURNSTATUS=1
                fi
                log_end_msg $RETURNSTATUS
                return $RETURNSTATUS
		;;
	all)
		echo -n "Shutting down ALSA..."
		[ -d /proc/asound ] || { echo "done (not loaded)." ; return $RETURNSTATUS ; }
		store_mixer_levels_if_allowed all || RETURNSTATUS=1
		try_to_zero_mixer_levels all || RETURNSTATUS=1
		;;
	*)
		echo -n "Shutting down ALSA card ${1}..."
		[ -d "/proc/asound/card$1" ] || [ -d "/proc/asound/$1" ] || { echo "done (not loaded)." ; return $RETURNSTATUS ; }
		store_mixer_levels_if_allowed "$1" || RETURNSTATUS=1
		try_to_zero_mixer_levels "$1" || RETURNSTATUS=1
		;;
	esac
	case "$RETURNSTATUS" in
		0) echo "done." ;;
		*) echo "failed." ;;
	esac
	return $RETURNSTATUS
}

# $* names of modules to remove, "all" for all modules
force_unload_modules()
{
	[ "$1" ] || bug
	if [ -d /var/run/alsa ] ; then
		:> /var/run/alsa/modules-removed
	else
		warn "Directory /var/run/alsa is not present."
	fi
	[ -d /proc/asound ] || { echo "Unloading ALSA sound driver modules: (none loaded)." ; return 0 ; }
	set_procs_using_sound()
	{
		procs_using_sound="$(
			lsof +D /dev -F rt \
			| awk '/^p/ {pid=$1} /^t/ {type=$1} /^r0x(74|e)..$/ && type == "tCHR" {print pid}' \
			| cut -c 2- \
			| uniq
		)"
		procs_using_sound="$(echo $procs_using_sound)"
	}
	set_procs_using_sound
	if [ "$procs_using_sound" ] ; then
		log_begin_msg "Terminating processes:"
		for attempt in 1 2 3 4 ; do
			echo -n " ${procs_using_sound}"
			kill $procs_using_sound || :
			sleep 1
			set_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
		set_procs_using_sound
		if [ "$procs_using_sound" ] ; then
			echo " (failed; still running: $procs_using_sound)."
			return 1
		fi
		echo "."
	fi
	echo -n "Unloading ALSA sound driver modules:"
	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 [ -d /var/run/alsa ] ; then
		MODULES_NOT_REMOVED="$(echo_snd_modules_loaded | grep -F -f /var/run/alsa/modules-removed)"
	else
		MODULES_NOT_REMOVED=""
	fi
	if [ "$MODULES_NOT_REMOVED" ] ; then
		MODULES_NOT_REMOVED="$(echo $MODULES_NOT_REMOVED)"
		echo " (failed; not removed: $MODULES_NOT_REMOVED)."
		return 1
	else
		echo "."
		return 0
	fi
}

reload_modules()
{
	RETURNSTATUS=0
	MODULES_TO_LOAD=""
	[ -d /var/run/alsa ] || warn "Directory /var/run/alsa is not present."
	[ -f /var/run/alsa/modules-removed ] && MODULES_TO_LOAD="$(echo $(cat /var/run/alsa/modules-removed))"
	[ "$MODULES_TO_LOAD" ] || { echo "Reloading sound driver modules: (none to reload)." ; return $RETURNSTATUS ; }
	echo -n "Reloading sound driver modules: $MODULES_TO_LOAD"
	for MDL in $MODULES_TO_LOAD ; do
		modprobe $MDL || RETURNSTATUS=1
	done
	case "$RETURNSTATUS" in
		0) echo "." ;;
		*) echo " (failed)." ;;
	esac
	return $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
		;;
	force-unload)
		force_unload_modules all || exit $?
		;;
	force-reload|force-restart)
		EXITSTATUS=0
		force_unload_modules all || EXITSTATUS=1
		reload_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) : ;;
			*) reload_modules || exit $? ;;
		esac
		;;
	*)
		echo "Usage: /etc/init.d/alsa {start [CARD]|stop [CARD]|restart [CARD]|force-unload|force-reload|suspend|resume}" >&2
		exit 3
		;;
esac
