aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore15
-rw-r--r--LICENSE15
-rw-r--r--Makefile41
-rw-r--r--alsause.1137
-rw-r--r--alsause.c302
-rw-r--r--config.mk8
6 files changed, 518 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..603ea03
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*\#*
+*~
+*.o
+*.a
+*.lo
+*.su
+*.so
+*.so.*
+*.dll
+*.dylib
+*.gch
+*.gcov
+*.gcno
+*.gcda
+/alsause
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fccd785
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ISC License
+
+© 2024 Mattias Andrée <maandree@kth.se>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..31cea4c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+.POSIX:
+
+CONFIGFILE = config.mk
+include $(CONFIGFILE)
+
+OBJ =\
+ alsause.o
+
+HDR =
+
+all: alsause
+$(OBJ): $(HDR)
+
+.c.o:
+ $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS)
+
+alsause: $(OBJ)
+ $(CC) -o $@ $(OBJ) $(LDFLAGS)
+
+install: alsause
+ mkdir -p -- "$(DESTDIR)$(PREFIX)/bin"
+ mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/"
+ cp -- alsause "$(DESTDIR)$(PREFIX)/bin/"
+ cp -- alsause.1 "$(DESTDIR)$(MANPREFIX)/man1/"
+
+post-install:
+ chown -- '0:0' "$(DESTDIR)$(PREFIX)/bin/alsause"
+ chmod -- 4755 "$(DESTDIR)$(PREFIX)/bin/alsause"
+
+uninstall:
+ -rm -f -- "$(DESTDIR)$(PREFIX)/bin/alsause"
+ -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/alsause.1"
+
+clean:
+ -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda
+ -rm -f -- alsause
+
+.SUFFIXES:
+.SUFFIXES: .o .c
+
+.PHONY: all install post-install uninstall clean
diff --git a/alsause.1 b/alsause.1
new file mode 100644
index 0000000..d433d58
--- /dev/null
+++ b/alsause.1
@@ -0,0 +1,137 @@
+.TH ALSAUSE 1 ALSAUSE
+.SH NAME
+alsause - Set default ALSA PCM device
+.SH SYNPOSIS
+.B alsause
+.RB [( \-r
+|
+.I name
+|
+.IR card \fB:\fP device )
+.RI [ command
+.RI [ argument "] ...]]"
+.SH DESCRIPTION
+The
+.B alsause
+utility lists all ALSA PCM devices or
+sets, either for the user or for a program
+it starts, the default ALSA PCM device.
+.PP
+No options or operands are specified,
+.B alsause
+will list all ALSA PCM devices, otherwise
+it will set the default ALSA PCM device.
+.PP
+If
+.I command
+is specified,
+.B alsause
+will run that command with the default
+ALSA PCM device set only for that process
+and it's child processes.
+.SH OPTIONS
+The
+.B alsause
+utility conforms to the Base Definitions volume of POSIX.1-2017,
+.IR "Section 12.2" ", " "Utility Syntax Guidelines" .
+.PP
+The following option is supported:
+.TP
+.B \-r
+Remove the user-specified default ALSA PCM device,
+falling back to the system-wide default ALSA PCM device.
+.SH OPERANDS
+The following operands shall be supported:
+.TP
+.IR card : device
+The card number and device number to use as the default
+ALSA PCM device.
+.TP
+.I name
+The card and device, by name, to use as the default
+ALSA PCM device. This string is on the form output by the
+.B alsause
+utility itself:
+.BR "\(dq%s - %s\(dq" ,
+.RI < "card name" >,
+.RI < "device name" >.
+.TP
+.I command
+The name of the utility to be invoked.
+.TP
+.I argument
+A string to pass as an argument for the invoked utility.
+.SH STDIN
+Not used.
+.SH INPUT FILES
+None.
+.SH ENVIRONMENT VARIABLES
+The following environment variable affects the execution of
+.TP
+.I HOME
+Used to override the path to the user's home directroy.
+.TP
+.I PATH
+Determine the location of the
+.IR command ,
+as described in the Base Definitions volume of POSIX.1-2017,
+.IR "Chapter 8" ", " "Environment Variables" .
+.PP
+Additionally, environment variables that affect ALSA takes
+precedence over the default device set by the
+.B alsause
+utility.
+.SH ASYNCHRONOUS EVENTS
+Default.
+.SH STDOUT
+If no option or operand is specified, each ALSA PCM device is the form:
+.RS
+
+\fB\(dq%u:%u\et%s - %s\en\(dq,\fP <\fIcard number\fP>\fB,\fP <\fIdevice number\fP>\fB,\fP <\fIcard name\fP>\fB,\fP <\fIdevice name\fP>
+
+.RE
+Otherwise, the
+.B alsause
+utility does not write to standard output.
+.SH STDERR
+The standard error shall be used only for diagnostic messages.
+.SH OUTPUT FILES
+If the the default ALSA PCM device is set, the
+.B alsause
+utility may create, modify, replace, or unlink the file
+.I .asoundrc
+placed in the user's home directory.
+.SH EXTENDED DESCRIPTION
+None.
+.SH EXIT STATUS
+If
+.I command
+is invoked, the exit status of
+.B alsause
+is the exit status of
+.IR command ;
+otherwise, the
+.B alsause
+utility exits with one of the following values.
+.TP
+0
+The
+.I alsause
+utility completed successfully.
+.TP
+125
+An error occurred in the
+.I alsause
+utility.
+.TP
+126
+The utility specified by
+.I command
+was found but could not be invoked.
+.TP
+127
+The utility specified by
+.I command
+could not be found.
+.SH SEE ALSO
+None.
diff --git a/alsause.c b/alsause.c
new file mode 100644
index 0000000..e157e71
--- /dev/null
+++ b/alsause.c
@@ -0,0 +1,302 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/mount.h>
+#include <sched.h>
+#include <alsa/asoundlib.h>
+#include <libsimple.h>
+#include <libsimple-arg.h>
+
+NUSAGE(125, "[(-r | name | card:device) [command [argument] ...]]");
+
+
+static char *
+get_config_path(void)
+{
+ const char *home = getenv("HOME");
+ char *ret;
+ if (!home) {
+ struct passwd *pw;
+ errno = 0;
+ pw = getpwuid(getuid());
+ if (!pw || !pw->pw_dir)
+ eprintf("pwgetuid <real user>: %s", (!errno || errno == EIO) ? "User does not exist" : strerror(errno));
+ home = pw->pw_dir;
+ }
+ if (strlen(home) > SIZE_MAX - sizeof("/.asoundrc") ||
+ !(ret = malloc(strlen(home) + sizeof("/.asoundrc")))) {
+ errno = ENOMEM;
+ eprintf("malloc:");
+ }
+ stpcpy(stpcpy(ret, home), "/.asoundrc");
+ return ret;
+}
+
+
+#if defined(__GNUC__)
+__attribute__((__pure__))
+#endif
+static int
+is_name(const char *s)
+{
+ if (!isdigit(*s))
+ return 1;
+ if (*s == '0' && s[1] != ':')
+ return 1;
+ while (isdigit(*s))
+ s++;
+ if (*s != ':')
+ return 1;
+ s++;
+
+ if (!isdigit(*s))
+ return 1;
+ if (*s == '0' && s[1])
+ return 1;
+ while (isdigit(*s))
+ s++;
+ if (*s)
+ return 1;
+ return 0;
+}
+
+
+static int
+list_outputs(const char *find, int *card_out, int *device_out)
+{
+ snd_ctl_t *ctl;
+ snd_ctl_card_info_t *info;
+ snd_pcm_info_t *pcminfo;
+ char dev[sizeof("hw:") + 3 * sizeof(int)];
+ int r, card, device, failed = 0;
+
+ snd_ctl_card_info_alloca(&info);
+ snd_pcm_info_alloca(&pcminfo);
+
+ for (card = -1;;) {
+ r = snd_card_next(&card);
+ if (r < 0) {
+ weprintf("snd_card_next: %s", snd_strerror(r));
+ return -1;
+ }
+ if (card < 0)
+ break;
+
+ sprintf(dev, "hw:%i", card);
+ r = snd_ctl_open(&ctl, dev, 0);
+ if (r < 0) {
+ weprintf("snd_ctl_open %s 0: %s", dev, snd_strerror(r));
+ failed = 1;
+ continue;
+ }
+
+ r = snd_ctl_card_info(ctl, info);
+ if (r < 0) {
+ weprintf("snd_ctl_card_info <%s>: %s", dev, snd_strerror(r));
+ failed = 1;
+ goto close_card;
+ }
+
+ for (device = -1;;) {
+ r = snd_ctl_pcm_next_device(ctl, &device);
+ if (r < 0) {
+ weprintf("snd_ctl_pcm_next_device <%s>: %s", dev, snd_strerror(r));
+ failed = 1;
+ goto close_card;
+ }
+ if (device < 0)
+ break;
+
+ snd_pcm_info_set_device(pcminfo, (unsigned int)device);
+ snd_pcm_info_set_subdevice(pcminfo, 0);
+ snd_pcm_info_set_stream(pcminfo, SND_PCM_STREAM_PLAYBACK);
+ r = snd_ctl_pcm_info(ctl, pcminfo);
+ if (r < 0) {
+ if (r == -ENOENT)
+ continue;
+ weprintf("snd_ctl_pcm_info <%s>: %s (%i)", dev, snd_strerror(r), r);
+ failed = 1;
+ goto close_card;
+ }
+
+ if (find) {
+ const char *s, *n = find;
+ s = snd_ctl_card_info_get_name(info);
+ if (strncmp(n, s, strlen(s)))
+ continue;
+ n = &n[strlen(s)];
+ if (strncmp(n, " - ", 3))
+ continue;
+ n = &n[3];
+ s = snd_pcm_info_get_name(pcminfo);
+ if (strcmp(n, s))
+ continue;
+ *card_out = snd_ctl_card_info_get_card(info);
+ *device_out = device;
+ snd_ctl_close(ctl);
+ return 0;
+ } else {
+ printf("%i:%i\t%s - %s\n",
+ snd_ctl_card_info_get_card(info), device,
+ snd_ctl_card_info_get_name(info),
+ snd_pcm_info_get_name(pcminfo));
+ }
+ }
+
+ close_card:
+ snd_ctl_close(ctl);
+ }
+
+ if (find) {
+ *card_out = -1;
+ *device_out = -1;
+ }
+ return -failed;
+}
+
+
+static int
+writeall(int fd, void *data, size_t len)
+{
+ char *text = data;
+ ssize_t r;
+ while (len) {
+ r = write(fd, text, len);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ return -1;
+ }
+ text = &text[r];
+ len -= (size_t)r;
+ }
+ return 0;
+}
+
+
+int
+main(int argc, char *argv[])
+{
+ int reset = 0;
+
+ libsimple_default_failure_exit = 125;
+
+ ARGBEGIN {
+ case 'r':
+ reset = 1;
+ break;
+ default:
+ usage();
+ } ARGEND;
+
+ if (!argc) {
+ if (reset) {
+ char *path = get_config_path();
+ if (unlink(path) && errno != ENOENT)
+ eprintf("unlink %s:", path);
+ free(path);
+ } else {
+ if (list_outputs(NULL, NULL, NULL))
+ return libsimple_default_failure_exit;
+ }
+ } else {
+ char *path = get_config_path();
+ char *text, temppath[sizeof("/tmp/alsause.") + 3 * sizeof(uintmax_t)];
+ int len, fd, treefd;
+ if (reset) {
+ text = NULL;
+ len = 0;
+ } else if (is_name(argv[0])) {
+ int card, device;
+ if (list_outputs(argv[0], &card, &device))
+ return 1;
+ if (card == -1 || device == -1)
+ eprintf("audio output \"%s\" not found", argv[0]);
+ len = snprintf(NULL, 0, "defaults.pcm.card %i\ndefaults.pcm.device %i\n", card, device);
+ if (len < 0)
+ eprintf("snprintf NULL 0:");
+ text = malloc((size_t)len + 1U);
+ if (!text)
+ eprintf("malloc:");
+ if (sprintf(text, "defaults.pcm.card %i\ndefaults.pcm.device %i\n", card, device) != len)
+ abort();
+ argc--;
+ argv++;
+ } else {
+ char *card = argv[0];
+ char *device = strchr(card, ':');
+ if (!device)
+ usage();
+ *device++ = '\0';
+ if (strchr(device, ':'))
+ usage();
+ len = snprintf(NULL, 0, "defaults.pcm.card %s\ndefaults.pcm.device %s\n", card, device);
+ if (len < 0)
+ eprintf("snprintf NULL 0:");
+ text = malloc((size_t)len + 1U);
+ if (!text)
+ eprintf("malloc:");
+ if (sprintf(text, "defaults.pcm.card %s\ndefaults.pcm.device %s\n", card, device) != len)
+ abort();
+ argc--;
+ argv++;
+ }
+
+ if (!argc) {
+ fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0666);
+ if (fd < 0)
+ eprintf("open %s O_CREAT|O_TRUNC|O_WRONLY 0666:", path);
+ if (fchown(fd, getuid(), getgid()))
+ weprintf("fchown %s <read user> <real path>:", path);
+ if (writeall(fd, text, (size_t)len) || (close(fd) && (errno != EINTR)))
+ eprintf("write %s:", path);
+ free(text);
+ free(path);
+ return 0;
+ }
+
+ if (unshare(CLONE_NEWNS))
+ eprintf("unshare CLONE_NEWNS:");
+ if (mount("none", "/", NULL, MS_REC | MS_SLAVE, NULL))
+ eprintf("mount none / NULL MS_REC|MS_SLAVE NULL:");
+
+ fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0666);
+ if (fd >= 0) {
+ fchown(fd, getuid(), getgid());
+ close(fd);
+ }
+
+ sprintf(temppath, "/tmp/alsause.%ju", (uintmax_t)getpid());
+ unlink(temppath);
+ fd = open(temppath, O_CREAT | O_EXCL | O_WRONLY, 0666);
+ if (fd < 0)
+ eprintf("open %s O_CREAT|O_EXCL|O_WRONLY 0666:", temppath);
+ if (fchown(fd, getuid(), getgid()))
+ weprintf("fchown %s <read user> <real path>:", temppath);
+ if (writeall(fd, text, (size_t)len) || (close(fd) && (errno != EINTR)))
+ eprintf("write %s:", temppath);
+ free(text);
+
+ treefd = open_tree(AT_FDCWD, temppath, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ if (treefd < 0) {
+ weprintf("open_tree AT_FDCWD %s OPEN_TREE_CLONE:", temppath);
+ unlink(temppath);
+ exit(libsimple_default_failure_exit);
+ }
+ if (move_mount(treefd, "", AT_FDCWD, path, MOVE_MOUNT_F_EMPTY_PATH)) {
+ weprintf("move_mount %s \"\" AT_FDCWD %s MOVE_MOUNT_F_EMPTY_PATH:", temppath, path);
+ unlink(temppath);
+ exit(libsimple_default_failure_exit);
+ }
+
+ if (unlink(temppath))
+ weprintf("unlink %s:", temppath);
+ if (setegid(getgid()))
+ eprintf("setegid <real group>:");
+ if (seteuid(getuid()))
+ eprintf("seteuid <real user>:");
+
+ execvp(argv[0], argv);
+ enprintf(errno == ENOENT ? 127 : 126, "execvp %s:", argv[0]);
+ }
+
+ return 0;
+}
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..9284edf
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,8 @@
+PREFIX = /usr
+MANPREFIX = $(PREFIX)/share/man
+
+CC = c99
+
+CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE
+CFLAGS =
+LDFLAGS = -lasound -lsimple