├── README.md ├── confs-available ├── README.md ├── network.conf ├── db_file.conf ├── plugin-playlist-lastfm.conf ├── plugin-despotify.conf ├── zeroconf.conf ├── database-proxy.conf ├── tcp-keep-alive.conf ├── output-audio-fifo-visualization.conf ├── general.conf ├── paths.conf ├── client-limits.conf ├── README └── output-audio-alsa.conf ├── confs-enabled ├── README ├── README.md ├── 02_paths.conf ├── 03_general.conf ├── 05_network.conf ├── 06_zeroconf.conf ├── 04_client-limits.conf └── 01_output-audio-alsa.conf ├── README ├── .gitignore ├── LICENSE ├── examples ├── bash-example.sh ├── systemd_mpd-configure.service ├── get-interfaces.py └── get-interfaces-for-python.sh ├── mpd-configure.conf ├── mpd-monitor ├── mpd-configure └── alsa-capabilities /README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /confs-available/README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /confs-enabled/README: -------------------------------------------------------------------------------- 1 | ../confs-available/README -------------------------------------------------------------------------------- /confs-enabled/README.md: -------------------------------------------------------------------------------- 1 | ../confs-available/README.md -------------------------------------------------------------------------------- /confs-enabled/02_paths.conf: -------------------------------------------------------------------------------- 1 | ../confs-available/paths.conf -------------------------------------------------------------------------------- /confs-enabled/03_general.conf: -------------------------------------------------------------------------------- 1 | ../confs-available/general.conf -------------------------------------------------------------------------------- /confs-enabled/05_network.conf: -------------------------------------------------------------------------------- 1 | ../confs-available/network.conf -------------------------------------------------------------------------------- /confs-enabled/06_zeroconf.conf: -------------------------------------------------------------------------------- 1 | ../confs-available/zeroconf.conf -------------------------------------------------------------------------------- /confs-enabled/04_client-limits.conf: -------------------------------------------------------------------------------- 1 | ../confs-available/client-limits.conf -------------------------------------------------------------------------------- /confs-enabled/01_output-audio-alsa.conf: -------------------------------------------------------------------------------- 1 | ../confs-available/output-audio-alsa.conf -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: README for mpd-configure 4 | --- 5 | 6 | Repository moved to https://gitlab.com/ronalde/mpd-configure due to Microsoft deal 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cat-stevens-wild-world-hdtracks-24-192.wav 2 | tests/samples/cat-stevens-wild-world-hdtracks-24-192.wav 3 | tests/samples/cat-stevens-wild-world-hdtracks-24-96.wav 4 | tests/samples/cat-stevens-wild-world-hdtracks-32-96.wav 5 | .design 6 | -------------------------------------------------------------------------------- /confs-available/network.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### general network settings, see `client_limits.conf` for network 5 | ### resource limits. 6 | ### 7 | 8 | ## mpd should listen on this interface. 9 | bind_to_address "${G_NETWORK_BINDTOADDRESS:-${CONF_MPD_NETWORK_ADDRESS}}" 10 | 11 | ## mpd should listen on this port. 12 | port "${G_NETWORK_PORT:-${CONF_MPD_NETWORK_PORT}}" 13 | 14 | -------------------------------------------------------------------------------- /confs-available/db_file.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### common configuration parameters regarding the database used by mpd. 5 | ### 6 | 7 | ## (required) path to the file where mpd keeps its 'database', usually 8 | ## called `tag_cache` 9 | ## 10 | ## conflicts with the database proxy plugin (in 11 | ## `../confs-available/database-proxy.conf`) 12 | ## 13 | db_file "${G_PATHS_DBFILE:-${CONF_MPD_DBFILE}}" 14 | 15 | -------------------------------------------------------------------------------- /confs-available/plugin-playlist-lastfm.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### set preferences for the lastfm scrobbler plugin. 5 | ### 6 | 7 | playlist_plugin { 8 | ## name of the playlist in mpd 9 | name "${G_PLUGINPLAYLISTLASTFM_NAME:-Last FM}" 10 | ## username of a lastfm account 11 | user "${G_PLUGINPLAYLISTLASTFM_USER}" 12 | ## password of a lastfm account 13 | password "${G_PLUGINPLAYLISTLASTFM_PASSWORD}" 14 | } 15 | -------------------------------------------------------------------------------- /confs-available/plugin-despotify.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### settings for the (not working?) despotify plugin. 5 | ### 6 | 7 | ## username of a spotify account 8 | despotify_user "${G_PLUGINDESPOTIFY_DESPOTIFYUSER}" 9 | 10 | ## password for despotify_user 11 | despotify_password "${G_PLUGINDESPOTIFY_DESPOTIFYPASSWORD}" 12 | 13 | ## requested highest bitrates for spotify 14 | despotify_high_bitrate "${G_PLUGINDESPOTIFY_DESPOTIFYHIGHBITRATE:-yes}" 15 | -------------------------------------------------------------------------------- /confs-available/zeroconf.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### start zeroconf section. 5 | ### 6 | 7 | ## if set service information will be published with Zeroconf (through 8 | ## avahi or bonjour). 9 | zeroconf_enabled "${G_ZEROCONF_ZEROCONFENABLED:-True}" 10 | 11 | ## the service name to publish. The script by default uses 12 | ## `MPD through ${ALSA_AIF_DEVLABEL}' 13 | ## as the name for the published service, where ALSA_AIF_DEVLABEL 14 | ## contains the display name of the audio interface. 15 | zeroconf_name "${G_ZEROCONF_ZEROCONFNAME:-${CONF_ZEROCONF_LABEL}}" 16 | -------------------------------------------------------------------------------- /confs-available/database-proxy.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### common configuration parameters regarding using a database proxy with mpd. 5 | ### 6 | 7 | ## the database (usually called `tag_cache`) is provided and managed 8 | ## either by this mpd server, using the 'db_file' parameter, or by a 9 | ## remote mpd host (which does have a db_file parameter) in conjuction 10 | ## with the following proxy setting. 11 | ## 12 | ## In the latter case the 'host' parameter below should point at the 13 | ## mpd host which does have the db_file parameter. 14 | ## 15 | ## remember, either enable this plugin or set the 'db_file' parameter 16 | ## (using the provided '../confs-available/database.conf' file) 17 | 18 | database { 19 | plugin "proxy" 20 | host "${G_PATHS_DATABASEPROXYHOST:-${CONF_MPD_DATABASEPROXYHOST}}" 21 | } 22 | -------------------------------------------------------------------------------- /confs-available/tcp-keep-alive.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### set limits to system and network resource usage, see 5 | ### source at `./src/client/ClientGlobal.cxx' for documentation. 6 | ### 7 | ### ADVANCED USES ONLY 8 | ### in normal circumstances this file should not be necessary. 9 | 10 | ## enable tcp keepalive on new client connections. 11 | tcp_keep_alive "${G_TCPKEEPALIVE_TCPKEEPALIVE}" 12 | 13 | ## time in seconds since last communication before keepalive probing is started. 14 | tcp_keep_alive_idle "${G_TCPKEEPALIVE_TCPKEEPALIVEIDLE}" 15 | 16 | ## time in seconds between keepalive probes. 17 | tcp_keep_alive_interval "${G_TCPKEEPALIVE_TCPKEEPALIVEINTERVAL}" 18 | 19 | ## number of failed probes before the connection is pronounced dead. 20 | tcp_keep_alive_count "${G_TCPKEEPALIVE_TCPKEEPALIVECOUNT}" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Ronald van Engelen, all rights reserved. 2 | This is free software. It is licensed for use, modification and 3 | redistribution under the terms of the 4 | GNU General Public License, version 3 or later 5 | 6 | 7 | mpd-configure is free software: you can redistribute it and/or modify it 8 | under the terms of the GNU General Public License as published by the 9 | Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | mpd-configure is distributed in the hope that it will be useful, but 13 | WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | See the GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License along 18 | with this program. If not, see . 19 | 20 | the GNU General Public License, version 3 or later 21 | -------------------------------------------------------------------------------- /examples/bash-example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## sample script for advanced usage of alsa-capabilities. this script 3 | ## returns the monitor file for the interface specified in $1. if 4 | ## empty uses `hw:0,0`. 5 | 6 | HWADDRESS="${1:-hw:0,0}" 7 | 8 | ## store the monitorfile 9 | declare -a ALSA_AIF_MONITORFILES=() 10 | 11 | if [[ -f alsa-capabilities ]]; then 12 | source alsa-capabilities 13 | elif [[ -f ../alsa-capabilities ]]; then 14 | source ../alsa-capabilities 15 | elif [[ -f $(pwd)/alsa-capabilities ]]; then 16 | source $(pwd)/alsa-capabilities 17 | else 18 | printf "could not find \`alsa-capabilities'.\n" 19 | exit 1 20 | fi 21 | 22 | ## call return_alsa_interface from alsa-capabilities with the `-a` 23 | ## option set to $HWADDRESS 24 | return_alsa_interface -a "${HWADDRESS}" -q 25 | 26 | printf "the audio card with alsa hardware address %s can be monitored with:\n" "${HWADDRESS}" 1>&2; 27 | ## print the resulting path to the monitoring file to std_out 28 | printf "%s\n" "${ALSA_AIF_MONITORFILES[@]}" 29 | -------------------------------------------------------------------------------- /confs-available/output-audio-fifo-visualization.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### Defines a fifo audio output section and other parameters which 5 | ### can be used for visualization of the audio stream played by mpd. 6 | ### NOTE: this demands extra processing for the host running mpd and 7 | ### should not be considered suitable for audiophile purposes. 8 | ### 9 | ### ncmpcpp can be made to visualize this audio output, by adding the 10 | ### following lines to its configuration file ~/.ncmpcpp/config: 11 | ## visualizer_fifo_path = "/tmp/mpd.fifo" 12 | ## visualizer_output_name = "my_fifo" 13 | ## visualizer_sync_interval = "30" 14 | ## visualizer_in_stereo = "yes" 15 | ## visualizer_type = "wave" (spectrum/wave) 16 | ## visualizer_type = "spectrum" (spectrum/wave) 17 | ## visualizer_look = "+|" 18 | ### (EOF) 19 | ### 20 | ### see source at `./src/output/plugins/FifoOutputPlugin.cxx' for 21 | ### documentation. 22 | ### 23 | 24 | ## define the audio output 25 | audio_output { 26 | type "fifo" 27 | name "fifo audio output for visualization" 28 | path "/tmp/mpd.fifo" 29 | #format "44100:16:2" 30 | } 31 | -------------------------------------------------------------------------------- /confs-available/general.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### general parameters, ie not specific to (audio) outputs, (network) 5 | ### inputs or plugins. 6 | ### 7 | 8 | ## character set used for the filesystem. 9 | filesystem_charset "${G_GENERAL_FILESYSTEMCHARSET:-UTF-8}" 10 | 11 | ## character set to which ID3v1 tags are converted to by libid3tag. 12 | id3v1_encoding "${G_GENERAL_ID3V1ENCODING:-UTF-8}" 13 | 14 | ## specify logging level for mpd. Can be one of "default" for minimal 15 | ## logging, "secure" to log details about connections to mpd or 16 | ## "verbose" to log debugging information. 17 | ## mpd defaults to "default". 18 | log_level "${G_GENERAL_LOGLEVEL:-default}" 19 | 20 | ## whether to support automatic update of database when files in 21 | ## music_dir are changed. 22 | ## mpd defaults to "no". 23 | auto_update "${G_GENERAL_AUTOUPDATE:-yes}" 24 | 25 | ## directory depth being watched for auto_update. 26 | ## mpd has no default; 0 means no subdirectories, and from mpd 27 | ## viewpoint there's is no upper limit. the script sets this 28 | ## automatically to the maximum path depth the filesystem on which the 29 | ## music directory resides. 30 | auto_update_depth "${G_GENERAL_AUTOUPDATEDEPTH:-${CONF_MPD_AUTOUPDATEDEPTH}}" 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /confs-available/paths.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### common configuration parameters regarding paths used by mpd. 5 | ### 6 | 7 | ## directory where music is located. scripts defaults to environments 8 | ## `XDG_MUSIC_DIR'. 9 | music_directory "${G_PATHS_MUSICDIRECTORY:-${CONF_MPD_MUSICDIR}}" 10 | 11 | ## (required) path to the file where mpd will send its logging to. 12 | log_file "${G_PATHS_LOGFILE:-${CONF_MPD_LOGFILE}}" 13 | 14 | ## directory where saved playlists are stored. 15 | playlist_directory "${G_PATHS_PLAYLISTDIRECTORY:-${CONF_MPD_PLAYLISTDIR}}" 16 | 17 | ## path to the file in which mpd's process ID is stored in. 18 | pid_file "${G_PATHS_PIDFILE:-${CONF_MPD_PIDFILE}}" 19 | 20 | ## path to the state file in which the status of mpd will be saved. 21 | state_file "${G_PATHS_STATEFILE:-${CONF_MPD_STATEFILE}}" 22 | 23 | ## path to the sticker database, which manages dynamic information 24 | ## attached to songs. 25 | sticker_file "${G_PATHS_STICKERFILE:-${CONF_MPD_STICKERFILE}}" 26 | 27 | ## use absolute paths for file names in playlists, instead of relative 28 | ## paths. 29 | ## mpd defaults to "no". 30 | save_absolute_paths_in_playlists "${G_PATHS_SAVEABSOLUTEPATHSINPLAYLISTS:-no}" 31 | -------------------------------------------------------------------------------- /confs-available/client-limits.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### set limits to system and network resource usage, see 5 | ### source at `./src/client/ClientGlobal.cxx' for documentation. 6 | ### 7 | 8 | ### ADVANCED USES ONLY 9 | ### in normal circumstances this file should not be necessary. 10 | 11 | 12 | ## close connection to inactive clients after number of seconds. 13 | ## mpd default is 60. 14 | connection_timeout "${G_CLIENTLIMITS_CONNECTIONTIMEOUT:-60}" 15 | 16 | ## maximum number of clients connected at the same time. 17 | ## mpd default is 5. 18 | max_connections "${G_CLIENTLIMITS_MAXCONNECTIONS:-10}" 19 | 20 | ## maximum number of items in a playlist. automatically calculated by 21 | ## the script if the music directory specified with 22 | ## `CONF_MPD_MUSICDIR` in `../mpd-configure.conf` is accessible. 23 | ## mpd default is "16384" songs (ie ~ 1.300 albums). 24 | max_playlist_length "${G_CLIENTLIMITS_MAXPLAYLISTLENGTH}" 25 | 26 | ## maximum size (in KiB) of a command list. automatically calclulated 27 | ## by script ( G_CLIENTLIMITS_MAXPLAYLISTLENGTH / 8 ). 28 | ## mpd default is "2048" KiB. 29 | max_command_list_size "${G_CLIENTLIMITS_MAXCOMMANDLISTSIZE}" 30 | 31 | ## maximum size of the output buffer for a mpd client (in KiB). should 32 | ## be four times `max_command_size'. when left empty, the script 33 | ## calculates this. 34 | ## mpd default is ( 4 * 2048 = 8192 KiB). 35 | max_output_buffer_size "${G_CLIENTLIMITS_MAXOUTPUTBUFFERSIZE}" 36 | -------------------------------------------------------------------------------- /mpd-configure.conf: -------------------------------------------------------------------------------- 1 | ## settings for mpd-configure 2 | ## 3 | ## see README for documentation and default values. 4 | ## 5 | 6 | ### troubleshooting 7 | ## print extensive information to std_err about the scripts' internal 8 | ## program flow. 9 | #DEBUG=1 10 | ## normally commented lines and empty lines from configuration 11 | ## snippets are not included in the output, uncomment to include them 12 | ## in the output. 13 | #INCLUDE_COMMENTS=True 14 | 15 | ### limit audio interfaces returned by `alsa-capabilities`: 16 | ## 1. by filtering on *type*, one of 'analog', digital, or 'usb'. 17 | #LIMIT_INTERFACE_TYPE="usb" 18 | ## 2. by (additional) filtering on device and/or interface *name*, 19 | ## in the form of a regular expression. 20 | #LIMIT_INTERFACE_FILTER="USB Audio" 21 | 22 | ### handling of pulseaudio 23 | ## disable it permanently and stop it if running. 24 | #OPT_DISABLE_PULSEAUDIO=True 25 | ## disable and stop pulseaudio temporary during detection of alsa 26 | ## interfaces while restoring its configuration and run state afterwards. 27 | #OPT_STOP_PULSEAUDIO=True 28 | 29 | ## paths: examples are according to Debian guidelines 30 | #CONF_MPD_MUSICDIR="/srv/media/music" 31 | #CONF_MPD_HOMEDIR="/var/lib/mpd" 32 | 33 | ### additional settings for parameters defined in enabled 34 | ### configuration snippet files (in `./confs-enabled/*.conf`). 35 | 36 | ## example for setting the listening address and network port in 37 | ## `network.conf`: 38 | ## 1. mpd.conf parameter: `bind_to_address` 39 | #CONF_MPD_NETWORK_BINDTOADDRESS="0.0.0.0" 40 | ## 2. mpd.conf parameter: `port` 41 | #CONF_MPD_NETWORK_PORT="6600" 42 | 43 | ## example for specifying a database proxy mpd host in 44 | ## `database-proxy.conf`: 45 | ## 1. mpd.conf database plugin parameter: 46 | #CONF_MPD_DATABASEPROXYHOST="myfastmpd.local" 47 | -------------------------------------------------------------------------------- /examples/systemd_mpd-configure.service: -------------------------------------------------------------------------------- 1 | ## Sample systemd service file for mpd-configure which updates 2 | ## /etc/mpd.conf prior to starting the mpd.service. 3 | ## 4 | ## This file should be copied to 5 | ## /usr/lib/systemd/system/mpd-configure.service 6 | ## or, on Debian based ditros to: 7 | ## /lib/systemd/system/mpd-configure.service 8 | ## after which it should be made known by running: 9 | ## systemctl daemon-reload 10 | ## 11 | ## This example assumes the scripts are downloaded to 12 | ## /var/lib/mpd/mpd-configure and that its config file 13 | ## (/var/lib/mpd/mpd-configure/mpd-configure.conf) is empty (or has 14 | ## proper settings). 15 | ## Also make sure that the `User` specified below has access to the 16 | ## target file specified by `CONF_MPD_CONFFILE`. 17 | ## In this example the first available USB Audio Class playback device 18 | ## recognized by alsa will be used. 19 | ## 20 | ## After copying the file it can be managed using systemd. For example 21 | ## to start it manually: 22 | ## systemctl start mpd-configure 23 | ## or 24 | ## systemctl restart mpd 25 | ## 26 | ## To enable it to start on boot: 27 | ## systemctl enable mpd-configure 28 | 29 | [Unit] 30 | Description=Automatic configuration for mpd 31 | ## make sure sound and network are working 32 | After=network.target sound.target 33 | ## make sure it runs before mpd is started 34 | Before=mpd.service 35 | ## instruct systemd that mpd should be started, without it being a 36 | ## forced requirement. This way the mpd-configure.service remains 37 | ## independent from the succesful starting of the mpd.service. 38 | Wants=mpd.service 39 | 40 | [Service] 41 | Environment="CONF_MPD_MUSICDIR=/srv/media/music" 42 | Environment="CONF_MPD_HOMEDIR=/var/lib/mpd" 43 | ExecStart=/var/lib/mpd/mpd-configure/mpd-configure \ 44 | --limit usb --noprompts --nobackup --quiet --output /etc/mpd.conf 45 | 46 | [Install] 47 | WantedBy=default.target 48 | -------------------------------------------------------------------------------- /examples/get-interfaces.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ## sample python 3.x script to use alsa-capabilities from within python. 4 | ## needs `./get-interfaces-for-python.sh` which in turn sources 5 | ## `../alsa-capabilities. 6 | ## 7 | ## start it with: 8 | ## python get-interfaces.py 9 | 10 | import subprocess, os.path, re, sys 11 | from signal import signal, SIGPIPE, SIG_DFL 12 | 13 | ## limit the type of interfaces returned 14 | limittype = 'usb' 15 | 16 | ## name of custom bash script, that sources `alsa-capabilities' which 17 | ## returns a string consiting of one line per interface, each with the 18 | ## format `Interface X on Y (hw:a,b)' 19 | helperscriptname = 'get-interfaces-for-python.sh' 20 | script = os.path.join(os.path.dirname(os.path.abspath(__file__)), helperscriptname) 21 | if os.path.exists(script): 22 | print("ok 2: %s" % script) 23 | else: 24 | sys.exit("error: could not find script \`%s' (tried \`%s')" % (helperscriptname, script)) 25 | 26 | ## call the script, trapping std_err and storing std_out to `aif_output' 27 | script_output = subprocess.check_output( 'LIMIT_INTERFACE_TYPE="%s" %s' % (limittype, script), \ 28 | shell=True, 29 | stderr=None, \ 30 | preexec_fn = lambda: signal(SIGPIPE, SIG_DFL)) 31 | 32 | ## create an empty list for holding interfaces with pairs of `('hw:a,b', 'Interface X on Y')' 33 | interfaces_list = [] 34 | 35 | lenoflabel = 0 36 | 37 | ## process each line of output (eg each interface) 38 | for interface in script_output.splitlines(): 39 | ## split the line on `()' 40 | interface_split = re.split(b'[()]', interface) 41 | ## store the label (eg. `Interface X on Y') 42 | interface_label = interface_split[0].strip() 43 | lenoflabel = len(interface_label) if len(interface_label) > lenoflabel else lenoflabel 44 | ## store the index (ef. `hw:a,b') 45 | interface_index = interface_split[1] 46 | ## append the pair to the list 47 | interfaces_list.append((interface_index, interface_label)) 48 | 49 | ## sample output 50 | print("Found the following audio interfaces of type `%s':\n" % limittype) 51 | print("hwaddr label ") 52 | print("%s %s" % ('='*len('hwaddr'), '='*lenoflabel)) 53 | for aif in interfaces_list: 54 | print( "%s %s" % (aif[0].rjust(len('hwaddr')), aif[1])) 55 | 56 | -------------------------------------------------------------------------------- /examples/get-interfaces-for-python.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## sample helper script demoing usage of `alsa-capabilities` from within a 4 | ## python script, in this case `get-interfaces.py` in the current directory. 5 | ## 6 | ## to see it in action start it by using: 7 | ## python get-interfaces.py 8 | 9 | LANG=C 10 | 11 | function fetch_alsa_hwaddresses() { 12 | ## use alsa-capabilities script to get alsa output interfaces; if 13 | ## one interface is found, use that, otherwise display all of them 14 | ## and ask the user which to use. 15 | ## 16 | ## stores the hardware address and label in global variables 17 | ## ALSA_AIF_HWADDRESS and ALSA_AIF_DEVLABEL. 18 | 19 | [[ ! -z "${DEBUG}" ]] && debug "entering \`${FUNCNAME}' with arguments \`$@'" 20 | 21 | ## prompt when multiple alsa interfaces are found 22 | msg_multiple_alsahw="Multiple interfaces found." 23 | 24 | ## when more than one matching output device is found and 25 | ## USER_PROMPTS is not set, prompt the user to select one, 26 | ## defaults to the first device found. 27 | 28 | ## put the result of alsa-capabilities in the appropriate arrays 29 | return_alsa_interface -q 30 | 31 | for key in "${!ALSA_AIF_HWADDRESSES[@]}"; do 32 | ## echo each interface 33 | printf "Interface %s on %s (%s)\n" \ 34 | "${ALSA_AIF_LABELS[${key}]}" \ 35 | "${ALSA_AIF_DEVLABELS[${key}]}" \ 36 | "${ALSA_AIF_HWADDRESSES[$key]}" 37 | done 38 | } 39 | 40 | ### program start 41 | 42 | ## store the current directory 43 | SCRIPT_DIR=$(dirname $0) 44 | ALSA_CAPABILITIES_FILE="alsa-capabilities" 45 | ALSA_CAPABILITIES_SCRIPT="${SCRIPT_DIR}/../${ALSA_CAPABILITIES_FILE}" 46 | 47 | [[ -f "${ALSA_CAPABILITIES_SCRIPT}" ]] && \ 48 | source "${ALSA_CAPABILITIES_SCRIPT}" || \ 49 | die "required script \`${ALSA_CAPABILITIES_SCRIPT}' not found" 50 | 51 | ## global indexed arrays that will be filled from `alsacapabilities.sh' 52 | ## the ones used in this example are uncommented 53 | declare -a ALSA_AIF_HWADDRESSES=() 54 | #declare -a ALSA_AIF_DISPLAYTITLES=() 55 | #declare -a ALSA_AIF_MONITORFILES=() 56 | declare -a ALSA_AIF_DEVLABELS=() 57 | declare -a ALSA_AIF_LABELS=() 58 | #declare -a ALSA_AIF_UACCLASSES=() 59 | #declare -a ALSA_AIF_FORMATS=() 60 | #declare -a ALSA_AIF_CHARDEVS=() 61 | 62 | ## pass limits to the alsa-capabilities script 63 | LIMIT_INTERFACE_TYPE="${LIMIT_INTERFACE_TYPE:-}" 64 | if [[ ! -z ${LIMIT_INTERFACE_TYPE} ]]; then 65 | case ${LIMIT_INTERFACE_TYPE} in 66 | "analog") 67 | OPT_LIMIT_AO="True" ;; 68 | "digital") 69 | OPT_LIMIT_DO="True" ;; 70 | "usb"|"uac") 71 | OPT_LIMIT_UO="True" ;; 72 | esac 73 | fi 74 | LIMIT_INTERFACE_FILTER="${LIMIT_INTERFACE_FILTER:-}" 75 | [[ ! -z ${LIMIT_INTERFACE_FILTER} ]] && OPT_FILTER="${LIMIT_INTERFACE_FILTER}" 76 | 77 | ## return a string with interfaces 78 | fetch_alsa_hwaddresses 79 | 80 | -------------------------------------------------------------------------------- /confs-available/README: -------------------------------------------------------------------------------- 1 | README for configuration snippet files 2 | ====================================== 3 | 4 | This directory contains *configuration snippet files* for 5 | `mpd-configure`, which contain configuration directives for mpd 6 | grouped by function or domain to increase managebility. 7 | 8 | 9 | Modifying (default) settings 10 | ---------------------------- 11 | 12 | Make sure the configuration value is specified in an enabled snippet 13 | file (see below) while looking up the appropriate `VARIABLE 14 | NAME` and its default value. The value marked with xxx is the default 15 | value used by the script: `${VARNAME:-xxx}`. 16 | 17 | 18 | ```bash 19 | grep "max_playlist_length" confs-enabled/*.conf 20 | ## returns: 21 | confs-enabled/04_client-limits.conf:max_playlist_length "${G_CLIENTLIMITS_MAXPLAYLISTLENGTH:-16777216}" 22 | 23 | ``` 24 | 25 | So, to change the default value for the `max_playlist_length` setting in the 26 | resulting `mpd.conf` from `"16777216"` to `"1000"` , set the value of `G_CLIENTLIMITS_MAXPLAYLISTLENGTH` in `mpd-configure.conf` or on the command line. 27 | 28 | ```bash 29 | # add the following line to `./mpd-configure.conf` and rerun the script 30 | G_CLIENTLIMITS_MAXPLAYLISTLENGTH="1000" 31 | bash mpd-configure > mpd.conf 32 | ``` 33 | 34 | Or, specify the value on the command line: 35 | 36 | ```bash 37 | G_CLIENTLIMITS_MAXPLAYLISTLENGTH="1000" bash mpd-configure > mpd.conf 38 | ``` 39 | 40 | This way you may update the sources from upstream without losing your 41 | preferences. 42 | 43 | 44 | Enabling a configuration snippet file 45 | ------------------------------------- 46 | 47 | Each file in `./confs-available/` may be enabled by symlinking it to 48 | `../confs-enabled/` and (re-)running the `mpd-configure` script, eg: 49 | 50 | ```bash 51 | cd mpd-configure # go to the root of the mpd-configure script 52 | cd confs-enabled/ # change to the directory with available configuration files 53 | ln -s ../confs-available/afile.conf # enable the settings in `somefile.conf` 54 | cd - # switch back to the 'root' 55 | bash mpd-configure > mpd.conf # create `mpd.conf` using all files in `./confs-enabled/*.conf 56 | ``` 57 | 58 | 59 | Disabling a configuration snippet file 60 | -------------------------------------- 61 | 62 | Remove the symlink in `./confs-enabled/afile.conf` and rerun the script. 63 | 64 | 65 | Adding custom files 66 | ------------------- 67 | 68 | Any file with extension `.conf` placed in `./confs-enabled/` will be 69 | used by the `mpd-configure` script. This way it is easy to extend the 70 | scripts functionality and reach by providing additional confguration 71 | snippet files. 72 | 73 | By default, each uncommented and non-empty line in your custom file 74 | will be copied to the resulting output, while variables in those files 75 | will be expanded to their values. 76 | 77 | Given a self made file `wildmidi.txt` in your home directory 78 | (`/home/frits`) with the following contents: 79 | 80 | ```bash 81 | decoder { 82 | plugin "wildmidi" 83 | config_file "${TIMIDITYCFG:-/etc/timidity/timidity.cfg}" 84 | } 85 | ``` 86 | 87 | Enable it by copying or symlinking it to `confs-enabled/`: 88 | 89 | ```bash 90 | cd mpd-configure ## or wherever you download the script 91 | cp ~/wildmidi.txt confs-enabled/wildmidi.conf 92 | ``` 93 | 94 | Run the script: 95 | 96 | ```bash 97 | TIMIDITYCFG="~/wildmidi.cfg" bash mpd-configure 98 | ``` 99 | 100 | Will produce: 101 | 102 | ```bash 103 | # ... rest of config file 104 | decoder { 105 | plugin "wildmidi" 106 | config_file "/home/frits/wildmidi.cfg}" 107 | } 108 | # ... rest of config file 109 | ``` 110 | -------------------------------------------------------------------------------- /confs-available/output-audio-alsa.conf: -------------------------------------------------------------------------------- 1 | ### mpd configuration snippet for mpd-configure. 2 | ### see `README` in current directory. 3 | ### 4 | ### Defines an alsa audio output section and other parameters which 5 | ### are important for bit-perfect operation, which are marked with 6 | ### (*). 7 | ### see source at `./src/output/plugins/AlsaOutputPlugin.cxx' for 8 | ### documentation. 9 | ### 10 | 11 | ## define the audio output 12 | audio_output { 13 | ## fixed to alsa (*) 14 | type "${G_OUTPUTAUDIOALSA_TYPE:-alsa}" 15 | 16 | ## name gets filled from script 17 | name "${G_OUTPUTAUDIOALSA_NAME:-${CONF_ALSA_AIF_DEVLABEL}}" 18 | 19 | ## hardware address of interface device gets filled from 20 | ## script (*) 21 | device "${G_OUTPUTAUDIOALSA_DEVICE:-${CONF_ALSA_AIF_HWADDRESS}}" 22 | 23 | ## do not resample audio samples. mpd default is "yes". (*) 24 | auto_resample "${G_OUTPUTAUDIOALSA_AUTORESAMPLE:-no}" 25 | 26 | ## do not convert sample formats. mpd default is "yes". (*) 27 | auto_format "${G_OUTPUTAUDIOALSA_AUTOFORMAT:-no}" 28 | 29 | ## do not do channel conversion. mpd default is "yes". (*) 30 | auto_channels "${G_OUTPUTAUDIOALSA_AUTOCHANNELS:-no}" 31 | 32 | ## do not enable replay gain on this audio interface. mpd 33 | ## default is "software" (*). also see the "replaygain" 34 | ## and "mixer_type" parameters below. 35 | replay_gain_handler "${G_OUTPUTAUDIOALSA_REPLAYGAINHANDLER:-none}" 36 | 37 | ## disable hardware or software volume. mpd defaults to 38 | ## "hardware" for devices which support it, and "none" for 39 | ## others (*). 40 | mixer_type "${G_OUTPUTAUDIOALSA_MIXERTYPE:-none}" 41 | 42 | 43 | ### CAUTION: ######################################################### 44 | # enabling `dsd_usb` may damage your hardware; only enable it if # 45 | # you know what you're doing # 46 | ###################################################################### 47 | 48 | ## enable "DSD over PCM (DoP)" over USB according to the 49 | ## proposed standard by PS Audio, dCS and others. 50 | dsd_usb "${G_OUTPUTAUDIOALSA_DSDUSB}" 51 | 52 | 53 | ### CAUTION: ######################################################### 54 | # the following settings should normally be left alone for best # 55 | # results; only set them if you know what you're doing # 56 | ###################################################################### 57 | 58 | ## set SND_PCM_ACCESS_MMAP_INTERLEAVED to use the same address 59 | ## space to address both memory and I/O devices, instead of 60 | ## seperate (direct) address spaces 61 | ## (SND_PCM_ACCESS_RW_INTERLEAVED). Sometimes needed for 62 | ## resource limited devices like plug computers. mpd default 63 | ## is "no". (*) 64 | use_mmap "${G_OUTPUTAUDIOALSA_USEMMAP}" 65 | 66 | ## time between hardware sample transfers (in μs) which sets 67 | ## libasound's `period_time'. should be one fourth of the 68 | ## buffer time for high resolution audio. mpd default is 69 | ## perfect and set automagically at beginning of each track in 70 | ## bit perfect mode (buffer_time / 4 = 0.125 seconds). (*) 71 | period_time "${G_OUTPUTAUDIOALSA_PERIODTIME}" 72 | 73 | ## length of the hardware sample buffer in microseconds (μs) 74 | ## which sets libasound's buffer_time. mpd default is ok: 75 | ## "500000" μs (ie 0.5 seconds). 76 | buffer_time "${G_OUTPUTAUDIOALSA_BUFFERTIME}" 77 | 78 | } 79 | 80 | ## adjust playback volume based on replaygain tags using the 81 | ## replaygain library (instead of normal software volume 82 | ## control). should be one of "off", no replaygain, "album" for album 83 | ## based replaygain, "track" for track based replaygain or "auto" 84 | ## which uses `track mode' for random play and `album mode' for normal 85 | ## playback. mpd default is "off". (*) 86 | replaygain "${G_OUTPUTAUDIOALSA_REPLAYGAIN:-off}" 87 | 88 | 89 | ### CAUTION: ######################################################### 90 | # the following settings should normally be left alone for best # 91 | # results; only set them if you know what you're doing # 92 | ###################################################################### 93 | 94 | ## size of the audio buffer in kibibytes (KiB), sets libasound's 95 | ## `audio_buffer'. mpd default is perfect and set automagically at 96 | ## beginning of each track in bit perfect mode. (*) 97 | audio_buffer_size "${G_OUTPUTAUDIOALSA_AUDIOBUFFERSIZE}" 98 | 99 | 100 | ## percentage of the audio buffer that will be filled before playing a 101 | ## song. should be increased if you hear skipping when manually 102 | ## changing songs. mpd default is "10%" which seems ok. 103 | buffer_before_play "${G_OUTPUTAUDIOALSA_BUFFERBEFOREPLAY}" 104 | 105 | -------------------------------------------------------------------------------- /mpd-monitor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## This script for bash 4.x on linux displays the way an audio file is 3 | ## streamed from storage, through mpd, to alsa (the DAC). 4 | ## 5 | ## Copyright (C) 2017 Ronald van Engelen 6 | ## This program is free software: you can redistribute it and/or modify 7 | ## it under the terms of the GNU General Public License as published by 8 | ## the Free Software Foundation, either version 3 of the License, or 9 | ## (at your option) any later version. 10 | ## 11 | ## This program is distributed in the hope that it will be useful, 12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | ## GNU General Public License for more details. 15 | ## 16 | ## You should have received a copy of the GNU General Public License 17 | ## along with this program. If not, see . 18 | ## 19 | ## Source: https://github.com/ronalde/mpd-configure 20 | ## See also: https://lacocina.nl/detect-alsa-output-capabilities 21 | 22 | LANG=C 23 | 24 | app_name_mm="mpd-monitor" 25 | app_version="0.1.0" 26 | app_info_url="https://lacocina.nl/mpd-display-status" 27 | 28 | def_mpd_host="localhost" 29 | def_mpd_port="6600" 30 | def_ssh_user="root" 31 | 32 | ## output formatting 33 | bold="$(tput bold)" 34 | dim="$(tput dim)" 35 | std="$(tput sgr0)" 36 | red=$(tput setaf 1) 37 | green=$(tput setaf 2) 38 | blue=$(tput setaf 4) 39 | white=$(tput setaf 7) 40 | 41 | arg_mpd_host="" 42 | arg_mpd_port="" 43 | arg_ssh_user="" 44 | arg_mpd_music_dir="" 45 | arg_mpd_password="" 46 | 47 | #PS4='+ $(date "+%s.%N")\011 ' 48 | #exec 3>&2 2>/tmp/bashstart.$$.log 49 | #set -x 50 | 51 | function display_usageinfo() { 52 | 53 | msg_usage="\ 54 | Usage: 55 | ${app_name_mm} -d|--mpd-music-dir PATH [optional arguments] 56 | -or- 57 | ${app_name_mm} PATH 58 | 59 | PATH should be the path to the directory used as 'music_directory' by 60 | mpd running on MPDHOST. 61 | 62 | Optional arguments: 63 | -m|--mpd-host MPDHOST The IP address or hostname on which mpd 64 | runs. 65 | Defaults to 'localhost'. 66 | -p|--mpd-port MPDPORT The port on which mpd on MPDHOST 67 | listens. 68 | Defaults to '6600'. 69 | -x|--mpd-password PASSWD The password for mpd. 70 | -u|--ssh-user SSHUSER The username for the ssh connection to 71 | MPDHOST used to get realtime alsa 72 | information on MPDHOST. 73 | (Only used when MPDHOST is remote, ie not 74 | 'localhost' or '127.0.x.y'.) 75 | 76 | Background information: 77 | ${app_info_url} 78 | " 79 | printf 1>&2 "%s\n" "${msg_usage}" 80 | } 81 | 82 | 83 | function analyze_command_line() { 84 | ## parse command line arguments using the `manual loop` method 85 | ## described in http://mywiki.wooledge.org/BashFAQ/035. 86 | while :; do 87 | case "${1:-}" in 88 | -h|--mpd-host) 89 | if [[ "${2}x" == "x" ]]; then 90 | die "argument \`$1' requires the dns host name or ip address for the host running mpd." 91 | else 92 | arg_mpd_host="$2" 93 | shift 2 94 | continue 95 | fi 96 | ;; 97 | -x|--mpd-password) 98 | if [[ "${2}x" == "x" ]]; then 99 | die "argument \`$1' requires a non-empty value." 100 | else 101 | arg_mpd_password="${2}" 102 | shift 2 103 | continue 104 | fi 105 | ;; 106 | -u|--ssh-user) 107 | if [[ "${2}x" == "x" ]]; then 108 | die "argument \`$1' requires a non-empty value." 109 | else 110 | arg_ssh_user="${2}" 111 | shift 2 112 | continue 113 | fi 114 | ;; 115 | -d|--mpd-music-dir) 116 | if [[ "${2}x" == "x" ]]; then 117 | die "argument \`$1' requires the path to the directory where the music for mpd is stored." 118 | break 119 | else 120 | arg_mpd_music_dir="$2" 121 | mpd_music_dir="${arg_mpd_music_dir}" 122 | shift 2 123 | continue 124 | fi 125 | ;; 126 | -\?|--help) 127 | display_usageinfo 128 | exit 129 | ;; 130 | --) 131 | shift 132 | break 133 | ;; 134 | -?*) 135 | printf "Notice: unknown option \`%s' ignored\n\n." "$1" 1>&2 136 | display_usageinfo 137 | exit 138 | ;; 139 | *) 140 | break 141 | esac 142 | done 143 | 144 | ## shift the options index to get the remainder of the command 145 | ## line arguments containing the font descriptions 146 | ## and return them as 0-separated strings. 147 | #printf 1>&2 "OPTIND: \`%s'\n" "${OPTIND}" 148 | shift $((OPTIND - 1)) 149 | if [[ "${arg_mpd_music_dir}x" == "x" ]]; then 150 | arg_single="$@" 151 | arg_mpd_music_dir="${arg_single}" 152 | if [[ ! -d "${arg_mpd_music_dir}" ]]; then 153 | printf 1>&2 "error: invalid mpd music directory \`%s' specified.\n\n" \ 154 | "${arg_mpd_music_dir}" 155 | display_usageinfo 156 | else 157 | ## single argument, use in defaults 158 | mpd_music_dir="${arg_mpd_music_dir}" 159 | mpd_host="${def_mpd_host}" 160 | mpd_port="${def_mpd_port}" 161 | mpd_password="" 162 | ssh_host="" 163 | fi 164 | else 165 | [[ ${DEBUG} ]] && \ 166 | declare -p arg_mpd_music_dir \ 167 | arg_mpd_host arg_mpd_port arg_mpd_password \ 168 | arg_ssh_user 1>&2 169 | fi 170 | 171 | } 172 | 173 | function die() { 174 | printf "error: %s\n" "$@" 1>&2 175 | exit 1 176 | } 177 | 178 | function bytes_to_si() { 179 | ## see: http://www.pixelbeat.org/docs/numfmt.html 180 | ## return the human notation for the number of bytes specified in $1. 181 | ## strip decimals from input 182 | bytes="${1}" 183 | si_size="" 184 | if [[ "${cmd_numfmt}x" != "x" ]]; then 185 | #bytes="$(LC_ALL=nonexisting numfmt --from=si ${si_size})" 186 | si_size="$(LC_ALL=nonexisting ${cmd_numfmt} --to=iec-i --suffix=B --format="%.1f" "${bytes}" 2>/dev/null)" 187 | fi 188 | if [[ "${si_size}x" == "x" ]]; then 189 | echo TODO 1>&2 190 | return 1 191 | fi 192 | printf "%s" "${si_size}" 193 | } 194 | 195 | 196 | function run_func() { 197 | ## run an internal function whose name is specified in $1 with 198 | ## args on the local host or remote through ssh. 199 | func="$1" 200 | shift 201 | args="$@" 202 | [[ ${DEBUG} ]] && \ 203 | declare -p 1>&2 func args 204 | if [[ "${alsa_ssh_userhost}x" == "x" ]]; then 205 | "${func} ${args}" 206 | else 207 | ## use declare -f to first print the function in the remote 208 | ## bash session, and then execute it. 209 | ssh ${alsa_ssh_userhost} -- \ 210 | "$(declare -f ${func}); ${func} ${args}" 211 | fi 212 | } 213 | 214 | 215 | function check_commands() { 216 | declare -a err 217 | msg="not found in PATH" 218 | cmd_mpc=$(type -p mpc) || err+=("mpc") 219 | cmd_soxi=$(type -p soxi) || err+=("soxi") 220 | cmd_exiftool=$(type -p exiftool) || err+=("exiftool") 221 | cmd_netcat=$(type -p nc) || err+=("netcat (nc)") 222 | cmd_bc=$(type -p bc) || err+=("bc") 223 | cmd_numfmt=$(type -p numfmt) 224 | if [[ ${#err[@]} -gt 0 ]]; then 225 | printf 1>&2 "required command \`%s' ${msg}\n" "${err[@]}" 226 | return 1 227 | fi 228 | } 229 | 230 | function get_csv_value() { 231 | data="$1" 232 | IFS=\; read type field value <<<"${data}" 233 | printf "%s" "${value//\"}" 234 | } 235 | 236 | function get_soxi_value() { 237 | fieldname="$1" 238 | printf 1>&2 "%s: fieldname='%s', value='%s'\n" \ 239 | "${FUNCNAME[0]}" "${fieldname}" "${soxi_info["${fieldname}"]}" 240 | declare vals="${soxi_info["${fieldname}"]}" 241 | #declare -p vals 1>&2 242 | value="$(get_csv_value "${vals[0]}")" 243 | printf "%s" "${value}" 244 | } 245 | 246 | function filter_soxi_duration() { 247 | ## expects: hh:mm:ss.xxx where xxx are the subseconds (milliseconds?) 248 | duration_fields=($1) 249 | IFS=":" read hours minutes seconds subseconds <<<"${duration_fields[0]//./:}" 250 | minutes=$(${cmd_bc} <<< "${minutes} + ( ${hours} * 60 )") 251 | printf "%s:%s" "${minutes}" "${seconds}" 252 | } 253 | 254 | 255 | function ret_mpdconf_contents() { 256 | ## return the contents of the mpd configuration file specified in 257 | ## $1. 258 | mpdconf_path="$1" 259 | alsa_ssh_userhost="${alsa_ssh_userhost:-}" 260 | if [[ -f "${mpdconf_path}" ]]; then 261 | cat "${mpdconf_path}" 262 | else 263 | printf 1>&2 "error: mpd configuration file %s does not exist on host %s.\n" \ 264 | "${mpdconf_path}" "$(hostname)" 265 | return 1 266 | fi 267 | } 268 | 269 | 270 | function ret_mpdconf_commandline() { 271 | ## return the command line arguments for running mpd 272 | alsa_ssh_userhost="${alsa_ssh_userhost:-}" 273 | mpd_pid="$(pidof mpd | awk '{print $1}')" 274 | xargs -0 echo < /proc/${mpd_pid}/cmdline 275 | } 276 | 277 | 278 | function ret_mpdconf_default() { 279 | ## function 280 | for conffile in $XDG_CONFIG_HOME/mpd/mpd.conf \ 281 | ~/.mpdconf \ 282 | ~/.mpd/mpd.conf \ 283 | /etc/mpd.conf; do 284 | if [[ "${conffile}x" != "x" ]]; then 285 | if [[ -f ${conffile} ]]; then 286 | [[ ${DEBUG} ]] && \ 287 | printf 1>&2 "using mpd configuration file %s\n" \ 288 | "${conffile}" 289 | echo ${conffile} 290 | return 0 291 | fi 292 | fi 293 | done 294 | printf 1>&2 "warning: no mpd configuration found.\n" 295 | return 1 296 | } 297 | 298 | 299 | function ret_mpdconf_path() { 300 | ## returns the name of the mpd.conf file in use by mpd (either 301 | ## local or remote) 302 | ## the command mpd itself (including a possible path) gets 303 | ## stripped before 304 | ## all possible mpd command line arguments 305 | mpd_args=(--no-config) 306 | mpd_args+=(--no-daemon) 307 | mpd_args+=(-v) 308 | mpd_args+=(--verbose) 309 | mpd_args+=(--stderr) 310 | ## strip each command line argument from all possible arguments 311 | ## the remainder is the mpd conf file 312 | mpd_real_args=($(run_func ret_mpdconf_commandline)) 313 | mpd_rest_args=(${mpd_real_args[@]:1}) 314 | for arg in ${mpd_args[@]}; do 315 | mpd_rest_args=(${mpd_rest_args[@]//${arg}}) 316 | done 317 | [[ ${DEBUG} ]] && \ 318 | declare -p mpd_args mpd_real_args mpd_rest_args 1>&2 319 | mpd_conf_file="${mpd_rest_args[0]}" 320 | if [[ "${mpd_conf_file}x" == "x" ]]; then 321 | [[ ${DEBUG} ]] && \ 322 | printf 1>&2 "no conffile specified on mpd command line, \ 323 | test default locations\n" 324 | run_func ret_mpdconf_default || return 1 325 | else 326 | printf "%s" "${mpd_conf_file}" 327 | fi 328 | } 329 | 330 | function get_mpdconf_contents() { 331 | mpdconf_path="$1" 332 | run_func ret_mpdconf_contents "${mpdconf_path}" || return 1 333 | } 334 | 335 | 336 | function get_mpd_alsa_outputs() { 337 | ## query the (local or remote) mpd configuration file for alsa audio outputs. 338 | ## returns newline separated list of alsa output devices 339 | ## TODO; handle multiple outputs 340 | # while read -r line; do 341 | # done< <(ssh ${alsa_ssh_userhost} -- "cat /ec/mpd.conf") 342 | mpdconf_path=$(ret_mpdconf_path) 343 | [[ ${DEBUG} ]] && printf 1>&2 "%s: mpdconf_path=\`%s'\n" "${FUNCNAME}" "${mpdconf_path}" 344 | #mpdconf_contents="$(get_mpdconf_contents "${mpdconf_path}")" 345 | #declare -p mpdconf_path mpdconf_contents 1>&2 346 | start_audio_output_re="^[[:space:]]*audio_output[[:space:]]*\{" 347 | end_audio_output_re="[[:space:]]*\}" 348 | ## only handle alsa audio outputs 349 | type_re="^[[:space:]]*type[[:space:]]*[\"'](.*)[\"']" 350 | device_re="^[[:space:]]*device[[:space:]]*[\"']hw:([0-9]+),([0-9]+)[\"']" 351 | audio_outputs=0 352 | inside_audio_output= 353 | inside_alsa_output= 354 | declare -a devices 355 | while read -r line; do 356 | # declare -p line inside_audio_output inside_alsa_output 1>&2 357 | if [[ "${inside_audio_output}x" == "x" ]]; then 358 | if [[ "${line}" =~ ${start_audio_output_re} ]]; then 359 | ## new audio_output started 360 | ((audio_outputs++)) 361 | inside_audio_output=true 362 | [[ ${DEBUG} ]] && \ 363 | printf 1>&2 "\n *** starting audio_output\n" 364 | elif [[ "${line}" =~ ${end_audio_output_re} ]]; then 365 | ## end of audio output 366 | inside_audio_output= 367 | fi 368 | else 369 | if [[ "${line}" =~ ${type_re} ]]; then 370 | type="${BASH_REMATCH[1]}" 371 | [[ ${DEBUG} ]] && \ 372 | printf 1>&2 " ** audio_output type: %s\n" "${type}" 373 | if [[ "${type}x" == "alsax" ]]; then 374 | inside_alsa_output=true 375 | else 376 | inside_alsa_output= 377 | fi 378 | elif [[ "${line}" =~ ${device_re} ]] && \ 379 | [[ "${inside_alsa_output}x" != "x" ]]; then 380 | alsa_dev="${BASH_REMATCH[1]}" 381 | alsa_int="${BASH_REMATCH[2]}" 382 | [[ ${DEBUG} ]] && \ 383 | printf 1>&2 " ** audio_output device: hw:%s,%s\n" \ 384 | "${alsa_dev}" "${alsa_int}" 385 | devices+=("hw:${alsa_dev},${alsa_int}") 386 | inside_audio_output= 387 | inside_alsa_output= 388 | fi 389 | fi 390 | done< <(get_mpdconf_contents "${mpdconf_path}") 391 | [[ ${DEBUG} ]] && \ 392 | declare -p audio_outputs devices 1>&2 393 | ## TODO: handle multiple devices 394 | for device in ${devices[@]}; do 395 | printf "%s\n" "${device}" 396 | done 397 | } 398 | 399 | function fill_arrays() { 400 | ret_mpc_info || return 1 401 | #ret_mpc_now 402 | ret_mpd_now || return 1 403 | ret_exif_info "${mpc_vals[file]}" 404 | ret_soxi_info "${mpc_vals[file]}" 405 | ## TODO get right card and interface numbers 406 | alsa_outputs=("$(get_mpd_alsa_outputs)") 407 | [[ ${DEBUG} ]] && declare -p alsa_outputs 1>&2 408 | #for output in ${alsa_outputs[0]}; do 409 | # ret_alsa_info "${output}" 410 | # done 411 | for output in ${alsa_outputs[@]}; do 412 | ret_alsa_info "${output}" 413 | done 414 | #[[ "${exif_vals[*]}x" == "x" ]] || ret_exif_info "${mpc_vals[file]}" 415 | #[[ "${soxi_vals[*]}x" == "x" ]] || ret_soxi_info "${mpc_vals[file]}" 416 | #[[ "${alsa_vals[*]}x" == "x" ]] || ret_alsa_info "1" "0" "root@sonida" 417 | } 418 | 419 | 420 | function alsa_format_outputencoding() { 421 | ## extract alsa output encoding format and store in appropriate array. 422 | alsa_encoding="$1" # ${alsa_vals[format]} 423 | encoding_re="([SU])([0-9]+)_([0-3]*[LB]E)" 424 | if [[ "${alsa_encoding}" =~ ${encoding_re} ]]; then 425 | signedness="${BASH_REMATCH[1]}" 426 | bitdepth="${BASH_REMATCH[2]}" 427 | alsa_encoding[bitdepth]="${bitdepth}" 428 | endianness="${BASH_REMATCH[3]}" 429 | case "${signedness}" in 430 | U) alsa_encoding[signedness]="unsigned" ;; 431 | S) alsa_encoding[signedness]="signed" ;; 432 | esac 433 | case "${endianness}" in 434 | *LE) alsa_encoding[endianness]="little endian" ;; 435 | *BE) alsa_encoding[endianness]="big endian" ;; 436 | esac 437 | else 438 | printf "ANOMALITY in %s: alsa_encoding '%s' does not match regex.\n" \ 439 | "${FUNCNAME[0]}" "${alsa_encoding}" 440 | return 1 441 | fi 442 | } 443 | 444 | 445 | function get_info() { 446 | 447 | ## check presence of needed commands 448 | check_commands || die "not all needed commands are installed." 449 | 450 | ## parse command line arguments 451 | analyze_command_line "$@" 452 | 453 | if [[ ! -d "${mpd_music_dir}" ]]; then 454 | die "error: can't access directory ${mpd_music_dir}." 455 | fi 456 | mpd_host="${arg_mpd_host:-${def_mpd_host}}" 457 | mpd_port="${arg_mpd_port:-${def_mpd_port}}" 458 | ssh_user="${arg_ssh_user:-${def_ssh_user}}" 459 | mpd_password="${arg_mpd_password:-}" 460 | ssh_host="${mpd_host:-}" 461 | localhost_re="127.0.[0-9]+.[0-9]+|localhost" 462 | if [[ "${mpd_host}" =~ ${localhost_re} ]]; then 463 | ## mpd runs local, no need for ssh to get alsa properties 464 | alsa_ssh_userhost="" 465 | else 466 | alsa_ssh_host="${mpd_host}" 467 | ## mpd runs remote, use ssh to get alsa properties 468 | alsa_ssh_userhost="${ssh_user}@${ssh_host}" 469 | fi 470 | [[ ${DEBUG} ]] && \ 471 | declare -p mpd_host ssh_host ssh_user mpd_music_dir 1>&2 472 | 473 | fill_arrays 474 | [[ ${DEBUG} ]] && \ 475 | declare -p mpc_vals mpd_vals 1>&2 476 | 477 | main_playing_tracktitle="${mpc_vals[title]}" 478 | main_playing_trackartist="${mpc_vals[artist]}" 479 | main_playing_trackperformers="${exif_vals[Performer]}" 480 | main_rt_tracknumber_album="${mpc_vals[track]}" 481 | main_rt_tracknumber_playlist="${mpc_vals[position]}" 482 | main_rt_playlistlength="${mpd_vals[rt_raw_playlistlength]}" 483 | main_playing_trackalbumname="${mpc_vals[album]}" 484 | main_playing_trackalbumnumber="${mpc_vals[position]}" 485 | main_playing_trackalbumartist="${mpc_vals[albumartist]}" 486 | main_playing_trackduration="$(filter_soxi_duration "${soxi_vals[Duration]}")" 487 | [[ $? -ne 0 ]] && return 1 488 | file_path="${arg_mpd_music_dir}/${mpc_vals[file]}" 489 | if [[ ! -f "${file_path}" ]]; then 490 | return 1 491 | ## TODO 492 | fi 493 | a_du_output=($(du --bytes --dereference "${file_path}")) 494 | main_file_sizebytes="${a_du_output[0]}" 495 | ## TODO: declare -p main_file_sizebytes 1>&2 496 | main_file_sizehbytes="$(bytes_to_si "${main_file_sizebytes}")" 497 | [[ $? -ne 0 ]] && return 1 498 | msg_filesize="$(printf "%-10s: %-13s (%s)" \ 499 | "File size" "${main_file_sizebytes} bytes" "${main_file_sizehbytes}")" 500 | 501 | main_file_bitrate_raw="${soxi_vals[BitRate]}" 502 | main_file_bitrate_value="${main_file_bitrate_raw:0:-1}" 503 | main_file_bitrate_unit="${main_file_bitrate_raw:$(( ${#main_file_bitrate_raw} - 1 )):1}" 504 | main_file_bitrate_kvalue= 505 | if [[ "${main_file_bitrate_unit}x" == "Mx" ]]; then 506 | int="${main_file_bitrate_value%%.*}" 507 | dec="${main_file_bitrate_value##*.}" 508 | ## TODO 509 | [[ ${DEBUG} ]] && \ 510 | declare -p main_file_bitrate_value int dec 1>&2 511 | main_file_bitrate_kvalue=$(bc <<< "${main_file_bitrate_value} * 1000") 512 | else 513 | main_file_bitrate_kvalue="${main_file_bitrate_value}" 514 | fi 515 | main_file_bitrate_kvalue="${main_file_bitrate_kvalue%%.00}" 516 | main_file_bitrate="${main_file_bitrate_kvalue}" 517 | main_file_bitdepth="${soxi_vals[Precision]//-bit}" 518 | main_file_samplerate="${soxi_vals[SampleRate]}" 519 | #declare -p alsa_vals 1>&2 520 | main_output_samplerate_raw=(${alsa_vals[rate]}) 521 | main_output_samplerate="${main_output_samplerate_raw[0]}" 522 | ## get and store alsa output encoding 523 | alsa_format_outputencoding "${alsa_vals[format]}" 524 | main_output_bitdepth="${alsa_encoding[bitdepth]}" 525 | main_output_sampleencoding="${alsa_encoding[bitdepth]} bit (${alsa_encoding[signedness]} ${alsa_encoding[endianness]})" 526 | main_output_samplechannelcount="${alsa_vals[channels]}" 527 | 528 | main_playing_trackplayingtime="${mpc_vals[rt_time_elapsed_minutes]}:${mpc_vals[rt_time_elapsed_seconds]}" 529 | main_playing_trackplayingpercentage="${mpc_vals[rt_percentage_played]}%" 530 | #declare -p main_playing_trackplayingpercentage 1>&2 531 | main_mpd_bitdepth="${mpd_vals[rt_audio_bitdepth]}" 532 | main_mpd_samplerate="${mpd_vals[rt_audio_samplerate]}" 533 | main_mpd_bitrate="${mpd_vals[rt_audio_bitrate]}" 534 | 535 | ## determine if playback is bit perfect 536 | color_bitdepth_file_mpd="${red}" 537 | color_bitdepth_mpd_output="${red}" 538 | if [[ ${main_mpd_bitdepth} -ge ${main_file_bitdepth} ]]; then 539 | color_bitdepth_file_mpd="${green}" 540 | if [[ ${main_output_bitdepth} -ge ${main_mpd_bitdepth} ]]; then 541 | color_bitdepth_mpd_output="${green}" 542 | fi 543 | fi 544 | color_samplerate_file_mpd="${red}" 545 | color_samplerate_mpd_output="${red}" 546 | if [[ ${main_mpd_samplerate} -ge ${main_file_samplerate} ]]; then 547 | color_samplerate_file_mpd="${green}" 548 | if [[ ${main_output_samplerate} -ge ${main_mpd_samplerate} ]]; then 549 | color_samplerate_mpd_output="${green}" 550 | fi 551 | fi 552 | 553 | ## prepare output 554 | msg_header="$(printf "%-11s %-18s ${white}%8s${std} ${white}%8s${std} ${white}%8s${std}" " " " " "${label_input}" "${label_throughput}" "${label_output}")" 555 | msg_bitrate="$(printf "%-11s %-18s: ${dim}${white}%8s${std} > ${white}%8s${std} > ${dim}${white}%8s${std}" \ 556 | " " "${label_bitrate}" "${main_file_bitrate}" "${main_mpd_bitrate}" "(n/a)")" 557 | msg_bitdepth="$(printf "%-11s %-18s: %8s ${color_bitdepth_file_mpd}> %8s ${color_bitdepth_mpd_output}> %8s${std}" \ 558 | " " "${label_bitdepth}" "${main_file_bitdepth}" "${main_mpd_bitdepth}" "${main_output_bitdepth}")" 559 | msg_samplerate="$(printf "%-11s %-18s: %8s ${color_samplerate_file_mpd}> %8s${std} ${color_samplerate_mpd_output}> %8s${std}" \ 560 | " " "${label_samplerate}" "${main_file_samplerate}" "${main_mpd_samplerate}" "${main_output_samplerate}")" 561 | 562 | msg_tracktitle="${bold}${white}${main_playing_tracktitle}${std}" 563 | msg_trackartist="${blue}${main_playing_trackartist}${std}" 564 | msg_tracknumber="$(printf "%-4s of %-3s" "#${bold}${blue}${main_rt_tracknumber_playlist}${std}" "${main_rt_playlistlength}")" 565 | msg_nowplaying="${msg_tracknumber}: ${msg_tracktitle}" 566 | msg_byline="$(printf "%-11sby %s" " " "${msg_trackartist}")" 567 | msg_albumline="$(printf "%-11strack %s from album %s" \ 568 | " " "${bold}${main_rt_tracknumber_album}${std}" "${bold}${blue}${main_playing_trackalbumname}${std}")" 569 | msg_playingtime="$(printf "%-13s playing %s from %s (%s)" \ 570 | " " "${bold}${std}${main_playing_trackplayingtime}${std}" "${main_playing_trackduration}" "${main_playing_trackplayingpercentage}")" 571 | 572 | echo -en "\ 573 | ${msg_playingtime} 574 | ${msg_nowplaying} 575 | ${msg_albumline} 576 | ${msg_byline} 577 | 578 | ${msg_header} 579 | ${msg_bitdepth} 580 | ${msg_samplerate} 581 | ${msg_bitrate} 582 | 583 | ${msg_filesize} 584 | \r" 585 | } 586 | 587 | function ret_formatted_time() { 588 | ## returns formatted time (hh:mm:ss) from number of seconds ($1) 589 | ## https://stackoverflow.com/questions/12199631/convert-seconds-to-hours-minutes-seconds 590 | nr_seconds="${1}" 591 | hours=$(bc <<< "${nr_seconds}/3600") 592 | minutes=$(bc <<< "(${nr_seconds}%3600)/60") 593 | seconds=$(bc <<< "${nr_seconds}%60") 594 | printf "%02d:%02d:%02d" \ 595 | "${hours}" "${minutes}" "${seconds}" 596 | } 597 | 598 | function ret_mpd_now() { 599 | ## echo status command to mpd host on mpd port using netcat, to 600 | ## fill mpc_vals array with rt_ fields 601 | 602 | [[ ${DEBUG} ]] && \ 603 | declare -p cmd_netcat mpd_host mpd_port 1>&2 604 | res=$(${cmd_netcat} --help 2>&1 >/dev/null) 605 | if [[ $? -ne 0 ]]; then 606 | netcat_args=(-N) 607 | else 608 | netcat_args=(--close) 609 | fi 610 | ## get realtime mpd status using netcat 611 | mpd_status="$(${cmd_netcat} ${netcat_args[@]} ${mpd_host} ${mpd_port} <<< "status")" 612 | [[ ${DEBUG} ]] && \ 613 | declare -p mpd_status 1>&2 614 | linecounter=1 615 | prefix="rt_raw" 616 | key_val_re="^([^\:]+):[[:space:]]+(.*)" 617 | ## store each line in an associative array, by splitting the mpc 618 | ## output lines (paramater:value) 619 | while read -r line; do 620 | if [[ "${line}" =~ ${key_val_re} ]]; then 621 | field="${BASH_REMATCH[1]}" 622 | value="${BASH_REMATCH[2]}" 623 | mpd_vals["${prefix}_${field}"]="${value}" 624 | fi 625 | done<<<"${mpd_status}" 626 | ## store the double colon separated values 627 | ## (samplerate:bitdepth:channelcount, eg. 48000:24:2) of the 628 | ## audiofield in separate variables and an array 629 | IFS=: read audio_samplerate audio_bitdepth audio_channelcount <<< "${mpd_vals[${prefix}_audio]}" 630 | mpd_vals["rt_audio_samplerate"]="${audio_samplerate}" 631 | mpd_vals["rt_audio_bitdepth"]="${audio_bitdepth}" 632 | mpd_vals["rt_audio_channelcount"]="${audio_channelcount}" 633 | mpd_vals["rt_audio_bitrate"]="${mpd_vals[${prefix}_bitrate]}" 634 | 635 | ## store the (elapsed) playing time value (seconds:frames, 636 | ## eg. 20:123) in separate variables and an array 637 | IFS=: read time_elapsed_seconds time_duration_seconds <<< "${mpd_vals[${prefix}_time]}" 638 | mpd_vals["rt_time_elapsed_seconds"]="${time_elapsed_seconds}" 639 | mpd_vals["rt_time_duration_seconds"]="${time_duration_seconds}" 640 | mpd_vals["rt_time_elapsed_formatted"]="$(ret_formatted_time "${time_elapsed_seconds}")" 641 | mpd_vals["rt_time_duration_formatted"]="$(ret_formatted_time "${time_duration_seconds}")" 642 | #mpd_vals["rt_time_tracktotal"]="${mpd_vals[${prefix}_tracktotal]}" 643 | ## old 644 | #mpd_val rt_time_tracktotal: '223' 645 | #mpd_val rt_raw_time: '53:223' 646 | #mpd_val rt_raw_elapsed: '52.911' 647 | #mpd_val rt_raw_duration: '223.168' 648 | 649 | if [[ ${DEBUG} ]]; then 650 | for k in "${!mpd_vals[@]}"; do 651 | if [[ "${k}" =~ ^rt ]]; then 652 | printf 1>&2 "mpd_val %s: '%s'\n" "${k}" "${mpd_vals[${k}]}" 653 | fi 654 | done 655 | fi 656 | } 657 | 658 | function ret_mpc_info() { 659 | ## get information from mpd using mpc with the 'current' command 660 | ## and a format string. 661 | if [[ "${mpd_password}x" == "x" ]]; then 662 | mpc_args=(-h "${mpd_host}") 663 | else 664 | mpc_args=(-h "${mpd_password}@${mpd_host}") 665 | fi 666 | mpc_args+=(-p "${mpd_port}") 667 | mpc_args+=(current) 668 | mpc_args+=(-f "${mpc_format_string}") 669 | [[ ${DEBUG} ]] && \ 670 | declare -p mpc_format_string 1>&2 671 | res="$(${cmd_mpc} "${mpc_args[@]}" &)" 672 | if [[ $? -ne 0 ]]; then 673 | die "${FUNCNAME[0]}: mpd on %s is not running." 674 | else 675 | if [[ "${res}x" == "x" ]]; then 676 | printf 1>&2 "mpd on %s is not currently playing anything.\n" \ 677 | "${mpd_host}" 678 | return 1 679 | fi 680 | fi 681 | field_val_re="^([^\:]+):(.*)" 682 | while read -r line; do 683 | if [[ "${line}" =~ ${field_val_re} ]]; then 684 | field="${BASH_REMATCH[1]}" 685 | val="${BASH_REMATCH[2]}" 686 | mpc_vals["${field}"]="${val}" 687 | fi 688 | done<<<"${res}" 689 | } 690 | 691 | function ret_soxi_info() { 692 | rel_file_path="${1}" 693 | file_path="${arg_mpd_music_dir}/${rel_file_path}" 694 | if [[ -f "${file_path}" ]]; then 695 | res="$(${cmd_soxi} "${file_path}")" 696 | field_val_re='([^\:]+):[[:space:]]*(.*)' 697 | while read -r line; do 698 | if [[ "${line}" =~ ${field_val_re} ]]; then 699 | field="${BASH_REMATCH[1]}" 700 | field="${field// /}" 701 | val="${BASH_REMATCH[2]}" 702 | soxi_vals["${field}"]="${val}" 703 | if [[ "${field}x" == "Sample Encodingx" ]]; then 704 | break 705 | fi 706 | fi 707 | done<<<"${res}" 708 | fi 709 | } 710 | 711 | function ret_exif_info() { 712 | rel_file_path="${1}" 713 | file_path="${arg_mpd_music_dir}/${rel_file_path}" 714 | if [[ -f "${file_path}" ]]; then 715 | res="$(${cmd_exiftool} -j "${file_path}")" 716 | field_val_re='"([^\:]+)":(.*)' 717 | while read -r line; do 718 | if [[ "${line}" =~ ${field_val_re} ]]; then 719 | field="${BASH_REMATCH[1]}" 720 | val="${BASH_REMATCH[2]}" 721 | exif_vals["${field}"]="${val}" 722 | fi 723 | done<<<"${res}" 724 | fi 725 | } 726 | 727 | function get_hwparams() { 728 | ## tries to get the contents of 729 | ## /proc/asound/card${alsa_card_no}/pcm${alsa_dev_no}p/sub0/hw_params 730 | ## using alsa_card $1 and alsa_dev $2. 731 | ## (called by run_func so could be remote). 732 | ## returns newline separated file contents or an error. 733 | alsa_card_no="$1" 734 | alsa_dev_no="$2" 735 | [[ ! -z ${DEBUG} ]] && \ 736 | declare -p alsa_card_no alsa_dev_no 1>&2 737 | hwparams_file="/proc/asound/card${alsa_card_no}/pcm${alsa_dev_no}p/sub0/hw_params" 738 | if [[ -f "${hwparams_file}" ]]; then 739 | ## return the contents of the file 740 | xargs -0 echo < "${hwparams_file}" 741 | else 742 | [[ ${DEBUG} ]] && \ 743 | printf 1>&2 "error accessing file: '%s' on host '%s'\n" \ 744 | "${hwparams_file}" "$(hostname)" 745 | return 1 746 | fi 747 | } 748 | 749 | function ret_alsa_info() { 750 | ## TODO: fix handling of mpd output(s) 751 | ## expects "hw:x,y" 752 | alsa_hw="${1//hw:/}" 753 | alsa_dev_no="${alsa_hw##*,}" 754 | alsa_card_no="${alsa_hw%%,*}" 755 | alsa_ssh_userhost="${alsa_ssh_userhost:-}" 756 | unset mpd_outputs 757 | declare -a mpd_outputs 758 | [[ ${DEBUG} ]] && \ 759 | declare -p alsa_hw alsa_card_no alsa_dev_no alsa_ssh_userhost 760 | declare -a mpc_args 761 | if [[ "${mpd_password}x" == "x" ]]; then 762 | mpc_args=(-h "${mpd_host}") 763 | else 764 | mpc_args=(-h "${mpd_password}@${mpd_host}") 765 | fi 766 | mpc_args+=(-p "${mpd_port}") 767 | mpc_args+=(output) 768 | [[ ${DEBUG} ]] && \ 769 | declare -p mpd_host mpd_port mpc_args 1>&2 770 | mpc_output_re="Output[[:space:]]([0-9]+)[[:space:]]\(([^\(]+)\)" 771 | while read -r line; do 772 | if [[ "${line}" =~ ${mpc_output_re} ]]; then 773 | mpd_outputs+=("${BASH_REMATCH[2]}") 774 | fi 775 | done< <(${cmd_mpc} "${mpc_args[@]}") 776 | [[ ${DEBUG} ]] && \ 777 | declare -p mpd_outputs 1>&2 778 | key_val_re="^([^\:]+):[[:space:]]+(.*)" 779 | while read -r line; do 780 | if [[ "${line}" =~ ${key_val_re} ]]; then 781 | field="${BASH_REMATCH[1]}" 782 | val="${BASH_REMATCH[2]}" 783 | alsa_vals["${field}"]="${val}" 784 | [[ ${DEBUG} ]] && \ 785 | declare -p line field val 1>&2 786 | else 787 | [[ ${DEBUG} ]] && \ 788 | declare -p line 1>&2 789 | return 1 790 | fi 791 | done< <(run_func get_hwparams "${alsa_card_no} ${alsa_dev_no}") 792 | } 793 | 794 | function terminal_size() { # Calculate the size of the terminal 795 | terminal_cols="$(tput cols)" 796 | terminal_rows="$(tput lines)" 797 | } 798 | 799 | 800 | function main_loop() { 801 | ## unused 802 | mpc_args=(-h "${mpd_host}") 803 | counter=0 804 | while : ; do 805 | #"${cmd_mpc}" "${mpc_args[@]}" idle 806 | sleep 0.5 807 | get_info || return 1 808 | fill_arrays 809 | # \r" 810 | ((counter++)) 811 | echo -en "bla die bla $counter\ntwee bla die bla $counter\r" 812 | 813 | #tclsh /etc/shairport/smartie/smartie-cat.tcl -tty /dev/ttyUSB0 814 | done 815 | printf 1>&2 "main_loop done.\n" 816 | } 817 | 818 | unset mpc_vals 819 | unset mpd_vals 820 | unset alsa_vals 821 | unset exif_vals 822 | unset soxi_vals 823 | 824 | declare -A alsa_vals 825 | declare -A mpc_vals 826 | declare -A mpd_vals 827 | declare -A exif_vals 828 | declare -A soxi_vals 829 | 830 | ## input | nfs://srv/media/music/.../track11.aiff 831 | ## | AIFF (Little Endian) PCM in 24bit/192kHz @ 4.503bit/s 832 | label_input="Storage" 833 | label_throughput="MPD" 834 | label_output="DAC" 835 | label_samplerate="Sample rate (Hz)" 836 | label_bitdepth="Bit depth (bit)" 837 | label_bitrate="Bit rate (kbit/s)" 838 | 839 | main_file_bitdepth= 840 | main_file_bitrate= 841 | main_file_bitrate_kvalue= 842 | main_file_bitrate_raw= 843 | main_file_bitrate_unit= 844 | main_file_bitrate_value= 845 | main_file_samplerate= 846 | main_file_sizebytes= 847 | main_mpd_bitdepth= 848 | main_mpd_bitrate= 849 | main_mpd_samplerate= 850 | main_output_bitdepth= 851 | main_output_samplechannelcount= 852 | main_output_sampleencoding= 853 | main_output_samplerate= 854 | main_output_samplerate_raw= 855 | main_playing_trackalbumartist= 856 | main_playing_trackalbumname= 857 | main_playing_trackalbumnumber= 858 | main_playing_trackartist= 859 | main_playing_trackduration= 860 | main_playing_trackperformers= 861 | main_playing_trackplayingpercentage= 862 | main_playing_trackplayingtime= 863 | main_playing_tracktitle= 864 | main_rt_tracknumber_album= 865 | main_rt_tracknumber_playlist= 866 | main_rt_playlistlength= 867 | 868 | declare -A alsa_encoding 869 | declare -a mpc_fields=( 870 | album 871 | albumartist 872 | artist 873 | comment 874 | composer 875 | date 876 | disc 877 | file 878 | genre 879 | mdate 880 | mtime 881 | name 882 | performer 883 | position 884 | time 885 | title 886 | length 887 | track 888 | ) 889 | 890 | mpc_format_string= 891 | for mpc_field in ${mpc_fields[@]}; do 892 | mpc_format_string+="${mpc_field}:[%${mpc_field}%]\n" 893 | done 894 | 895 | ## if the script is not sourced by another script but run within its 896 | ## own shell call function `analyze_commandline_args' 897 | [[ "${BASH_SOURCE[0]:-}" != "${0}" ]] || \ 898 | get_info "$@" 899 | 900 | #set +x 901 | #exec 2>&3 3>&- 902 | -------------------------------------------------------------------------------- /mpd-configure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## 3 | ## `mpd-configure' is a bash script that tries to ease the 4 | ## configuration of mpd for audiophile purposes. 5 | ## 6 | ## Copyright (C) 2015 Ronald van Engelen 7 | ## This program is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## This program is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with this program. If not, see . 19 | ## 20 | ## The script, helpers and documentation are published at 21 | ## https://github.com/ronalde/mpd-configure 22 | ## 23 | ## Also see `README' 24 | 25 | LANG=C 26 | APP_NAME_MPDCONFIGURE="mpd-configure" 27 | APP_URL="https://github.com/ronalde/${APP_NAME_MPDCONFIGURE}/" 28 | APP_VERSION_MPDCONFIGURE="0.9.7" 29 | 30 | ### defaults 31 | ## default network address to listen to 32 | CONF_MPD_NETWORK_ADRESS_DEFAULT="0.0.0.0" 33 | ## default network port to listen on 34 | CONF_MPD_NETWORK_PORT_DEFAULT="6600" 35 | ## default maximum number of files in music directory 36 | MAX_PLAYLIST_LENGTH_DEFAULT="16384" 37 | 38 | function die_configure() { 39 | printf "\nError in %s (v%s):\n%s\n" \ 40 | "${APP_NAME_MPDCONFIGURE}" "${APP_VERSION_MPDCONFIGURE}" "$@" 1>&2; 41 | exit 1 42 | } 43 | 44 | function debug_configure() { 45 | printf "DEBUG %s *** %s.\n" "${APP_NAME_MPDCONFIGURE}" "$@" 1>&2; 46 | } 47 | 48 | function debug_function() { 49 | printf "DEBUG %-18s:\n" "${APP_NAME_MPDCONFIGURE}" 1>&2; 50 | printf "\tentering function \`%s',\n" "$1" 1>&2; 51 | printf "\twith arguments \`%s'.\n" "$2" 1>&2; 52 | } 53 | 54 | 55 | function really_write() { 56 | ## writes the contents of CONF_CONTENTS to the file specified in $1. 57 | ## returns success or error with descriptive string 58 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 59 | 60 | mpd_conffile="$1" 61 | ## do the actual writing 62 | #debug_configure "${CONF_CONTENTS}" 63 | res="$(echo -e "${CONF_CONTENTS}" > "${mpd_conffile}")" 64 | if [[ $? -ne 0 ]]; then 65 | ## return an error with descriptive message 66 | printf "Could not write to \`%s'" "${mpd_conffile}" 67 | return 1 68 | else 69 | ## return the result 70 | printf "${mpd_conffile}" 71 | fi 72 | } 73 | 74 | function check_or_create_targetdir() { 75 | ## checks if the parent directory for target mpd configuration $1 76 | ## exists and is writable, otherwise attempts to create 77 | ## it. 78 | ## returns success or an error with descriptive string. 79 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 80 | 81 | mpd_conf_file="$1" 82 | mpd_conf_dir="$(dirname "${mpd_conf_file}")" 83 | errors=() 84 | 85 | if [[ ! -d "${mpd_conf_dir}" ]]; then 86 | [[ ! -z ${DEBUG} ]] && \ 87 | debug_configure "${FUNCNAME[0]}: conf dir \`${mpd_conf_dir}' does not exist, \ 88 | try to create it" 89 | res="$(mkdir -p "${mpd_conf_dir}" 2>&1)" 90 | if [[ $? -ne 0 ]]; then 91 | [[ ! -z ${DEBUG} ]] && \ 92 | debug_configure "${FUNCNAME[0]}: error creating mpd_conf_dir \ 93 | \`${mpd_conf_dir}': \`${err}'" 94 | printf " %s can't create parent directory \`%s'.\n" \ 95 | "-" "${mpd_conf_dir}" 96 | return 1 97 | else 98 | [[ ! -z ${DEBUG} ]] && \ 99 | debug_configure "${FUNCNAME[0]}: successfully created \ 100 | mpd_conf_dir \`${mpd_conf_dir}'" 101 | printf "%s" "${mpd_conf_dir}" 102 | fi 103 | else 104 | [[ ! -z ${DEBUG} ]] && \ 105 | debug_configure "${FUNCNAME[0]}: mpd_conf_dir \`${mpd_conf_dir}' already exists" 106 | ## conf dir exists, check if it writable 107 | if [[ ! -w "${mpd_conf_dir}" ]]; then 108 | [[ ! -z ${DEBUG} ]] && \ 109 | debug_configure "${FUNCNAME[0]}: mpd_conf_dir \`${mpd_conf_dir}' already exists" 110 | printf " - no write permission in target directory \`%s'." "${mpd_conf_dir}" 111 | return 1 112 | else 113 | [[ ! -z ${DEBUG} ]] && \ 114 | debug_configure "${FUNCNAME[0]}: conf dir \`${mpd_conf_dir}' is writable" 115 | printf "%s" "${mpd_conf_dir}" 116 | fi 117 | fi 118 | } 119 | 120 | function check_existing_conffile() { 121 | ## checks if target mpd configuration file $1 exists and is 122 | ## writable. 123 | ## returns success or an error with descriptive string. 124 | mpd_conf_file="$1" 125 | if [[ -f "${mpd_conf_file}" ]]; then 126 | if [[ ! -w "${mpd_conf_file}" ]]; then 127 | ## not writable, exit with error 128 | printf " %s existing file \`%s' is present, but is not writable,\n" \ 129 | "-" "${mpd_conf_file}" 130 | return 1 131 | else 132 | [[ ! -z ${DEBUG} ]] && \ 133 | debug_configure "conffile \`${mpd_conf_file}' exists and is writable" 134 | ## return the path to indicate it does exits and is writable 135 | printf "%s" "${mpd_conf_file}" 136 | fi 137 | else 138 | [[ ! -z ${DEBUG} ]] && \ 139 | debug_configure "conffile \`${mpd_conf_file}' does not exist" 140 | ## return success (but no path) 141 | return 0 142 | fi 143 | } 144 | 145 | function make_temp_conf() { 146 | ## makes a temporary file for storing the mpd configuration file. 147 | ## returns the path to the created file or error. 148 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 149 | 150 | ## try to make a temp file in /tmp 151 | tempfile="$(mktemp /tmp/${APP_NAME_MPDCONFIGURE}.XXXX.conf)" 152 | if [[ $? -ne 0 ]]; then 153 | [[ ! -z ${DEBUG} ]] && \ 154 | debug_configure "Unable to create a temporary file \`${tempfile}'" 155 | printf " %s unable to create a temporary file \`%s'\n" \ 156 | "-" "${tempfile}" 1>&2; 157 | # TODO: print to std_out 158 | die "Unable to create a temporary file \`${tempfile}'" 159 | else 160 | [[ ! -z ${DEBUG} ]] && \ 161 | debug_configure "temporary file \`${tempfile}' created" 162 | printf " will store the generated file in a temporary file instead.\n" 1>&2; 163 | ## set a new target path 164 | mpd_conf_file="${tempfile}" 165 | printf "%s" "${mpd_conf_file}" 166 | fi 167 | } 168 | 169 | function backup_original_conf() { 170 | ## create a backup of the current config file $1 171 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 172 | 173 | mpd_conf_file="$1" 174 | 175 | if [[ ! -z ${TEMP_CONF_BACKUP} ]]; then 176 | if [[ -f "${TEMP_CONF_BACKUP}" ]]; then 177 | printf "TEMP_CONF_BACKUP specified to \`%s' but already exists; not overwriting." \ 178 | "${TEMP_CONF_BACKUP}" 179 | return 1 180 | fi 181 | else 182 | TEMP_CONF_BACKUP="$(mktemp -p /tmp ${mpd_conf_file##*/}.XXXX)" 183 | if [[ $? -ne 0 ]]; then 184 | printf "error creating a temporary file which should hold the backup." 185 | return 1 186 | else 187 | [[ ! -z ${DEBUG} ]] && \ 188 | debug_configure "will create backup in \`${TEMP_CONF_BACKUP}'." 189 | fi 190 | fi 191 | res="$(cp -av "${mpd_conf_file}" "${TEMP_CONF_BACKUP}")" 192 | if [[ $? -ne 0 ]]; then 193 | ## error creating a backup with copy 194 | printf "${res}" 195 | return 1 196 | else 197 | printf "%s" "${TEMP_CONF_BACKUP}" 198 | fi 199 | 200 | 201 | } 202 | 203 | 204 | 205 | function write_conffile() { 206 | ## prepares to write to the target mpd configuration file specified by $1. 207 | ## returns success or an error with descriptive string. 208 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 209 | 210 | declare -a errors 211 | error_message="" 212 | mpd_conf_file="$1" 213 | existing_file= 214 | mpd_confdir="" 215 | maketemp= 216 | existing_file="$(check_existing_conffile "${mpd_conf_file}")" 217 | ## check if the file exists 218 | if [[ $? -ne 0 ]]; then 219 | ## the file exists but is not writable 220 | maketemp="${existing_file}" 221 | else 222 | if [[ -z "${existing_file}" ]]; then 223 | ## new file, check the (parent) path 224 | target_dir_writable="$(check_or_create_targetdir "${mpd_conf_file}")" 225 | if [[ $? -ne 0 ]]; then 226 | maketemp="${target_dir_writable}" 227 | fi 228 | fi 229 | fi 230 | 231 | if [[ ! -z ${maketemp} ]]; then 232 | ## either existing file or new target directory is not writable 233 | [[ ! -z ${DEBUG} ]] && \ 234 | debug_configure "can't write to target file \`${mpd_conf_file}': \`${maketemp}'" 235 | printf "\nError: unable to write to %s \`%s':\n" \ 236 | "${MSG_MPD_CONFFILE}" "${mpd_conf_file}" 1>&2; 237 | printf "%s\n" "${maketemp}" 1>&2; 238 | mpd_conf_file="$(make_temp_conf)" 239 | else 240 | ## only prompt when both OVERWRITE_EXISTING_CONFFILE and 241 | ## DISABLE_PROMPTS are not set 242 | if [[ ! -z ${existing_file} ]]; then 243 | if [[ -z ${OPT_QUIET} ]]; then 244 | printf " %s existing %s found in \`%s',\n" \ 245 | "-" "${MSG_MPD_CONFFILE}" "${mpd_conf_file}" 1>&2; 246 | fi 247 | ## check if a prompt is needed 248 | if [[ ! -z ${SKIP_BACKUP} ]] || [[ ! -z ${DISABLE_PROMPTS} ]]; then 249 | ## prompt not needed 250 | if [[ -z ${OPT_QUIET} ]]; then 251 | printf " but user requested overwriting of original conf file\n" 1>&2; 252 | printf " by setting either SKIP_BACKUP or DISABLE_PROMPTS.\n" 1>&2; 253 | fi 254 | overwrite="yes" 255 | else 256 | ## prompt needed 257 | prompt=" overwrite it (while making a backup of the original)?: " 258 | overwrite="$(read -e -p "${prompt}" -i "Yes" \ 259 | overwrite && echo -e "${overwrite}")" 260 | fi 261 | ## if settings or prompt returns (downcased) `yes' then overwrite the file 262 | if ! [[ "${overwrite,,}" =~ yes ]]; then 263 | ## user chose no to overwrite; try to save in temporary file, otherwise 264 | ## exit with message 265 | printf "\nNot overwriting existing %s \`%s'.\n" \ 266 | "${MSG_MPD_CONFFILE}" "${mpd_conf_file}" 1>&2; 267 | mpd_conf_file="$(make_temp_conf)" 268 | else 269 | ## original file needs to be overwritten, check if a backup is needed 270 | if [[ ! -z ${SKIP_BACKUP} ]]; then 271 | if [[ -z ${OPT_QUIET} ]]; then 272 | printf " %s will overwrite existing conffile without making a backup,\n" 1>&2; 273 | printf " as requested by the user by setting SKIP_BACKUP.\n" 1>&2; 274 | fi 275 | else 276 | ## backup is needed 277 | temp_backup_file="$(backup_original_conf "${mpd_conf_file}")" 278 | if [[ $? -ne 0 ]]; then 279 | printf "could not create backup of \`%s' (%s).\n" \ 280 | "${mpd_conf_file}" "${temp_backup_file}" 1>&2; 281 | printf "will print config to stdout instead.\n" 1>&2; 282 | return 1 283 | else 284 | printf " %s backup of existing %s created in \`%s'\n" \ 285 | "-" "${MSG_MPD_CONFFILE}" "${temp_backup_file}" 1>&2; 286 | fi 287 | 288 | fi 289 | fi 290 | fi 291 | fi 292 | ## do the actual writing 293 | fileok="$(really_write "${mpd_conf_file}")" 294 | if [[ $? -ne 0 ]]; then 295 | die "unspecified error" 296 | else 297 | printf " %s mpd-configure succesfully created a %s in:\n" \ 298 | "-" "${MSG_MPD_CONFFILE}" 1>&2; 299 | ## return the resulting file to the calling function 300 | printf "%s" "${mpd_conf_file}" 301 | fi 302 | 303 | } 304 | 305 | 306 | function command_not_found() { 307 | ## give installation instructions when a command is not available 308 | ## and exit with error, 309 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 310 | 311 | command="$1" 312 | package="$2" 313 | instructions="$3" 314 | msg="Error: command \`${command}' not found. " 315 | 316 | [[ -z "${instructions}" ]] && \ 317 | msg+="Users of Debian (or deratives, like Ubuntu) can install it with: \ 318 | \n sudo apt-get install ${package}" || \ 319 | msg+="${instructions}" 320 | 321 | die_configure "${msg}" 322 | 323 | } 324 | 325 | function prompt_select_hwaddress() { 326 | ## display instructions for multiple interfaces and prompt the 327 | ## user to enter one of the found hwardware addresses, with that 328 | ## of the first available interface filled in. 329 | ## returns the index number of the item entered in the array or an 330 | ## error with description. 331 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 332 | 333 | selected_address=0 334 | selected_key= 335 | prompt_messages+=("Multiple interfaces found:") 336 | default_aif_hwaddress="${ALSA_AIF_HWADDRESSES[0]}" 337 | prompt_messages+=(" specify the hardware address of the interface you wish to use") 338 | prompt_messages+=(" for mpd and press [ENTER] to confirm: ") 339 | ## format the message 340 | prompt="$(printf " %s\n" "${prompt_messages[@]}")" 341 | ## prompt the user and store the input in alsa_aif_hwaddress 342 | alsa_aif_hwaddress="$(read -e -p "${prompt}" \ 343 | -i "${default_aif_hwaddress}" alsa_aif_hwaddress && \ 344 | echo -e "${alsa_aif_hwaddress}")" 345 | [[ ! -z ${DEBUG} ]] && \ 346 | debug_configure "${FUNCNAME[0]}: user entered \`${alsa_aif_hwaddress}'." 347 | ## look up the chosen hardware address in the proper array 348 | for key in "${!ALSA_AIF_HWADDRESSES[@]}"; do 349 | ## set the user selected key and exit loop 350 | if [[ "${ALSA_AIF_HWADDRESSES[$key]}" = \ 351 | "${alsa_aif_hwaddress}" ]]; then 352 | let selected_key=${key} 353 | break 354 | fi 355 | done 356 | 357 | ## handle empty or invalid input 358 | msg="" 359 | if [[ "${ALSA_AIF_HWADDRESSES[$selected_key]}" \ 360 | != "${alsa_aif_hwaddress}" ]]; then 361 | prompt_messages=(" NOTE:") 362 | if [[ -z ${alsa_aif_hwaddress} ]]; then 363 | [[ ! -z ${DEBUG} ]] && \ 364 | debug_configure "${FUNCNAME[0]}: no hwaddress entered" 365 | msg="no hardware address " 366 | else 367 | [[ ! -z ${DEBUG} ]] && \ 368 | debug_configure "${FUNCNAME[0]}: an invalid hwaddress \`${alsa_aif_hwaddress}' entered" 369 | msg="an invalid hardware address (${alsa_aif_hwaddress})" 370 | fi 371 | ## return the problem and error code 372 | printf " %s %s was entered." "-" "${msg}" 373 | return 1 374 | else 375 | ## a valid addres was entered; return its index number 376 | [[ ! -z ${DEBUG} ]] && \ 377 | debug_configure "${FUNCNAME[0]}: chosen key \`${selected_key}' for \ 378 | interface \`${selected_address}'" 379 | 380 | printf "%s" "${selected_key}" 381 | fi 382 | } 383 | 384 | function select_interface_index() { 385 | ## select and the hardware address of one of the number of 386 | ## interfaces found ($1) and return its index number in the 387 | ## ALSA_AIF_HWADDRESSES array. if one interface is found, use its 388 | ## index. In case of multiple interfaces show a list of all 389 | ## interfaces and prompt the user to enter one of them, unless 390 | ## DISABLE_PROMPTS is set, which defaults to the hardware address 391 | ## of the first interface found. 392 | ## returns the index of the chosen hardware address or an error 393 | ## with description. 394 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 395 | 396 | interface_count="$1" 397 | ## variable to hold the index number for the ALSA_AIF_HWADDRESSES 398 | ## array holding the chosen hardware address 399 | selected_array_index=0 400 | 401 | case ${interface_count} in 402 | 0) 403 | printf "0 interfaces found (THIS SHOULD NOT HAPPEN)" 404 | return 1 405 | ;; 406 | 1) 407 | ## single interface found; use it 408 | selected_array_index=0 409 | ;; 410 | *) 411 | ## multiple interfaces found 412 | if [[ ! -z "${DISABLE_PROMPTS}" ]]; then 413 | ## return the address of the first interface 414 | ## found and inform the user of the decision 415 | printf " %s multiple interfaces found, \ 416 | but user set DISABLE_PROMPTS,\n" \ 417 | "-" 1>&2; 418 | printf " therefore using the first \ 419 | one found (${ALSA_AIF_HWADDRESSES[0]}).\n" \ 420 | "-" 1>&2; 421 | selected_array_index=0 422 | else 423 | ## prompt the user 424 | selected_array_index="$(prompt_select_hwaddress)" 425 | if [[ $? -ne 0 ]]; then 426 | ## inform the calling function about the error 427 | printf "%s" "${selected_array_index}" 428 | return 1 429 | fi 430 | fi 431 | ;; 432 | esac 433 | 434 | ## return the selected address to the calling function 435 | printf "%s" "${selected_array_index}" 436 | } 437 | 438 | function fetch_alsa_hwaddresses() { 439 | ## use alsa-capabilities script to get alsa output interfaces, 440 | ## thereby filling the approriate global arrays. 441 | ## returns success or an error. 442 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 443 | 444 | ## when more than one matching output device is found and 445 | ## DISABLE_PROMPTS is not set, prompt the user to select one, 446 | ## otherwise use the first device found. 447 | prompt_messages=() 448 | selected_array_index=0 449 | 450 | ## call return_alsa_interface from the sourced alsa-capabilities 451 | ## for filling up the array ALSA_AIF_HWADDRESSES 452 | 453 | return_alsa_interface 1>&2; 454 | if [[ $? -ne 0 ]]; then 455 | ## something went wrong in return_alsa_interface 456 | printf "error in return_alsa_interface." 1>&2; 457 | return 1 458 | else 459 | interface_count="${#ALSA_AIF_HWADDRESSES[@]}" 460 | [[ ! -z ${DEBUG} ]] && \ 461 | debug_configure "${FUNCNAME[0]}: \ 462 | audio interfaces after filtering: \`${interface_count}'." 463 | fi 464 | return 0 465 | } 466 | 467 | 468 | function check_readable_path() { 469 | ## checks if path ($1), needed for ($2) is readable, or adds 470 | ## problem description ($2) to problems array. 471 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 472 | 473 | path="$1" 474 | purpose="$2" 475 | problem="$3" 476 | 477 | msg="\n - can't access or read ${purpose} \`${path}',\ 478 | \n you won't be able to ${problem}.\n" 479 | if [[ ! -d "${path}" ]]; then 480 | if [[ ! -f "${path}" ]]; then 481 | [[ ! -z ${DEBUG} ]] && \ 482 | debug_configure "${FUNCNAME[0]} (line ${LINENO}): $(printf "${msg}")" 483 | PROBLEMS+=("${msg}") 484 | return 1 485 | fi 486 | fi 487 | } 488 | 489 | 490 | function check_writeable_path() { 491 | ## checks if path ($1), needed for ($2) is writeable, or adds 492 | ## problem description ($2) to problems array. 493 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 494 | 495 | path="$1" 496 | purpose="$2" 497 | problem="$3" 498 | 499 | msg="\n - can't write to ${purpose} \`${path}',\ 500 | \n you won't be able to ${problem}.\n" 501 | 502 | if [[ ! -w "${path}" ]]; then 503 | [[ ! -z ${DEBUG} ]] && \ 504 | debug_configure "${FUNCNAME[0]} (line ${LINENO}): $(printf "${msg}")" 505 | PROBLEMS+=("${msg}") 506 | return 1 507 | fi 508 | } 509 | 510 | function parse_configuration_line() { 511 | ## parse a line from mpd configuration templates, replacing 512 | ## variables in those templates with values from this scripts 513 | ## configuration files and returning them to the calling function. 514 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 515 | 516 | line="$@" 517 | 518 | ## construct the regular expression for configuration items 519 | local brm='([[:alpha:][:alnum:]_]*)[[:space:]]*\"\$\{(.*)\}\"$' 520 | ## match the regexp 521 | if [[ "${line}" =~ ${brm} ]] ; then 522 | ## line is mpd configuration: `config_value "${...}"' 523 | 524 | ## given a line of `config_value "${...}"' 525 | ## the name of the variable with or without a default value: `...' 526 | var_part="${BASH_REMATCH[2]}" 527 | var_template="${var_part}" 528 | var_search="${var_template}" 529 | if [[ "${var_part}" =~ ^(.*):-(.*)$ ]]; then 530 | ## the line contains a variable with default value: 531 | ## `config_value "${VARNAME:-default_value}"' 532 | 533 | ## store the variable name: `VARNAME' 534 | var_template="${BASH_REMATCH[1]}" 535 | var_value="${BASH_REMATCH[2]}" 536 | ## the variable name including its default value `VARNAME:-default_value' 537 | var_search="${BASH_REMATCH[1]}:-${BASH_REMATCH[2]}" 538 | fi 539 | ## assign the name of the variable (`VARNAME') to $expanded_name 540 | exp_name=var_template 541 | ## assign the value of the variable (`$VARNAME') to $expanded_value 542 | local exp_value="${!exp_name}" 543 | ## return the line with its real/expanded value 544 | if [[ ! -z "${!exp_value}" ]]; then 545 | printf "%s" "${line//\$\{${var_search}\}/${!exp_value}}" 546 | else 547 | [[ -z ${INCLUDE_COMMENTS} ]] && \ 548 | printf "" || \ 549 | printf "# %s (value not set)" "${line//\$\{${var_search}\}/${!exp_value}}" 550 | [[ ! -z "${DEBUG}" ]] && \ 551 | debug_configure "not writing empty configuration setting \`${var_template}'." 552 | fi 553 | else 554 | ## a normal line; return it 555 | printf "%s" "${line}" 556 | fi 557 | 558 | } 559 | 560 | function source_enabled_confs() { 561 | ## source conf snippets in ./confs-enabled/*.conf while 562 | ## substituting the variables in those conf files with the ones 563 | ## generated in this script. 564 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 565 | 566 | ## temporary file needed to source enabled configuration snippets files 567 | tempconfs="$(mktemp)" 568 | ## fill the file with all configuration lines from `confs-enabled/*.conf` 569 | sed 's/^\([[:alnum:][:space:]_]*\)[[:space:]]*"\${\(.*\):-\(.*\)}"$/export \2="\${\2:-\3}"/' \ 570 | ${SCRIPT_DIR}/confs-enabled/*.conf | grep -E ^export > "${tempconfs}" 571 | ## source it (needed for variable expansion) 572 | source "${tempconfs}" || die_configure "could not source \`${tempconfs}'" 573 | ## remove it when DEBUG_CONFIGURE is not set 574 | [[ ! -z "${DEBUG}" ]] && \ 575 | debug_configure "sourced configuration values stored in \`${tempconfs}'.\n" || \ 576 | rm "${tempconfs}" 577 | 578 | ## iterate each file 579 | for conf_file in ${CONFS_ENABLED}; do 580 | [[ ! -z "${DEBUG}" ]] && debug_configure "${LINENO}: $(declare -p conf_file).\n" 581 | ## check if the symlink points to a valid file 582 | conffile_path="$(readlink -f "${conf_file}")" 583 | if [[ ! -f "${conffile_path}" ]]; then 584 | ## process the next one if the symlink is broken 585 | [[ ! -z "${DEBUG}" ]] && debug_configure "not a valid symlink \`${conf_file}'" 586 | continue 587 | fi 588 | ## the file exists, parse it 589 | printf "\n## start processing \`%s'\n" "${conf_file##*/}" 590 | ## iterate each line 591 | while IFS='' read -r line ; do 592 | ## skip commented lines 593 | ## create trimmed version of line 594 | if [[ -z ${INCLUDE_COMMENTS} ]]; then 595 | trimmed=$([[ "${line}" =~ [[:space:]]*([^[:space:]]|[^[:space:]].*[^[:space:]])[[:space:]]* ]]; echo -n "${BASH_REMATCH[1]}") 596 | if [[ "${trimmed#\#*}" = "${trimmed}" ]]; then 597 | if [[ ! -z "${trimmed}" ]]; then 598 | ## parse the line 599 | line="$(parse_configuration_line "${line}")" 600 | ## original code 601 | [[ ! -z "${line}" ]] && \ 602 | printf "%s\n" "${line}" 603 | fi 604 | fi 605 | else 606 | line="$(parse_configuration_line "${line}")" 607 | printf "%s\n" "${line}" 608 | fi 609 | done <${conf_file} 610 | printf "## done processing\n" 611 | done 612 | } 613 | 614 | function perform_automagic() { 615 | ### automagic configuration stuff 616 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 617 | 618 | ## client limits 619 | ## if the directory specified in `CONF_MPD_MUSICDIR` is 620 | ## accessible, calculate the number of audio files and double that 621 | ## for `max_playlist_length` parameter. 622 | 623 | default_length=${MAX_PLAYLIST_LENGTH_DEFAULT} 624 | nr_musicfiles=0 625 | 626 | if [[ "${G_CLIENTLIMITS_MAXPLAYLISTLENGTH}x" == "x" ]]; then 627 | [[ ! -z ${DEBUG} ]] && \ 628 | debug_configure "G_CLIENTLIMITS_MAXPLAYLISTLENGTH not set, trying to count the number of music files ..." 629 | ## setting not configured 630 | [[ ! -z ${DEBUG} ]] && \ 631 | debug_configure "$(declare -p G_PATHS_MUSICDIRECTORY)" 632 | if [[ -d "${G_PATHS_MUSICDIRECTORY}" ]]; then 633 | ## music dir exists; calculate number of files 634 | ## TODO: add permission errors to errorlog 635 | ## ls without stat() calls is much faster than find or bash globbing, thanks 636 | ## http://stackoverflow.com/questions/1427032/fast-linux-file-count-for-a-large-number-of-files 637 | let nr_musicfiles="$(ls -fR "${G_PATHS_MUSICDIRECTORY}" 2>/dev/null | wc -l)" 638 | [[ ! -z ${DEBUG} ]] && \ 639 | debug_configure "Done counting, nr of files found: \`${nr_musicfiles}'" 640 | let double_nr_musicfiles=$(( ${nr_musicfiles} * 2 )) 641 | if [[ ${double_nr_musicfiles} -gt ${default_length} ]]; then 642 | G_CLIENTLIMITS_MAXPLAYLISTLENGTH="${double_nr_musicfiles}" 643 | else 644 | G_CLIENTLIMITS_MAXPLAYLISTLENGTH="${default_length}" 645 | fi 646 | else 647 | ## music dir not available; use default 648 | G_CLIENTLIMITS_MAXPLAYLISTLENGTH="${default_length}" 649 | [[ ! -z ${DEBUG} ]] && \ 650 | debug_configure "Music dir not available, nr of files set to default: \`${G_CLIENTLIMITS_MAXPLAYLISTLENGTH}'." 651 | fi 652 | else 653 | [[ ! -z ${DEBUG} ]] && \ 654 | debug_configure "$(declare -p G_CLIENTLIMITS_MAXPLAYLISTLENGTH)" 655 | fi 656 | if [[ "${G_CLIENTLIMITS_MAXPLAYLISTLENGTH}x" != "x" ]]; then 657 | ## if empty set `G_CLIENTLIMITS_MAXCOMMANDLISTSIZE` to 1/8 of 658 | ## `G_CLIENTLIMITS_MAXPLAYLISTLENGTH` 659 | if [[ "${G_CLIENTLIMITS_MAXCOMMANDLISTSIZE}x" == "x" ]]; then 660 | let G_CLIENTLIMITS_MAXCOMMANDLISTSIZE=$(( ${G_CLIENTLIMITS_MAXPLAYLISTLENGTH} / 8 )) 661 | fi 662 | ## if empty, set `MAXOUTPUTBUFFERSIZE` to 1/2 of 663 | ## `G_CLIENTLIMITS_MAXPLAYLISTLENGTH` 664 | if [[ "${G_CLIENTLIMITS_MAXOUTPUTBUFFERSIZE}x" == "x" ]]; then 665 | let G_CLIENTLIMITS_MAXOUTPUTBUFFERSIZE=$(( ${G_CLIENTLIMITS_MAXPLAYLISTLENGTH} / 2 )) 666 | fi 667 | fi 668 | 669 | if [[ ! -z "${DEBUG}" ]]; then 670 | debug_configure "$(declare -p G_CLIENTLIMITS_MAXPLAYLISTLENGTH)" 671 | debug_configure "$(declare -p G_CLIENTLIMITS_MAXCOMMANDLISTSIZE)" 672 | debug_configure "$(declare -p G_CLIENTLIMITS_MAXOUTPUTBUFFERSIZE)" 673 | fi 674 | 675 | ## get network name for zeroconf 676 | if [[ -z "${G_ZEROCONF_ZEROCONFNAME}" ]]; then 677 | ## `G_ZEROCONF_ZEROCONFNAME` is not set; set default zeroconf label 678 | [[ ! -z ${DEBUG} ]] && \ 679 | debug_configure "G_ZEROCONF_ZEROCONFNAME not set ..." 680 | 681 | hostname_string="" 682 | ipv6_re="*::*" 683 | ## only try to get hostname if it isn't link local 684 | if [[ ! "${CONF_MPD_NETWORK_ADRESS}" =~ 127\. ]] || \ 685 | [[ "${CONF_MPD_NETWORK_ADRESS}" != "0.0.0.0" ]] || \ 686 | [[ ! "${CONF_MPD_NETWORK_ADRESS}" =~ ${ipv6_re} ]]; then 687 | ## mpd is configured to use a 'real' ip address; try to get fqdn name 688 | [[ ! -z ${DEBUG} ]] && \ 689 | debug_configure "mpd is configured to use a 'real' ip address ..." 690 | CMD_HOSTNAME="$(type -p hostname)" 691 | if [[ $? -eq 0 ]]; then 692 | hostname="$(${CMD_HOSTNAME})" 693 | [[ ! -z ${DEBUG} ]] && \ 694 | debug_configure "mpd hostname set by ${CMD_HOSTNAME} to \`${hostname}'." 695 | fi 696 | if [[ -z "${hostname}" ]]; then 697 | # use bash's internal hostname 698 | hostname="${HOSTNAME}" 699 | [[ ! -z ${DEBUG} ]] && \ 700 | debug_configure "mpd hostname set by script to HOSTNAME environment variable: \`${hostname}'." 701 | fi 702 | else 703 | [[ ! -z ${DEBUG} ]] && \ 704 | debug_configure "${FUNCNAME[0]} (${LINENO}: \ 705 | $(declare -p G_ZEROCONF_ZEROCONFNAME) (set by user)" 706 | fi 707 | ## set string to use if hostname contains something 708 | if [[ "${hostname}x" != "x" ]]; then 709 | hostname_string="on ${hostname} " 710 | fi 711 | defname="MPD ${hostname_string}(${CONF_ALSA_AIF_DEVLABEL})" 712 | G_ZEROCONF_ZEROCONFNAME="${ZEROCONF_NAME:-${def_name}}" 713 | debug_configure "${FUNCNAME[0]} (${LINENO}: \ 714 | $(declare -p G_ZEROCONF_ZEROCONFNAME)" 715 | 716 | 717 | fi 718 | 719 | ## cap to the maximum nr of characters (64) 720 | G_ZEROCONF_ZEROCONFNAME="$(printf "%.*s\n" 64 "${G_ZEROCONF_ZEROCONFNAME}")" 721 | 722 | ## default directory depth for auto updates 723 | if [[ "${CONF_MPD_AUTOUPDATEDEPTH}x" == "x" ]]; then 724 | G_GENERAL_AUTOUPDATEDEPTH="$(getconf PATH_MAX "${G_PATHS_MUSICDIRECTORY}")" 725 | fi 726 | } 727 | 728 | function get_mpd_musicdir() { 729 | ### get and check music dir 730 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 731 | 732 | ## check if user set the music dir configuration parameters 733 | if [[ ! -z "${CONF_MPD_MUSICDIR}" ]]; then 734 | G_PATHS_MUSICDIRECTORY="${CONF_MPD_MUSICDIR}" 735 | [[ ! -z ${DEBUG} ]] && \ 736 | debug_configure "CONF_MPD_MUSICDIR set by user to \`${G_PATHS_MUSICDIRECTORY}'." 737 | else 738 | [[ ! -z ${DEBUG} ]] && \ 739 | debug_configure "CONF_MPD_MUSICDIR not set by user ..." 740 | 741 | ## try getting the `MUSIC' directory from XDG and use that, 742 | ## otherwise user current dir 743 | CMD_XDGUSERDIR="$(type -p xdg-user-dir)" 744 | if [[ $? -eq 0 ]]; then 745 | [[ ! -z ${DEBUG} ]] && \ 746 | debug_configure "CMD_XDGUSERDIR found in path: \`${CMD_XDGUSERDIR}'." 747 | xdg_music_dir="$(${CMD_XDGUSERDIR} MUSIC)" 748 | if [[ "$?" -eq 0 ]]; then 749 | G_PATHS_MUSICDIRECTORY="${xdg_music_dir}" 750 | [[ ! -z ${DEBUG} ]] && \ 751 | debug_configure "G_PATHS_MUSICDIRECTORY set to xdg-music-dir (${G_PATHS_MUSICDIRECTORY})" 752 | else 753 | [[ ! -z ${DEBUG} ]] && \ 754 | debug_configure "G_PATHS_MUSICDIRECTORY left empty (${G_PATHS_MUSICDIRECTORY})" 755 | fi 756 | else 757 | if [[ ! -z ${DEBUG} ]]; then 758 | debug_configure "CMD_XDGUSERDIR not found in path." 759 | debug_configure "G_PATHS_MUSICDIRECTORY left empty (${G_PATHS_MUSICDIRECTORY})" 760 | fi 761 | fi 762 | fi 763 | 764 | ## use current dir if still empty 765 | if [[ -z ${G_PATHS_MUSICDIRECTORY} ]]; then 766 | G_PATHS_MUSICDIRECTORY="$(pwd)" 767 | [[ ! -z ${DEBUG} ]] && \ 768 | debug_configure "G_PATHS_MUSICDIRECTORY set to current dir: \`${G_PATHS_MUSICDIRECTORY}'." 769 | fi 770 | 771 | check_readable_path "${G_PATHS_MUSICDIRECTORY}" "music directory" "listen to music" 772 | 773 | } 774 | 775 | function get_current_mpdconf_path() { 776 | ## get and return the mpd.conf file in use by the system 777 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 778 | 779 | ## for debian based distros 780 | default_path="/etc/default/mpd" 781 | default_re="[[:space:]]*MPDCONF[[:space:]]*=[[:space:]]*(.*)" 782 | conffile="" 783 | declare -a default_mpd_conf_paths 784 | if [[ "${XDG_CONFIG_HOME}x" == "x" ]]; then 785 | XDG_CONFIG_HOME="~/.config" 786 | fi 787 | declare -a default_mpd_conf_paths=( 788 | "${XDG_CONFIG_HOME}/mpd/mpd.conf" 789 | "~/.mpdconf" 790 | "~/.mpd/mpd.conf" 791 | "/etc/mpd.conf") 792 | if [[ "${CONF_MPD_CONFFILE}x" != "x" ]]; then 793 | if [[ -f "${default_path}" ]]; then 794 | while read -r line; do 795 | if [[ "${line}" =~ ${default_re} ]]; then 796 | [[ ! -z "${DEBUG}" ]] && \ 797 | debug_configure "${FUNCNAME[0]}: default ${MSG_CONF_TITLE}: \`${conffile}'." 798 | conffile="${BASH_REMATCH[1]}" 799 | break 800 | fi 801 | done<"${default_path}" 802 | fi 803 | 804 | if [[ "${conffile}x" != "x" ]]; then 805 | if [[ ! -f "${conffile}" ]]; then 806 | [[ ! -z "${DEBUG}" ]] && \ 807 | debug_configure "${FUNCNAME[0]}: default ${MSG_CONF_TITLE} \`${conffile}' \ 808 | does not exist." 809 | ## default conffile does not exist 810 | else 811 | [[ ! -z "${DEBUG}" ]] && \ 812 | debug_configure "${FUNCNAME[0]}: default ${MSG_CONF_TITLE} \`${conffile}' exists." 813 | fi 814 | else 815 | ## no default conffile, try usual suspects 816 | for conffile in "${default_mpd_conf_paths[@]}"; do 817 | if [[ -f "${conffile}" ]]; then 818 | [[ ! -z "${DEBUG}" ]] && \ 819 | debug_configure "${FUNCNAME[0]}: non-default ${MSG_CONF_TITLE} \`${conffile}' found." 820 | break 821 | fi 822 | done 823 | fi 824 | fi 825 | 826 | if [[ "${conffile}x" == "x" ]]; then 827 | conffile="${default_mpd_conf_paths[0]}" 828 | [[ ! -z "${DEBUG}" ]] && \ 829 | debug_configure "${FUNCNAME[0]}: no ${MSG_CONF_TITLE} found; will use the first default one: ${conffile}." 830 | fi 831 | 832 | if [[ -f "${conffile}" ]]; then 833 | [[ ! -z "${DEBUG}" ]] && \ 834 | debug_configure "${FUNCNAME[0]}: ${MSG_CONF_TITLE} \`${conffile}' exists." 835 | printf "%s" "${conffile}" 836 | else 837 | [[ ! -z "${DEBUG}" ]] && \ 838 | debug_configure "${FUNCNAME[0]}: ${MSG_CONF_TITLE} \`${conffile}' does not (yet) exist." 839 | printf "%s" "${conffile}" 840 | fi 841 | 842 | } 843 | 844 | 845 | function get_mpd_homedir() { 846 | ### get and check working directory for mpd 847 | ## according to mpd source 848 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 849 | 850 | ## if `CONF_MPD_HOMEDIR` not set by user use `XDG_CONFIG_MPDDIR` 851 | if [[ "${CONF_MPD_HOMEDIR}x" != "x" ]]; then 852 | [[ ! -z ${DEBUG} ]] && \ 853 | debug_configure "CONF_MPD_HOMEDIR set by user to \`${CONF_MPD_HOMEDIR}'." 854 | else 855 | CONF_MPD_HOMEDIR="$(dirname "$(get_current_mpdconf_path)")" 856 | [[ ! -z ${DEBUG} ]] && \ 857 | debug_configure "CONF_MPD_HOMEDIR set by script to \`${CONF_MPD_HOMEDIR}'." 858 | fi 859 | ## check if it is accessible or can be made 860 | ## where to store data 861 | MPD_DATADIR="${MPD_DATADIR:-${CONF_MPD_HOMEDIR}}" 862 | CONF_MPD_PLAYLISTDIR="${MPD_PLAYLISTDIR:-${MPD_DATADIR}/playlists}" 863 | CONF_MPD_DBFILE="${MPD_DBFILE:-${MPD_DATADIR}/tag_cache}" 864 | CONF_MPD_LOGFILE="${MPD_LOGFILE:-${MPD_DATADIR}/mpd.log}" 865 | CONF_MPD_PIDFILE="${MPD_PIDFILE:-${MPD_DATADIR}/pid}" 866 | CONF_MPD_STATEFILE="${MPD_STATEFILE:-${MPD_DATADIR}/state}" 867 | CONF_MPD_STICKERFILE="${MPD_STICKERFILE:-${MPD_DATADIR}/sticker}" 868 | } 869 | 870 | 871 | function conf_header() { 872 | ## construct the byline for the configuration file 873 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 874 | 875 | formatted_date="$(date -Is)" 876 | 877 | printf "\n%s\n%s begin of %s\n%s created by \`%s' (version %s) on %s.\n%s see: %s\n%s" \ 878 | "${MSG_CONF_LINESEP}" "${MSG_CONF_SEP}" "${MSG_CONF_TITLE}" \ 879 | "${MSG_CONF_SEP}" "${APP_NAME_MPDCONFIGURE}" "${APP_VERSION_MPDCONFIGURE}" "${formatted_date}" \ 880 | "${MSG_CONF_SEP}" "${APP_URL}" "${MSG_CONF_LINESEP}" 881 | 882 | } 883 | 884 | function conf_footer() { 885 | ## construct the footer for the configuration file 886 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 887 | 888 | printf "\n\n%s\n%s end of %s\n%s" \ 889 | "${MSG_CONF_LINESEP}" "${MSG_CONF_SEP}" \ 890 | "${MSG_CONF_TITLE}" "${MSG_CONF_LINESEP}" 891 | } 892 | 893 | 894 | function do_configure() { 895 | ## main function 896 | [[ ! -z "${DEBUG}" ]] && debug_function "${FUNCNAME[0]}" "$*" 897 | 898 | analyze_command_line_conf "$@" || exit 899 | 900 | ## fill the ALSA_AIF_HWADDRESSES and ALSA_AIF_DEVLABELS arrays and 901 | ## get a count of number of audio output interfaces 902 | fetch_alsa_hwaddresses 903 | if [[ $? -ne 0 ]]; then 904 | die_configure "${FUNCNAME[0]}: error running \`fetch_alsa_hwaddresses'" 905 | else 906 | interface_count=${#ALSA_AIF_HWADDRESSES[@]} 907 | fi 908 | [[ ! -z ${DEBUG} ]] && \ 909 | debug_configure "${FUNCNAME[0]}: fetch_alsa_hwaddresses returned ${interface_count} interfaces." 910 | 911 | ## get the index number of the selected hardware address in the 912 | ## ALSA_AIF_HWADDRESSES array 913 | selected_array_index="$(select_interface_index "${interface_count}")" 914 | if [[ $? -ne 0 ]]; then 915 | ## select_interface_index returned a descriptive error 916 | die_configure "${selected_array_index}" 917 | else 918 | ## store the hardware address and label for usage in audio_conf 919 | CONF_ALSA_AIF_HWADDRESS="${ALSA_AIF_HWADDRESSES[${selected_array_index}]}" 920 | CONF_ALSA_AIF_DEVLABEL="${ALSA_AIF_DEVLABELS[${selected_array_index}]} - \ 921 | ${ALSA_AIF_LABELS[${selected_array_index}]}" 922 | fi 923 | [[ ! -z ${DEBUG} ]] && \ 924 | debug_configure "${FUNCNAME[0]}: CONF_ALSA_AIF_HWADDRESS=\`${CONF_ALSA_AIF_HWADDRESS}'." 925 | 926 | if [[ ! -z "${CONF_MPD_CONFFILE}" ]]; then 927 | default_mpd_conffile="$(get_current_mpdconf_path "${CONF_MPD_CONFFILE}")" 928 | if [[ $? -eq 0 ]]; then 929 | if [[ "${CONF_MPD_CONFFILE}" != "${default_mpd_conffile}" ]]; then 930 | printf " %s output %s set to \`%s', but according to system\n" \ 931 | "-" "${MSG_MPD_CONFFILE}" "${CONF_MPD_CONFFILE}" 1>&2; 932 | printf " \`%s' will be used by mpd by default. " \ 933 | "${default_mpd_conffile}" 1>&2; 934 | printf "%s in order to use the new file, \n start mpd with:\n" "-" 1>&2; 935 | printf " mpd [other options] %s\n" \ 936 | "${CONF_MPD_CONFFILE}" 1>&2; 937 | printf " or rerun this script and set the \`--output' option to \`%s'.\n" \ 938 | "${CONF_MPD_CONFFILE}" 939 | fi 940 | fi 941 | fi 942 | 943 | ## get and check paths 944 | get_mpd_musicdir 945 | [[ $? -eq 0 ]] || die_configure "error in get_mpd_musicdir." 946 | 947 | ## get and check paths 948 | get_mpd_homedir 949 | [[ $? -eq 0 ]] || die_configure "error in get_mpd_homedir." 950 | 951 | ### perform automagic configuration 952 | perform_automagic 953 | [[ $? -eq 0 ]] || die_configure "error in perform_automagic." 954 | 955 | [[ ! -z ${DEBUG} ]] && \ 956 | debug_configure "done with perform_automagic." 957 | 958 | ## store conf snippets in `CONF_CONTENTS` 959 | header="$(conf_header)" 960 | footer="$(conf_footer)" 961 | 962 | ## iterate each line in enabled files in `confs-enabled/*.conf` 963 | CONF_CONTENTS="${header}$(source_enabled_confs)${footer}" 964 | 965 | ## displays the contents of CONF_CONTENTS or write them to 966 | ## `CONF_MPD_CONFFILE` if set 967 | if [[ ! -z "${CONF_MPD_CONFFILE}" ]]; then 968 | [[ ! -z ${DEBUG} ]] && \ 969 | debug_configure "CONF_MPD_CONFFILE set to \`${CONF_MPD_CONFFILE}'." 970 | 971 | res="$(write_conffile "${CONF_MPD_CONFFILE}")" 972 | printf "%s\n" "${res}" 973 | else 974 | [[ ! -z ${DEBUG} ]] && \ 975 | debug_configure "CONF_MPD_CONFFILE left empty; printing to std_out." 976 | ## display the results (print to std_out) 977 | printf "%s\n" "${CONF_CONTENTS}" 978 | fi 979 | 980 | exit 981 | } 982 | 983 | function display_usageinfo_conf() { 984 | ## display syntax and exit 985 | [[ ! -z ${DEBUG} ]] && \ 986 | debug_function_ac "${FUNCNAME[0]}" "$*" 987 | 988 | msg=$(cat <&2; 1035 | } 1036 | 1037 | 1038 | function analyze_command_line_conf() { 1039 | ## parse command line arguments using the `manual loop` method 1040 | ## described in http://mywiki.wooledge.org/BashFAQ/035. 1041 | [[ ! -z ${DEBUG} ]] && \ 1042 | debug_function_ac "${FUNCNAME[0]}" "$*" 1043 | 1044 | while :; do 1045 | case "${1:-}" in 1046 | -o|--output) 1047 | if [ -n "${2:-}" ]; then 1048 | [[ ! -z ${DEBUG} ]] && \ 1049 | debug "$(printf "option \`%s' set to \`%s'.\n" "$1" "$2")" 1050 | CONF_MPD_CONFFILE="$2" 1051 | shift 2 1052 | continue 1053 | else 1054 | ## in alsa-capabilities 1055 | analyze_opt_limit "$1" 1056 | exit 1 1057 | fi 1058 | ;; 1059 | -l|--limit) 1060 | if [ -n "${2:-}" ]; then 1061 | [[ ! -z ${DEBUG} ]] && \ 1062 | debug "$(printf "option \`%s' set to \`%s'.\n" "$1" "$2")" 1063 | OPT_LIMIT="True" 1064 | analyze_opt_limit "$1" "$2" 1065 | shift 2 1066 | continue 1067 | else 1068 | ## in alsa-capabilities 1069 | analyze_opt_limit "$1" 1070 | exit 1 1071 | fi 1072 | ;; 1073 | -c|--customfilter) 1074 | if [ -n "${2:-}" ]; then 1075 | [[ ! -z ${DEBUG} ]] && \ 1076 | debug "$(printf "option \`%s' set to \`%s'.\n" "$1" "$2")" 1077 | OPT_FILTER="${2}" 1078 | shift 2 1079 | continue 1080 | else 1081 | printf "ERROR: option \`%s' requires a non-empty argument.\n" "$1" 1>&2 1082 | exit 1 1083 | fi 1084 | ;; 1085 | -a|--address) 1086 | if [ -n "${2:-}" ]; then 1087 | [[ ! -z ${DEBUG} ]] && \ 1088 | debug "option \`$1' set to \`$2'" 1089 | OPT_HWFILTER="$2" 1090 | shift 2 1091 | continue 1092 | else 1093 | printf "ERROR: option \`%s' requires a alsa hardware address \ 1094 | as an argument (eg \`hw:x,y')\n" "$1" 1>&2 1095 | exit 1 1096 | fi 1097 | ;; 1098 | -q|--quiet|--silent) 1099 | [[ ! -z ${DEBUG} ]] && \ 1100 | debug "option \`$1' set" 1101 | OPT_QUIET=true 1102 | shift 1103 | continue 1104 | ;; 1105 | -n|--noprompts|--noprompt) 1106 | [[ ! -z ${DEBUG} ]] && \ 1107 | debug "option \`$1' set" 1108 | DISABLE_PROMPTS=true 1109 | shift 1110 | continue 1111 | ;; 1112 | --nobackup|--no-backup) 1113 | [[ ! -z ${DEBUG} ]] && \ 1114 | debug "option \`$1' set" 1115 | SKIP_BACKUP=true 1116 | shift 1117 | continue 1118 | ;; 1119 | -h|-\?|--help) 1120 | display_usageinfo_conf 1121 | exit 1122 | ;; 1123 | --) 1124 | shift 1125 | break 1126 | ;; 1127 | -?*) 1128 | printf "Notice: unknown option \`%s' ignored\n\n." "$1" 1>&2 1129 | display_usageinfo_conf 1130 | exit 1131 | ;; 1132 | *) 1133 | break 1134 | esac 1135 | done 1136 | 1137 | } 1138 | 1139 | 1140 | ### program start 1141 | 1142 | ## check if we're re being sourced 1143 | SOURCED="" 1144 | 1145 | if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then 1146 | SOURCED=True 1147 | ## store the current directory 1148 | SCRIPT_DIR=$(pwd) 1149 | [[ ! -z ${DEBUG} ]] && \ 1150 | debug_configure "script ${0} is being sourced; using SCRIPT_DIR \`${SCRIPT_DIR}'." 1151 | else 1152 | ## store the directory in which the script resides 1153 | SCRIPT_DIR=$(dirname $0) 1154 | [[ ! -z ${DEBUG} ]] && \ 1155 | debug_configure "script ${0} is being run; using SCRIPT_DIR \`${SCRIPT_DIR}'." 1156 | fi 1157 | 1158 | if [[ ! -z ${DEBUG} ]]; then 1159 | debug_configure "debugging turned on by user." 1160 | fi 1161 | 1162 | ## no problem if GNU find is not found; fail silently 1163 | 1164 | 1165 | ## source own helper script if found 1166 | ALSA_CAPABILITIES_FILE="alsa-capabilities" 1167 | ALSA_CAPABILITIES_SCRIPT="${SCRIPT_DIR}/${ALSA_CAPABILITIES_FILE}" 1168 | 1169 | if [[ -f "${ALSA_CAPABILITIES_SCRIPT}" ]]; then 1170 | [[ ! -z ${DEBUG} ]] && \ 1171 | debug_configure "will source ALSA_CAPABILITIES_SCRIPT \`${ALSA_CAPABILITIES_SCRIPT}' ..." 1172 | source "${ALSA_CAPABILITIES_SCRIPT}" 1173 | if [[ $? -eq 0 ]]; then 1174 | [[ ! -z ${DEBUG} ]] && debug_configure "... done." 1175 | else 1176 | die_configure "error sourcing ALSA_CAPABILITIES_SCRIPT \`${ALSA_CAPABILITIES_SCRIPT}'." 1177 | fi 1178 | else 1179 | die_configure "required script \`${ALSA_CAPABILITIES_SCRIPT}' not found." 1180 | fi 1181 | 1182 | ## ugly temporary hack; see github issue #8 1183 | set +u 1184 | set +e 1185 | 1186 | ## list of enabled configuration snippet files 1187 | CONFS_ENABLED=${SCRIPT_DIR}/confs-enabled/*.conf 1188 | [[ ! -z ${DEBUG} ]] && debug_configure "CONFS_ENABLED: \`${CONFS_ENABLED}'." 1189 | 1190 | ## global indexed arrays that will be filled from `alsacapabilities.sh' 1191 | declare -a ALSA_AIF_HWADDRESSES=() 1192 | declare -a ALSA_AIF_DEVLABELS=() 1193 | declare -a ALSA_AIF_LABELS=() 1194 | 1195 | ## for storing (potential) problems 1196 | declare -a PROBLEMS=() 1197 | 1198 | ## set to non empty to disable prompting 1199 | DISABLE_PROMPTS="${DISABLE_PROMPTS:-}" 1200 | ## set to non empty to overwrite an existing conf file without making 1201 | ## a backup, which it does when only DISABLE_PROMPTS is set 1202 | SKIP_BACKUP="${SKIP_BACKUP:-}" 1203 | TEMP_CONF_BACKUP="${TEMP_CONF_BACKUP:-}" 1204 | 1205 | ## source the config file if its present 1206 | PREFERENCES_FILE="${SCRIPT_DIR}/mpd-configure.conf" 1207 | if [[ -f "${PREFERENCES_FILE}" ]]; then 1208 | source "${PREFERENCES_FILE}" 1209 | [[ ! -z ${DEBUG} ]] && \ 1210 | debug_configure "PREFERENCES_FILE sourced: \`${PREFERENCES_FILE}'" 1211 | else 1212 | [[ ! -z ${DEBUG} ]] && \ 1213 | debug_configure "PREFERENCES_FILE \`${PREFERENCES_FILE}' not found." 1214 | fi 1215 | 1216 | CONF_MPD_CONFFILE="${CONF_MPD_CONFFILE:-}" 1217 | 1218 | ## for backwards compatibility 1219 | [[ ! -z "${CONF_ZEROCONF_ENABLED}" ]] || [[ ! -z "${ENABLE_LASTFM}" ]] && \ 1220 | die_configure "configuration settings \`CONF_ZEROCONF_ENABLED' and 1221 | \`ENABLE_LASTFM' \nare no longer valid. please consult \`README'." 1222 | 1223 | ## for backwards compatibility 1224 | if [[ ! -z "${CONF_MPD_HOST}" ]] || [[ ! -z "${CONF_MPD_NETWORK_ADRESS}" ]]; then 1225 | if [[ ! -z "${CONF_MPD_HOST}" ]] && [[ ! -z "${CONF_MPD_NETWORK_ADRESS}" ]]; then 1226 | [[ ! -z ${DEBUG} ]] && \ 1227 | "Both CONF_MPD_HOST and CONF_MPD_NETWORK_ADRESS set by user, using the latter" 1228 | G_NETWORK_BINDTOADDRESS="${CONF_MPD_NETWORK_ADRESS}" 1229 | [[ ! -z ${DEBUG} ]] && \ 1230 | debug_configure "CONF_MPD_NETWORK_ADRESS set by user to: \`${CONF_MPD_NETWORK_ADRESS}'." 1231 | else 1232 | 1233 | if [[ ! -z "${CONF_MPD_HOST}" ]]; then 1234 | CONF_MPD_NETWORK_ADRESS="${CONF_MPD_HOST}" 1235 | G_NETWORK_BINDTOADDRESS="${CONF_MPD_NETWORK_ADRESS}" 1236 | [[ ! -z ${DEBUG} ]] && \ 1237 | debug_configure "CONF_MPD_NETWORK_ADRESS set by user through CONF_MPD_HOST to: \`${CONF_MPD_HOST}'." 1238 | fi 1239 | if [[ ! -z "${CONF_MPD_NETWORK_ADRESS}" ]]; then 1240 | G_NETWORK_BINDTOADDRESS="${CONF_MPD_NETWORK_ADRESS}" 1241 | [[ ! -z ${DEBUG} ]] && \ 1242 | debug_configure "CONF_MPD_NETWORK_ADRESS set by user to: \`${CONF_MPD_NETWORK_ADRESS}'." 1243 | fi 1244 | fi 1245 | else 1246 | ## both not set; default 1247 | CONF_MPD_NETWORK_ADRESS="${CONF_MPD_NETWORK_ADRESS_DEFAULT}" 1248 | G_NETWORK_BINDTOADDRESS="${CONF_MPD_NETWORK_ADRESS}" 1249 | [[ ! -z ${DEBUG} ]] && \ 1250 | debug_configure "CONF_MPD_NETWORK_ADRESS set to default: \`${CONF_MPD_NETWORK_ADRESS}'." 1251 | fi 1252 | 1253 | if [[ ! -z "${CONF_ZEROCONF_LABEL}" ]]; then 1254 | G_ZEROCONF_ZEROCONFNAME="${CONF_ZEROCONF_LABEL}" 1255 | [[ ! -z ${DEBUG} ]] && \ 1256 | debug_configure "G_ZEROCONF_ZEROCONFNAME set to CONF_ZEROCONF_LABEL: \`${CONF_ZEROCONF_LABEL}'" 1257 | fi 1258 | ## global variable for holding the contents of the conf file 1259 | CONF_CONTENTS="" 1260 | 1261 | ## general messages and fixed strings 1262 | MSG_MPD_CONFFILE="mpd configuration file" 1263 | MSG_CONF_LINESEP="$(printf '#%.0s' {1..76})" 1264 | MSG_CONF_SEP="###" 1265 | MSG_CONF_TITLE="mpd configuration file" 1266 | 1267 | 1268 | ## pass limits to the alsa-capabilities script 1269 | if [[ ! -z ${LIMIT_INTERFACE_TYPE} ]]; then 1270 | case ${LIMIT_INTERFACE_TYPE} in 1271 | "analog") 1272 | OPT_LIMIT_AO="True" 1273 | [[ ! -z ${DEBUG} ]] && \ 1274 | debug_configure "Only process analog interfaces (OPT_LIMIT_AO): ${OPT_LIMIT_AO}" 1275 | ;; 1276 | 1277 | "digital") 1278 | OPT_LIMIT_DO="True" 1279 | [[ ! -z ${DEBUG} ]] && \ 1280 | debug_configure "Only process digital interfaces (OPT_LIMIT_DO): ${OPT_LIMIT_DO}" 1281 | ;; 1282 | "usb"|"uac") 1283 | OPT_LIMIT_UO="True" 1284 | [[ ! -z ${DEBUG} ]] && \ 1285 | debug_configure "Only process usb interfaces (OPT_LIMIT_UO): ${OPT_LIMIT_UO}" 1286 | ;; 1287 | esac 1288 | fi 1289 | 1290 | if [[ ! -z ${LIMIT_INTERFACE_FILTER} ]]; then 1291 | OPT_FILTER="${LIMIT_INTERFACE_FILTER}" 1292 | [[ ! -z ${DEBUG} ]] && \ 1293 | debug_configure "OPT_FILTER set to LIMIT_INTERFACE_FILTER: \`${LIMIT_INTERFACE_FILTER}'" 1294 | fi 1295 | 1296 | ## display (potential) problematic situations, like no right access to 1297 | ## files and directories 1298 | if [[ ! -z "${DEBUG}" ]]; then 1299 | if [[ ${#PROBLEMS[@]} -gt 0 ]]; then 1300 | debug_configure "\nPotential problems found:\n${PROBLEMS[*]}" 1301 | else 1302 | debug_configure "No potential problems found." 1303 | fi 1304 | fi 1305 | 1306 | ## if the script is sourced do nothing, otherwise run main flow 1307 | if [[ -z "${SOURCED}" ]]; then 1308 | [[ ! -z ${DEBUG} ]] && \ 1309 | debug_configure "start main function \`do_configure' ..." 1310 | 1311 | do_configure "$@" 1312 | 1313 | [[ ! -z ${DEBUG} ]] && \ 1314 | debug_configure "... main function \`do_configure' done." 1315 | else 1316 | [[ ! -z ${DEBUG} ]] && \ 1317 | debug_configure "you may run main function \`do_configure'." 1318 | 1319 | fi 1320 | 1321 | ### done 1322 | -------------------------------------------------------------------------------- /alsa-capabilities: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC2191 3 | ## ^^ we frequently use arrays for passing arguments to functions. 4 | 5 | ## This script for linux with bash 4.x displays a list with the audio 6 | ## capabilities of each alsa audio output interface and stores them in 7 | ## arrays for use in other scripts. This functionality is exposed by 8 | ## the `return_alsa_interface' function which is avaliable after 9 | ## sourcing the file. When ran from a shell, it will call that 10 | ## function. 11 | ## 12 | ## Copyright (C) 2014 Ronald van Engelen 13 | ## This program is free software: you can redistribute it and/or modify 14 | ## it under the terms of the GNU General Public License as published by 15 | ## the Free Software Foundation, either version 3 of the License, or 16 | ## (at your option) any later version. 17 | ## 18 | ## This program is distributed in the hope that it will be useful, 19 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | ## GNU General Public License for more details. 22 | ## 23 | ## You should have received a copy of the GNU General Public License 24 | ## along with this program. If not, see . 25 | ## 26 | ## Source: https://github.com/ronalde/mpd-configure 27 | ## See also: https://lacocina.nl/detect-alsa-output-capabilities 28 | 29 | LANG=C 30 | 31 | APP_NAME_AC="alsa-capabilities" 32 | APP_VERSION="0.9.5" 33 | APP_INFO_URL="https://lacocina.nl/detect-alsa-output-capabilities" 34 | 35 | ## set DEBUG to a non empty value to display internal program flow to 36 | ## stderr 37 | DEBUG="${DEBUG:-}" 38 | ## set PROFILE to a non empty value to get detailed timing 39 | ## information. Normal output is suppressed. 40 | PROFILE="${PROFILE:-}" 41 | ## to see how the script behaves with a certain output of aplay -l 42 | ## on a particular host, store it's output in a file and supply 43 | ## the file path as the value of TESTFILE, eg: 44 | ## `TESTFILE=/tmp/somefile ./bash-capabilities 45 | ## All hardware and device tests will fail or produce fake outputs 46 | ## (hopefully with some grace). 47 | TESTFILE="${TESTFILE:-}" 48 | 49 | ### generic functions 50 | function die() { 51 | printf 1>&2 "\nError:\n%s\n\n" "$@" 52 | exit 1 53 | } 54 | 55 | function debug() { 56 | lineno="$1" 57 | message="$2" 58 | printf 1>&2 "=%.0s" {1..100} 59 | printf 1>&2 "\nDEBUG *** %s (%4d): %s\n" \ 60 | "${APP_NAME_AC}" \ 61 | "${lineno}" \ 62 | "${message}" 63 | } 64 | 65 | function command_not_found() { 66 | ## give installation instructions for package $2 when command $1 67 | ## is not available, optional with non default instructions $3 68 | ## and exit with error 69 | command="$1" 70 | package="$2" 71 | instructions="${3:-}" 72 | msg="command \`${command}' (package \`${package}') not found. " 73 | if [[ -z "${instructions}" ]]; then 74 | msg+="See 'Requirements' on ${APP_INFO_URL}." 75 | else 76 | msg+="${instructions}" 77 | fi 78 | die "${msg}" 79 | } 80 | 81 | ### alsa related functions 82 | function get_aplay_output() { 83 | ## use aplay to do a basic alsa sanity check using aplay -l, or 84 | ## optionally using $TESTFILE containing the stored output of 85 | ## 'aplay -l'. 86 | ## returns the raw output of aplay or an error. 87 | res="" 88 | aplay_msg_nosoundcards_regexp="no[[:space:]]soundcards" 89 | if [[ "${TESTFILE}x" != "x" ]]; then 90 | if [[ ! -f "${TESTFILE}" ]]; then 91 | # shellcheck disable=SC2059 92 | printf 1>&2 "${MSG_APLAY_ERROR_NOSUCHTESTFILE}" \ 93 | "${TESTFILE}" 94 | return 1 95 | else 96 | ## get the output from a file for testing purposes 97 | # shellcheck disable=SC2059 98 | printf 1>&2 "${MSG_APLAY_USINGTESTFILE}\n" \ 99 | "${TESTFILE}" 100 | # shellcheck disable=SC2059 101 | res="$(< "${TESTFILE}")" || \ 102 | ( printf "${MSG_APLAY_ERROR_OPENINGTESTFILE}" && \ 103 | return 1 ) 104 | fi 105 | else 106 | ## run aplay -l to check for alsa errors or display audio cards 107 | res="$(${CMD_APLAY} -l 2>&1)" || \ 108 | ( 109 | # shellcheck disable=SC2059 110 | printf "${MSG_APLAY_ERROR_GENERAL}\n" "${res}" 111 | ## TODO: react on specific aplay error 112 | [[ ${DEBUG} ]] && debug "${LINENO}" "\`${CMD_APLAY} -l' returned error: \`${res}'" 113 | return 1 114 | ) 115 | ## check for no soundcards 116 | if [[ "${res}" =~ ${aplay_msg_nosoundcards_regexp} ]]; then 117 | printf "%s\n" "${MSG_APLAY_ERROR_NOSOUNDCARDS}" 118 | ## TODO: react on specific aplay error 119 | [[ ${DEBUG} ]] && debug "${LINENO}" "\`${CMD_APLAY} -l' returned no cards: \`${res}'" 120 | return 1 121 | fi 122 | fi 123 | ## return the result to the calling function 124 | printf "%s" "${res}" 125 | } 126 | 127 | function handle_doublebrackets() { 128 | ## return the name of the alsa card / device, even when they 129 | ## contain brackets. 130 | string="$*" 131 | bracketcounter=0 132 | for (( i=0; i<${#string}; i++ )); do 133 | char="${string:$i:1}" 134 | if [[ "${char}" = "[" ]]; then 135 | (( bracketcounter++ )) 136 | elif [[ "${char}" = "]" ]]; then 137 | (( bracketcounter-- )) 138 | fi 139 | if (( bracketcounter > 0 )); then 140 | ## inside outer brackets 141 | if (( bracketcounter < 2 )) && [[ "${char}" == "[" ]]; then 142 | [[ ${DEBUG} ]] && \ 143 | debug "${LINENO}" "name with brackets found." 144 | else 145 | # shellcheck disable=SC2059 146 | printf "${char}" 147 | fi 148 | fi 149 | done 150 | } 151 | 152 | function return_output_human() { 153 | ## print default output to std_err. 154 | ## called by fetch_alsa_outputinterfaces. 155 | printf "%s\n" "${alsa_if_display_title}" 1>&2; 156 | printf " - %-17s = %-60s\n" \ 157 | "${MSG_ALSA_DEVNAME}" \ 158 | "${alsa_dev_label}" 1>&2; 159 | printf " - %-17s = %-60s\n" \ 160 | "${MSG_ALSA_IFNAME}" "${alsa_if_label}" 1>&2; 161 | printf " - %-17s = %-60s\n" \ 162 | "${MSG_ALSA_UACCLASS}" "${alsa_if_uacclass}" 1>&2; 163 | printf " - %-17s = %-60s\n" \ 164 | "${MSG_ALSA_CHARDEV}" "${alsa_if_chardev}" 1>&2; 165 | if [[ ! -z ${formats_res_err} ]]; then 166 | ## device is locked by an unspecified process 167 | printf " - %-17s = %-60s\n" \ 168 | "${MSG_ALSA_ENCODINGFORMATS}" \ 169 | "${MSG_ERROR_GETTINGFORMATS}" 1>&2; 170 | printf " %-17s %-60s\n" \ 171 | " " \ 172 | "${formats_res[@]}" 1>&2; 173 | else 174 | formatcounter=0 175 | if [[ ! -z ${OPT_SAMPLERATES} ]]; then 176 | MSG_ALSA_ENCODINGFORMATS="samplerates (Hz)" 177 | fi 178 | printf " - %-17s = " \ 179 | "${MSG_ALSA_ENCODINGFORMATS}" 1>&2; 180 | # shellcheck disable=SC2141 181 | while IFS="\n" read -r line; do 182 | (( formatcounter++ )) 183 | if (( formatcounter > 1 )); then 184 | printf "%-23s" " " 1>&2; 185 | fi 186 | printf "%-60s\n" "${line}" 1>&2; 187 | done<<<"${alsa_if_formats[@]}" 188 | fi 189 | printf " - %-17s = %-60s\n" \ 190 | "${MSG_ALSA_MONITORFILE}" "${alsa_if_monitorfile}" 1>&2; 191 | printf " - %-17s = %-60s\n" \ 192 | "${MSG_ALSA_STREAMFILE}" "${alsa_if_streamfile}" 1>&2; 193 | printf "\n" 194 | } 195 | 196 | function key_val_to_json() { 197 | ## returns a json "key": "val" pair. 198 | key="$1" 199 | val="$2" 200 | ## check if val is a number 201 | if printf -v numval "%d" "${val}" 2>/dev/null; then 202 | ## it is 203 | printf '"%s": %d' \ 204 | "${key}" "${numval}" 205 | else 206 | printf '"%s": "%s"' \ 207 | "${key}" "${val}" 208 | fi 209 | printf "\n" 210 | } 211 | 212 | function ret_json_format() { 213 | ## returns the json formatted encoding format and possibly sample 214 | ## rates. 215 | formats_raw="$1" 216 | declare -a json_formats 217 | if [[ "${formats_raw}" =~ ':' ]]; then 218 | ## sample rates included 219 | while read -r line; do 220 | split_re="(.*):(.*)" 221 | if [[ "${line}" =~ ${split_re} ]]; then 222 | format=${BASH_REMATCH[1]} 223 | IFS=" " samplerates=(${BASH_REMATCH[2]}) 224 | printf -v sr_out "\t\t\"%s\",\n" \ 225 | "${samplerates[@]}" 226 | sr_out="${sr_out%,*}" 227 | label_samplerates='"samplerates"' 228 | output_line="{ 229 | $(key_val_to_json format "${format// /}"), 230 | ${label_samplerates}: [ 231 | ${sr_out} 232 | ] 233 | }," 234 | output_lines+=("${output_line}") 235 | fi 236 | done<<<"${formats_raw}" 237 | printf -v json_formats "\t%s\n" "${output_lines[@]}" 238 | ## strip the continuation comma from the last element 239 | json_formats="${json_formats%,*}" 240 | else 241 | ## no sample rates included 242 | IFS="," formats_res=(${formats_raw}) 243 | printf -v json_formats '\t\t"%s",\n' \ 244 | "${formats_res[@]// /}" 245 | ## strip the continuation comma from the last element 246 | json_formats="${json_formats%,*}" 247 | fi 248 | printf "%s" "${json_formats}" 249 | } 250 | 251 | function ret_json_card() { 252 | ## print json formatted output to std_out. 253 | ## called by fetch_alsa_outputinterfaces. 254 | #cur_aif_no="$1" 255 | local str_formats_res="$1" 256 | last_aif="$2" 257 | printf -v encoding_formats_val "[\n %s\n\t]" \ 258 | "$(ret_json_format "${str_formats_res}")" 259 | ## using to indexed arrays in order to preserve order of fields 260 | declare -a json_keyvals 261 | json_fields=( 262 | id 263 | hwaddr 264 | description 265 | cardnumber 266 | interfacenumber 267 | cardname 268 | interfacename 269 | chardev 270 | monitorfile 271 | streamfile 272 | usbaudioclass 273 | ) 274 | json_values=(${cur_aif_no}) 275 | json_values+=(${alsa_if_hwaddress}) 276 | #a_json_keyvals[description]= 277 | json_values+=("${alsa_if_title_label}") 278 | #a_json_keyvals[cardnumber]= 279 | json_values+=(${alsa_dev_nr}) 280 | #a_json_keyvals[interfacenumber]= 281 | json_values+=(${alsa_if_nr}) 282 | #a_json_keyvals[cardname]= 283 | json_values+=("${alsa_dev_label}") 284 | #a_json_keyvals[interfacename]= 285 | json_values+=("${alsa_if_label}") 286 | #a_json_keyvals[chardev]= 287 | json_values+=(${alsa_if_chardev}) 288 | #a_json_keyvals[monitorfile]= 289 | json_values+=(${alsa_if_monitorfile}) 290 | #a_json_keyvals[streamfile]= 291 | json_values+=(${alsa_if_streamfile}) 292 | #a_json_keyvals[usbaudioclass]= 293 | json_values+=("${alsa_if_uacclass}") 294 | 295 | for json_fieldno in "${!json_fields[@]}"; do 296 | json_keyvals+=("$(key_val_to_json \ 297 | "${json_fields[${json_fieldno}]}" "${json_values[${json_fieldno}]}")") 298 | done 299 | printf -v str_json_keyvals "\t%s,\n" "${json_keyvals[@]}" 300 | # shellcheck disable=SC1078,SC1079,SC2027 301 | aif_json="""\ 302 | { 303 | ${str_json_keyvals%,*} 304 | \"encodingformats\": "${encoding_formats_val}" 305 | }\ 306 | """ 307 | printf "%s" "${aif_json}" 308 | if [[ "${last_aif}x" == "x" ]]; then 309 | printf "," 310 | fi 311 | printf "\n" 312 | } 313 | 314 | function return_output_json() { 315 | ## print json formatted output to std_out. 316 | ## called by fetch_alsa_outputinterfaces. 317 | json_cards="$1" 318 | json='{ 319 | "alsa_outputdevices": [ 320 | %s 321 | ] 322 | }' 323 | # shellcheck disable=SC2059 324 | printf "${json}\n" "${json_cards%,*}" 325 | } 326 | 327 | 328 | 329 | 330 | 331 | function fetch_alsa_outputinterfaces() { 332 | ## parses each output interface returned by `get_aplay_output' 333 | ## after filtering (when the appropriate commandline options are 334 | ## given), stores its capabilities in the appropriate global 335 | ## indexed arrays and displays them. 336 | json_output= 337 | msg=() 338 | aplay_lines=() 339 | integer_regexp='^[0-9]+$' 340 | aplay_card_regexp="^card[[:space:]][0-9]+:" 341 | ## exit on error 342 | #aplay_output="$ 343 | ## reset the counter for interfaces without filtering 344 | NR_AIFS_BEFOREFILTERING=0 345 | ## modify the filter for aplay -l when OPT_HWFILTER is set 346 | if [[ ! -z "${OPT_HWFILTER}" ]]; then 347 | # the portion without `hw:', eg 0,1 348 | alsa_filtered_hwaddr="${OPT_HWFILTER#hw:*}" 349 | alsa_filtered_cardnr="${alsa_filtered_hwaddr%%,*}" 350 | alsa_filtered_devicenr="${alsa_filtered_hwaddr##*,}" 351 | if [[ ! ${alsa_filtered_cardnr} =~ ${integer_regexp} ]] || \ 352 | [[ ! ${alsa_filtered_devicenr} =~ ${integer_regexp} ]]; then 353 | msg+=("Invalid OPT_HWFILTER (\`${OPT_HWFILTER}') specified.") 354 | msg+=("Should be \`hw:x,y' were x and y are both integers.") 355 | printf -v msg_str "%s\n" "${msg[@]}" 356 | die "${msg_str}" 357 | fi 358 | aplay_card_regexp="^card[[:space:]]${alsa_filtered_cardnr}:[[:space:]].*" 359 | aplay_device_regexp="[[:space:]]device[[:space:]]${alsa_filtered_devicenr}:" 360 | aplay_card_device_regexp="${aplay_card_regexp}${aplay_device_regexp}" 361 | else 362 | aplay_card_device_regexp="${aplay_card_regexp}" 363 | fi 364 | ## iterate each line of aplay output 365 | while read -r line ; do 366 | ## filter for `^card' and then for `OPT_CUSTOMFILTER' to get matching 367 | ## lines from aplay and store them in an array 368 | if [[ "${line}" =~ ${aplay_card_device_regexp} ]]; then 369 | [[ ${DEBUG} ]] && \ 370 | ( msg_debug="aplay -l output line: \`${line}'. with OPT_CUSTOMFILTER: ${OPT_CUSTOMFILTER}" 371 | debug "${LINENO}" "${msg_debug}") 372 | ## raise the counter for interfaces without filtering 373 | ((NR_AIFS_BEFOREFILTERING++)) 374 | if [[ "${OPT_CUSTOMFILTER}x" != "x" ]]; then 375 | ## check if line matches `OPT_CUSTOMFILTER' 376 | if [[ "${line}" =~ ${OPT_CUSTOMFILTER} ]]; then 377 | [[ ${DEBUG} ]] && \ 378 | debug "${LINENO}" "match: ${line}" 379 | ## store the line in an array 380 | aplay_lines+=("${line}") 381 | else 382 | [[ ${DEBUG} ]] && \ 383 | debug "${LINENO}" "no match with filter ${OPT_CUSTOMFILTER}: ${line}" 384 | fi 385 | else 386 | ## store the line in an array 387 | aplay_lines+=("${line}") 388 | fi 389 | fi 390 | done< <(get_aplay_output "${aplay_card_regexp}") || \ 391 | die "get_aplay_output '${aplay_card_regexp}' returned an error." 392 | #< "${aplay_output}" 393 | ## check whether soundcards were found 394 | NR_AIFS_AFTERFILTERING=${#aplay_lines[@]} 395 | if (( NR_AIFS_AFTERFILTERING < 1 )); then 396 | die "${#aplay_lines[@]} soundcards found" 397 | fi 398 | 399 | ## loop through each item in the array 400 | cur_aif_no=0 401 | for line in "${aplay_lines[@]}"; do 402 | ((cur_aif_no++)) 403 | ## set if type to default (ie analog) 404 | alsa_if_type="ao" 405 | ## construct bash regexp for sound device 406 | ## based on aplay.c: 407 | ## printf(_("card %i: %s [%s], device %i: %s [%s]\n"), 408 | ## 1 card, 409 | ## 2 snd_ctl_card_info_get_id(info), 410 | ## 3 snd_ctl_card_info_get_name(info), 411 | ## 4 dev, 412 | ## 5 snd_pcm_info_get_id(pcminfo), 413 | ## 6 snd_pcm_info_get_name(pcminfo)); 414 | ## 415 | ## portion (ie before `,') 416 | ## caution: snd_{pcm,ctl}_card_info_get_name(info) could 417 | ## return an empty string between square brackets, while 418 | ## string returned by snd_{pcm,ctl}_card_info_get_id may 419 | ## contain square brackets 420 | alsa_regexp_common="[[:space:]]([0-9]+):[[:space:]](.*)\]" 421 | alsa_dev_regexp="card${alsa_regexp_common}" 422 | alsa_if_regexp="device${alsa_regexp_common}" 423 | ## same for interface portion 424 | alsa_dev_if_regexp="^${alsa_dev_regexp},[[:space:]]${alsa_if_regexp}$" 425 | ## unset / empty out all variables 426 | alsa_dev_nr="" 427 | alsa_dev_label="" 428 | alsa_if_nr="" 429 | alsa_if_name="" 430 | alsa_if_label="" 431 | before_bracket_re="^([^[]+)\[" 432 | if [[ "${line}" =~ ${alsa_dev_if_regexp} ]]; then 433 | alsa_dev_nr="${BASH_REMATCH[1]}" 434 | alsa_dev_name_raw="${BASH_REMATCH[2]}" 435 | alsa_if_nr="${BASH_REMATCH[3]}" 436 | alsa_if_name_raw="${BASH_REMATCH[4]}" 437 | if [[ "${alsa_dev_name_raw}" =~ ${before_bracket_re} ]]; then 438 | alsa_dev_name_beforebracket="${BASH_REMATCH[1]}" 439 | alsa_dev_name_betweenbrackets="${alsa_dev_name_raw//${alsa_dev_name_beforebracket}}" 440 | 441 | if [[ ${DEBUG} ]]; then 442 | debug "${LINENO}" "#####: alsa_dev_name_beforebracket \`${alsa_dev_name_beforebracket}'" 443 | debug "${LINENO}" "#####: alsa_dev_name_betweenbrackets \`${alsa_dev_name_betweenbrackets}'" 444 | fi 445 | else 446 | printf -v msg_err "%s: alsa_dev_name_raw \`%s' did not match regexp before_bracket_re (\`%s')\n" \ 447 | "${LINENO}" "${alsa_dev_name_raw}" "${before_bracket_re}" 448 | die "${msg_err}" 449 | break 450 | fi 451 | if [[ "${alsa_if_name_raw}" =~ ${before_bracket_re} ]]; then 452 | alsa_if_name_beforebracket="${BASH_REMATCH[1]}" 453 | alsa_if_name_betweenbrackets="${alsa_if_name_raw//${alsa_if_name_beforebracket}}" 454 | if [[ ${DEBUG} ]]; then 455 | debug "${LINENO}" "#####: alsa_if_name_beforebracket \`${alsa_if_name_beforebracket}'" 456 | debug "${LINENO}" "#####: alsa_if_name_betweenbrackets \`${alsa_if_name_betweenbrackets}'" 457 | fi 458 | else 459 | printf -v msg_err "%s: alsa_if_name_raw \`%s' did not match regexp before_bracket_re (\`%s')\n" \ 460 | "${LINENO}" "${alsa_if_name_raw}" "${before_bracket_re}" 461 | die "${msg_err}" 462 | break 463 | fi 464 | else 465 | printf -v msg_err "%s: aplay line did not match alsa_dev_if_regexp (\`%s'):\n%s\n" \ 466 | "${LINENO}" "${alsa_dev_if_regexp}" "${line}" 467 | die "${msg_err}" 468 | break 469 | fi 470 | ## format the names 471 | ## alsa_{dev,if}_name_beforebracket includes trailing space 472 | ## alsa_{dev,if}_name_betweenbrackets includes leading square bracket 473 | ## strip both 474 | ## courtesy: https://unix.stackexchange.com/a/360648 475 | shopt -s extglob 476 | alsa_dev_name_beforebracket="${alsa_dev_name_beforebracket%%+([[:space:]])}" 477 | alsa_dev_name_betweenbrackets="${alsa_dev_name_beforebracket##+([)}" 478 | alsa_if_name_beforebracket="${alsa_if_name_beforebracket%%+([[:space:]])}" 479 | alsa_if_name_betweenbrackets="${alsa_if_name_beforebracket##+([)}" 480 | shopt -u extglob 481 | ## do not include identical or empty name between square brackets 482 | if [[ "${alsa_dev_name_beforebracket}x" == "${alsa_dev_name_betweenbrackets}x" ]] || \ 483 | [[ "${alsa_dev_name_betweenbrackets}x" == "x" ]]; then 484 | alsa_dev_label="${alsa_dev_name_beforebracket}" 485 | else 486 | alsa_dev_label="${alsa_dev_name_beforebracket} [${alsa_dev_name_betweenbrackets}]" 487 | fi 488 | if [[ "${alsa_if_name_beforebracket}x" == "${alsa_if_name_betweenbrackets}x" ]] || \ 489 | [[ "${alsa_if_name_betweenbrackets}x" == "x" ]]; then 490 | alsa_if_label="${alsa_if_name_beforebracket}" 491 | else 492 | alsa_if_label="${alsa_if_name_beforebracket} [${alsa_if_name_betweenbrackets}]" 493 | fi 494 | declare -a alsa_if_formats=() 495 | alsa_if_hwaddress="hw:${alsa_dev_nr},${alsa_if_nr}" 496 | ## construct the path to the character device for the 497 | ## interface (ie `/dev/snd/xxx') 498 | alsa_if_chardev="/dev/snd/pcmC${alsa_dev_nr}D${alsa_if_nr}p" 499 | ## construct the path to the hwparams file 500 | alsa_if_hwparamsfile="/proc/asound/card${alsa_dev_nr}/pcm${alsa_if_nr}p/sub0/hw_params" 501 | ## before determining whether this is a usb device, assume 502 | ## the monitor file is the hwparams file 503 | alsa_if_monitorfile="${alsa_if_hwparamsfile}" 504 | ## assume stream file for the interface (ie 505 | ## `/proc/asound/cardX/streamY') to determine whether 506 | ## the interface is a uac device, and if so, which class it is 507 | alsa_if_streamfile="/proc/asound/card${alsa_dev_nr}/stream${alsa_if_nr}" 508 | ## assume no uac device 509 | alsa_if_uacclass="${MSG_PROP_NOTAPPLICABLE}" 510 | 511 | if [[ ! -z ${TESTFILE} ]]; then 512 | ## device is not real 513 | alsa_if_formats+=("(${MSG_ERROR_CHARDEV_NOFORMATS})") 514 | alsa_if_uacclass_nr="?" 515 | else 516 | ## check if the hwparams file exists 517 | if [[ ! -f "${alsa_if_hwparamsfile}" ]]; then 518 | alsa_if_hwparamsfile="${alsa_if_hwparamsfile} (error: not accessible)" 519 | fi 520 | ## check if the chardev exists 521 | if [[ ! -c "${alsa_if_chardev}" ]]; then 522 | msg_err="alsa_if_chardev \`${alsa_if_chardev}': ${MSG_ERROR_NOT_CHARDEV} " 523 | [[ ${DEBUG} ]] && \ 524 | debug "${LINENO}" "${msg_err}" 525 | alsa_if_chardev="${alsa_if_chardev} (${MSG_ERROR_NOT_CHARDEV})" 526 | else 527 | [[ ${DEBUG} ]] && \ 528 | debug "${LINENO}" "alsa_if_chardev \`${alsa_if_chardev}' is a valid chardev." 529 | fi 530 | ## check whether the monitor file exists; it always should 531 | if [[ ! -f ${alsa_if_monitorfile} ]]; then 532 | msg_err="${alsa_if_monitorfile} ${MSG_ERROR_NOFILE} (${MSG_ERROR_UNEXPECTED})" 533 | alsa_if_monitorfile="${msg_err}" 534 | [[ ${DEBUG} ]] && \ 535 | debug "${LINENO}" "${msg_err}" 536 | fi 537 | ## check whether the streamfile exists; it only should 538 | ## exist in the case of a uac interface 539 | if [[ ! -f "${alsa_if_streamfile}" ]]; then 540 | msg_err="${alsa_if_streamfile} ${MSG_ERROR_NOFILE} (${MSG_ERROR_UNEXPECTED})" 541 | [[ ${DEBUG} ]] && \ 542 | debug "${LINENO}" "${msg_err}" 543 | ## no uac interface 544 | alsa_if_streamfile="${MSG_PROP_NOTAPPLICABLE}" 545 | else 546 | [[ ${DEBUG} ]] && \ 547 | debug "${LINENO}" "using alsa_if_streamfile \`${alsa_if_streamfile}'." 548 | ## set interface to usb out 549 | alsa_if_type="uo" 550 | ## uac devices will use the stream file instead of 551 | ## hwparams file to monitor 552 | ## alsa_if_monitorfile="${alsa_if_streamfile}" 553 | ## get the type of uac endpoint 554 | alsa_if_uac_ep="$(return_alsa_uac_ep "${alsa_if_streamfile}")" 555 | # shellcheck disable=SC2181 556 | if [[ $? -ne 0 ]]; then 557 | [[ ${DEBUG} ]] && \ 558 | debug "${LINENO}" "could not determine alsa_if_uac_ep." 559 | alsa_if_uacclass_nr="?" 560 | else 561 | [[ ${DEBUG} ]] && \ 562 | debug "${LINENO}" "alsa_if_uac_ep set to \`${alsa_if_uac_ep}'." 563 | ## lookup the uac class in the array for this type of endpoint (EP) 564 | ## (for readability) 565 | alsa_if_uacclass="${UO_EP_LABELS[${alsa_if_uac_ep}]}" 566 | ## the uac class number (0, 1, 2 or 3) according to ./sound/usb/card.h 567 | alsa_if_uacclass_nr="${alsa_if_uacclass% - *}" 568 | classnr_regexp='^[0-3]+$' 569 | if [[ ! ${alsa_if_uacclass_nr} =~ ${classnr_regexp} ]]; then 570 | [[ ${DEBUG} ]] && \ 571 | debug "${LINENO}" "invalid uac class number \`${alsa_if_uacclass_nr}'. \ 572 | ${MSG_ERROR_UNEXPECTED}" 573 | alsa_if_uacclass_nr="?" 574 | fi 575 | fi 576 | 577 | fi 578 | fi 579 | ## for non-uac interfaces: check whether it is some other 580 | ## digital interface 581 | if [[ ! "${alsa_if_type}" = "uo" ]]; then 582 | for filter in "${DO_INTERFACE_FILTER[@]}"; do 583 | ## `,,' downcases the string, while `*var*' does a 584 | ## wildcard match 585 | if [[ "${alsa_if_name,,}" == *"${filter}"* ]]; then 586 | [[ ${DEBUG} ]] && \ 587 | debug "${LINENO}" "match = ${alsa_if_name,,}: ${filter}" 588 | ## set ao type to d(igital)o(out) 589 | alsa_if_type="do" 590 | ## exit this for loop 591 | break 592 | fi 593 | done 594 | fi 595 | ## see if the interface type matches the user specified 596 | ## filters and if so construct titles and store a pair of 597 | ## hardware address and monitoring file in the proper array 598 | match= 599 | case "${alsa_if_type}" in 600 | "ao") 601 | ## only if neither `OPT_LIMIT_DO' and `OPT_LIMIT_UO' are set 602 | [[ ! -z ${OPT_LIMIT_DO} || ! -z ${OPT_LIMIT_UO} ]] && \ 603 | continue || match="true" 604 | ;; 605 | "do") 606 | ## only if neither `OPT_LIMIT_AO' and `OPT_LIMIT_UO' are set 607 | [[ ! -z ${OPT_LIMIT_AO} || ! -z ${OPT_LIMIT_UO} ]] && \ 608 | continue || match="true" 609 | ;; 610 | "uo") 611 | ## only if `OPT_LIMIT_AO' is not set 612 | [[ ! -z ${OPT_LIMIT_AO} ]] && \ 613 | continue || match="true" 614 | esac 615 | if [[ ! -z ${match} ]]; then 616 | ## put each encoding format and possibily the sample rates 617 | ## in an array 618 | alsa_if_formats=() 619 | formats_res_err= 620 | str_formats_res="$(return_alsa_formats \ 621 | "${alsa_dev_nr}" \ 622 | "${alsa_if_nr}" \ 623 | "${alsa_if_type}" \ 624 | "${alsa_if_streamfile}" \ 625 | "${alsa_if_chardev}")" 626 | # shellcheck disable=SC2181 627 | if [[ $? -ne 0 ]]; then 628 | formats_res_err=1 629 | fi 630 | alsa_if_formats+=("${str_formats_res}") 631 | alsa_if_title_label="${ALSA_IF_LABELS[${alsa_if_type}]}" 632 | ## reconstruct the label if it contained square brackets 633 | ## construct the display title 634 | printf -v alsa_if_display_title \ 635 | " %s) %s \`%s'" \ 636 | "${cur_aif_no}" \ 637 | "${alsa_if_title_label}" \ 638 | "${alsa_if_hwaddress}" 639 | ## store the details of the current interface in global arrays 640 | ALSA_AIF_HWADDRESSES+=("${alsa_if_hwaddress}") 641 | ALSA_AIF_MONITORFILES+=("${alsa_if_monitorfile}") 642 | ALSA_AIF_DISPLAYTITLES+=("${alsa_if_display_title}") 643 | ALSA_AIF_DEVLABELS+=("${alsa_dev_label}") 644 | ALSA_AIF_LABELS+=("${alsa_if_label}") 645 | ALSA_AIF_UACCLASSES+=("${alsa_if_uacclass}") 646 | ALSA_AIF_FORMATS="${alsa_if_formats[*]}" 647 | ALSA_AIF_CHARDEVS+=("${alsa_if_chardev}") 648 | fi 649 | if [[ -z "${OPT_QUIET}" ]] && [[ "${OPT_JSON}x" == "x" ]]; then 650 | ## print the list to std_err 651 | res_human="$(return_output_human)" || exit 1 652 | printf 1>&2 "%s\n" "${res_human}" 653 | fi 654 | if [[ "${OPT_JSON}x" != "x" ]]; then 655 | if [[ ${cur_aif_no} -lt ${#aplay_lines[@]} ]]; then 656 | printf -v json_output "%s%s\n" \ 657 | "${json_output}" \ 658 | "$(ret_json_card "${str_formats_res}" "")" 659 | fi 660 | fi 661 | done 662 | if [[ "${OPT_JSON}x" != "x" ]]; then 663 | res_json="$(return_output_json "${json_output}")" || exit 1 664 | printf "%s\n" "${res_json}" 665 | fi 666 | } 667 | 668 | function get_locking_process() { 669 | ## return a string describing the command and id of the 670 | ## process locking the audio interface with card nr $1 and dev nr 671 | ## $2 based on its status file in /proc/asound. 672 | ## returns a comma separated string containing the locking cmd and 673 | ## pid, or an error when the interface is not locked (ie 674 | ## 'closed'). 675 | alsa_card_nr="$1" 676 | alsa_if_nr="$2" 677 | proc_statusfile="/proc/asound/card${alsa_card_nr}/pcm${alsa_if_nr}p/sub0/status" 678 | owner_pid= 679 | owner_stat= 680 | owner_cmd= 681 | parent_pid= 682 | parent_cmd= 683 | locking_cmd= 684 | locking_pid= 685 | ## specific for mpd: each alsa output plugin results in a locking 686 | ## process indicated by `owner_pid` in 687 | ## /proc/asound/cardX/pcmYp/sub0/status: `owner_pid : 28022' 688 | ## this is a child process of the mpd parent process (`28017'): 689 | ##mpd(28017,mpd)-+-{decoder:flac}(28021) 690 | ## |-{io}(28019) 691 | ## |-{output:Peachtre}(28022) <<< owner_pid / child 692 | ## `-{player}(28020) 693 | owner_pid_re="owner_pid[[:space:]]+:[[:space:]]+([0-9]+)" 694 | [[ ${DEBUG} ]] && \ 695 | debug "${LINENO}" "examining status file ${proc_statusfile}." 696 | while read -r line; do 697 | if [[ "${line}" =~ ${owner_pid_re} ]]; then 698 | owner_pid="${BASH_REMATCH[1]}" 699 | break 700 | elif [[ "${line}" == "closed" ]]; then 701 | return 1 702 | fi 703 | done<"${proc_statusfile}" 704 | [[ ${DEBUG} ]] && \ 705 | debug "${LINENO}" "done examining status file ${proc_statusfile}." 706 | if [[ -z ${owner_pid} ]]; then 707 | ## device is unused 708 | [[ ${DEBUG} ]] && \ 709 | debug "${LINENO}" "${FUNCNAME[0]} called, but no owner_pid found in \`${proc_statusfile}'." 710 | return 1 711 | else 712 | [[ ${DEBUG} ]] && \ 713 | debug "${LINENO}" "found owner pid in status file \`${proc_statusfile}': \`${owner_pid}'." 714 | fi 715 | ## check if owner_pid is a child 716 | ## construct regexp for getting the ppid from /proc 717 | ## eg: /proc/837/stat: 718 | ## 837 (output:Pink Fau) S 1 406 406 0 -1 ... 719 | ## ^^^ ^^^ 720 | ## +++-> owner_pid +++-> parent_pid 721 | parent_pid_re="(${owner_pid})[[:space:]]\(.*\)[[:space:]][A-Z][[:space:]][0-9]+[[:space:]]([0-9]+)" 722 | # shellcheck disable=SC2162 723 | read owner_stat < "/proc/${owner_pid}/stat" 724 | [[ ${DEBUG} ]] && \ 725 | debug "${LINENO}" "owner_stat: \`${owner_stat}'" 726 | if [[ "${owner_stat}" =~ ${parent_pid_re} ]]; then 727 | parent_pid="${BASH_REMATCH[2]}" 728 | if [[ "x${parent_pid}" == "x${owner_pid}" ]]; then 729 | ## device is locked by the process with id owner_pid, look up command 730 | ## eg: /proc/837/cmdline: /usr/bin/mpd --no-daemon /var/lib/mpd/mpd.conf 731 | # shellcheck disable=SC2162 732 | read owner_cmd < "/proc/${owner_pid}/cmdline" 733 | [[ ${DEBUG} ]] && \ 734 | debug "${LINENO}" "cmd \`${owner_cmd}' with id \`${owner_pid}' has no parent." 735 | locking_pid="${owner_pid}" 736 | locking_cmd="${owner_cmd}" 737 | else 738 | ## device is locked by the parent of the process with owner_pid 739 | # shellcheck disable=SC2162 740 | read owner_cmd < "/proc/${owner_pid}/cmdline" 741 | # shellcheck disable=SC2162 742 | read parent_cmd < "/proc/${parent_pid}/cmdline" 743 | [[ ${DEBUG} ]] && \ 744 | debug "${LINENO}" "cmd \`${owner_cmd}' with id \`${owner_pid}' \ 745 | has parent cmd \`${parent_cmd}' with id \`${parent_pid}'." 746 | locking_pid="${parent_pid}" 747 | locking_cmd="${parent_cmd}" 748 | fi 749 | ## return comma separated list (pid,cmd) to calling function 750 | locking_cmd="$(while read -r -d $'\0' line; do \ 751 | printf "%s " "${line}"; \ 752 | done< "/proc/${locking_pid}/cmdline")" 753 | printf "%s,%s" "${locking_pid}" "${locking_cmd%% }" 754 | else 755 | ## should not happen; TODO: handle 756 | parent_pid= 757 | fi 758 | } 759 | 760 | function ret_highest_alsa_samplerate() { 761 | ## check the highest supported rate of type $3 for format $2 on 762 | ## interface $1 763 | ## returns the highest supported rate. 764 | alsa_if_hwaddress="$1" 765 | encoding_format="$2" 766 | type="$3" 767 | if [[ "${type}" == "audio" ]]; then 768 | rates=(${SAMPLERATES_AUDIO[@]}) 769 | else 770 | rates=(${SAMPLERATES_VIDEO[@]}) 771 | fi 772 | for rate in "${rates[@]}"; do 773 | res="$(check_samplerate "${alsa_if_hwaddress}" "${encoding_format}" "${rate}")" 774 | # shellcheck disable=SC2181 775 | if [[ $? -ne 0 ]]; then 776 | ## too high; try next one 777 | continue 778 | else 779 | ## match; return it 780 | printf "%s" "${rate}" 781 | break 782 | fi 783 | done 784 | } 785 | 786 | function ret_supported_alsa_samplerates() { 787 | ## use aplay to get supported sample rates for playback for 788 | ## specified non-uac interface ($1) and encoding format ($2). 789 | ## returns a space separated list of valid rates. 790 | alsa_if_hwaddress="$1" 791 | encoding_format="$2" 792 | declare -a rates 793 | [[ ${DEBUG} ]] && \ 794 | debug "${LINENO}" "getting sample rates for device \`${alsa_if_hwaddress}' \ 795 | using encoding_format \`${encoding_format}'." 796 | ## check all audio/video rates from high to low; break when rate is 797 | ## supported while adding all the lower frequencies 798 | highest_audiorate="$(ret_highest_alsa_samplerate \ 799 | "${alsa_if_hwaddress}" "${encoding_format}" "audio")" 800 | highest_videorate="$(ret_highest_alsa_samplerate \ 801 | "${alsa_if_hwaddress}" "${encoding_format}" "video")" 802 | for rate in "${SAMPLERATES_AUDIO[@]}"; do 803 | if [[ ${rate} -le ${highest_audiorate} ]]; then 804 | ## supported; assume all lower rates are supported too 805 | rates+=("${rate}") 806 | fi 807 | done 808 | for rate in "${SAMPLERATES_VIDEO[@]}"; do 809 | if [[ ${rate} -le ${highest_videorate} ]]; then 810 | ## supported; assume all lower rates are supported too 811 | rates+=("${rate}") 812 | fi 813 | done 814 | ## sort and retrun trhe newline separated sample rates 815 | sort -u -n <(printf "%s\n" "${rates[@]}") 816 | } 817 | 818 | function check_samplerate() { 819 | ## use aplay to check if the specified alsa interface ($1) 820 | ## supports encoding format $2 and sample rate $3 821 | ## returns a string with the supported sample rate or nothing 822 | alsa_if_hwaddress="$1" 823 | format="$2" 824 | samplerate="$3" 825 | declare -a aplay_args_early 826 | aplay_args_early+=(--device="${alsa_if_hwaddress}") 827 | aplay_args_early+=(--format="${format}") 828 | aplay_args_early+=(--channels="2") 829 | aplay_args_early+=(--nonblock) 830 | declare -a aplay_args_late 831 | ## set up regular expressions to match aplay's output errors 832 | ## unused 833 | # shellcheck disable=SC2034 834 | rate_notaccurate_re=".*Warning:.*not[[:space:]]accurate[[:space:]]\(requested[[:space:]]=[[:space:]]([0-9]+)Hz,[[:space:]]got[[:space:]]=[[:space:]]([0-9]+)Hz\).*" 835 | # shellcheck disable=SC2034 836 | badspeed_re=".*bad[[:space:]]speed[[:space:]]value.*" 837 | # shellcheck disable=SC2034 838 | sampleformat_nonavailable_re=".*Sample[[:space:]]format[[:space:]]non[[:space:]]available.*" 839 | # shellcheck disable=SC2034 840 | wrongformat_re=".*wrong[[:space:]]extended[[:space:]]format.*" 841 | ## used 842 | default_re=".*Playing[[:space:]]raw[[:space:]]data.*" 843 | [[ ${DEBUG} ]] && \ 844 | debug "${LINENO}" "testing rate ${samplerate}" 845 | unset aplay_args_late 846 | ## set fixed sample rate 847 | aplay_args_late+=(--rate="${samplerate}") 848 | ## generate aplay error using random noise to check whether sample 849 | ## rate is supported for this interface and format 850 | # shellcheck disable=SC2145 851 | printf -v aplay_args "%s " "${aplay_args_early[@]} ${aplay_args_late[@]}" 852 | read -r firstline<<<"$(return_reversed_aplay_error "${aplay_args}")" || return 1 853 | if [[ "${firstline}" =~ ${default_re} ]]; then 854 | [[ ${DEBUG} ]] && \ 855 | debug "${LINENO}" "success" 856 | printf "%s" "${samplerate}" 857 | else 858 | return 1 859 | fi 860 | } 861 | 862 | function return_reversed_aplay_error() { 863 | ## force aplay to output error message containing supported 864 | ## encoding formats, by playing PSEUDO_AUDIO in a non-existing 865 | ## format. 866 | ## returns the output of aplay while reversing its return code 867 | aplay_args="$1" 868 | cmd_aplay="${CMD_APLAY} ${aplay_args}" 869 | LANG=C ${cmd_aplay} 2>&1 <<< "${PSEUDO_SILENT_AUDIO}" || \ 870 | ( [[ ${DEBUG} ]] && \ 871 | debug "${LINENO}" "\`${cmd_aplay}' returned error (which is good)." 872 | return 0 ) && \ 873 | ( [[ ${DEBUG} ]] && \ 874 | debug "${LINENO}" "\`${cmd_aplay}' returned error (which is not good)." 875 | return 1 ) 876 | } 877 | 878 | function return_nonuac_formats() { 879 | ## use aplay to determine supported formats of non-uac interface (hw:$1,$2) 880 | alsa_dev_nr="$1" 881 | alsa_if_nr="$2" 882 | aplay_args=(--device=hw:${alsa_dev_nr},${alsa_if_nr}) 883 | aplay_args+=(--channels=2) 884 | aplay_args+=(--format=MPEG) 885 | aplay_args+=(--nonblock) 886 | printf -v str_args "%s " "${aplay_args[@]}" 887 | return_reversed_aplay_error "${str_args}" || \ 888 | return 1 889 | } 890 | 891 | function return_uac_formats_rates() { 892 | ## get encodings formats with samplerates for uac type interface 893 | ## using its streamfile $1 (which saves calls to applay). 894 | ## returns newline separated list (FORMAT:RATE,RATE,...). 895 | alsa_if_streamfile="$1" 896 | interface_re="^[[:space:]]*Interface[[:space:]]([0-9])" 897 | format_re="^[[:space:]]*Format:[[:space:]](.*)" 898 | rates_re="^[[:space:]]*Rates:[[:space:]](.*)" 899 | capture_re="^Capture:" 900 | inside_interface= 901 | format_found= 902 | declare -A uac_formats_rates 903 | ## iterate lines in the streamfile 904 | while read -r line; do 905 | if [[ "${line}" =~ ${capture_re} ]]; then 906 | ## end of playback interfaces 907 | break 908 | else 909 | ## we're not dealing with a capture interface 910 | if [[ "${line}" =~ ${interface_re} ]]; then 911 | ## new interface found 912 | inside_interface=true 913 | ## reset (previous) format_found 914 | format_found= 915 | ## continue with next line 916 | else 917 | ## continuation of interface 918 | if [[ "${inside_interface}x" != "x" ]]; then 919 | ## parse lines below `Interface:` 920 | if [[ "${format_found}x" == "x" ]]; then 921 | ## check for new `Format:` 922 | if [[ "${line}" =~ ${format_re} ]]; then 923 | ## new format found 924 | format_found="${BASH_REMATCH[1]}" 925 | uac_formats_rates[${format_found}]="" 926 | [[ ${DEBUG} ]] && \ 927 | debug "${LINENO}" "format found: \`${format_found}'" 928 | ## next: sample rates or new interface 929 | fi 930 | else 931 | ## parse lines below `Format:` 932 | if [[ "${line}" =~ ${rates_re} ]]; then 933 | ## sample rates for interface/format found; 934 | ## return and reset both 935 | uac_formats_rates[${format_found}]="${BASH_REMATCH[1]}" 936 | [[ ${DEBUG} ]] && \ 937 | debug "${LINENO}" "(format=${format_found}) \ 938 | rates=${BASH_REMATCH[1]}" 939 | format_found= 940 | inside_interface= 941 | continue 942 | fi 943 | fi 944 | fi 945 | fi 946 | fi 947 | done<"${alsa_if_streamfile}" 948 | for format in "${!uac_formats_rates[@]}"; do 949 | printf "%s:%s\n" \ 950 | "${format}" "${uac_formats_rates[${format}]// /}" 951 | done 952 | } 953 | 954 | function return_alsa_formats() { 955 | ## fetch and return a comma separated string of playback formats 956 | ## for the interface specified in $1, of type $2. For non-uac 957 | ## interfaces: feed dummy input to aplay (--format=MPEG). For uac 958 | ## types: filter it directly from its stream file $3. 959 | alsa_dev_nr="$1" 960 | alsa_if_nr="$2" 961 | alsa_if_type="$3" 962 | alsa_if_streamfile="$4" 963 | alsa_if_chardev="$5" 964 | format="${format:-}" 965 | rawformat="${rawformat:-}" 966 | parent_pid= 967 | parent_cmd= 968 | declare -A uac_formats 969 | if [[ "${alsa_if_type}" = "uo" ]]; then 970 | ## uac type; use streamfile to get encoding formats and/or 971 | ## samplerates (in the form of 'FORMAT: RATE RATE ...'). 972 | while read -r line; do 973 | key="${line%:*}" 974 | value="${line//${key}:/}" 975 | uac_formats["${key}"]="${value}" 976 | done< <(return_uac_formats_rates "${alsa_if_streamfile}") 977 | ## return the formatted line(s) 978 | if [[ "${OPT_SAMPLERATES}x" == "x" ]]; then 979 | ## print comma separated list of formats 980 | # shellcheck disable=SC2068 981 | printf -v str_formats "%s, " "${!uac_formats[@]}" 982 | printf "%-20s" "${str_formats%*, }" 983 | else 984 | ## for each format, print "FORMAT1:rate1,rate2,..." 985 | # shellcheck disable=SC2068 986 | for key in ${!uac_formats[@]}; do 987 | printf "%s:%s\n" "${key}" "${uac_formats[${key}]}" 988 | done 989 | fi 990 | else 991 | ## non-uac type: if interface is not locked, use aplay to 992 | ## determine formats 993 | ## because of invalid file format, aplay is forced to return 994 | ## supported formats (=200 times faster than --dump-hw-params) 995 | declare -a rawformats 996 | format_re="^-[[:space:]]+([[:alnum:]_]*)$" 997 | res="$(get_locking_process "${alsa_dev_nr}" "${alsa_if_nr}")" 998 | # shellcheck disable=SC2181 999 | if [[ $? -ne 0 ]]; then 1000 | ## device is not locked, iterate aplay output 1001 | [[ ${DEBUG} ]] && \ 1002 | debug "${LINENO}" "device is not locked; will iterate aplay_out" 1003 | while read -r line; do 1004 | if [[ "${line}" =~ ${format_re} ]]; then 1005 | rawformats+=(${BASH_REMATCH[1]}) 1006 | fi 1007 | done< <(return_nonuac_formats "${alsa_dev_nr}" "${alsa_if_nr}") || return 1 1008 | ## formats (and minimum/maximum sample rates) gathered, check if 1009 | ## all sample rates should be checked 1010 | [[ ${DEBUG} ]] && debug "${LINENO}" "$(declare -p rawformats)" 1011 | if [[ "${OPT_SAMPLERATES}x" == "x" ]]; then 1012 | ## just return the comma separated format(s) 1013 | printf -v str_formats "%s, " "${rawformats[@]}" 1014 | printf "%-20s" "${str_formats%*, }" 1015 | else 1016 | ## check all sample rates for each format. warning: 1017 | ## slowness ahead for non-uac interfaces, because of 1018 | ## an aplay call for each unsupported sample rate + 1 1019 | ## and each format 1020 | for rawformat in "${rawformats[@]}"; do 1021 | sorted_rates="" 1022 | while read -r line; do 1023 | sorted_rates+="${line}," 1024 | #printf -v str_rates "%s " "${line}" 1025 | done< <(ret_supported_alsa_samplerates \ 1026 | "${alsa_if_hwaddress}" "${rawformat}") 1027 | ## return each format newline separated with a space 1028 | ## separated list of supported sample rates 1029 | printf "%s:%s\n" "${rawformat}" "${sorted_rates%*,}" 1030 | done 1031 | fi 1032 | else 1033 | ## in use by another process 1034 | ## res contains pid,cmd of locking process 1035 | locking_pid="${res%,*}" 1036 | locking_cmd="${res#*,}" 1037 | [[ ${DEBUG} ]] && \ 1038 | debug "${LINENO}" "\ 1039 | device is in use by command ${locking_cmd} with process id ${locking_pid}." 1040 | ## return the error instead of the formats 1041 | printf "by command \`%s' with PID %s." \ 1042 | "${locking_cmd}" "${locking_pid}" 1043 | return 1 1044 | fi 1045 | fi 1046 | } 1047 | 1048 | function return_alsa_uac_ep() { 1049 | ## returns the usb audio class endpoint as a fixed number. 1050 | ## needs path to stream file as single argument ($1) 1051 | ## based on ./sound/usb/proc.c: 1052 | ## printf " Endpoint: %d %s (%s)\n", 1053 | ## 1: fp->endpoint & USB_ENDPOINT_NUMBER_MASK (0x0f) > [0-9] 1054 | ## TODO: unsure which range this is; have seen 1, 3 and 5 1055 | ## 2: USB_DIR_IN: "IN|OUT", 1056 | ## 3: USB_ENDPOINT_SYNCTYPE: "NONE|ASYNC|ADAPTIVE|SYNC" 1057 | alsa_if_streamfile_path="$1" 1058 | ep_mode="" 1059 | ep_label_filter="Endpoint:" 1060 | ep_label_regexp="^[[:space:]]*${ep_label_filter}" 1061 | ep_num_filter="([0-9]+)" #1 1062 | ep_num_regexp="[[:space:]]${ep_num_filter}" 1063 | ep_direction_filter="OUT" 1064 | ep_direction_regexp="[[:space:]]${ep_direction_filter}" 1065 | ep_synctype_filter="(${UO_EP_NONE_FILTER}|${UO_EP_ADAPT_FILTER}|${UO_EP_ASYNC_FILTER}|${UO_EP_SYNC_FILTER})" #2 1066 | ep_synctype_regexp="[[:space:]]\(${ep_synctype_filter}\)$" 1067 | ep_regexp="${ep_label_regexp}${ep_num_regexp}${ep_direction_regexp}${ep_synctype_regexp}" 1068 | ## iterate the contents of the streamfile 1069 | while read -r line; do 1070 | if [[ "${line}" =~ ${ep_regexp} ]]; then 1071 | ep_mode="${BASH_REMATCH[2]}" 1072 | [[ ${DEBUG} ]] && \ 1073 | debug "${LINENO}" "matching endpoint found in line \`${line}': \`${ep_mode}'." 1074 | break 1075 | fi 1076 | done<"${alsa_if_streamfile_path}" 1077 | if [[ "${ep_mode}x" == "x" ]]; then 1078 | [[ ${DEBUG} ]] && \ 1079 | debug "${LINENO}" "no matching endpoints found. ${MSG_ERROR_UNEXPECTED}" 1080 | return 1 1081 | else 1082 | ## return the filtered endpoint type 1083 | printf "%s" "${ep_mode}" 1084 | fi 1085 | } 1086 | 1087 | 1088 | ### command line parsing 1089 | 1090 | function analyze_opt_limit() { 1091 | ## check if the argument for the `-l' (limit) option is proper 1092 | option="$1" 1093 | opt_limit="${2-}" 1094 | declare -a args 1095 | prev_opt=0 1096 | declare msg 1097 | case ${opt_limit} in 1098 | a|analog) 1099 | OPT_LIMIT_AO="True" 1100 | [[ ${DEBUG} ]] && \ 1101 | debug "${LINENO}" "OPT_LIMIT_AO set to \`${OPT_LIMIT_AO}'" 1102 | return 0 1103 | ;; 1104 | u|usb|uac) 1105 | OPT_LIMIT_UO="True" 1106 | [[ ${DEBUG} ]] && \ 1107 | debug "${LINENO}" "OPT_LIMIT_UO set to \`${OPT_LIMIT_UO}'" 1108 | return 0 1109 | ;; 1110 | d|digital) 1111 | OPT_LIMIT_DO="True" 1112 | [[ ${DEBUG} ]] && \ 1113 | debug "${LINENO}" "OPT_LIMIT_DO set to \`${OPT_LIMIT_DO}'" 1114 | return 0 1115 | ;; 1116 | *) 1117 | ## construct list of option pairs: "x (or 'long option')" 1118 | for arg_index in "${!OPT_LIMIT_ARGS[@]}"; do 1119 | if [[ $(( arg_index % 2)) -eq 0 ]]; then 1120 | ## even (short option): new array item 1121 | args+=("") 1122 | else 1123 | ## odd (long option): add value to previous array item 1124 | prev_opt=$(( arg_index - 1 )) 1125 | args[-1]="${OPT_LIMIT_ARGS[${prev_opt}]} (or '${OPT_LIMIT_ARGS[${arg_index}]}')" 1126 | fi 1127 | done 1128 | args_val=$(printf "%s, " "${args[@]}") 1129 | # shellcheck disable=SC2059 1130 | msg_vals="$(printf " ${args_val%*, }\n")" 1131 | msg_custom="maybe you could try to use the custom filter option, eg:" 1132 | msg_trail="for limit option \`${option}' specified. should be one of:\n" 1133 | if [[ ! -z ${opt_limit} ]]; then 1134 | str_re="" 1135 | for (( i=0; i<${#opt_limit}; i++ )); do 1136 | char="${opt_limit:$i:1}" 1137 | str_re+="[${char^^}${char,,}]" 1138 | done 1139 | msg="invalid value \`${opt_limit}' " 1140 | # shellcheck disable=SC2059 1141 | msg+="$(printf "${msg_trail}${msg_vals}\n${msg_custom}")" 1142 | ## display instructions to use the custom filter 1143 | msg+="$(printf "\n bash $0 -c \"%s\"\n" "${str_re}")" 1144 | else 1145 | # shellcheck disable=SC2059 1146 | msg="$(printf "no value for ${msg_trail}${msg_vals}")" 1147 | fi 1148 | 1149 | ## display the option pairs, stripping the trailing comma 1150 | printf "%s\n" "${msg}" 1>&2; 1151 | exit 1 1152 | esac 1153 | } 1154 | 1155 | 1156 | function display_usageinfo() { 1157 | ## display syntax and exit 1158 | msg=$(cat < ] [-a ] [-s] [ -q ] 1161 | 1162 | Displays a list of each alsa audio output interface with its details 1163 | including its alsa hardware address (\`hw:x,y'). 1164 | 1165 | The list may be filtered by using the limit option \`-l' with an 1166 | argument to only show interfaces that fit the limit. In addition, a 1167 | custom filter may be specified as an argument for the \`c' option. 1168 | 1169 | The \`-q (quiet)' and \`-a (address)' options are meant for usage in 1170 | other scripts. The script returns 0 on success or 1 in case of no 1171 | matches or other errors. 1172 | 1173 | -l TYPEFILTER, --limit TYPEFILTER 1174 | Limit the interfaces to TYPEFILTER. Can be one of 1175 | \`a' (or \`analog'), \`d' (or \`digital'), \`u' 1176 | (or \`usb'), the latter for USB Audio Class (UAC1 1177 | or UAC2) devices. 1178 | -c REGEXP, --customlimit REGEXP 1179 | Limit the available interfaces further to match 1180 | \`REGEXP'. 1181 | -a HWADDRESS, --address HWADDRESS 1182 | Limit the returned interface further to the one 1183 | specified with HWADDRESS, eg. \`hw:0,1' 1184 | -s, --samplerates Adds a listing of the supported sample rates for 1185 | each format an interface supports. 1186 | CAUTION: Besides being slow this option 1187 | PLAYS NOISE ON EACH OUTPUT! 1188 | -q, --quiet Surpress listing each interface with its details, 1189 | ie. only store the details of each card in the 1190 | appropriate arrays. 1191 | -h, --help Show this help message 1192 | 1193 | Version ${APP_VERSION}. For more information see: 1194 | ${APP_INFO_URL} 1195 | EOF 1196 | ) 1197 | printf "%s\n" "${msg}" 1>&2; 1198 | } 1199 | 1200 | 1201 | function analyze_command_line() { 1202 | ## parse command line arguments using the `manual loop` method 1203 | ## described in http://mywiki.wooledge.org/BashFAQ/035. 1204 | while :; do 1205 | case "${1:-}" in 1206 | -l|--limit) 1207 | if [ -n "${2:-}" ]; then 1208 | [[ ${DEBUG} ]] && \ 1209 | debug "${LINENO}" "$(printf "option \`%s' set to \`%s'.\n" "$1" "$2")" 1210 | analyze_opt_limit "$1" "$2" 1211 | shift 2 1212 | continue 1213 | else 1214 | analyze_opt_limit "$1" 1215 | exit 1 1216 | fi 1217 | ;; 1218 | -c|--customfilter) 1219 | if [ -n "${2:-}" ]; then 1220 | [[ ${DEBUG} ]] && \ 1221 | debug "${LINENO}" "$(printf "option \`%s' set to \`%s'.\n" "$1" "$2")" 1222 | OPT_CUSTOMFILTER="${2}" 1223 | shift 2 1224 | continue 1225 | else 1226 | printf "ERROR: option \`%s' requires a non-empty argument.\n" "$1" 1>&2 1227 | exit 1 1228 | fi 1229 | ;; 1230 | -a|--address) 1231 | if [ -n "${2:-}" ]; then 1232 | [[ ${DEBUG} ]] && \ 1233 | debug "${LINENO}" "option \`$1' set to \`$2'" 1234 | OPT_HWFILTER="$2" 1235 | shift 2 1236 | continue 1237 | else 1238 | printf "ERROR: option \`%s' requires a alsa hardware address \ 1239 | as an argument (eg \`hw:x,y')\n" "$1" 1>&2 1240 | exit 1 1241 | fi 1242 | ;; 1243 | -s|--samplerates) 1244 | ## deprecated 1245 | [[ ${DEBUG} ]] && \ 1246 | debug "${LINENO}" "option \`$1' set" 1247 | OPT_SAMPLERATES=true 1248 | shift 1249 | continue 1250 | ;; 1251 | -q|--quiet|--silent) 1252 | [[ ${DEBUG} ]] && \ 1253 | debug "${LINENO}" "option \`$1' set" 1254 | OPT_QUIET=true 1255 | shift 1256 | continue 1257 | ;; 1258 | -j|--json) 1259 | OPT_JSON=true 1260 | shift 1261 | continue 1262 | ;; 1263 | -h|-\?|--help) 1264 | display_usageinfo 1265 | exit 1266 | ;; 1267 | --) 1268 | shift 1269 | break 1270 | ;; 1271 | -?*) 1272 | printf "Notice: unknown option \`%s' ignored\n\n." "$1" 1>&2 1273 | display_usageinfo 1274 | exit 1275 | ;; 1276 | *) 1277 | break 1278 | esac 1279 | done 1280 | } 1281 | 1282 | 1283 | function return_alsa_interface() { 1284 | ## main function; see display_usageinfo() 1285 | profile_file= 1286 | ## start profiling 1287 | if [[ ${PROFILE} ]]; then 1288 | profile_file="/tmp/alsa-capabilities.$$.log" 1289 | PS4='+ $(date "+%s.%N")\011 ' 1290 | exec 3>&2 2>${profile_file} 1291 | set -x 1292 | fi 1293 | ## check if needed commands are available 1294 | CMD_PASUSPENDER=$(type -p pasuspender) 1295 | CMD_APLAY="$(type -p aplay)" || \ 1296 | command_not_found "aplay" "alsa-utils" 1297 | if [[ "${CMD_PASUSPENDER}x" != "x" ]]; then 1298 | CMD_APLAY="${CMD_PASUSPENDER} -- ${CMD_APLAY}" 1299 | fi 1300 | # shellcheck disable=SC2181 1301 | if [[ $? -ne 0 ]]; then 1302 | die "The script cannot continue without aplay." 1303 | else 1304 | [[ ${DEBUG} ]] && \ 1305 | debug "${LINENO}" "Using \`${CMD_APLAY}' as aplay command." 1306 | fi 1307 | ## parse command line arguments 1308 | analyze_command_line "$@" 1309 | ## create a list of alsa audio output interfaces and parse it. 1310 | fetch_alsa_outputinterfaces 1311 | ## exit with error if no matching output line was found 1312 | if [[ ${#ALSA_AIF_HWADDRESSES[@]} -eq 0 ]]; then 1313 | msg="\n${MSG_MATCH_IF_NONE_UNLIMITED}" 1314 | ## display information about the number of interfaces before filtering 1315 | if [[ ${NR_AIFS_BEFOREFILTERING} -ne 0 ]]; then 1316 | # shellcheck disable=SC2059 1317 | printf -v msg "${msg}\n${MSG_MATCH_IF_NONE_LIMITED}" \ 1318 | "${NR_AIFS_BEFOREFILTERING}" 1319 | printf 1>&2 "%s\n" "${msg}" 1320 | fi 1321 | fi 1322 | [[ ${DEBUG} ]] && \ 1323 | debug "${LINENO}" "Number of audio interfaces after filtering: \ 1324 | ${#ALSA_AIF_HWADDRESSES[@]}" 1325 | if [[ ${PROFILE} ]]; then 1326 | ## end profiling 1327 | set +x 1328 | exec 2>&3 3>&- 1329 | debug "${LINENO}" "Profiling information stored in: ${profile_file}" 1330 | fi 1331 | ## return success if interfaces are found 1332 | return 0 1333 | } 1334 | 1335 | ### global variables 1336 | 1337 | ## indexed arrays to store the details of interfaces of one would 1338 | ## declare such an array in another script, that array would be filled 1339 | ## instead of these. See examples/bash-example.sh for usage. 1340 | set +u 1341 | 1342 | [[ "${ALSA_AIF_HWADDRESSES[*]}x" == "x" ]] && declare -a ALSA_AIF_HWADDRESSES=() 1343 | [[ "${ALSA_AIF_DISPLAYTITLES[*]}x" == "x" ]] && declare -a ALSA_AIF_DISPLAYTITLES=() 1344 | [[ "${ALSA_AIF_MONITORFILES[*]}x" == "x" ]] && declare -a ALSA_AIF_MONITORFILES=() 1345 | [[ "${ALSA_AIF_DEVLABELS[*]}x" == "x" ]] && declare -a ALSA_AIF_DEVLABELS=() 1346 | [[ "${ALSA_AIF_LABELS[*]}" == "x" ]] && declare -a ALSA_AIF_LABELS=() 1347 | [[ "${ALSA_AIF_UACCLASSES[*]}x" == "x" ]] && declare -a ALSA_AIF_UACCLASSES=() 1348 | [[ "${ALSA_AIF_FORMATS[*]}x" == "x" ]] && declare -a ALSA_AIF_FORMATS=() 1349 | [[ "${ALSA_AIF_CHARDEVS[*]}x" == "x" ]] && declare -a ALSA_AIF_CHARDEVS=() 1350 | 1351 | set -u 1352 | 1353 | ## counter for unfiltered interfaces 1354 | NR_AIFS_BEFOREFILTERING=0 1355 | NR_AIFS_AFTERFILTERING=0 1356 | 1357 | ## static filter for digital interfaces 1358 | DO_FILTER_LIST="$(cat <