#!/bin/bash version=0.1.0 tmpdir=$TMPDIR if test -z "$tmpdir"; then tmpdir="/tmp" fi tmpdir=$tmpdir/alsa-delay-script delay= pcard= pdev= ccard= cdev= yes= quiet= clean= remove= pdevice="default:%s" pdevice_set= cdevice="plughw:%s,%s,%s" pctl= cctl= one= arg= mix= pa= useprocfs=yes fuser_prg=fuser insmod_prg=insmod rmmod_prg=rmmod lsmod_prg=lsmod modprobe_prg=modprobe chkconfig_prg=chkconfig systemctl_prg=systemctl pidof_prg=pidof alsaloop_prg=alsaloop amixer_prg=amixer test -x /sbin/fuser && fuser_prg=/sbin/fuser test -x /sbin/insmod && insmod_prg=/sbin/insmod test -x /sbin/rmmod && rmmod_prg=/sbin/rmmod test -x /sbin/lsmod && lsmod_prg=/sbin/lsmod test -x /sbin/modprobe && modprobe_prg=/sbin/modprobe test -x /sbin/chkconfig && chkconfig_prg=/sbin/chkconfig test -x /bin/systemctl && systemctl_prg=/bin/systemctl test -x /sbin/pidof && pidof_prg=/sbin/pidof test -x /usr/bin/alsaloop && alsaloop_prg=/usr/bin/alsaloop test -x /usr/local/bin/alsaloop && alsaloop_prg=/usr/local/bin/alsaloop test -x /usr/bin/amixer && amixer_prg=/usr/bin/amixer modprobeconf=/etc/modprobe.d/alsa.conf test -r /etc/modprobe.conf && modprobeconf=/etc/modprobe.conf && useprocfs="" alsaloopconf=/etc/alsaloop.conf test -r modprobe.work && modprobeconf=modprobe.work test -r modprobe.work && alsaloopconf=alsaloop.conf usage() { echo "Usage: $0 [OPTION]... [[,]]" cat < is ALSA card index (number) or string card identifier is ALSA device number Use 'aplay -l' to list available cards and devices. Operation modes: -h, --help print this help, then exit -q, --quiet quiet mode -y, --yes do not ask any questions - answer is always yes -c, --clean clean temporary directory and exit -r, --remove remove the alsa-delay config modifications and exit --tmpdir= set temporary directory Alsaloop options: --pdevice= force playback device --cdevice= force capture device --pctl= force playback ctl device --cctl= force capture ctl device --one= pass this argument to last thread --arg= pass this argument to all threads --mix= redirect ALSA mixer controls to OSS mixer (default is Master) --pa Redirect PA to alsaloop Note: For devices, the string %s is replaced with the card index and second string %s is replaced with the device index and third string %s is replaced the substream index (0-7). Example: hw:%s,%s,%s (card, device, substream) EOF } while : do case "$1" in -h|--help) usage exit 0 ;; -q|--quiet) quiet=true ;; -y|--yes) yes=true ;; -c|--clean) clean="full" ;; -r|--remove) remove="full" ;; --tmpdir*) case "$#,$1" in *,*=*) tmpdir=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) tmpdir="$2" shift ;; esac tmpdir="$tmpdir/alsa-compile-script" ;; --pdevice*) case "$#,$1" in *,*=*) pdevice=`expr "z$1" : 'z-[^=]*=\(.*\)'` pdevice_set=yes ;; 1,*) usage ;; *) pdevice="$2" pdevice_set=yes shift ;; esac ;; --cdevice*) case "$#,$1" in *,*=*) cdevice=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) cdevice="$2" shift ;; esac ;; --pctl*) case "$#,$1" in *,*=*) pctl=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) pctl="$2" shift ;; esac ;; --cctl*) case "$#,$1" in *,*=*) cctl=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) cctl="$2" shift ;; esac ;; --one*) case "$#,$1" in *,*=*) one1=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) one1="$2" shift ;; esac one="$one $one1" ;; --arg*) case "$#,$1" in *,*=*) arg1=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) arg1="$2" shift ;; esac arg="$arg $arg1" ;; --mix*) case "$#,$1" in *,*=*) mix=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; 1,*) usage ;; *) mix="$2" shift ;; esac arg="$arg $arg1" ;; --pa) pa=true ;; *) if test -n "$1"; then ok= if test -z "$delay"; then delay="$1" if test "$delay" -lt 1000; then delay=$[$delay * 1000] fi ok=true fi if test -z "$ok" -a -z "$pcard"; then pcard="$1" ok=true fi if test -z "$ok"; then echo "Unknown parameter '$1'" break fi else break fi ;; esac shift done test -z "$pdevice_set" -a -n "$pa" && pdevice="plug:dmix:%s" test -z "$delay" && delay="50000" test -z "$pcard" && pcard="1" pdev=$(echo $pcard | cut -s -d , -f 2) pcard=$(echo $pcard | cut -d , -f 1) test -z "$pdev" && pdev="0" test -z "$ccard" && ccard="Loopback,1" cdev=$(echo $ccard | cut -s -d , -f 2) ccard=$(echo $ccard | cut -d , -f 1) test -z "$cdev" && cdev="0" # Echo "true" or "false", depending on $yes and user response to prompt # $1 is prompt message question_bool() { if test "$yes" = "yes"; then echo "true" else echo >&2 -n "$1 (Y/ ) " read i local i=${i:0:1} if test "$i" = "Y" -o "$i" = "y"; then echo "true" else echo "false" fi fi } # Safe exit safe_exit() { if test -r /etc/pulse/client.conf ; then if grep "# alsa-delay-line-to-be-removed-1234" /etc/pulse/client.conf 2> /dev/null > /dev/null ; then grep -v alsa-delay-line-to-be-removed-1234 /etc/pulse/client.conf > /etc/pulse/client.conf.new if test -s /etc/pulse/client.conf.new; then mv /etc/pulse/client.conf.new /etc/pulse/client.conf fi fi fi exit $1 } # Log and execute $@ and check success do_cmd() { if test -z "$quiet"; then echo "> $@" fi $@ || safe_exit 1 } # Cache or restore $protocol and $url and $package in $tmpdir check_environment() { if ! test -d $tmpdir ; then mkdir -p $tmpdir if ! test -d $tmpdir; then echo >&2 "Unable to create directory $tmpdir." exit 1 fi fi echo "Using temporary tree: $tmpdir" test -x /bin/depmod && depmodbin=/bin/depmod test -x /sbin/depmod && depmodbin=/sbin/depmod if test -z "$depmodbin"; then echo >&2 "Unable to find depmod utility." exit 1 fi test -x /bin/modinfo && modinfobin=/bin/modinfo test -x /sbin/modinfo && modinfobin=/sbin/modinfo if test -z "$modinfobin"; then echo >&2 "Unable to find modinfo utility." exit 1 fi } # Kill processes currently accessing the audio devices kill_audio_apps() { local pids0=$($fuser_prg /dev/snd/* 2> /dev/null) local pids1=$($fuser_prg /dev/mixer* 2> /dev/null) local pids2=$($fuser_prg /dev/sequencer* 2> /dev/null) local pids= for pid in $pids0 $pids1 $pids2; do local pids="$pids $pid" done if ! test -z "$pids"; then echo echo "WARNING! An audio application uses ALSA driver:" echo for pid in $pids; do ps --no-headers -p $pids || safe_exit 1 done echo if test $(question_bool "Would you like to kill these apps?") = "true"; then if test -w /etc/pulse/client.conf ; then echo "autospawn = no # alsa-delay-line-to-be-removed-1234" >> /etc/pulse/client.conf fi for pid in $pids; do do_cmd kill $pid done sleep 2 local killed= for pid in $pids; do local a=$(ps --no-headers -p $pids) if test -n "$a"; then do_cmd kill -9 $pid local killed="true" fi done if test "$killed" = "true"; then sleep 2 for pid in $pids; do local a=$(ps --no-headers -p $pids) if test -n "$a"; then echo >&2 "Unable to kill application:" echo >&2 " $a" safe_exit 1 fi done fi else echo >&2 "Cannot continue with running audio applications." safe_exit 1 fi fi } # Echo the list of configured sound modules configured_modules() { if test -z "$useprocfs"; then cat $modprobeconf | grep -E "^alias snd-card-" | cut -d ' ' -f 3 else cat /proc/asound/modules | colrm 1 3 fi } # Echo the list of loaded sound modules current_modules() { $lsmod_prg | cut -d ' ' -f 1 | grep -E "^(snd[_-])" } # The loopback kernel driver detection aloop_present() { if test -r /proc/asound/card0/id; then local id=$(cat /proc/asound/card0/id) else local id="" fi if test "$id" = "Loopback"; then echo "yes" fi } # Remove kernel modules, using two phases # $@ is module names my_rmmod() { local phase2= while test -n "$1"; do if ! $rmmod_prg $1 2> /dev/null > /dev/null; then local phase2="$phase2 $1" else echo "> rmmod $1" fi shift done for mod in $phase2; do echo "> rmmod $mod" if ! $rmmod_prg $mod ; then echo >&2 "Unable to remove kernel module $mod." safe_exit 1 fi done } # Reload kernel modules kernel_modules() { kill_audio_apps if test "$1" = "unload"; then local present=$(aloop_present) if test "$present" = "yes"; then if ! $rmmod_prg snd-aloop; then echo >&2 "Unable to remove kernel module snd-aloop." safe_exit 1 fi fi fi local curmods=$(current_modules) local usermods=$(configured_modules) my_rmmod $curmods $modprobe_prg soundcore || safe_exit 1 if test "$1" = "load"; then if ! $modprobe_prg snd-aloop; then echo >&2 "Unable to install kernel module snd-aloop." safe_exit 1 fi fi for mod in $usermods; do if ! $modprobe_prg $mod; then echo >&2 "Unable to install kernel module $mod." safe_exit 1 fi done echo "Kernel modules ready:" cat /proc/asound/cards sleep 0.5 } # If $package is alsa-driver then remove current modules kernel_modules_remove() { local curmods=$(current_modules) if test -z "$curmods"; then echo "No ALSA kernel modules to remove." safe_exit 0 fi kill_audio_apps my_rmmod $curmods echo "ALSA kernel modules removed." } function clean() { echo -n "Removing tree $tmpdir:" if test -d "$tmpdir"; then if ! rm -rf "$tmpdir"; then echo " failed" safe_exit 1 fi fi echo " success" } function reindex_modprobe_conf() { if test -z "$useprocfs"; then cat > $tmpdir/run.awk < 1) printf(",") printf("%i", c) } continue } printf(" %s", l[i]) } printf("\n") } else if (l[1] == "remove") { printf(l[1]) flag = 0 for (i = 2; i <= length(l); i++) { if (l[i] == "/usr/sbin/alsactl" || l[i] == "store") { flag++; printf(" %s", l[i]); continue; } if (flag == 2) { c = int(l[i]) + 1; printf(" %i", c); flag = 0; continue; } flag = 0; printf(" %s", l[i]); } printf("\n") } else { print line } } BEGIN { aloop=0; } /alias snd-card-0 snd-aloop/ { aloop=1; next; } /options snd-card-0 index=0/ { if (aloop) next; } /options snd-aloop/ { next; } /alias snd-card-/ { rewrite_line(\$0); next; } /options snd-/ { rewrite_line(\$0); next; } /remove snd-/ { rewrite_line(\$0); next; } { print \$0; } EOF cat $modprobeconf | awk -f $tmpdir/run.awk > $modprobeconf.new rm $tmpdir/run.awk || safe_exit 1 else local index=1 echo -n "" > $modprobeconf.new declare -a modules printed for mod in $(cat /proc/asound/modules | grep -Ev "^2[6789]" | colrm 1 3); do if test "$mod" != "snd_aloop" -a "$mod" != "snd-aloop"; then if test -z ${modules[$mod]}; then modules[$mod]=$index else modules[$mod]="${modules[mod]},$index" fi echo "alias snd-card-$index $mod" >> $modprobeconf.new echo "options snd-card-$index index=$index" >> $modprobeconf.new fi index=$(expr $index + 1) done for mod in $(cat /proc/asound/modules | grep -Ev "^2[6789]" | colrm 1 3); do if test "$mod" != "snd_aloop" -a "$mod" != "snd-aloop"; then if test -z ${printed[$mod]}; then echo "options $mod index=${modules[$mod]}" >> $modprobeconf.new printed[$mod]=yes fi fi done fi cat >> $modprobeconf.new < /dev/null | grep -E "^alias snd-card-0 snd-aloop") local present=$(aloop_present) if test -n "$check"; then echo "Module snd-aloop is already installed." if ! test "$present" = "yes"; then kernel_modules load fi else reindex_modprobe_conf if ! test "$present" = "yes"; then kernel_modules load else kernel_modules fi fi } function myprintf() { local cnt=$(echo "$1" | grep -o "%" | wc -l) if test $cnt -eq 1; then printf "$1" "$2" else if test $cnt -eq 2; then printf "$1" "$2" "$3" else printf "$1" "$2" "$3" "$4" fi fi } function check_ctl_name() { $amixer_prg -D "$1" contents | grep "name='$2'" } function check_oss_mixer() { grep ": mixer" /proc/asound/oss/devices } function get_card_id() { $amixer_prg -D "$1" info | head -1 | cut -d '/' -f 1 | cut -d ' ' -f 3- | awk '{ print substr($0, 2, length($0)-2) }' } function generate_alsaloop_line() { local file="$1" local idx="$2" local carg=$(myprintf "$cdevice" $ccard $cdev $idx) local parg=$(myprintf "$pdevice" $pcard $pdev $idx) local res="-C $carg -P $parg -T $idx -t $delay" if test -n "$cctl"; then local carg=$(myprintf "$cctl" $pcard $cdev $idx) local res="$res -Y $carg" fi if test -n "$pctl"; then local parg=$(myprintf "$pctl" $pcard $pdev $idx) local res="$res -X $parg" fi if test -n "$arg"; then local res="$res $arg" fi if test $idx -eq 7; then if test -n "$one"; then local res="$res $one" fi if test -z "$pctl"; then local res="$res -X hw:$pcard" fi local mymix="$mix" if test -z "$mymix"; then local mymix="Master" for mymix in Master "Master Mono" Headphone Headphone2 PCM Speaker \ "Desktop Speaker" Beep Front Rear Center LFE Side \ Surround ; do local check=$(check_ctl_name hw:$pcard "$mymix Playback Switch") if test -n "$check"; then local res="$res -m \"name='$mymix Playback Switch'\"" fi local check=$(check_ctl_name hw:$pcard "$mymix Playback Volume") if test -n "$check"; then local res="$res -m \"name='$mymix Playback Volume'\"" fi done else local check=$(check_ctl_name hw:$pcard "$mymix Playback Switch") if test -n "$check"; then local res="$res -m \"name='$mymix Playback Switch'\"" fi local check=$(check_ctl_name hw:$pcard "$mymix Playback Volume") if test -n "$check"; then local res="$res -m \"name='$mymix Playback Volume'\"" fi fi local check=$(check_oss_mixer) if test -n "$check"; then local res="$res -O \"$mymix@VOLUME\"" fi fi echo $res >> $file } function generate_alsaloop_conf() { local idx=0 rm -f $alsaloopconf.new while test $idx -lt 8; do generate_alsaloop_line $alsaloopconf.new $idx idx=$(expr $idx + 1) done mv $alsaloopconf.new $alsaloopconf || safe_exit 1 } function kill_alsaloop() { local pid=$($pidof_prg alsaloop) if test -n "$pid"; then echo "Killing alsaloop..." fi while test -n "$pid"; do kill $pid sleep 0.01 pid=$($pidof_prg alsaloop) done } function restart_alsaloop() { echo "Restarting alsaloop: delay $delay us." if test -d /etc/systemd/system; then systemctl start alsaloop.service else /etc/init.d/alsaloop start fi sleep 0.4 local pid=$($pidof_prg alsaloop) if test -z "$pid"; then echo "ERROR: Not started, check /var/log/messages for details" else if test -d /etc/systemd/system; then systemctl status alsaloop.service else ps -p $pid fi fi } function create_systemd_service() { if test -r /etc/systemd/system/alsaloop.service; then echo "/etc/systemd/system/alsaloop.service exists" else cat >> /etc/systemd/system/alsaloop.service <> /etc/init.d/alsaloop < # For RedHat and cousins: # chkconfig: 2345 99 01 # description: Start alsaloop daemon # processname: alsaloop ### BEGIN INIT INFO # Provides: alsaloop # Required-Start: \$local_fs # Required-Stop: \$local_fs # Should-Start: # Short-Description: ALSALOOP # Description: ALSALOOP ### END INIT INFO # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation; either version 2, or (at your option) any later # version. # You should have received a copy of the GNU General Public License (for # example COPYING); if not, write to the Free Software Foundation, Inc., 675 # Mass Ave, Cambridge, MA 02139, USA. # This code was originally developed as a Senior Thesis by Michael Cornwell # at the Concurrent Systems Laboratory (now part of the Storage Systems # Research Center), Jack Baskin School of Engineering, University of # California, Santa Cruz. http://ssrc.soe.ucsc.edu/. alsaloop_opts="--daemonize --workaround serialopen --config $alsaloopconf" ALSALOOP_BIN=/usr/bin/alsaloop test -x /usr/local/bin/alsaloop && ALSALOOP_BIN=/usr/local/bin/alsaloop # Source function library . /etc/rc.d/init.d/functions RETVAL=0 prog=alsaloop pidfile=/var/lock/subsys/alsaloop start() { modprobe snd-aloop echo -n \$"Starting \$prog: " daemon \$ALSALOOP_BIN \$alsaloop_opts RETVAL=\$? echo [ \$RETVAL = 0 ] && touch \$pidfile return \$RETVAL } stop() { echo -n \$"Shutting down \$prog: " killproc \$ALSALOOP_BIN RETVAL=\$? echo rm -f \$pidfile return \$RETVAL } case "\$1" in start) start ;; stop) stop ;; restart) stop start ;; try-restart) if [ -f \$pidfile ]; then stop start fi ;; status) status $prog RETVAL=$? ;; *) echo \$"Usage: \$0 {start|stop|restart|try-restart}" RETVAL=3 esac exit \$RETVAL EOF chmod 755 /etc/init.d/alsaloop || safe_exit 1 $chkconfig_prg --add alsaloop $chkconfig_prg alsaloop on fi } function modify_pa_conf() { if grep Loopback /etc/pulse/default.pa > /dev/null; then echo "/etc/pulse/default.pa already modified" else if test -f /etc/pulse/default.pa; then local pcardid=$(get_card_id "hw:$pcard") if ! test -f /etc/pulse/default.pa.alsa-delay.save ; then cp /etc/pulse/default.pa /etc/pulse/default.pa.alsa-delay.save fi cat /etc/pulse/default.pa | \ sed -e 's/^load-module module-udev-detect/#load-module module-udev-detect # commented by alsa-delay/' \ -e 's/^load-module module-detect/#load-module module-detect # commented by alsa-delay/' \ -e "s/^### Automatically load driver modules for Bluetooth hardware/load-module module-alsa-card device_id=Loopback\nload-module module-alsa-card device_id=$pcardid\n\n### Automatically load driver modules for Bluetooth hardware/" > /etc/pulse/default.pa.new if test -s /etc/pulse/default.pa.new; then mv /etc/pulse/default.pa.new /etc/pulse/default.pa echo "/etc/pulse/default.pa changed" fi fi fi local file= local asoundconf= if test -d /etc/alsa -a -f /etc/alsa/pulse-default.conf; then file=/etc/alsa/alsaloop-default.conf asoundconf=yes fi if test -f /usr/share/alsa/alsa.conf.d/99-pulseaudio-default.conf ; then file=/usr/share/alsa/alsa.conf.d/A0-alsaloop-default.conf fi if test -z "$file"; then echo "unknown PulseAudio setup (asound.conf)" elif test -f $file; then echo "$file exists" else cat >> $file < /etc/asound.conf.new if test -s /etc/asound.conf.new; then mv /etc/asound.conf.new /etc/asound.conf fi fi fi } function remove() { if test -f /etc/pulse/default.pa.alsa-delay.save ; then echo "Restoring /etc/pulse/default.pa" mv /etc/pulse/default.pa.alsa-delay.save /etc/pulse/default.pa fi if test -f /etc/init.d/alsaloop ; then echo "Removing /etc/init.d/alsaloop service" $chkconfig_prg alsaloop off $chkconfig_prg --del alsaloop rm /etc/init.d/alsaloop fi if test -f /etc/systemd/system/alsaloop.service ; then echo "Removing /etc/systemd/system/alsaloop.service" $systemctl_prg disable alsaloop.service rm /etc/systemd/system/alsaloop.service fi if test -f $alsaloopconf ; then echo "Removing $alsaloopconf" rm $alsaloopconf fi if test -f /etc/alsa/alsaloop-default.conf ; then echo "Removing /etc/alsa/alsaloop-default.conf" rm /etc/alsa/alsaloop-default.conf fi if test -f /usr/share/alsa/alsa.conf.d/A0-alsaloop-default.conf ; then echo "Removing /usr/share/alsa/alsa.conf.d/A0-alsaloop-default.conf" rm /usr/share/alsa/alsa.conf.d/A0-alsaloop-default.conf fi if grep "alsaloop-default.conf" /etc/asound.conf > /dev/null 2> /dev/null ; then echo "Modifying /etc/asound.conf" cat /etc/asound.conf | sed 's/alsaloop-default.conf/pulse-default.conf/g' > /etc/asound.conf.new if test -s /etc/asound.conf.new; then mv /etc/asound.conf.new /etc/asound.conf fi fi if test -f $modprobeconf.alsa-delay.save ; then echo "Restoring $modprobeconf" if test -s $modprobeconf.alsa-delay.save ; then mv $modprobeconf.alsa-delay.save $modprobeconf else rm $modprobeconf.alsa-delay.save rm $modprobeconf fi fi if test $(aloop_present) = "yes"; then kernel_modules unload fi } rundir=$(pwd) export LC_ALL=C export LANGUAGE=C check_environment if test -n "$clean"; then clean safe_exit 0 fi if test -n "$remove"; then kill_alsaloop remove clean safe_exit 0 fi kill_alsaloop if test -d /etc/systemd/system ; then create_systemd_service else create_initd fi modify_modprobe_conf generate_alsaloop_conf if test -n "$pa"; then modify_pa_conf fi restart_alsaloop clean safe_exit 0