alsa-capabilities 49 KB
Newer Older
1 2
#!/usr/bin/env bash

3 4 5 6 7 8
## This script for linux with bash 4.x displays a list with the audio
## capabilities of each alsa audio output interface and stores them in
## arrays for use in other scripts.  This functionality is exposed by
## the `return_alsa_interface' function which is avaliable after
## sourcing the file. When ran from a shell, it will call that
## function.
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
##
##  Copyright (C) 2014 Ronald van Engelen <ronalde+github@lacocina.nl>
##  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 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
## Source:    https://github.com/ronalde/mpd-configure
Ronald van Engelen's avatar
Ronald van Engelen committed
25
## See also:  https://lacocina.nl/detect-alsa-output-capabilities
26 27 28

LANG=C

29
APP_NAME_AC="alsa-capabilities"
30
APP_VERSION="0.9.4"
31
APP_INFO_URL="https://lacocina.nl/detect-alsa-output-capabilities"
32

33 34
## set DEBUG to a non empty value to display internal program flow to
## stderr
35
DEBUG="${DEBUG:-}"
36 37
## set PROFILE to a non empty value to get detailed timing
## information. Normal output is suppressed.
38
PROFILE="${PROFILE:-}"
39 40 41 42 43 44
## to see how the script behaves with a certain output of aplay -l
## on a particular host, store it's output in a file and supply
## the file path as the value of TESTFILE, eg:
## `TESTFILE=/tmp/somefile ./bash-capabilities
## All hardware and device tests will fail or produce fake outputs
## (hopefully with some grace).
45
TESTFILE="${TESTFILE:-}"
46 47 48

### generic functions
function die() {
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
    calling_function_key="$(( ${#FUNCNAME[@]} - 2 ))"
    calling_function_name="${FUNCNAME[${calling_function_key}]}"
    printf 1>&2 "\nError in %s (v%s): function %s:\n%s\n" \
		"${APP_NAME_AC}" \
		"${APP_VERSION}" \
		"${calling_function_name}" \
		"$@" 
    printf 1>&2 "\tfunction stack:\n"
    for (( key=${#FUNCNAME[@]}-1 ; key>=0 ; key-- )) ; do
	((counter++))
	printf 1>&2 "\t* %d: %s\n" \
		    "${counter}" "${FUNCNAME[${key}]}"
    done
    printf 1>&2 "=%.0s"  {1..100}
    printf 1>&2 "\n"
64 65 66 67
    exit 1
}

function debug() {
68 69 70 71 72 73 74 75 76
    declare -a stack
    counter=0
    printf 1>&2 "=%.0s"  {1..100}
    calling_function_key="$(( ${#FUNCNAME[@]} - 2 ))"
    calling_function_name="${FUNCNAME[${calling_function_key}]}"
    printf 1>&2 "\nDEBUG (%s) *** function: %s %s\n" \
		"${APP_NAME_AC}" \
		"${calling_function_name}" \
		"$@"
Ronald van Engelen's avatar
Ronald van Engelen committed
77 78
}

79 80 81 82 83 84
function command_not_found() {
    ## give installation instructions for package $2 when command $1
    ## is not available, optional with non default instructions $3
    ## and exit with error
    command="$1"
    package="$2"
85
    instructions="${3:-}"
Ronald van Engelen's avatar
Ronald van Engelen committed
86
    msg="command \`${command}' not found. "
87
    if [[ -z "${instructions}" ]]; then
88
	msg+="See 'Requirements' on ${APP_INFO_URL}."
89 90 91 92 93 94 95
    else
	msg+="${instructions}"
    fi
    die "${msg}"
}

### alsa related functions
96
function get_aplay_output() {
97 98 99 100
    ## use aplay to do a basic alsa sanity check using aplay -l, or
    ## optionally using $TESTFILE containing the stored output of
    ## 'aplay -l'.
    ## returns the raw output of aplay or an error.
101
    res=""
102
    aplay_msg_nosoundcards_regexp="no[[:space:]]soundcards"
103
    if [[ "${TESTFILE}x" != "x" ]]; then
104
	if [[ ! -f "${TESTFILE}" ]]; then
105
	    # shellcheck disable=SC2059
106 107
	    printf 1>&2 "${MSG_APLAY_ERROR_NOSUCHTESTFILE}" \
			"${TESTFILE}"
108
	    return 1
109
	else
110
	    ## get the output from a file for testing purposes
111
	    # shellcheck disable=SC2059
112 113
	    printf  1>&2 "${MSG_APLAY_USINGTESTFILE}\n" \
			 "${TESTFILE}"
114 115 116 117
	    # shellcheck disable=SC2059
	    res="$(< "${TESTFILE}")" || \
		( printf "${MSG_APLAY_ERROR_OPENINGTESTFILE}" && \
		      return 1 )
118 119
	fi
    else
120
    	## run aplay -l to check for alsa errors or display audio cards
121
	res="$(${CMD_APLAY} -l 2>&1)" || \
122 123 124 125
	    (
		printf "${MSG_APLAY_ERROR_GENERAL}\n" "${res}"
		return 1
	    )
126
	## check for no soundcards
127 128 129
	if [[ "${res}" =~ ${aplay_msg_nosoundcards_regexp} ]]; then
	    printf "%s\n" "${MSG_APLAY_ERROR_NOSOUNDCARDS}"
	    return 1
130
	fi
131
    fi
132 133
    ## return the result to the calling function
    printf "%s" "${res}"
134 135
}

136 137 138
function handle_doublebrackets() {
    ## return the name of the alsa card / device, even when they
    ## contain brackets.
139
    string="$*"
140 141 142 143 144 145 146 147 148 149 150
    bracketcounter=0
    for (( i=0; i<${#string}; i++ )); do
	char="${string:$i:1}"
	if [[ "${char}" = "[" ]]; then
	    (( bracketcounter++ ))
	elif [[ "${char}" = "]" ]]; then
	    (( bracketcounter-- ))
	fi
	if [[ ${bracketcounter} -gt 0 ]]; then
	    ## inside outer brackets
	    if [[ ${bracketcounter} -lt 2 ]] && [[ "${char}" == "[" ]]; then
151 152
		[[ ${DEBUG} ]] && \
		    debug "(${LINENO}): name with brackets found."
153
	    else
154
		# shellcheck disable=SC2059
155 156 157 158 159 160
		printf "${char}"
	    fi
	fi
    done
}

161
function return_output_human() {
162 163 164 165
    ## print default output to std_err.
    ## called by fetch_alsa_outputinterfaces.
    printf "%s\n" "${alsa_if_display_title}" 1>&2;
    printf " - %-17s = %-60s\n" \
166 167
	   "${MSG_ALSA_DEVNAME}" \
	   "${alsa_dev_label}" 1>&2;
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
    printf " - %-17s = %-60s\n" \
	   "${MSG_ALSA_IFNAME}" "${alsa_if_label}" 1>&2;
    printf " - %-17s = %-60s\n" \
	   "${MSG_ALSA_UACCLASS}" "${alsa_if_uacclass}" 1>&2;
    printf " - %-17s = %-60s\n" \
	   "${MSG_ALSA_CHARDEV}" "${alsa_if_chardev}" 1>&2;
    if [[ ! -z ${formats_res_err} ]]; then
	## device is locked by an unspecified process
	printf " - %-17s = %-60s\n" \
	       "${MSG_ALSA_ENCODINGFORMATS}" \
	       "${MSG_ERROR_GETTINGFORMATS}"  1>&2;
	printf "   %-17s   %-60s\n" \
	       " " \
	       "${formats_res[@]}"  1>&2;
    else
	formatcounter=0
	if [[ ! -z ${OPT_SAMPLERATES} ]]; then 
185
	    MSG_ALSA_ENCODINGFORMATS="samplerates (Hz)"
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
	fi
	printf " - %-17s = " \
	       "${MSG_ALSA_ENCODINGFORMATS}" 1>&2;
	# shellcheck disable=SC2141
	while IFS="\n" read -r line; do
	    (( formatcounter++ ))
	    if [[ ${formatcounter} -gt 1 ]]; then
		printf "%-23s" " " 1>&2;
	    fi
	    printf "%-60s\n" "${line}" 1>&2;
	done<<<"${alsa_if_formats[@]}"
    fi
    printf " - %-17s = %-60s\n" \
	   "${MSG_ALSA_MONITORFILE}" "${alsa_if_monitorfile}" 1>&2;
    printf " - %-17s = %-60s\n" \
	   "${MSG_ALSA_STREAMFILE}" "${alsa_if_streamfile}" 1>&2;
    printf "\n"
}

205
function key_val_to_json() {
206
    ## returns a json "key": "val" pair.
207 208
    key="$1"
    val="$2"
209
    ## check if val is a number
210
    if printf -v numval "%d" "${val}" 2>/dev/null; then
211
	## it is
212 213 214 215 216 217 218 219 220 221
	printf '"%s": %d' \
	       "${key}" "${numval}"
    else
	printf '"%s": "%s"' \
	       "${key}" "${val}"
    fi
    printf "\n"
}

function ret_json_format() {
222 223
    ## returns the json formatted encoding format and possibly sample
    ## rates.
224 225 226 227 228 229 230 231 232
    formats_raw="$1"
    declare -a json_formats
    if [[ "${formats_raw}" =~ ':' ]]; then
	## sample rates included
	while read line; do
	    split_re="(.*):(.*)"
	    if [[ "${line}" =~ ${split_re} ]]; then
		format=${BASH_REMATCH[1]}
		IFS=" " samplerates=(${BASH_REMATCH[2]})
233 234
		printf -v sr_out "\t\t\"%s\",\n" \
		       "${samplerates[@]}"
235 236 237
		sr_out="${sr_out%,*}"
		format_json=""
		label_samplerates='"samplerates"'
238 239
		output_line="{
           $(key_val_to_json format "${format// /}"),
240 241 242 243 244 245 246
           ${label_samplerates}: [
${sr_out}
           ]
          }," 
		output_lines+=("${output_line}")
	    fi
	done<<<"${formats_raw}"
247 248
	printf -v json_formats "\t%s\n" "${output_lines[@]}"
	## strip the continuation comma from the last element
249 250 251 252 253
	json_formats="${json_formats%,*}"
    else
	## no sample rates included
	IFS="," formats_res=(${formats_raw})
	printf -v json_formats '\t\t"%s",\n' \
254 255
	       "${formats_res[@]// /}"
	## strip the continuation comma from the last element
256 257
	json_formats="${json_formats%,*}"
    fi
258
    printf "%s" "${json_formats}"
259 260 261 262 263 264 265 266
}

function ret_json_card() {
    ## print json formatted output to std_out.
    ## called by fetch_alsa_outputinterfaces.
    #cur_aif_no="$1"
    formats_res="$1"
    last_aif="$2"
267
    printf -v encoding_formats_val "[\n %s\n\t]" \
268
	   "$(ret_json_format "${formats_res}")"
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    declare -A a_json_keyvals
    a_json_keyvals[id]=${cur_aif_no}
    a_json_keyvals[hwaddr]=${alsa_if_hwaddress}
    a_json_keyvals[description]="${alsa_if_title_label}"
    a_json_keyvals[cardnumber]=${alsa_dev_nr}
    a_json_keyvals[interfacenumber]=${alsa_if_nr}
    a_json_keyvals[cardname]="${alsa_dev_label}"
    a_json_keyvals[interfacename]="${alsa_if_label}"
    a_json_keyvals[chardev]=${alsa_if_chardev}
    a_json_keyvals[monitorfile]=${alsa_if_monitorfile}
    a_json_keyvals[streamfile]=${alsa_if_streamfile}
    a_json_keyvals[usbaudioclass]="${alsa_if_uacclass}"
    aif_json=
    declare -a json_keyvals
    for key in "${!a_json_keyvals[@]}"; do
	json_keyvals+=("$(key_val_to_json "${key}" "${a_json_keyvals[${key}]}")") 
    done
    printf -v str_json_keyvals "\t%s,\n" "${json_keyvals[@]}"
287 288
    aif_json="\
     {
289 290
${str_json_keyvals%,*}
        \"encodingformats\": "${encoding_formats_val}" 
291 292 293 294 295 296 297 298
     }"
    printf "%s" "${aif_json}"
    if [[ "${last_aif}x" == "x" ]]; then
	printf ","
    fi
    printf "\n"
}

299
function return_output_json() {
300 301 302 303
    ## print json formatted output to std_out.
    ## called by fetch_alsa_outputinterfaces.
    json_cards="$1"
    json='{
304
 "alsa_outputdevices": [ 
305 306 307
    %s
  ]
}'
308
    printf "${json}\n" "${json_cards%,*}"
309 310 311 312 313 314
}





315 316 317 318 319
function fetch_alsa_outputinterfaces() {
    ## parses each output interface returned by `get_aplay_output'
    ## after filtering (when the appropriate commandline options are
    ## given), stores its capabilities in the appropriate global
    ## indexed arrays and displays them.
320
    json_output=
321
    msg=()
322 323 324
    aplay_lines=()
    integer_regexp='^[0-9]+$'
    aplay_card_regexp="^card[[:space:]][0-9]+:"
325
    ## exit on error
Ronald van Engelen's avatar
Ronald van Engelen committed
326 327
    aplay_output="$(get_aplay_output "${aplay_card_regexp}")" ||  \
	die "${aplay_output}"
328 329
    ## reset the counter for interfaces without filtering
    NR_AIFS_BEFOREFILTERING=0
330 331
    ## modify the filter for aplay -l when OPT_HWFILTER is set
    if [[ ! -z "${OPT_HWFILTER}" ]]; then
332 333
	# the portion without `hw:', eg 0,1
	alsa_filtered_hwaddr="${OPT_HWFILTER#hw:*}"
334 335
	alsa_filtered_cardnr="${alsa_filtered_hwaddr%%,*}"
	alsa_filtered_devicenr="${alsa_filtered_hwaddr##*,}"
336
	if [[ ! ${alsa_filtered_cardnr} =~ ${integer_regexp} ]] || \
337
	       [[ ! ${alsa_filtered_devicenr} =~ ${integer_regexp} ]]; then
338 339
	    msg+=("Invalid OPT_HWFILTER (\`${OPT_HWFILTER}') specified.")
	    msg+=("Should be \`hw:x,y' were x and y are both integers.")
340 341
	    printf -v msg_str "%s\n" "${msg[@]}"
	    die "${msg_str}"
342 343 344 345 346 347
	fi
	aplay_card_regexp="^card[[:space:]]${alsa_filtered_cardnr}:[[:space:]].*"
	aplay_device_regexp="[[:space:]]device[[:space:]]${alsa_filtered_devicenr}:"
	aplay_card_device_regexp="${aplay_card_regexp}${aplay_device_regexp}"
    else
	aplay_card_device_regexp="${aplay_card_regexp}"
348 349
    fi

350
    ## iterate each line of aplay output
351
    while read -r line ; do
352
	## filter for `^card' and then for `OPT_CUSTOMFILTER' to get matching
353
	## lines from aplay and store them in an array
354

355
	if [[ "${line}" =~ ${aplay_card_device_regexp} ]]; then
356 357 358
	    [[ ${DEBUG} ]] && \
		( debug ": aplay -l output line: \`${line}'" && \
		     debug ": with OPT_CUSTOMFILTER: ${OPT_CUSTOMFILTER}" )
359 360
	    ## raise the counter for interfaces without filtering
	    let NR_AIFS_BEFOREFILTERING+=1
361 362 363
	    if [[ "${OPT_CUSTOMFILTER}x" != "x" ]]; then
		## check if line matches `OPT_CUSTOMFILTER'
		if [[ "${line}" =~ ${OPT_CUSTOMFILTER} ]]; then
364 365
		    [[ ${DEBUG} ]] && \
			debug ":                match: ${line}"
366 367 368
		    ## store the line in an array
		    aplay_lines+=("${line}")
		else
369 370
		    [[ ${DEBUG} ]] && \
			debug ": no match with filter ${OPT_CUSTOMFILTER}: ${line}"
371 372
		fi
	    else
373 374 375 376 377 378
		## store the line in an array
		aplay_lines+=("${line}")
	    fi
	fi
    done <<< "${aplay_output}"

379
    ## check whether soundcards were found
380
    if [[ ${#aplay_lines[@]} -lt 1 ]]; then
381 382 383
	die "${#aplay_lines[@]} soundcards found"
    fi

384
    ## loop through each item in the array
385
    cur_aif_no=0
386
    for line in "${aplay_lines[@]}"; do
387
	((cur_aif_no++))
388 389
	## set if type to default (ie analog)
	alsa_if_type="ao"
390
	## construct bash regexp for sound device
391 392 393 394 395 396 397 398
	## based on aplay.c:
	## printf(_("card %i: %s [%s], device %i: %s [%s]\n"),
	## 1 card,
	## 2 snd_ctl_card_info_get_id(info),
	## 3 snd_ctl_card_info_get_name(info),
	## 4 dev,
	## 5 snd_pcm_info_get_id(pcminfo),
	## 6 snd_pcm_info_get_name(pcminfo));
399
	##
400
	## portion (ie before `,')
401
	alsa_dev_regexp="card[[:space:]]([0-9]+):[[:space:]](.*)[[:space:]]\[(.*)\]"
402
	## same for interface portion
403 404
	alsa_if_regexp=",[[:space:]]device[[:space:]]([0-9]+):[[:space:]](.*)[[:space:]]\[(.*)\]"
	alsa_dev_if_regexp="^${alsa_dev_regexp}${alsa_if_regexp}$"
405

406 407 408 409 410 411 412
	## unset / empty out all variables
	alsa_dev_nr=""
	alsa_dev_name=""
	alsa_dev_label=""
	alsa_if_nr=""
	alsa_if_name=""
	alsa_if_label=""
413

414
	## start matching and collect errors in array
415
	errors=()
416 417 418 419

	## see if the name contains square brackets, ie it ends with `]]'
	name=""
	alsacard=""
420 421
	separator_start="*##"
	separator_end="##*"
422 423 424
	name_re="card[[:space:]][0-9]+:[[:space:]](.*)\[.*\[.*\]\].*"
	brackets_re="card[[:space:]]([0-9]+):(.*\])\],[[:space:]](device[[:space:]][0-9]+:.*\])"
	if [[ "${line}" =~ ${brackets_re} ]]; then
425 426
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): #####: line with brackets \`${line}'"
427 428
	    if [[ "${line}" =~ ${name_re} ]]; then
		name="${BASH_REMATCH[1]}"
429 430
		[[ ${DEBUG} ]] && \
		    debug "(${LINENO}): #####: name \`${name}'"
431 432 433 434 435
	    fi
	fi
	if [[ ! -z "${name}" ]]; then
	    if [[ "${line}" =~ ${brackets_re} ]]; then
		## construct string without brackets
436
		alsacard="$(handle_doublebrackets "${BASH_REMATCH[2]}")"
437 438
		[[ ${DEBUG} ]] && \
		    debug "(${LINENO}): #####: alsacard: \`${alsacard}'"
439

440
		## replace `name [something]' with `name *##something##*'
441 442
		alsacard="${alsacard//\[/${separator_start}}"
		alsacard="${alsacard//\]/${separator_end}}"
443
		line="card ${BASH_REMATCH[1]}: ${name}[${alsacard}], ${BASH_REMATCH[3]}"
444 445
		[[ ${DEBUG} ]] && \
		    debug "(${LINENO}): #####: replace line with \`${line}'"
446 447
	    fi
	fi
448 449

	## match the current line with the regexp
450
	if [[ "${line}" =~ ${alsa_dev_if_regexp} ]]; then
451 452 453
	    [[ ! -z "${BASH_REMATCH[1]}" ]] && \
		alsa_dev_nr="${BASH_REMATCH[1]}" || \
		    errors+=("could not fetch device number")
454
	    if [[ ! -z "${BASH_REMATCH[2]}" ]]; then
455 456
		alsa_dev_name="${BASH_REMATCH[2]}"
		## reconstruct original name if it contained square brackets
457 458
		alsa_dev_name="${alsa_dev_name//${separator_start}/\[}"
		alsa_dev_name="${alsa_dev_name//${separator_end}/\]}"
459 460 461
	    else
		errors+=("could not fetch device name")
	    fi
462 463 464 465 466 467 468 469 470 471 472 473 474
	    [[ ! -z "${BASH_REMATCH[3]}" ]] && \
		alsa_dev_label="${BASH_REMATCH[3]}" || \
		    errors+=("could not fetch device label")
	    [[ ! -z "${BASH_REMATCH[4]}" ]] && \
		alsa_if_nr="${BASH_REMATCH[4]}" || \
		    errors+=("could not fetch interface number")
	    [[ ! -z "${BASH_REMATCH[5]}" ]] && \
		alsa_if_name="${BASH_REMATCH[5]}" || \
		    errors+=("could not fetch interface name")
	    [[ ! -z "${BASH_REMATCH[6]}" ]] && \
		alsa_if_label="${BASH_REMATCH[6]}" || \
		    errors+=("could not fetch interface label")
	    ## consider empty numbers and names of devices and interfaces fatal
475 476 477 478
	    if [[ -z ${alsa_dev_nr} ]] || \
		   [[ -z ${alsa_dev_name} ]] || \
		   [[ -z ${alsa_if_nr} ]] || \
		   [[ -z ${alsa_if_name} ]]; then
479 480
		printf -v msg_err "%s\n" "${errors[@]}"
		die "${mg_err}"
481 482
		break
	    fi
483 484
	    if [[ ${DEBUG} ]] && [[ "${#errors[@]}" -ne 0 ]]; then
		debug "(${LINENO}): $(printf "errors: %s\n" "${errors[@]}")"
485
	    fi
486

487
	    declare -a alsa_if_formats=()
488
	    alsa_if_hwaddress="hw:${alsa_dev_nr},${alsa_if_nr}"
489 490 491 492 493
	    ## construct the path to the character device for the
	    ## interface (ie `/dev/snd/xxx')
	    alsa_if_chardev="/dev/snd/pcmC${alsa_dev_nr}D${alsa_if_nr}p"
	    ## construct the path to the hwparams file
	    alsa_if_hwparamsfile="/proc/asound/card${alsa_dev_nr}/pcm${alsa_if_nr}p/sub0/hw_params"
494 495 496 497 498 499
	    ## before determining whether this is a usb device, assume
	    ## the monitor file is the hwparams file
	    alsa_if_monitorfile="${alsa_if_hwparamsfile}"
	    ## assume stream file for the interface (ie
	    ## `/proc/asound/cardX/streamY') to determine whether
	    ## the interface is a uac device, and if so, which class it is
500
	    alsa_if_streamfile="/proc/asound/card${alsa_dev_nr}/stream${alsa_if_nr}"
501
	    ## assume no uac device
502
	    alsa_if_uacclass="${MSG_PROP_NOTAPPLICABLE}"
503 504 505

	    if [[ ! -z ${TESTFILE} ]]; then
		## device is not real
506
		alsa_if_formats+=("(${MSG_ERROR_CHARDEV_NOFORMATS})")
507
		alsa_if_uacclass_nr="?"
508
	    else
509 510 511 512 513
		## check if the hwparams file exists
		if [[ ! -f "${alsa_if_hwparamsfile}" ]]; then
		    alsa_if_hwparamsfile="${alsa_if_hwparamsfile} (error: not accessible)"
		fi
		## check if the chardev exists
514
		if [[ ! -c "${alsa_if_chardev}" ]]; then
515 516
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): alsa_if_chardev \`${alsa_if_chardev}' is not a chardev."
517 518
		    alsa_if_chardev="${alsa_if_chardev} (${MSG_ERROR_NOT_CHARDEV})"
		else
519 520
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): alsa_if_chardev \`${alsa_if_chardev}' is a valid chardev."
521 522 523
		fi
		## check whether the monitor file exists; it always should
		if [[ ! -f ${alsa_if_monitorfile} ]]; then
524 525
		    msg_err="${alsa_if_monitorfile} ${MSG_ERROR_NOFILE} (${MSG_ERROR_UNEXPECTED})"
		    alsa_if_monitorfile="${msg_err}"
526 527
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): ${MSG_ERROR_UNEXPECTED}: alsa_if_monitorfile \
528 529 530 531 532
\`${alsa_if_monitorfile}' ${MSG_ERROR_NOFILE}"
		fi
		## check whether the streamfile exists; it only should
		## exist in the case of a uac interface
		if [[ ! -f "${alsa_if_streamfile}" ]]; then
533 534
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): alsa_if_streamfile \`${alsa_if_streamfile}' \
535 536
${MSG_ERROR_NOFILE}"
		    ## no uac interface
537
		    alsa_if_streamfile="${MSG_PROP_NOTAPPLICABLE}"
538
		else
539 540
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): using alsa_if_streamfile \`${alsa_if_streamfile}'."
541 542 543 544
		    ## set interface to usb out
		    alsa_if_type="uo"
		    ## uac devices will use the stream file instead of
		    ## hwparams file to monitor
545
		    ## alsa_if_monitorfile="${alsa_if_streamfile}"
546 547
		    ## get the type of uac endpoint
		    alsa_if_uac_ep="$(return_alsa_uac_ep "${alsa_if_streamfile}")"
548
		    # shellcheck disable=SC2181
549
		    if [[ $? -ne 0 ]]; then
550 551
			[[ ${DEBUG} ]] && \
			    debug "(${LINENO}): could not determine alsa_if_uac_ep."
552 553
			alsa_if_uacclass_nr="?"
		    else
554 555
			[[ ${DEBUG} ]] && \
			    debug "(${LINENO}): alsa_if_uac_ep set to \`${alsa_if_uac_ep}'."
556 557 558 559 560 561 562
			## lookup the uac class in the array for this type of endpoint (EP)
			## (for readability)
			alsa_if_uacclass="${UO_EP_LABELS[${alsa_if_uac_ep}]}"
			## the uac class number (0, 1, 2 or 3) according to ./sound/usb/card.h
			alsa_if_uacclass_nr="${alsa_if_uacclass% - *}"
			classnr_regexp='^[0-3]+$'
			if [[ ! ${alsa_if_uacclass_nr} =~ ${classnr_regexp} ]]; then
563 564
			    [[ ${DEBUG} ]] && \
				debug "(${LINENO}): invalid uac class number \`${alsa_if_uacclass_nr}'. \
565 566 567 568
${MSG_ERROR_UNEXPECTED}"
			    alsa_if_uacclass_nr="?"
			fi
		    fi
569

570 571
		fi
	    fi
572
	fi
573 574
	## for non-uac interfaces: check whether it is some other
	## digital interface
575
	if [[ ! "${alsa_if_type}" = "uo" ]]; then
576
	    for filter in "${DO_INTERFACE_FILTER[@]}"; do
577 578
		## `,,' downcases the string, while `*var*' does a
		## wildcard match
579
		if [[ "${alsa_if_name,,}" == *"${filter}"* ]]; then
580 581
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): match = ${alsa_if_name,,}: ${filter}"
582 583 584 585 586 587 588
		    ## set ao type to d(igital)o(out)
		    alsa_if_type="do"
		    ## exit this for loop
		    break
		fi
	    done
	fi
589 590 591 592
	## see if the interface type matches the user specified
	## filters and if so construct titles and store a pair of
	## hardware address and monitoring file in the proper array
	match=
593 594 595
	case "${alsa_if_type}" in
	    "ao")
		## only if neither `OPT_LIMIT_DO' and `OPT_LIMIT_UO' are set
596 597
		[[ ! -z ${OPT_LIMIT_DO} || ! -z ${OPT_LIMIT_UO} ]] && \
		    continue || match="true"
598 599 600
		;;
	    "do")
		## only if neither `OPT_LIMIT_AO' and `OPT_LIMIT_UO' are set
601 602
		[[ ! -z ${OPT_LIMIT_AO} || ! -z ${OPT_LIMIT_UO} ]] && \
		    continue || match="true"
603 604 605
		;;
	    "uo")
		## only if `OPT_LIMIT_AO' is not set
606 607
		[[ ! -z ${OPT_LIMIT_AO} ]] && \
		    continue || match="true"
608
	esac
609 610 611
	if [[ ! -z ${match} ]]; then
	    ## put each encoding format and possibily the sample rates
	    ## in an array
612 613 614 615 616
	    alsa_if_formats=()
	    formats_res_err=
	    formats_res="$(return_alsa_formats \
"${alsa_dev_nr}" \
"${alsa_if_nr}" \
617
"${alsa_if_type}" \
618 619
"${alsa_if_streamfile}" \
"${alsa_if_chardev}")"
620
	    # shellcheck disable=SC2181
621 622
	    if [[ $? -ne 0 ]]; then
		formats_res_err=1
623
	    fi
624
	    alsa_if_formats+=("${formats_res[@]}")
625
	    alsa_if_title_label="${ALSA_IF_LABELS[${alsa_if_type}]}"
626
	    ## reconstruct the label if it contained square brackets
627
	    if [[ "${alsa_dev_label}" =~ .*${separator_start}.* ]]; then
628 629 630
		alsa_dev_label="${alsa_dev_label//\*##/\[}"
		alsa_dev_label="${alsa_dev_label//##\*/\]}"
	    fi
631
	    ## construct the display title
632 633
	    printf -v alsa_if_display_title \
		   " %s) %s \`%s'" \
634
		   "${cur_aif_no}" \
635 636
		   "${alsa_if_title_label}" \
		   "${alsa_if_hwaddress}"
637 638 639 640 641 642
	    ## store the details of the current interface in global arrays
	    ALSA_AIF_HWADDRESSES+=("${alsa_if_hwaddress}")
	    ALSA_AIF_MONITORFILES+=("${alsa_if_monitorfile}")
	    ALSA_AIF_DISPLAYTITLES+=("${alsa_if_display_title}")
	    ALSA_AIF_DEVLABELS+=("${alsa_dev_label}")
	    ALSA_AIF_LABELS+=("${alsa_if_label}")
643
	    ALSA_AIF_UACCLASSES+=("${alsa_if_uacclass}")
644
	    ALSA_AIF_FORMATS="${alsa_if_formats[*]}"
645 646
	    ALSA_AIF_CHARDEVS+=("${alsa_if_chardev}")
	fi
647
	if [[ -z "${OPT_QUIET}" ]] && [[ "${OPT_JSON}x" == "x" ]]; then
648
	    ## print the list to std_err
649 650
	    res_human="$(return_output_human)" || exit 1
	    printf 1>&2 "%s\n" "${res_human}"
651 652 653 654 655 656 657
	fi
	if [[ "${OPT_JSON}x" != "x" ]]; then
	    if [[ ${cur_aif_no} -lt ${#aplay_lines[@]} ]]; then 
		printf -v json_output "%s%s\n" \
		       "${json_output}" \
		       "$(ret_json_card "${formats_res[@]}" "")"
	    fi
658 659
	fi
    done
660
    if [[ "${OPT_JSON}x" != "x" ]]; then
661 662
	res_json="$(return_output_json "${json_output}")" || exit 1
	printf "%s\n" "${res_json}"
663
    fi
664 665
}

666
function get_locking_process() {
667
    ## return a string describing the command and id of the
668
    ## process locking the audio interface with card nr $1 and dev nr
669
    ## $2 based on its status file in /proc/asound.
670 671 672
    ## returns a comma separated string containing the locking cmd and
    ## pid, or an error when the interface is not locked (ie
    ## 'closed').
673 674 675 676
    alsa_card_nr="$1"
    alsa_if_nr="$2"
    proc_statusfile="/proc/asound/card${alsa_card_nr}/pcm${alsa_if_nr}p/sub0/status"
    owner_pid=
677 678
    owner_stat=
    owner_cmd=
679 680
    parent_pid=
    parent_cmd=
681 682 683 684 685 686
    locking_cmd=
    locking_pid=
    ## specific for mpd: each alsa output plugin results in a locking
    ## process indicated by `owner_pid` in
    ## /proc/asound/cardX/pcmYp/sub0/status: `owner_pid   : 28022'
    ## this is a child process of the mpd parent process (`28017'):
687 688
    ##mpd(28017,mpd)-+-{decoder:flac}(28021)
    ##               |-{io}(28019)
689
    ##               |-{output:Peachtre}(28022) <<< owner_pid / child
690 691
    ##               `-{player}(28020)
    owner_pid_re="owner_pid[[:space:]]+:[[:space:]]+([0-9]+)"
692 693
    [[ ${DEBUG} ]] && \
	debug "examining status file ${proc_statusfile}." 
694 695 696 697
    while read -r line; do
	if [[ "${line}" =~ ${owner_pid_re} ]]; then
	    owner_pid="${BASH_REMATCH[1]}"
	    break
698 699
	elif [[ "${line}" == "closed" ]]; then
	    return 1
700 701
	fi
    done<"${proc_statusfile}"
702 703
    [[ ${DEBUG} ]] && \
	debug "done examining status file ${proc_statusfile}." 
704
    if [[ -z ${owner_pid} ]]; then
705
	## device is unused
706 707
	[[ ${DEBUG} ]] && \
	    debug "(${LINENO}): ${FUNCNAME[0]} called, but no owner_pid found in \`${proc_statusfile}'."
708
	return 1
709
    else
710 711
	[[ ${DEBUG} ]] && \
	    debug "(${LINENO}): found owner pid in status file \`${proc_statusfile}': \`${owner_pid}'."
712
    fi
713 714 715 716 717 718 719
    ## check if owner_pid is a child
    ## construct regexp for getting the ppid from /proc
    ## eg: /proc/837/stat:
    ## 837 (output:Pink Fau) S 1 406 406 0 -1 ...
    ## ^^^                       ^^^
    ## +++-> owner_pid           +++-> parent_pid
    parent_pid_re="(${owner_pid})[[:space:]]\(.*\)[[:space:]][A-Z][[:space:]][0-9]+[[:space:]]([0-9]+)"
720 721
    # shellcheck disable=SC2162
    read owner_stat < "/proc/${owner_pid}/stat"
722 723
    [[ ${DEBUG} ]] && \
	debug "(${LINENO}): owner_stat: \`${owner_stat}'"
724 725 726 727 728
    if [[ "${owner_stat}" =~ ${parent_pid_re} ]]; then
	parent_pid="${BASH_REMATCH[2]}"
	if [[ "x${parent_pid}" == "x${owner_pid}" ]]; then
	    ## device is locked by the process with id owner_pid, look up command
	    ## eg: /proc/837/cmdline: /usr/bin/mpd --no-daemon /var/lib/mpd/mpd.conf
729 730
	    # shellcheck disable=SC2162
	    read owner_cmd < "/proc/${owner_pid}/cmdline"
731 732
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): cmd \`${owner_cmd}' with id \`${owner_pid}' has no parent."
733 734
	    locking_pid="${owner_pid}"
	    locking_cmd="${owner_cmd}"
735
	else
736
	    ## device is locked by the parent of the process with owner_pid
737 738 739 740
	    # shellcheck disable=SC2162	    
	    read owner_cmd < "/proc/${owner_pid}/cmdline"
	    # shellcheck disable=SC2162	    
	    read parent_cmd < "/proc/${parent_pid}/cmdline"
741 742
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): cmd \`${owner_cmd}' with id \`${owner_pid}' \
743 744 745
has parent cmd \`${parent_cmd}' with id \`${parent_pid}'."
	    locking_pid="${parent_pid}"
	    locking_cmd="${parent_cmd}"
746
	fi
747 748 749 750 751
	## return comma separated list (pid,cmd) to calling function
	locking_cmd="$(while read -r -d $'\0' line; do \
			     printf "%s " "${line}"; \
			     done< "/proc/${locking_pid}/cmdline")"
	printf "%s,%s" "${locking_pid}" "${locking_cmd%% }"
752 753 754 755
    else
	## should not happen; TODO: handle
	parent_pid=
    fi 
756 757
}

758 759 760 761
function ret_highest_alsa_samplerate() {
    ## check the highest supported rate of type $3 for format $2 on
    ## interface $1
    ## returns the highest supported rate.
762
    alsa_if_hwaddress="$1"
763 764 765 766 767 768 769 770 771 772 773 774 775
    encoding_format="$2"
    type="$3"
    if [[ "${type}" == "audio" ]]; then
	rates=(${SAMPLERATES_AUDIO[@]})
    else
	rates=(${SAMPLERATES_VIDEO[@]})
    fi
    for rate in "${rates[@]}"; do
	res="$(check_samplerate "${alsa_if_hwaddress}" "${encoding_format}" "${rate}")"
	# shellcheck disable=SC2181	
	if [[ $? -ne 0 ]]; then
	    ## too high; try next one
	    continue
776
	else
777 778
	    ## match; return it
	    printf "%s" "${rate}"
779 780 781
	    break
	fi
    done
782
}
783

784 785 786 787 788 789
function ret_supported_alsa_samplerates() {
    ## use aplay to get supported sample rates for playback for
    ## specified non-uac interface ($1) and encoding format ($2).
    ## returns a space separated list of valid rates.
    alsa_if_hwaddress="$1"
    encoding_format="$2"
790
    declare -a rates
791 792
    [[ ${DEBUG} ]] && \
	debug "(${LINENO}): getting sample rates for device \`${alsa_if_hwaddress}' \
793 794 795 796 797
using encoding_format \`${encoding_format}'."    
    ## check all audio/video rates from high to low; break when rate is
    ## supported while adding all the lower frequencies
    highest_audiorate="$(ret_highest_alsa_samplerate \
"${alsa_if_hwaddress}" "${encoding_format}" "audio")"
798 799
    highest_videorate="$(ret_highest_alsa_samplerate \
"${alsa_if_hwaddress}" "${encoding_format}" "video")"
800 801 802
    for rate in "${SAMPLERATES_AUDIO[@]}"; do
	if [[ ${rate} -le ${highest_audiorate} ]]; then
	    ## supported; assume all lower rates are supported too
803
	    rates+=("${rate}")
804 805 806 807 808
	fi		    
    done
    for rate in "${SAMPLERATES_VIDEO[@]}"; do
	if [[ ${rate} -le ${highest_videorate} ]]; then
	    ## supported; assume all lower rates are supported too
809
	    rates+=("${rate}")
810
	fi		    
811
    done
812 813
    ## sort and retrun trhe newline separated sample rates
    sort -u -n <(printf "%s\n" "${rates[@]}")
814 815 816
}

function check_samplerate() {
817 818 819
    ## use aplay to check if the specified alsa interface ($1)
    ## supports encoding format $2 and sample rate $3
    ## returns a string with the supported sample rate or nothing
820 821 822
    alsa_if_hwaddress="$1"
    format="$2"
    samplerate="$3"
Ronald van Engelen's avatar
Ronald van Engelen committed
823 824 825 826 827 828
    declare -a aplay_args_early
    aplay_args_early+=(--device="${alsa_if_hwaddress}")
    aplay_args_early+=(--format="${format}")
    aplay_args_early+=(--channels="2")
    aplay_args_early+=(--nonblock)
    declare -a aplay_args_late
829
    ## set up regular expressions to match aplay's output errors
830 831
    ## unused
    # shellcheck disable=SC2034
832
    rate_notaccurate_re=".*Warning:.*not[[:space:]]accurate[[:space:]]\(requested[[:space:]]=[[:space:]]([0-9]+)Hz,[[:space:]]got[[:space:]]=[[:space:]]([0-9]+)Hz\).*"
833
    # shellcheck disable=SC2034    
834
    badspeed_re=".*bad[[:space:]]speed[[:space:]]value.*"
835
    # shellcheck disable=SC2034    
836
    sampleformat_nonavailable_re=".*Sample[[:space:]]format[[:space:]]non[[:space:]]available.*"
837
    # shellcheck disable=SC2034    
838
    wrongformat_re=".*wrong[[:space:]]extended[[:space:]]format.*"
839
    ## used
840
    default_re=".*Playing[[:space:]]raw[[:space:]]data.*"
841 842
    [[ ${DEBUG} ]] && \
	debug "(${LINENO}): testing rate ${samplerate}"
Ronald van Engelen's avatar
Ronald van Engelen committed
843
    unset aplay_args_late
844
    ## set fixed sample rate
Ronald van Engelen's avatar
Ronald van Engelen committed
845
    aplay_args_late+=(--rate="${samplerate}")
846 847 848 849 850
    ## generate aplay error using random noise to check whether sample
    ## rate is supported for this interface and format
    printf -v aplay_args "%s " "${aplay_args_early[@]} ${aplay_args_late[@]}"
    read -r firstline<<<"$(return_reversed_aplay_error "${aplay_args}")" || return 1
    if [[ "${firstline}" =~ ${default_re} ]]; then
851
	[[ ${DEBUG} ]] && \
852 853
	    debug "(${LINENO}): success"
	printf "%s" "${samplerate}"
854
    else
855
	return 1
856
    fi
857
}
858

859 860 861 862 863
function return_reversed_aplay_error() {
    ## force aplay to output error message containing supported
    ## encoding formats, by playing PSEUDO_AUDIO in a non-existing
    ## format.
    ## returns the output of aplay while reversing its return code
Ronald van Engelen's avatar
Ronald van Engelen committed
864
    aplay_args="$1"
865 866 867 868
    cmd_aplay="${CMD_APLAY} ${aplay_args}"
    LANG=C ${cmd_aplay} 2>&1 <<< "${PSEUDO_SILENT_AUDIO}" || \
	return 0 && \
	    return 1
Ronald van Engelen's avatar
Ronald van Engelen committed
869
}
870

871
function return_nonuac_formats() {
872
    ## use aplay to determine supported formats of non-uac interface (hw:$1,$2)
873 874
    alsa_dev_nr="$1"
    alsa_if_nr="$2"
Ronald van Engelen's avatar
Ronald van Engelen committed
875 876 877 878
    aplay_args=(--device=hw:${alsa_dev_nr},${alsa_if_nr})
    aplay_args+=(--channels=2)
    aplay_args+=(--format=MPEG)
    aplay_args+=(--nonblock)
879 880
    printf -v str_args "%s " "${aplay_args[@]}"
    return_reversed_aplay_error "${str_args}" || \
Ronald van Engelen's avatar
Ronald van Engelen committed
881
	return 1
882 883
}

884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946
function return_uac_formats_rates() {
    ## get encodings formats with samplerates for uac type interface
    ## using its streamfile $1 (which saves calls to applay).
    ## returns newline separated list (FORMAT:RATE,RATE,...).
    alsa_if_streamfile="$1"
    interface_re="^[[:space:]]*Interface[[:space:]]([0-9])"
    format_re="^[[:space:]]*Format:[[:space:]](.*)"
    rates_re="^[[:space:]]*Rates:[[:space:]](.*)"
    capture_re="^Capture:"
    inside_interface=
    format_found=
    declare -A uac_formats_rates
    ## iterate lines in the streamfile
    while read -r line; do
	if [[ "${line}" =~ ${capture_re} ]]; then
	    ## end of playback interfaces
	    break
	else
	    ## we're not dealing with a capture interface
	    if [[ "${line}" =~ ${interface_re} ]]; then
		## new interface found
		inside_interface=true
		## reset (previous) format_found
		format_found=
		## continue with next line
	    else
		## continuation of interface 
		if [[ "${inside_interface}x" != "x" ]]; then
		    ## parse lines below `Interface:`
		    if [[ "${format_found}x" == "x" ]]; then
			## check for new `Format:`
			if [[ "${line}" =~ ${format_re} ]]; then
			    ## new format found
			    format_found="${BASH_REMATCH[1]}"
			    uac_formats_rates[${format_found}]=""
			    [[ ${DEBUG} ]] && \
				debug "(${LINENO}): format found: \`${format_found}'"
			    ## next: sample rates or new interface
			fi
		    else
			## parse lines below `Format:`
			if [[ "${line}" =~ ${rates_re} ]]; then
			    ## sample rates for interface/format found;
			    ## return and reset both
			    uac_formats_rates[${format_found}]="${BASH_REMATCH[1]}"
			    [[ ${DEBUG} ]] && \
				debug "(format=${format_found}) \
rates=${BASH_REMATCH[1]}"
			    format_found=
			    inside_interface=
			    continue
			fi
		    fi
		fi
	    fi
	fi
    done<"${alsa_if_streamfile}"
    for format in "${!uac_formats_rates[@]}"; do
	printf "%s:%s\n" \
	       "${format}" "${uac_formats_rates[${format}]// /}"
    done
}

947
function return_alsa_formats() {
948 949 950 951
    ## fetch and return a comma separated string of playback formats
    ## for the interface specified in $1, of type $2. For non-uac
    ## interfaces: feed dummy input to aplay (--format=MPEG). For uac
    ## types: filter it directly from its stream file $3.
Ronald van Engelen's avatar
Ronald van Engelen committed
952
    alsa_dev_nr="$1"
953 954 955 956
    alsa_if_nr="$2"
    alsa_if_type="$3"
    alsa_if_streamfile="$4"
    alsa_if_chardev="$5"
Ronald van Engelen's avatar
Ronald van Engelen committed
957 958
    format="${format:-}"
    rawformat="${rawformat:-}"
959
    formats=()
Ronald van Engelen's avatar
Ronald van Engelen committed
960 961
    parent_pid=
    parent_cmd=
962
    declare -A uac_formats
963
    if [[ "${alsa_if_type}" = "uo" ]]; then
964 965 966 967 968 969 970
	## uac type; use streamfile to get encoding formats and/or
	## samplerates (in the form of 'FORMAT: RATE RATE ...').
	while read line; do
	    key="${line%:*}"
	    value="${line//${key}:/}"
	    uac_formats["${key}"]="${value}"
	done< <(return_uac_formats_rates "${alsa_if_streamfile}")
971
	## return the formatted line(s)
972 973 974
	if [[ "${OPT_SAMPLERATES}x" == "x" ]]; then
	    ## print comma separated list of formats
	    # shellcheck disable=SC2068
975
	    printf -v str_formats "%s, " "${!uac_formats[@]}"
976 977
	    printf "%-20s" "${str_formats%*, }"
	else	    
978
	    ## for each format, print "FORMAT1:rate1,rate2,..."
979
	    # shellcheck disable=SC2068
980
	    for key in ${!uac_formats[@]}; do
981
	 	printf "%s:%s\n" "${key}" "${uac_formats[${key}]}"
982 983 984
	    done
	fi
    else
985 986
	## non-uac type: if interface is not locked, use aplay to
	## determine formats
987 988
	## because of invalid file format, aplay is forced to return
	## supported formats (=200 times faster than --dump-hw-params)
989
	declare -a rawformats
990
	format_re="^-[[:space:]]+([[:alnum:]_]*)$"
991
	res="$(get_locking_process "${alsa_dev_nr}" "${alsa_if_nr}")"
992
	# shellcheck disable=SC2181
993
	if [[ $? -ne 0 ]]; then
994
	    ## device is not locked, iterate aplay output
995 996
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): device is not locked; will iterate aplay_out"
997 998 999 1000
	    while read -r line; do
		if [[ "${line}" =~ ${format_re} ]]; then
		    rawformats+=(${BASH_REMATCH[1]})
		fi
1001
	    done< <(return_nonuac_formats "${alsa_dev_nr}" "${alsa_if_nr}") || return 1
1002
	    ## formats (and minimum/maximum sample rates) gathered, check if
1003
	    ## all sample rates should be checked
1004
	    [[ ${DEBUG} ]] && debug "$(declare -p rawformats)"
1005 1006 1007 1008
	    if [[ "${OPT_SAMPLERATES}x" == "x" ]]; then
		## just return the comma separated format(s)
		printf -v str_formats "%s, " "${rawformats[@]}"
		printf "%-20s" "${str_formats%*, }"
1009 1010 1011 1012 1013
	    else
		## check all sample rates for each format.  warning:
		## slowness ahead for non-uac interfaces, because of
		## an aplay call for each unsupported sample rate + 1
		## and each format
1014
		for rawformat in "${rawformats[@]}"; do
1015 1016 1017 1018 1019 1020 1021 1022 1023
		    sorted_rates=""
		    while read line; do
			sorted_rates+="${line},"
			#printf -v str_rates "%s " "${line}"
		    done< <(ret_supported_alsa_samplerates \
				"${alsa_if_hwaddress}" "${rawformat}")
		    ## return each format newline separated with a space
		    ## separated list of supported sample rates
		    printf "%s:%s\n" "${rawformat}" "${sorted_rates%*,}"
1024 1025
		done
	    fi
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
	else
	    ## in use by another process
	    ## res contains pid,cmd of locking process
	    locking_pid="${res%,*}"
	    locking_cmd="${res#*,}"
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): \
device is in use by command ${locking_cmd} with process id ${locking_pid}."
	    ## return the error instead of the formats
	    printf "by command \`%s' with PID %s." \
		   "${locking_cmd}" "${locking_pid}"
	    return 1  
1038
	fi
1039
    fi
1040 1041 1042
}

function return_alsa_uac_ep() {
1043
    ## returns the usb audio class endpoint as a fixed number.
1044
    ## needs path to stream file as single argument ($1)
1045 1046 1047 1048 1049 1050
    ## based on ./sound/usb/proc.c:
    ##  printf "    Endpoint: %d %s (%s)\n",
    ##   1: fp->endpoint & USB_ENDPOINT_NUMBER_MASK (0x0f) > [0-9]
    ## TODO: unsure which range this is; have seen 1, 3 and 5
    ##   2: USB_DIR_IN: "IN|OUT",
    ##   3: USB_ENDPOINT_SYNCTYPE: "NONE|ASYNC|ADAPTIVE|SYNC"
1051
    alsa_if_streamfile_path="$1"
1052
    ep_mode=""
1053 1054 1055 1056 1057 1058 1059 1060 1061
    ep_label_filter="Endpoint:"
    ep_label_regexp="^[[:space:]]*${ep_label_filter}"
    ep_num_filter="([0-9]+)"                         #1
    ep_num_regexp="[[:space:]]${ep_num_filter}"
    ep_direction_filter="OUT"
    ep_direction_regexp="[[:space:]]${ep_direction_filter}"
    ep_synctype_filter="(${UO_EP_NONE_FILTER}|${UO_EP_ADAPT_FILTER}|${UO_EP_ASYNC_FILTER}|${UO_EP_SYNC_FILTER})"                                   #2
    ep_synctype_regexp="[[:space:]]\(${ep_synctype_filter}\)$"
    ep_regexp="${ep_label_regexp}${ep_num_regexp}${ep_direction_regexp}${ep_synctype_regexp}"
1062
    ## iterate the contents of the streamfile
1063
    while read -r line; do
1064 1065
	if [[ "${line}" =~ ${ep_regexp} ]]; then
	    ep_mode="${BASH_REMATCH[2]}"
1066 1067
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): matching endpoint found in line \`${line}': \`${ep_mode}'."
1068
	    break
1069
	fi
1070
    done<"${alsa_if_streamfile_path}"
1071
    if [[ "${ep_mode}x" == "x" ]]; then
1072 1073
	[[ ${DEBUG} ]] && \
	    debug "(${LINENO}): no matching endpoints found. ${MSG_ERROR_UNEXPECTED}"
1074 1075
	return 1
    else
1076
	## return the filtered endpoint type
1077 1078
	printf "%s" "${ep_mode}"
    fi
1079 1080 1081 1082 1083 1084 1085
}


### command line parsing

function analyze_opt_limit() {
    ## check if the argument for the `-l' (limit) option is proper
1086 1087
    option="$1"
    opt_limit="${2-}"
1088 1089
    declare -a args
    prev_opt=0
1090
    declare msg
1091
    case ${opt_limit} in
1092
        a|analog)
1093
	    OPT_LIMIT_AO="True"
1094 1095
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): OPT_LIMIT_AO set to \`${OPT_LIMIT_AO}'"
1096
	    return 0
1097
	    ;;
1098
        u|usb|uac)
1099
	    OPT_LIMIT_UO="True"
1100 1101
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): OPT_LIMIT_UO set to \`${OPT_LIMIT_UO}'"
1102
	    return 0
1103 1104 1105
	    ;;
        d|digital)
	    OPT_LIMIT_DO="True"
1106 1107
	    [[ ${DEBUG} ]] && \
		debug "(${LINENO}): OPT_LIMIT_DO set to \`${OPT_LIMIT_DO}'"
1108
	    return 0
1109 1110
	    ;;
	*)
1111
	    ## construct list of option pairs: "x (or 'long option')"
1112
	    for arg_index in "${!OPT_LIMIT_ARGS[@]}"; do
1113 1114 1115 1116 1117 1118 1119 1120 1121 1122
		if [[ $(( arg_index % 2 )) -eq 0 ]]; then
		    ## even (short option): new array item
		    args+=("")
		else
		    ## odd (long option): add value to previous array item
		    prev_opt=$(( arg_index - 1 ))
		    args[-1]="${OPT_LIMIT_ARGS[${prev_opt}]} (or '${OPT_LIMIT_ARGS[${arg_index}]}')"
		fi
	    done
	    args_val=$(printf "%s, " "${args[@]}")
1123
	    # shellcheck disable=SC2059
1124 1125 1126 1127
	    msg_vals="$(printf " ${args_val%*, }\n")"
	    msg_custom="maybe you could try to use the custom filter option, eg:"
	    msg_trail="for limit option \`${option}' specified. should be one of:\n"
	    if [[ ! -z ${opt_limit} ]]; then
1128 1129 1130 1131 1132
		str_re=""
		for (( i=0; i<${#opt_limit}; i++ )); do
		    char="${opt_limit:$i:1}"
		    str_re+="[${char^^}${char,,}]"
		done
1133
		msg="invalid value \`${opt_limit}' "
1134
		# shellcheck disable=SC2059
1135 1136
		msg+="$(printf "${msg_trail}${msg_vals}\n${msg_custom}")"
		## display instructions to use the custom filter
1137
		msg+="$(printf "\n bash $0 -c \"%s\"\n" "${str_re}")"
1138
	    else
1139
		# shellcheck disable=SC2059
1140
		msg="$(printf "no value for ${msg_trail}${msg_vals}")"
1141
	    fi
1142

1143 1144
	    ## display the option pairs, stripping the trailing comma
	    printf "%s\n" "${msg}" 1>&2;
1145
	    exit 1
1146 1147 1148 1149
    esac
}


1150
function display_usageinfo() {
1151 1152 1153
    ## display syntax and exit
    msg=$(cat <<EOF
Usage:
1154
${APP_NAME_AC} [ -l a|d|u ]  [ -c <filter> ] [-a <hwaddress>] [-s] [ -q ]
1155

1156 1157
Displays a list of each alsa audio output interface with its details
including its alsa hardware address (\`hw:x,y').
1158 1159

The list may be filtered by using the limit option \`-l' with an
Ronald van Engelen's avatar
Ronald van Engelen committed
1160 1161
argument to only show interfaces that fit the limit. In addition, a
custom filter may be specified as an argument for the \`c' option.
1162

1163
The \`-q (quiet)' and \`-a (address)' options are meant for usage in
Ronald van Engelen's avatar
Ronald van Engelen committed
1164 1165 1166
other scripts. The script returns 0 on success or 1 in case of no
matches or other errors.

1167
  -l TYPEFILTER, --limit TYPEFILTER
Ronald van Engelen's avatar
Ronald van Engelen committed
1168
                     Limit the interfaces to TYPEFILTER. Can be one of
1169 1170 1171
                     \`a' (or \`analog'), \`d' (or \`digital'), \`u'
                     (or \`usb'), the latter for USB Audio Class (UAC1
                     or UAC2) devices.
1172
  -c REGEXP, --customlimit REGEXP
1173
                     Limit the available interfaces further to match
Ronald van Engelen's avatar
Ronald van Engelen committed
1174
                     \`REGEXP'.
1175
  -a HWADDRESS, --address HWADDRESS
1176
                     Limit the returned interface further to the one
Ronald van Engelen's avatar
Ronald van Engelen committed
1177 1178 1179 1180 1181 1182 1183 1184 1185
                     specified with HWADDRESS, eg. \`hw:0,1'
  -s, --samplerates  Adds a listing of the supported sample rates for
                     each format an interface supports.
                     CAUTION: Besides being slow this option
                              PLAYS NOISE ON EACH OUTPUT!
  -q, --quiet        Surpress listing each interface with its details,
                     ie. only store the details of each card in the
                     appropriate arrays.
  -h, --help         Show this help message
1186 1187 1188

Version ${APP_VERSION}. For more information see:
${APP_INFO_URL}
1189
EOF
1190
       )
1191
    printf "%s\n" "${msg}" 1>&2;
1192 1193
}

1194

1195
function analyze_command_line() {
1196 1197 1198 1199 1200 1201
    ## parse command line arguments using the `manual loop` method
    ## described in http://mywiki.wooledge.org/BashFAQ/035.
    while :; do
        case "${1:-}" in
            -l|--limit)
		if [ -n "${2:-}" ]; then
1202 1203
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): $(printf "option \`%s' set to \`%s'.\n" "$1" "$2")"
1204 1205 1206 1207 1208 1209 1210
		    analyze_opt_limit "$1" "$2"
		    shift 2
                    continue
		else
		    analyze_opt_limit "$1"
                    exit 1
		fi
1211
		;;
1212 1213
	    -c|--customfilter)
		if [ -n "${2:-}" ]; then
1214 1215
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): $(printf "option \`%s' set to \`%s'.\n" "$1" "$2")"
1216
		    OPT_CUSTOMFILTER="${2}"
1217 1218 1219 1220 1221 1222
		    shift 2
                    continue
		else
                    printf "ERROR: option \`%s' requires a non-empty argument.\n" "$1" 1>&2
                    exit 1
		fi
1223
		;;
1224 1225
            -a|--address)
		if [ -n "${2:-}" ]; then
1226 1227
		    [[ ${DEBUG} ]] && \
			debug "(${LINENO}): option \`$1' set to \`$2'"
1228 1229 1230 1231 1232 1233 1234 1235
		    OPT_HWFILTER="$2"
		    shift 2
                    continue
		else
                    printf "ERROR: option \`%s' requires a alsa hardware address \
as an argument (eg \`hw:x,y')\n" "$1" 1>&2
                    exit 1
		fi
1236
		;;
1237
            -s|--samplerates)
1238
		## deprecated
1239 1240
		[[ ${DEBUG} ]] && \
		    debug "(${LINENO}): option \`$1' set"
1241
		OPT_SAMPLERATES=true
1242
		shift
1243
                continue