Commit Diff


commit - /dev/null
commit + 3baa2617f8fa715685929c415068ab226626a2a3
blob - /dev/null
blob + a8da1ad97ac240b0df0d7be7c216417cd8aa6ba8 (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,15 @@
+Copyright 2022 Omar Polo
+
+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.
blob - /dev/null
blob + f696ec5349ed952a8c3b4c52ee353be0523a0114 (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,47 @@
+PROG=	amused
+SRCS=	amused.c control.c log.c xmalloc.c player.c ctl.c playlist.c \
+	player_flac.c player_mad.c player_opus.c player_oggvorbis.c
+
+.include "amused-version.mk"
+
+CPPFLAGS +=	-DAMUSED_VERSION=\"${AMUSED_VERSION}\" \
+		-I/usr/local/include -I/usr/local/include/opus
+
+LDADD =	-levent -lm -lsndio -lutil \
+	-L/usr/local/lib -lmad -lvorbisfile -lopusfile -lFLAC
+DPADD =	${LIBEVENT} ${LIBM} ${LIBSNDIO} ${LIBUTIL}
+
+.if "${AMUSED_RELEASE}" == "Yes"
+PREFIX ?= /usr/local
+BINDIR ?= ${PREFIX}/bin
+MANDIR ?= ${PREFIX}/man/man
+.else
+CFLAGS += -Werror -Wall -Wstrict-prototypes -Wunused-variable
+PREFIX ?= ${HOME}
+BINDIR ?= ${PREFIX}/bin
+BINOWN ?= ${USER}
+.if !defined(BINGRP)
+BINGRP != id -g -n
+.endif
+DEBUG = -O0 -g
+.endif
+
+release: clean
+	sed -i -e 's/_RELEASE=No/_RELEASE=Yes/' amused-version.mk
+	${MAKE} dist
+	sed -i -e 's/_RELEASE=Yes/_RELEASE=No/' amused-version.mk
+
+dist: clean
+	mkdir /tmp/amused-${AMUSED_VERSION}
+	pax -rw * /tmp/amused-${AMUSED_VERSION}
+	find /tmp/amused-${AMUSED_VERSION} -type d -name obj -delete
+	rm /tmp/amused-${AMUSED_VERSION}/amused-dist.txt
+	tar -C /tmp -zcf amused-${AMUSED_VERSION}.tar.gz amused-${AMUSED_VERSION}
+	rm -rf /tmp/amused-${AMUSED_VERSION}
+	tar -ztf amused-${AMUSED_VERSION}.tar.gz | \
+		sed -e 's/^amused-${AMUSED_VERSION}//' | \
+		sort > amused-dist.txt.new
+	diff -u amused-dist.txt{,new}
+	rm amused-dist.txt.new
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + ae9b2a9dc2825c6e0d4b56d4a0a489641f4e7a56 (mode 644)
--- /dev/null
+++ amused-version.mk
@@ -0,0 +1,8 @@
+AMUSED_RELEASE=No
+AMUSED_VERSION_NUMBER=0.1
+
+.if ${AMUSED_RELEASE} == Yes
+AMUSED_VERSION=${AMUSED_VERSION_NUMBER}
+.else
+AMUSED_VERSION=${AMUSED_VERSION_NUMBER}-current
+.endif
blob - /dev/null
blob + 25d20a6822f5854ddc669f139ae4496bd5919d69 (mode 644)
--- /dev/null
+++ amused.1
@@ -0,0 +1,82 @@
+.\" Copyright (c) 2022 Omar Polo <op@openbsd.org>
+.\"
+.\" Permission to use, copy, modify, and 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.
+.\"
+.Dd February 16 2022
+.Dt AMUSED 1
+.Os
+.Sh NAME
+.Nm amused
+.Nd music player
+.Sh SYNOPSIS
+.Nm
+.Op Fl dv
+.Op Fl s Ar socket
+.Ar command
+.Op Ar argument ...
+.Sh DESCRIPTION
+.Nm
+is a music player daemon and command-line utility.
+.Pp
+If called without any sub-
+.Ar command
+it forks in the background and starts the daemon.
+Once the daemon is running, music files can be enqueued and the reproduction
+controlled.
+.Pp
+The following options are available:
+.Bl -tag -width 12m
+.It Fl d
+Do not daemonize.
+if this option is specified,
+.Nm
+will run in the foreground and log to
+.Em stderr .
+.It Fl v
+Produce more verbose output.
+.It Fl s Ar socket
+Use
+.Ar socket
+instead of the default
+.Pa /tmp/amused-$UID
+to communicate with the daemon.
+.El
+.Pp
+The following commands are available:
+.Bl -tag -width 12m
+.It Cm play
+Start playing the playlist.
+.It Cm pause
+Pause the reproduction.
+.It Cm toggle
+Toggle pause state.
+.It Cm stop
+Stop the music player.
+.It Cm restart
+Restart the current song from the beginning.
+.It Cm add Ar
+Enqueue the given files.
+.It Cm flush
+Erase the playlist.
+.El
+.Sh FILES
+.Bl -tag -width "/tmp/amused-$UID" -compact
+.It Pa /tmp/amused-$UID
+UNIX-domain socket used for communication with the daemon.
+.El
+.Sh AUTHORS
+.An -nosplit
+Then
+.Nm
+utility was written by
+.An Omar Polo Aq Mt op@openbsd.org .
blob - /dev/null
blob + fb8aab8c435d364c7c7a4bf64e6919e1d2bb6b71 (mode 644)
--- /dev/null
+++ amused.c
@@ -0,0 +1,380 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+
+#include <event.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sndio.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <imsg.h>
+
+#include "amused.h"
+#include "control.h"
+#include "log.h"
+#include "playlist.h"
+#include "xmalloc.h"
+
+struct sio_hdl	*hdl;
+char		*csock = NULL;
+int		 debug;
+int		 verbose;
+struct imsgev	*iev_player;
+
+const char	*argv0;
+pid_t		 player_pid;
+struct event	 ev_sigint;
+struct event	 ev_sigterm;
+
+enum amused_process {
+	PROC_MAIN,
+	PROC_PLAYER,
+};
+
+__dead void
+main_shutdown(void)
+{
+	pid_t	pid;
+	int	status;
+
+	/* close pipes. */
+	msgbuf_clear(&iev_player->ibuf.w);
+	close(iev_player->ibuf.fd);
+	free(iev_player);
+
+	log_debug("waiting for children to terminate");
+	do {
+		pid = wait(&status);
+		if (pid == -1) {
+			if (errno != EINTR && errno != ECHILD)
+				fatal("wait");
+		} else if (WIFSIGNALED(status))
+			log_warnx("player terminated; signal %d",
+			    WTERMSIG(status));
+	} while (pid != -1 || (pid == -1 && errno == EINTR));
+
+	log_info("terminating");
+	exit(0);
+}
+
+static void
+main_sig_handler(int sig, short event, void *arg)
+{
+	/*
+	 * Normal signal handler rules don't apply because libevent
+	 * decouples for us.
+	 */
+
+	switch (sig) {
+	case SIGTERM:
+	case SIGINT:
+		main_shutdown();
+		break;
+	default:
+		fatalx("unexpected signal %d", sig);
+	}
+}
+
+static void
+main_dispatch_player(int sig, short event, void *d)
+{
+	struct imsgev	*iev = d;
+	struct imsgbuf	*ibuf = &iev->ibuf;
+	struct imsg	 imsg;
+	ssize_t		 n;
+	int		 shut = 0;
+	const char	*song;
+
+	if (event & EV_READ) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatal("imsg_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if ((n = msgbuf_write(&ibuf->w)) == -1 && errno != EAGAIN)
+			fatal("msgbuf_write");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("imsg_get");
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case IMSG_EOF:
+		case IMSG_ERR:
+			song = playlist_advance();
+			if (song == NULL)
+				break;
+			/* XXX: watch out for failures! */
+			main_play_song(song);
+			break;
+		default:
+			log_debug("%s: error handling imsg %d", __func__,
+			    imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler. */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+static pid_t
+start_child(enum amused_process proc, int fd)
+{
+	const char	*argv[5];
+	int		 argc = 0;
+	pid_t		 pid;
+
+	switch (pid = fork()) {
+	case -1:
+		fatal("cannot fork");
+	case 0:
+		break;
+	default:
+		close(fd);
+		return pid;
+	}
+
+	if (fd != 3) {
+		if (dup2(fd, 3) == -1)
+			fatal("cannot setup imsg fd");
+	} else if (fcntl(F_SETFD, 0) == -1)
+		fatal("cannot setup imsg fd");
+
+	argv[argc++] = argv0;
+	switch (proc) {
+	case PROC_MAIN:
+		fatal("can not start main process");
+	case PROC_PLAYER:
+		argv[argc++] = "-Tp";
+		break;
+	}
+
+	if (debug)
+		argv[argc++] = "-d";
+	if (verbose)
+		argv[argc++] = "-v";
+	argv[argc++] = NULL;
+
+	/* obnoxious casts */
+	execvp(argv0, (char *const *)argv);
+	fatal("execvp %s", argv0);
+}
+
+/* daemon main routine */
+static __dead int
+amused_main(void)
+{
+	int	 pipe_main2player[2];
+	int	 control_fd;
+
+	log_init(debug, LOG_DAEMON);
+	log_setverbose(verbose);
+	log_procinit("main");
+
+	if (!debug)
+		daemon(1, 0);
+
+	if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
+	    PF_UNSPEC, pipe_main2player) == -1)
+		fatal("socketpair");
+
+	player_pid = start_child(PROC_PLAYER, pipe_main2player[1]);
+
+	event_init();
+
+	signal_set(&ev_sigint, SIGINT, main_sig_handler, NULL);
+	signal_set(&ev_sigterm, SIGTERM, main_sig_handler, NULL);
+
+	signal_add(&ev_sigint, NULL);
+	signal_add(&ev_sigterm, NULL);
+
+	signal(SIGHUP, SIG_IGN);
+	signal(SIGCHLD, SIG_IGN);
+	signal(SIGPIPE, SIG_IGN);
+
+	iev_player = xmalloc(sizeof(*iev_player));
+	imsg_init(&iev_player->ibuf, pipe_main2player[0]);
+	iev_player->handler = main_dispatch_player;
+	iev_player->events = EV_READ;
+	event_set(&iev_player->ev, iev_player->ibuf.fd, iev_player->events,
+	    iev_player->handler, iev_player);
+	event_add(&iev_player->ev, NULL);
+
+	if ((control_fd = control_init(csock)) == -1)
+		fatal("control socket setup failed %s", csock);
+	control_listen(control_fd);
+
+	if (pledge("stdio rpath unix sendfd", NULL) == -1)
+		fatal("pledge");
+
+	log_info("startup");
+	event_dispatch();
+	main_shutdown();
+}
+
+int
+main(int argc, char **argv)
+{
+	int ch, proc = PROC_MAIN;
+
+	log_init(1, LOG_DAEMON);	/* Log to stderr until daemonized */
+	log_setverbose(1);
+
+	argv0 = argv[0];
+	if (argv0 == NULL)
+		argv0 = "amused";
+
+	while ((ch = getopt(argc, argv, "ds:T:vV")) != -1) {
+		switch (ch) {
+		case 'd':
+			debug = 1;
+			break;
+		case 's':
+			free(csock);
+			csock = xstrdup(optarg);
+			break;
+		case 'T':
+			switch (*optarg) {
+			case 'p':
+				proc = PROC_PLAYER;
+				break;
+			default:
+				usage();
+			}
+			break;
+		case 'v':
+			verbose++;
+			break;
+		case 'V':
+			printf("%s version %s\n", getprogname(),
+			    AMUSED_VERSION);
+			exit(0);
+		default:
+			usage();
+		}
+	}
+	argv += optind;
+	argc -= optind;
+
+	if (proc == PROC_PLAYER)
+		exit(player(debug, verbose));
+
+	if (csock == NULL)
+		xasprintf(&csock, "/tmp/amused-%d", getuid());
+
+	if (argc == 0)
+		amused_main();
+	else
+		ctl(argc, argv);
+	return 0;
+}
+
+void
+imsg_event_add(struct imsgev *iev)
+{
+	iev->events = EV_READ;
+	if (iev->ibuf.w.queued)
+		iev->events |= EV_WRITE;
+
+	event_del(&iev->ev);
+	event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev);
+	event_add(&iev->ev, NULL);
+}
+
+int
+imsg_compose_event(struct imsgev *iev, uint16_t type, uint32_t peerid,
+    pid_t pid, int fd, const void *data, uint16_t datalen)
+{
+	int ret;
+
+	if ((ret = imsg_compose(&iev->ibuf, type, peerid, pid, fd, data,
+	    datalen)) != -1)
+		imsg_event_add(iev);
+
+	return ret;
+}
+
+int
+main_play_song(const char *song)
+{
+	char path[PATH_MAX] = { 0 };
+	int fd;
+
+	strlcpy(path, song, sizeof(path));
+	if ((fd = open(path, O_RDONLY)) == -1) {
+#if todo
+		log_warn("open %s", path);
+		return -1;
+#else
+		fatal("open %s", path);
+#endif
+	}
+
+	imsg_compose_event(iev_player, IMSG_PLAY, 0, 0, fd,
+	    path, sizeof(path));
+	return 0;
+}
+
+void
+main_enqueue(struct imsgev *iev, struct imsg *imsg)
+{
+	size_t datalen;
+	char path[PATH_MAX] = { 0 };
+	const char *err = NULL;
+
+	datalen = IMSG_DATA_SIZE(*imsg);
+	if (datalen != sizeof(path)) {
+		err = "data size mismatch";
+		goto err;
+	}
+
+	memcpy(path, imsg->data, sizeof(path));
+	if (path[datalen-1] != '\0') {
+		err = "malformed data";
+		goto err;
+	}
+
+	playlist_enqueue(path);
+	imsg_compose_event(iev, IMSG_CTL_ADD, 0, 0, -1, path, sizeof(path));
+	return;
+err:
+	imsg_compose_event(iev, IMSG_CTL_ERR, 0, 0, -1, err, strlen(err)+1);
+}
blob - /dev/null
blob + 5d76e295c518c59a068d76bdf4f4155a26ef40d9 (mode 644)
--- /dev/null
+++ amused.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#ifndef AMUSED_H
+#define AMUSED_H
+
+extern struct sio_hdl	*hdl;
+extern char		*csock;
+extern int		 debug;
+extern int		 verbose;
+extern int		 playing;
+extern struct imsgev	*iev_player;
+
+#define IMSG_DATA_SIZE(imsg)	((imsg).hdr.len - IMSG_HEADER_SIZE)
+
+enum imsg_type {
+	IMSG_PLAY,		/* fd + filename */
+	IMSG_RESUME,
+	IMSG_PAUSE,
+	IMSG_STOP,
+	IMSG_EOF,
+	IMSG_ERR,
+
+	IMSG_CTL_PLAY,
+	IMSG_CTL_TOGGLE_PLAY,
+	IMSG_CTL_PAUSE,
+	IMSG_CTL_STOP,
+	IMSG_CTL_RESTART,
+	IMSG_CTL_ADD,
+	IMSG_CTL_FLUSH,
+	IMSG_CTL_SHOW,
+
+	IMSG_CTL_ERR,
+};
+
+struct imsgev {
+	struct imsgbuf	 ibuf;
+	void		(*handler)(int, short, void *);
+	struct event	 ev;
+	short		 events;
+};
+
+enum actions {
+	NONE,
+	PLAY,
+	PAUSE,
+	TOGGLE,
+	STOP,
+	RESTART,
+	ADD,
+	FLUSH,
+	SHOW,
+};
+
+struct ctl_command;
+
+struct parse_result {
+	enum actions		 action;
+	char			**files;
+	struct ctl_command	*ctl;
+};
+
+struct ctl_command {
+	const char		*name;
+	enum actions		 action;
+	int			(*main)(struct parse_result *, int, char **);
+	const char		*usage;
+	int			 has_pledge;
+};
+
+/* amused.c */
+void		imsg_event_add(struct imsgev *iev);
+int		imsg_compose_event(struct imsgev *, uint16_t, uint32_t,
+		    pid_t, int, const void *, uint16_t);
+int		main_play_song(const char *);
+void		main_enqueue(struct imsgev *, struct imsg *);
+
+/* ctl.c */
+__dead void	usage(void);
+__dead void	ctl(int, char **);
+
+/* player.c */
+int	player_setrate(int);
+void	player_senderr(void);
+void	player_sendeof(void);
+int	player_shouldstop(void);
+int	player(int, int);
+
+void	play_oggvorbis(int fd);
+void	play_mp3(int fd);
+void	play_flac(int fd);
+void	play_opus(int fd);
+
+#endif
blob - /dev/null
blob + e24d6d2b6d90d0cd9c303969532e5eafc0c71fe5 (mode 644)
--- /dev/null
+++ control.c
@@ -0,0 +1,337 @@
+/*	$OpenBSD: control.c,v 1.8 2021/03/02 04:10:07 jsg Exp $	*/
+
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/un.h>
+
+#include <netinet/in.h>
+#include <net/if.h>
+
+#include <errno.h>
+#include <event.h>
+#include <imsg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "amused.h"
+#include "log.h"
+#include "control.h"
+#include "playlist.h"
+
+#define	CONTROL_BACKLOG	5
+
+struct {
+	struct event	ev;
+	struct event	evt;
+	int		fd;
+} control_state = {.fd = -1};
+
+struct ctl_conn {
+	TAILQ_ENTRY(ctl_conn)	entry;
+	struct imsgev		iev;
+};
+
+TAILQ_HEAD(ctl_conns, ctl_conn)	ctl_conns = TAILQ_HEAD_INITIALIZER(ctl_conns);
+
+struct ctl_conn	*control_connbyfd(int);
+struct ctl_conn	*control_connbypid(pid_t);
+void		 control_close(int);
+
+int
+control_init(char *path)
+{
+	struct sockaddr_un	 sun;
+	int			 fd;
+	mode_t			 old_umask;
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
+	    0)) == -1) {
+		log_warn("%s: socket", __func__);
+		return (-1);
+	}
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	strlcpy(sun.sun_path, path, sizeof(sun.sun_path));
+
+	if (unlink(path) == -1)
+		if (errno != ENOENT) {
+			log_warn("%s: unlink %s", __func__, path);
+			close(fd);
+			return (-1);
+		}
+
+	old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
+	if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) {
+		log_warn("%s: bind: %s", __func__, path);
+		close(fd);
+		umask(old_umask);
+		return (-1);
+	}
+	umask(old_umask);
+
+	if (chmod(path, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) == -1) {
+		log_warn("%s: chmod", __func__);
+		close(fd);
+		(void)unlink(path);
+		return (-1);
+	}
+
+	return (fd);
+}
+
+int
+control_listen(int fd)
+{
+	if (control_state.fd != -1)
+		fatalx("%s: received unexpected controlsock", __func__);
+
+	control_state.fd = fd;
+	if (listen(control_state.fd, CONTROL_BACKLOG) == -1) {
+		log_warn("%s: listen", __func__);
+		return (-1);
+	}
+
+	event_set(&control_state.ev, control_state.fd, EV_READ,
+	    control_accept, NULL);
+	event_add(&control_state.ev, NULL);
+	evtimer_set(&control_state.evt, control_accept, NULL);
+
+	return (0);
+}
+
+void
+control_accept(int listenfd, short event, void *bula)
+{
+	int			 connfd;
+	socklen_t		 len;
+	struct sockaddr_un	 sun;
+	struct ctl_conn		*c;
+
+	event_add(&control_state.ev, NULL);
+	if ((event & EV_TIMEOUT))
+		return;
+
+	len = sizeof(sun);
+	if ((connfd = accept4(listenfd, (struct sockaddr *)&sun, &len,
+	    SOCK_CLOEXEC | SOCK_NONBLOCK)) == -1) {
+		/*
+		 * Pause accept if we are out of file descriptors, or
+		 * libevent will haunt us here too.
+		 */
+		if (errno == ENFILE || errno == EMFILE) {
+			struct timeval evtpause = { 1, 0 };
+
+			event_del(&control_state.ev);
+			evtimer_add(&control_state.evt, &evtpause);
+		} else if (errno != EWOULDBLOCK && errno != EINTR &&
+		    errno != ECONNABORTED)
+			log_warn("%s: accept4", __func__);
+		return;
+	}
+
+	if ((c = calloc(1, sizeof(struct ctl_conn))) == NULL) {
+		log_warn("%s: calloc", __func__);
+		close(connfd);
+		return;
+	}
+
+	imsg_init(&c->iev.ibuf, connfd);
+	c->iev.handler = control_dispatch_imsg;
+	c->iev.events = EV_READ;
+	event_set(&c->iev.ev, c->iev.ibuf.fd, c->iev.events,
+	    c->iev.handler, &c->iev);
+	event_add(&c->iev.ev, NULL);
+
+	TAILQ_INSERT_TAIL(&ctl_conns, c, entry);
+}
+
+struct ctl_conn *
+control_connbyfd(int fd)
+{
+	struct ctl_conn	*c;
+
+	TAILQ_FOREACH(c, &ctl_conns, entry) {
+		if (c->iev.ibuf.fd == fd)
+			break;
+	}
+
+	return (c);
+}
+
+struct ctl_conn *
+control_connbypid(pid_t pid)
+{
+	struct ctl_conn	*c;
+
+	TAILQ_FOREACH(c, &ctl_conns, entry) {
+		if (c->iev.ibuf.pid == pid)
+			break;
+	}
+
+	return (c);
+}
+
+void
+control_close(int fd)
+{
+	struct ctl_conn	*c;
+
+	if ((c = control_connbyfd(fd)) == NULL) {
+		log_warnx("%s: fd %d: not found", __func__, fd);
+		return;
+	}
+
+	msgbuf_clear(&c->iev.ibuf.w);
+	TAILQ_REMOVE(&ctl_conns, c, entry);
+
+	event_del(&c->iev.ev);
+	close(c->iev.ibuf.fd);
+
+	/* Some file descriptors are available again. */
+	if (evtimer_pending(&control_state.evt, NULL)) {
+		evtimer_del(&control_state.evt);
+		event_add(&control_state.ev, NULL);
+	}
+
+	free(c);
+}
+
+void
+control_dispatch_imsg(int fd, short event, void *bula)
+{
+	struct ctl_conn	*c;
+	struct imsg	 imsg;
+	ssize_t		 n;
+	const char	*song;
+
+	if ((c = control_connbyfd(fd)) == NULL) {
+		log_warnx("%s: fd %d: not found", __func__, fd);
+		return;
+	}
+
+	if (event & EV_READ) {
+		if (((n = imsg_read(&c->iev.ibuf)) == -1 && errno != EAGAIN) ||
+		    n == 0) {
+			control_close(fd);
+			return;
+		}
+	}
+	if (event & EV_WRITE) {
+		if (msgbuf_write(&c->iev.ibuf.w) <= 0 && errno != EAGAIN) {
+			control_close(fd);
+			return;
+		}
+	}
+
+	for (;;) {
+		if ((n = imsg_get(&c->iev.ibuf, &imsg)) == -1) {
+			control_close(fd);
+			return;
+		}
+		if (n == 0)
+			break;
+
+		switch (imsg.hdr.type) {
+		case IMSG_CTL_PLAY:
+			switch (play_state) {
+			case STATE_STOPPED:
+				song = playlist_advance();
+				if (song == NULL)
+					break;
+				/* XXX: watch out for failures! */
+				main_play_song(song);
+				break;
+			case STATE_PLAYING:
+				/* do nothing */
+				break;
+			case STATE_PAUSED:
+				imsg_compose_event(iev_player, IMSG_RESUME,
+				    0, 0, -1, NULL, 0);
+				break;
+			}
+			break;
+		case IMSG_CTL_TOGGLE_PLAY:
+			switch (play_state) {
+			case STATE_STOPPED:
+				song = playlist_advance();
+				if (song == NULL)
+					break;
+				/* XXX: watch out for failures! */
+				main_play_song(song);
+				break;
+			case STATE_PLAYING:
+				imsg_compose_event(iev_player, IMSG_PAUSE, 0, 0, -1,
+				    NULL, 0);
+				break;
+			case STATE_PAUSED:
+				imsg_compose_event(iev_player, IMSG_RESUME, 0, 0, -1,
+				    NULL, 0);
+				break;
+			}
+			break;
+		case IMSG_CTL_PAUSE:
+			if (play_state != STATE_PLAYING)
+				break;
+			imsg_compose_event(iev_player, IMSG_PAUSE, 0, 0, -1, NULL, 0);
+			break;
+		case IMSG_CTL_STOP:
+			if (play_state == STATE_STOPPED)
+				break;
+			imsg_compose_event(iev_player, IMSG_STOP, 0, 0, -1, NULL, 0);
+			break;
+		case IMSG_CTL_RESTART:
+			imsg_compose_event(iev_player, IMSG_STOP, 0, 0, -1, NULL, 0);
+			song = playlist_current();
+			if (song == NULL)
+				break;
+			/* XXX: watch out for failures */
+			main_play_song(song);
+			break;
+		case IMSG_CTL_ADD:
+			main_enqueue(&c->iev, &imsg);
+			break;
+		case IMSG_CTL_FLUSH:
+			playlist_truncate();
+		case IMSG_CTL_SHOW:
+		default:
+			log_debug("%s: error handling imsg %d", __func__,
+			    imsg.hdr.type);
+			break;
+		}
+		imsg_free(&imsg);
+	}
+
+	imsg_event_add(&c->iev);
+}
+
+int
+control_imsg_relay(struct imsg *imsg)
+{
+	struct ctl_conn	*c;
+
+	if ((c = control_connbypid(imsg->hdr.pid)) == NULL)
+		return (0);
+
+	return (imsg_compose_event(&c->iev, imsg->hdr.type, 0, imsg->hdr.pid,
+	    -1, imsg->data, IMSG_DATA_SIZE(*imsg)));
+}
blob - /dev/null
blob + e75b3c1d5a53c935ef71364bbaabaa5c054b9acd (mode 644)
--- /dev/null
+++ control.h
@@ -0,0 +1,23 @@
+/*	$OpenBSD: control.h,v 1.3 2021/01/19 16:54:00 florian Exp $	*/
+
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+int	control_init(char *);
+int	control_listen(int fd);
+void	control_accept(int, short, void *);
+void	control_dispatch_imsg(int, short, void *);
+int	control_imsg_relay(struct imsg *);
blob - /dev/null
blob + 786a76debc3c4310400ca69fa83a257edc3ddddf (mode 644)
--- /dev/null
+++ ctl.c
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+#include <sys/un.h>
+
+#include <errno.h>
+#include <event.h>
+#include <limits.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <imsg.h>
+
+#include "amused.h"
+#include "log.h"
+#include "xmalloc.h"
+
+static struct imsgbuf *ibuf;
+
+int	ctl_noarg(struct parse_result *, int, char **);
+int	ctl_add(struct parse_result *, int, char **);
+
+struct ctl_command ctl_commands[] = {
+	{ "play",	PLAY,		ctl_noarg,	"" },
+	{ "pause",	PAUSE,		ctl_noarg,	"" },
+	{ "toggle",	TOGGLE,		ctl_noarg,	"" },
+	{ "stop",	STOP,		ctl_noarg,	"" },
+	{ "restart",	RESTART,	ctl_noarg,	"" },
+	{ "add",	ADD,		ctl_add,	"files...", 1 },
+	{ "flush",	FLUSH,		ctl_noarg,	"" },
+	{ NULL },
+};
+
+__dead void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-dv] [-s socket]\n", getprogname());
+	fprintf(stderr, "%s version %s\n", getprogname(), AMUSED_VERSION);
+	exit(1);
+}
+
+static __dead void
+ctl_usage(struct ctl_command *ctl)
+{
+	fprintf(stderr, "usage: %s [-v] [-s socket] %s %s\n", getprogname(),
+	    ctl->name, ctl->usage);
+	exit(1);
+}
+
+static int
+parse(int argc, char **argv)
+{
+	struct ctl_command	*ctl = NULL;
+	struct parse_result	 res;
+	int			 i, status;
+
+	memset(&res, 0, sizeof(res));
+
+	for (i = 0; ctl_commands[i].name != NULL; ++i) {
+		if (strncmp(ctl_commands[i].name, argv[0], strlen(argv[0]))
+		    == 0) {
+			if (ctl != NULL) {
+				fprintf(stderr,
+				    "ambiguous argument: %s\n", argv[0]);
+				usage();
+			}
+			ctl = &ctl_commands[i];
+		}
+	}
+
+	if (ctl == NULL) {
+		fprintf(stderr, "unknown argument: %s\n", argv[0]);
+		usage();
+	}
+
+	res.action = ctl->action;
+	res.ctl = ctl;
+
+	if (!ctl->has_pledge) {
+		/* pledge(2) default if command doesn't have its own */
+		if (pledge("stdio", NULL) == -1)
+			fatal("pledge");
+	}
+
+	status = ctl->main(&res, argc, argv);
+	close(ibuf->fd);
+	free(ibuf);
+	return status;
+}
+
+static int
+enqueue_tracks(char **files)
+{
+	char res[PATH_MAX];
+	int enq = 0;
+
+	for (; *files != NULL; ++files) {
+		memset(&res, 0, sizeof(res));
+		if (realpath(*files, res) == NULL) {
+			log_warn("realpath %s", *files);
+			continue;
+		}
+
+		imsg_compose(ibuf, IMSG_CTL_ADD, 0, 0, -1,
+		    res, sizeof(res));
+		enq++;
+	}
+
+	return enq == 0;
+}
+
+static int
+show_complete(struct imsg *imsg, int *ret)
+{
+	size_t datalen;
+	char path[PATH_MAX];
+
+	datalen = IMSG_DATA_SIZE(*imsg);
+	if (datalen == 0)
+		return 1;
+
+	if (datalen != sizeof(path))
+		fatalx("%s: data size mismatch", __func__);
+	memcpy(path, imsg->data, sizeof(path));
+	if (path[datalen-1] != '\0')
+		fatalx("%s: data corrupted?", __func__);
+
+	printf("%s\n", path);
+	return 0;
+}
+
+static int
+ctlaction(struct parse_result *res)
+{
+	struct imsg imsg;
+	ssize_t n;
+	int ret = 0, done = 1;
+
+	switch (res->action) {
+	case PLAY:
+		imsg_compose(ibuf, IMSG_CTL_PLAY, 0, 0, -1, NULL, 0);
+		break;
+	case PAUSE:
+		imsg_compose(ibuf, IMSG_CTL_PAUSE, 0, 0, -1, NULL, 0);
+		break;
+	case TOGGLE:
+		imsg_compose(ibuf, IMSG_CTL_TOGGLE_PLAY, 0, 0, -1, NULL, 0);
+		break;
+	case STOP:
+		imsg_compose(ibuf, IMSG_CTL_STOP, 0, 0, -1, NULL, 0);
+		break;
+	case RESTART:
+		imsg_compose(ibuf, IMSG_CTL_RESTART, 0, 0, -1, NULL, 0);
+		break;
+	case ADD:
+		ret = enqueue_tracks(res->files);
+		break;
+	case FLUSH:
+		imsg_compose(ibuf, IMSG_CTL_FLUSH, 0, 0, -1, NULL, 0);
+		break;
+	case SHOW:
+		done = 0;
+		imsg_compose(ibuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
+		break;
+	case NONE:
+		/* action not expected */
+		fatalx("invalid action %u", res->action);
+		break;
+	}
+
+	if (ret != 0)
+		goto end;
+
+	imsg_flush(ibuf);
+
+	while (!done) {
+		if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+			fatalx("imsg_read error");
+		if (n == 0)
+			fatalx("pipe closed");
+
+		while (!done) {
+			if ((n = imsg_get(ibuf, &imsg)) == -1)
+				fatalx("imsg_get error");
+			if (n == 0)
+				break;
+
+			switch (res->action) {
+			case SHOW:
+				done = show_complete(&imsg, &ret);
+				break;
+			default:
+				done = 1;
+				break;
+			}
+
+			imsg_free(&imsg);
+		}
+	}
+
+end:
+	return ret;
+}
+
+int
+ctl_noarg(struct parse_result *res, int argc, char **argv)
+{
+	if (argc != 1)
+		ctl_usage(res->ctl);
+	return ctlaction(res);
+}
+
+int
+ctl_add(struct parse_result *res, int argc, char **argv)
+{
+	if (argc < 2)
+		ctl_usage(res->ctl);
+	res->files = argv+1;
+	return ctlaction(res);
+}
+
+__dead void
+ctl(int argc, char **argv)
+{
+	struct sockaddr_un	 sun;
+	int			 ctl_sock;
+
+	log_init(1, LOG_DAEMON);
+	log_setverbose(verbose);
+
+	if ((ctl_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
+		fatal("socket");
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	strlcpy(sun.sun_path, csock, sizeof(sun.sun_path));
+
+	if (connect(ctl_sock, (struct sockaddr *)&sun, sizeof(sun)) == -1)
+		fatal("connect %s", csock);
+
+	ibuf = xmalloc(sizeof(*ibuf));
+	imsg_init(ibuf, ctl_sock);
+
+	optreset = 1;
+	optind = 1;
+
+	exit(parse(argc, argv));
+}
blob - /dev/null
blob + 50b0a0e95ac84837dc120d024ba12c1206a04f4e (mode 644)
--- /dev/null
+++ log.c
@@ -0,0 +1,199 @@
+/*	$OpenBSD: log.c,v 1.1 2018/07/10 16:39:54 florian Exp $	*/
+
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <syslog.h>
+#include <errno.h>
+#include <time.h>
+
+#include "log.h"
+
+static int		 debug;
+static int		 verbose;
+static const char	*log_procname;
+
+void
+log_init(int n_debug, int facility)
+{
+	extern char	*__progname;
+
+	debug = n_debug;
+	verbose = n_debug;
+	log_procinit(__progname);
+
+	if (!debug)
+		openlog(__progname, LOG_PID | LOG_NDELAY, facility);
+
+	tzset();
+}
+
+void
+log_procinit(const char *procname)
+{
+	if (procname != NULL)
+		log_procname = procname;
+}
+
+void
+log_setverbose(int v)
+{
+	verbose = v;
+}
+
+int
+log_getverbose(void)
+{
+	return (verbose);
+}
+
+void
+logit(int pri, const char *fmt, ...)
+{
+	va_list	ap;
+
+	va_start(ap, fmt);
+	vlog(pri, fmt, ap);
+	va_end(ap);
+}
+
+void
+vlog(int pri, const char *fmt, va_list ap)
+{
+	char	*nfmt;
+	int	 saved_errno = errno;
+
+	if (debug) {
+		/* best effort in out of mem situations */
+		if (asprintf(&nfmt, "%s\n", fmt) == -1) {
+			vfprintf(stderr, fmt, ap);
+			fprintf(stderr, "\n");
+		} else {
+			vfprintf(stderr, nfmt, ap);
+			free(nfmt);
+		}
+		fflush(stderr);
+	} else
+		vsyslog(pri, fmt, ap);
+
+	errno = saved_errno;
+}
+
+void
+log_warn(const char *emsg, ...)
+{
+	char		*nfmt;
+	va_list		 ap;
+	int		 saved_errno = errno;
+
+	/* best effort to even work in out of memory situations */
+	if (emsg == NULL)
+		logit(LOG_ERR, "%s", strerror(saved_errno));
+	else {
+		va_start(ap, emsg);
+
+		if (asprintf(&nfmt, "%s: %s", emsg,
+		    strerror(saved_errno)) == -1) {
+			/* we tried it... */
+			vlog(LOG_ERR, emsg, ap);
+			logit(LOG_ERR, "%s", strerror(saved_errno));
+		} else {
+			vlog(LOG_ERR, nfmt, ap);
+			free(nfmt);
+		}
+		va_end(ap);
+	}
+
+	errno = saved_errno;
+}
+
+void
+log_warnx(const char *emsg, ...)
+{
+	va_list	 ap;
+
+	va_start(ap, emsg);
+	vlog(LOG_ERR, emsg, ap);
+	va_end(ap);
+}
+
+void
+log_info(const char *emsg, ...)
+{
+	va_list	 ap;
+
+	va_start(ap, emsg);
+	vlog(LOG_INFO, emsg, ap);
+	va_end(ap);
+}
+
+void
+log_debug(const char *emsg, ...)
+{
+	va_list	 ap;
+
+	if (verbose) {
+		va_start(ap, emsg);
+		vlog(LOG_DEBUG, emsg, ap);
+		va_end(ap);
+	}
+}
+
+static void
+vfatalc(int code, const char *emsg, va_list ap)
+{
+	static char	s[BUFSIZ];
+	const char	*sep;
+
+	if (emsg != NULL) {
+		(void)vsnprintf(s, sizeof(s), emsg, ap);
+		sep = ": ";
+	} else {
+		s[0] = '\0';
+		sep = "";
+	}
+	if (code)
+		logit(LOG_CRIT, "fatal in %s: %s%s%s",
+		    log_procname, s, sep, strerror(code));
+	else
+		logit(LOG_CRIT, "fatal in %s%s%s", log_procname, sep, s);
+}
+
+void
+fatal(const char *emsg, ...)
+{
+	va_list	ap;
+
+	va_start(ap, emsg);
+	vfatalc(errno, emsg, ap);
+	va_end(ap);
+	exit(1);
+}
+
+void
+fatalx(const char *emsg, ...)
+{
+	va_list	ap;
+
+	va_start(ap, emsg);
+	vfatalc(0, emsg, ap);
+	va_end(ap);
+	exit(1);
+}
blob - /dev/null
blob + 0fa046fc3afbe8c893d40f492481a4d3055dd2bc (mode 644)
--- /dev/null
+++ log.h
@@ -0,0 +1,45 @@
+/*	$OpenBSD: log.h,v 1.2 2021/12/13 18:28:40 deraadt Exp $ */
+
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include <stdarg.h>
+
+void	log_init(int, int);
+void	log_procinit(const char *);
+void	log_setverbose(int);
+int	log_getverbose(void);
+void	log_warn(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_warnx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_info(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_debug(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	logit(int, const char *, ...)
+	    __attribute__((__format__ (printf, 2, 3)));
+void	vlog(int, const char *, va_list)
+	    __attribute__((__format__ (printf, 2, 0)));
+__dead void fatal(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+__dead void fatalx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+
+#endif /* LOG_H */
blob - /dev/null
blob + ddc342051acb4a5567bd9e2f630bfc28d0c4e9d0 (mode 644)
--- /dev/null
+++ player.c
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+
+#include <limits.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <event.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <sndio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <imsg.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include "amused.h"
+#include "log.h"
+#include "xmalloc.h"
+
+static struct imsgbuf *ibuf;
+
+static int nextfd = -1;
+static char nextpath[PATH_MAX];
+
+volatile sig_atomic_t halted;
+
+static void
+player_signal_handler(int signo)
+{
+	halted = 1;
+}
+
+static void
+audio_init(void)
+{
+	struct sio_par par;
+
+	if ((hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0)) == NULL)
+		fatal("sio_open");
+
+	sio_initpar(&par);
+	par.bits = 16;
+	par.appbufsz = 50 * par.rate / 1000;
+	par.pchan = 2;
+	if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par))
+		fatal("couldn't set audio params");
+	if (par.bits != 16 || par.le != SIO_LE_NATIVE)
+		fatalx("unsupported audio params");
+	if (!sio_start(hdl))
+		fatal("sio_start");
+}
+
+int
+player_setrate(int rate)
+{
+	struct sio_par par;
+
+	log_debug("switching to sample rate %d", rate);
+
+	sio_stop(hdl);
+
+	sio_initpar(&par);
+	par.rate = rate;
+	if (!sio_setpar(hdl, &par) || !sio_getpar(hdl, &par)) {
+		log_warnx("invalid params");
+		return -1;
+	}
+
+	/* TODO: check the sample rate? */
+
+	if (!sio_start(hdl)) {
+		log_warn("sio_start");
+		return -1;
+	}
+	return 0;
+}
+
+int
+player_pendingimsg(void)
+{
+	struct pollfd pfd;
+	int r;
+
+	if (halted != 0)
+		return 1;
+
+	pfd.fd = ibuf->fd;
+	pfd.revents = POLLIN;
+
+	r = poll(&pfd, 1, 0);
+	if (r == -1)
+		fatal("poll");
+	return r;
+}
+
+void
+player_enqueue(struct imsg *imsg)
+{
+	size_t datalen;
+
+	if (nextfd != -1)
+		fatalx("track already enqueued");
+
+	datalen = IMSG_DATA_SIZE(*imsg);
+	if (datalen != sizeof(nextpath))
+		fatalx("%s: size mismatch", __func__);
+	memcpy(nextpath, imsg->data, sizeof(nextpath));
+	if (nextpath[datalen-1] != '\0')
+		fatalx("%s: corrupted path", __func__);
+	if ((nextfd = imsg->fd) == -1)
+		fatalx("%s: got invalid file descriptor", __func__);
+	log_debug("enqueued %s", nextpath);
+}
+
+/* process only one message */
+int
+player_dispatch(void)
+{
+	struct imsg	imsg;
+	ssize_t		n;
+	int		ret;
+
+	if (halted != 0)
+		return IMSG_STOP;
+
+	if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN)
+		fatalx("imsg_read");
+	if (n == 0)
+		fatalx("pipe closed");
+
+	if ((n = imsg_get(ibuf, &imsg)) == -1)
+		fatal("imsg_get");
+	if (n == 0) /* no more messages */
+		fatalx("expected at least a message");
+
+	ret = imsg.hdr.type;
+	switch (imsg.hdr.type) {
+	case IMSG_PLAY:
+		player_enqueue(&imsg);
+		ret = IMSG_STOP;
+		break;
+	case IMSG_RESUME:
+	case IMSG_PAUSE:
+	case IMSG_STOP:
+		break;
+	default:
+		fatalx("unknown imsg %d", imsg.hdr.type);
+	}
+
+	return ret;
+}
+
+void
+player_senderr(void)
+{
+	imsg_compose(ibuf, IMSG_ERR, 0, 0, -1, NULL, 0);
+	imsg_flush(ibuf);
+}
+
+void
+player_sendeof(void)
+{
+	imsg_compose(ibuf, IMSG_EOF, 0, 0, -1, NULL, 0);
+	imsg_flush(ibuf);
+}
+
+void
+player_playnext(void)
+{
+	int fd = nextfd;
+
+	assert(nextfd != -1);
+	nextfd = -1;
+
+	/* XXX: use magic(5) for this, not file extensions */
+	if (strstr(nextpath, ".ogg") != NULL)
+		play_oggvorbis(fd);
+	else if (strstr(nextpath, ".mp3") != NULL)
+		play_mp3(fd);
+	else if (strstr(nextpath, ".flac") != NULL)
+		play_flac(fd);
+	else if (strstr(nextpath, ".opus") != NULL)
+		play_opus(fd);
+	else {
+		log_warnx("unknown file type for %s", nextpath);
+		player_senderr();
+		return;
+	}
+
+	player_sendeof();
+}
+
+int
+player_pause(void)
+{
+	int r;
+
+	r = player_dispatch();
+	return r == IMSG_RESUME;
+}
+
+int
+player_shouldstop(void)
+{
+	if (!player_pendingimsg())
+		return 0;
+
+	switch (player_dispatch()) {
+	case IMSG_PAUSE:
+		if (player_pause())
+			break;
+		/* fallthrough */
+	case IMSG_STOP:
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+player(int debug, int verbose)
+{
+	int flags;
+	log_init(debug, LOG_DAEMON);
+	log_setverbose(verbose);
+
+	setproctitle("player");
+	log_procinit("player");
+
+#if 0
+	{
+		static int attached;
+
+		while (!attached)
+			sleep(1);
+	}
+#endif
+
+
+	audio_init();
+
+	/* mark fd as blocking i/o mode */
+	if ((flags = fcntl(3, F_GETFL)) == -1)
+		fatal("fcntl(F_GETFL)");
+	if (fcntl(3, F_SETFL, flags & ~O_NONBLOCK) == -1)
+		fatal("fcntl F_SETFL O_NONBLOCK");
+
+	ibuf = xmalloc(sizeof(*ibuf));
+	imsg_init(ibuf, 3);
+
+	signal(SIGINT, player_signal_handler);
+	signal(SIGTERM, player_signal_handler);
+
+	signal(SIGHUP, SIG_IGN);
+	signal(SIGPIPE, SIG_IGN);
+
+	if (pledge("stdio recvfd", NULL) == -1)
+		fatal("pledge");
+
+	while (!halted) {
+		while (nextfd == -1)
+			assert(player_dispatch() == IMSG_STOP);
+
+		player_playnext();
+	}
+
+	return 0;
+}
blob - /dev/null
blob + ce442b965abad7a55c1e19642875989337955088 (mode 644)
--- /dev/null
+++ player_flac.c
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+
+#include <err.h>
+#include <event.h>
+#include <inttypes.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <imsg.h>
+
+#include <FLAC/stream_decoder.h>
+
+#include "amused.h"
+
+static FLAC__StreamDecoderWriteStatus
+writecb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame,
+    const int32_t * const *buffer, void *data)
+{
+	static uint8_t buf[BUFSIZ];
+	int i;
+	size_t len;
+
+	if (player_shouldstop())
+		return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
+
+	for (i = 0, len = 0; i < frame->header.blocksize; ++i) {
+		if (len+4 >= sizeof(buf)) {
+			sio_write(hdl, buf, len);
+			len = 0;
+		}
+
+		buf[len++] = buffer[0][i] & 0xff;
+		buf[len++] = (buffer[0][i] >> 8) & 0xff;
+
+		buf[len++] = buffer[1][i] & 0xff;
+		buf[len++] = (buffer[1][i] >> 8) & 0xff;
+	}
+
+	if (len != 0)
+		sio_write(hdl, buf, len);
+
+	return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
+}
+
+static void
+metacb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *meta,
+    void *d)
+{
+	uint32_t sample_rate;
+	struct sio_par par;
+
+	if (meta->type == FLAC__METADATA_TYPE_STREAMINFO) {
+		sample_rate = meta->data.stream_info.sample_rate;
+
+		printf("sample rate: %d\n", sample_rate);
+		printf("channels: %d\n", meta->data.stream_info.channels);
+		printf("bps: %d\n", meta->data.stream_info.bits_per_sample);
+		printf("total samples: %"PRIu64"\n", meta->data.stream_info.total_samples);
+
+		if (player_setrate(sample_rate) == -1)
+			err(1, "player_setrate");
+
+		sio_stop(hdl);
+
+		sio_initpar(&par);
+		par.rate = sample_rate;
+		if (!sio_setpar(hdl, &par))
+			err(1, "sio_setpar");
+		if (!sio_getpar(hdl, &par))
+			err(1, "sio_getpar");
+		/* TODO: check that there is a sane sample rate? */
+		if (!sio_start(hdl))
+			err(1, "sio_start");
+	}
+}
+
+static void
+errcb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status,
+    void *data)
+{
+	warnx("error: %s", FLAC__StreamDecoderErrorStatusString[status]);
+}
+
+void
+play_flac(int fd)
+{
+	FILE *f;
+	int ok = 1;
+	FLAC__StreamDecoder *decoder = NULL;
+	FLAC__StreamDecoderInitStatus init_status;
+
+	if ((f = fdopen(fd, "r")) == NULL)
+		err(1, "fdopen");
+
+	decoder = FLAC__stream_decoder_new();
+	if (decoder == NULL)
+		err(1, "flac stream decoder");
+
+	FLAC__stream_decoder_set_md5_checking(decoder, 1);
+
+	init_status = FLAC__stream_decoder_init_FILE(decoder, f, writecb,
+	    metacb, errcb, NULL);
+	if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK)
+		errx(1, "flac decoder: %s",
+		    FLAC__StreamDecoderInitStatusString[init_status]);
+
+	ok = FLAC__stream_decoder_process_until_end_of_stream(decoder);
+	warnx("decoding %s", ok ? "succeeded" : "failed");
+	warnx("state: %s",
+	    FLAC__StreamDecoderStateString[FLAC__stream_decoder_get_state(decoder)]);
+
+	FLAC__stream_decoder_delete(decoder);
+}
blob - /dev/null
blob + 5dda0c58eb3755eae21ae708c60579876220804e (mode 644)
--- /dev/null
+++ player_mad.c
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+
+#include <err.h>
+#include <event.h>
+#include <fcntl.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <imsg.h>
+#include <unistd.h>
+
+#include <mad.h>
+
+#include "amused.h"
+
+struct mad_stream mad_stream;
+struct mad_frame mad_frame;
+struct mad_synth mad_synth;
+
+struct buffer {
+	const void *start;
+	size_t length;
+	int sample_rate;
+};
+
+static enum mad_flow
+input(void *d, struct mad_stream *stream)
+{
+	struct buffer *buffer = d;
+
+	if (buffer->length == 0)
+		return MAD_FLOW_STOP;
+
+	printf("decode time!  start=%p, len=%zu\n", buffer->start, buffer->length);
+	mad_stream_buffer(stream, buffer->start, buffer->length);
+	buffer->length = 0;
+	buffer->sample_rate = 0;
+	return MAD_FLOW_CONTINUE;
+}
+
+/* scale a mad sample to 16 bits */
+static inline int
+scale(mad_fixed_t sample)
+{
+	/* round */
+	sample += (1L << (MAD_F_FRACBITS - 16));
+
+	/* clip */
+	if (sample >= MAD_F_ONE)
+		sample = MAD_F_ONE - 1;
+	else if (sample < -MAD_F_ONE)
+		sample -= MAD_F_ONE;
+
+	/* quantize */
+	return sample >> (MAD_F_FRACBITS + 1 - 16);
+}
+
+static enum mad_flow
+output(void *data, const struct mad_header *header, struct mad_pcm *pcm)
+{
+	static uint8_t buf[BUFSIZ];
+	size_t len;
+	struct buffer *buffer = data;
+	int nsamples, i;
+	uint16_t sample;
+	const mad_fixed_t *leftch, *rightch;
+
+	if (player_shouldstop())
+		return MAD_FLOW_STOP;
+
+	nsamples = pcm->length;
+	leftch = pcm->samples[0];
+	rightch = pcm->samples[1];
+
+	if (buffer->sample_rate != pcm->samplerate) {
+		buffer->sample_rate = pcm->samplerate;
+		if (player_setrate(pcm->samplerate) == -1)
+			err(1, "player_setrate");
+	}
+
+	if (pcm->channels != 2) {
+		printf("mono not supported!\n");
+		return MAD_FLOW_STOP;
+	}
+
+	for (i = 0, len = 0; i < nsamples; ++i) {
+		if (len+4 >= sizeof(buf)) {
+			sio_write(hdl, buf, len);
+			/* fwrite(buf, 1, len, stdout); */
+			len = 0;
+		}
+
+		sample = scale(*leftch++);
+		buf[len++] = sample & 0xff;
+		buf[len++] = (sample >> 8) & 0xff;
+
+		sample = scale(*rightch++);
+		buf[len++] = sample & 0xff;
+		buf[len++] = (sample >> 8) & 0xff;
+	}
+
+	if (len != 0)
+		/* fwrite(buf, 1, len, stdout); */
+		sio_write(hdl, buf, len);
+
+	return MAD_FLOW_CONTINUE;
+}
+
+static enum mad_flow
+error(void *d, struct mad_stream *stream, struct mad_frame *frame)
+{
+	struct buffer *buffer = d;
+
+	warnx("decoding error 0x%04x (%s) at byte offset %zu",
+	    stream->error, mad_stream_errorstr(stream),
+	    stream->this_frame - (const unsigned char *)buffer->start);
+
+	return MAD_FLOW_CONTINUE;
+}
+
+static int
+decode(void *m, size_t len)
+{
+	struct buffer buffer;
+	struct mad_decoder decoder;
+	int result;
+
+	/* initialize our private message structure; */
+	buffer.start = m;
+	buffer.length = len;
+
+	/* configure input, output and error functions */
+	mad_decoder_init(&decoder, &buffer, input, 0 /* header */,
+	    0 /* filter */, output, error, 0 /* message */);
+
+	/* start decoding */
+	result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
+
+	/* release the decoder */
+	mad_decoder_finish(&decoder);
+
+	return result;
+}
+
+void
+play_mp3(int fd)
+{
+	struct stat stat;
+	void *m;
+
+	if (fstat(fd, &stat) == -1)
+		err(1, "fstat");
+	warnx("file size %lld", stat.st_size);
+
+	m = mmap(NULL, stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+	if (m == MAP_FAILED)
+		err(1, "mmap");
+
+	decode(m, stat.st_size);
+	munmap(m, stat.st_size);
+}
blob - /dev/null
blob + 2726b9419c7466e3933d2076a20fa4bc9af8cce9 (mode 644)
--- /dev/null
+++ player_oggvorbis.c
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+
+#include <err.h>
+#include <event.h>
+#include <fcntl.h>
+#include <math.h>
+#include <inttypes.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <imsg.h>
+#include <unistd.h>
+
+#include <vorbis/codec.h>
+#include <vorbis/vorbisfile.h>
+
+#include "amused.h"
+
+#ifndef nitems
+#define nitems(x) (sizeof(x)/sizeof(x[0]))
+#endif
+
+void
+play_oggvorbis(int fd)
+{
+	static uint8_t pcmout[4096];
+	FILE *f;
+	OggVorbis_File vf;
+	int current_section, eof = 0;
+
+	if ((f = fdopen(fd, "r")) == NULL)
+		err(1, "fdopen");
+
+	if (ov_open_callbacks(f, &vf, NULL, 0, OV_CALLBACKS_NOCLOSE) < 0)
+		errx(1, "input is not an Ogg bitstream");
+
+	{
+		char **ptr;
+		vorbis_info *vi;
+
+		vi = ov_info(&vf, -1);
+		for (ptr = ov_comment(&vf, -1)->user_comments; *ptr; ++ptr)
+			printf("%s\n", *ptr);
+
+		printf("bitstream is %d channel, %ldHz\n", vi->channels, vi->rate);
+
+		if (player_setrate(vi->rate) == -1)
+			err(1, "player_setrate");
+	}
+
+	while (!eof) {
+		long ret;
+
+		if (player_shouldstop())
+			break;
+
+		ret = ov_read(&vf, pcmout, sizeof(pcmout), 0, 2, 1,
+		    &current_section);
+		if (ret == 0)
+			eof = 1;
+		else if (ret < 0)
+			warnx("non-fatal error in the stream %ld", ret);
+		else {
+			/* TODO: deal with sample rate changes */
+			sio_write(hdl, pcmout, ret);
+		}
+	}
+
+	ov_clear(&vf);
+}
blob - /dev/null
blob + 43812e46cda8499ed6cdd447707100c0396ddc55 (mode 644)
--- /dev/null
+++ player_opus.c
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/uio.h>
+
+#include <err.h>
+#include <event.h>
+#include <fcntl.h>
+#include <math.h>
+#include <inttypes.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <imsg.h>
+#include <unistd.h>
+
+#include <opusfile.h>
+
+#include "amused.h"
+
+#ifndef nitems
+#define nitems(x) (sizeof(x)/sizeof(x[0]))
+#endif
+
+void
+play_opus(int fd)
+{
+	static uint16_t pcm[BUFSIZ];
+	static uint8_t out[BUFSIZ * 2];
+	OggOpusFile *of;
+	void *f;
+	int ret;
+	OpusFileCallbacks cb = {NULL, NULL, NULL, NULL};
+	int i, li, prev_li = -1;
+
+	if ((f = op_fdopen(&cb, fd, "r")) == NULL)
+		err(1, "fdopen");
+
+	of = op_open_callbacks(f, &cb, NULL, 0, &ret);
+	if (of == NULL)
+		errx(1, "failed open opus file");
+
+	for (;;) {
+		if (player_shouldstop())
+			break;
+
+		ret = op_read_stereo(of, pcm, nitems(pcm));
+		if (ret == OP_HOLE) /* corrupt file segment? */
+			continue;
+		if (ret < 0)
+			errx(1, "error decoding file (%d)", ret);
+		if (ret == 0)
+			break; /* eof */
+
+		li = op_current_link(of);
+		if (li != prev_li) {
+			const OpusHead *head;
+
+			prev_li = li;
+
+			head = op_head(of, li);
+			if (head->input_sample_rate) {
+				if (player_setrate(head->input_sample_rate)
+				    == -1)
+					err(1, "player_setrate");
+			}
+		}
+
+		for (i = 0; i < 2*ret; ++i) {
+			out[2*i+0] = pcm[i] & 0xFF;
+			out[2*i+1] = (pcm[i] >> 8) & 0xFF;
+		}
+		sio_write(hdl, out, 4*ret);
+	}
+
+	op_free(of);
+}
blob - /dev/null
blob + 46a6ac4889b64d4b782b42e6fe09adf838705cd4 (mode 644)
--- /dev/null
+++ playlist.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include <stdlib.h>
+#include <syslog.h>
+
+#include "log.h"
+#include "xmalloc.h"
+#include "playlist.h"
+
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+struct playlist	playlist;
+enum play_state	play_state;
+int		repeat_one;
+int		repeat_all = 1;
+ssize_t		play_off = -1;
+
+void
+playlist_enqueue(const char *path)
+{
+	size_t newcap;
+
+	if (playlist.len == playlist.cap) {
+		newcap = MAX(16, playlist.cap * 1.5);
+		playlist.songs = xrecallocarray(playlist.songs, playlist.cap,
+		    newcap, sizeof(*playlist.songs));
+		playlist.cap = newcap;
+	}
+
+	playlist.songs[playlist.len++] = xstrdup(path);
+}
+
+const char *
+playlist_current(void)
+{
+	if (playlist.len == 0 || play_off == -1)
+		return NULL;
+
+	return playlist.songs[play_off];
+}
+
+const char *
+playlist_advance(void)
+{
+	if (playlist.len == 0 || play_off+1 == playlist.len)
+		return NULL;
+
+	play_off++;
+	if (play_off == playlist.len) {
+		if (repeat_all)
+			play_off = 0;
+		else {
+			play_state = STATE_STOPPED;
+			play_off = -1;
+			return NULL;
+		}
+	}
+
+	play_state = STATE_PLAYING;
+	return playlist.songs[play_off];
+}
+
+void
+playlist_reset(void)
+{
+	play_off = -1;
+}
+
+void
+playlist_truncate(void)
+{
+	size_t i;
+
+	for (i = 0; i < playlist.len; ++i)
+		free(playlist.songs[i]);
+	free(playlist.songs);
+	playlist.songs = NULL;
+
+	playlist.len = 0;
+	playlist.cap = 0;
+	play_off = -1;
+}
blob - /dev/null
blob + 3b2e2ed26d78dceb8e3fa7c637cb95218fedc322 (mode 644)
--- /dev/null
+++ playlist.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022 Omar Polo <op@openbsd.org>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#ifndef PLAYLIST_H
+#define PLAYLIST_H
+
+struct playlist {
+	size_t	 len;
+	size_t	 cap;
+	char	**songs;
+};
+
+enum play_state {
+	STATE_STOPPED,
+	STATE_PLAYING,
+	STATE_PAUSED,
+};
+
+extern struct playlist	playlist;
+
+extern enum play_state	 play_state;
+extern int		 repeat_one;
+extern int		 repeat_all;
+extern ssize_t		 play_off;
+
+void			 playlist_enqueue(const char *);
+const char		*playlist_current(void);
+const char		*playlist_advance(void);
+void			 playlist_reset(void);
+void			 playlist_truncate(void);
+
+#endif
blob - /dev/null
blob + f05ceadf51129ffe6559594eeea12fa517db5f69 (mode 644)
--- /dev/null
+++ xmalloc.c
@@ -0,0 +1,100 @@
+/* $OpenBSD: xmalloc.c,v 1.4 2019/06/28 05:44:09 deraadt Exp $ */
+/*
+ * Author: Tatu Ylonen <ylo@cs.hut.fi>
+ * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
+ *                    All rights reserved
+ * Versions of malloc and friends that check their results, and never return
+ * failure (they call fatal if they encounter an error).
+ *
+ * As far as I am concerned, the code I have written for this software
+ * can be used freely for any purpose.  Any derived versions of this
+ * software must be clearly marked as such, and if the derived work is
+ * incompatible with the protocol description in the RFC file, it must be
+ * called by a name other than "ssh" or "Secure Shell".
+ */
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+
+#include "log.h"
+#include "xmalloc.h"
+
+void *
+xmalloc(size_t size)
+{
+	void *ptr;
+
+	if (size == 0)
+		fatal("xmalloc: zero size");
+	ptr = malloc(size);
+	if (ptr == NULL)
+		fatal("xmalloc: allocating %zu bytes", size);
+	return ptr;
+}
+
+void *
+xcalloc(size_t nmemb, size_t size)
+{
+	void *ptr;
+
+	if (size == 0 || nmemb == 0)
+		fatal("xcalloc: zero size");
+	ptr = calloc(nmemb, size);
+	if (ptr == NULL)
+		fatal("xcalloc: allocating %zu * %zu bytes", nmemb, size);
+	return ptr;
+}
+
+void *
+xreallocarray(void *ptr, size_t nmemb, size_t size)
+{
+	void *new_ptr;
+
+	new_ptr = reallocarray(ptr, nmemb, size);
+	if (new_ptr == NULL)
+		fatal("xreallocarray: allocating %zu * %zu bytes",
+		    nmemb, size);
+	return new_ptr;
+}
+
+void *
+xrecallocarray(void *ptr, size_t oldnmemb, size_t nmemb, size_t size)
+{
+	void *new_ptr;
+
+	new_ptr = recallocarray(ptr, oldnmemb, nmemb, size);
+	if (new_ptr == NULL)
+		fatal("xrecallocarray: allocating %zu * %zu bytes",
+		    nmemb, size);
+	return new_ptr;
+}
+
+char *
+xstrdup(const char *str)
+{
+	char *cp;
+
+	if ((cp = strdup(str)) == NULL)
+		fatal("xstrdup");
+	return cp;
+}
+
+int
+xasprintf(char **ret, const char *fmt, ...)
+{
+	va_list ap;
+	int i;
+
+	va_start(ap, fmt);
+	i = vasprintf(ret, fmt, ap);
+	va_end(ap);
+
+	if (i == -1)
+		fatal("xasprintf");
+
+	return i;
+}
blob - /dev/null
blob + 5e11cb4e79d681af3c5b8787d9308302336a092f (mode 644)
--- /dev/null
+++ xmalloc.h
@@ -0,0 +1,31 @@
+/* $OpenBSD: xmalloc.h,v 1.3 2015/11/17 18:25:03 tobias Exp $ */
+
+/*
+ * Author: Tatu Ylonen <ylo@cs.hut.fi>
+ * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
+ *                    All rights reserved
+ * Created: Mon Mar 20 22:09:17 1995 ylo
+ *
+ * Versions of malloc and friends that check their results, and never return
+ * failure (they call fatal if they encounter an error).
+ *
+ * As far as I am concerned, the code I have written for this software
+ * can be used freely for any purpose.  Any derived versions of this
+ * software must be clearly marked as such, and if the derived work is
+ * incompatible with the protocol description in the RFC file, it must be
+ * called by a name other than "ssh" or "Secure Shell".
+ */
+
+#ifndef XMALLOC_H
+#define XMALLOC_H
+
+void	*xmalloc(size_t);
+void	*xcalloc(size_t, size_t);
+void	*xreallocarray(void *, size_t, size_t);
+void	*xrecallocarray(void *, size_t, size_t, size_t);
+char	*xstrdup(const char *);
+int	 xasprintf(char **, const char *, ...)
+                __attribute__((__format__ (printf, 2, 3)))
+                __attribute__((__nonnull__ (2)));
+
+#endif	/* XMALLOC_H */