commit 2deb6d14d8d3ae89e0c8acb4f51009b9cd99fe7c
Author: lostd <lostd@2f30.org>
Date:   Thu,  2 Jun 2016 12:12:24 +0100
Initial import
Diffstat:
| A | Makefile | | | 11 | +++++++++++ | 
| A | README | | | 44 | ++++++++++++++++++++++++++++++++++++++++++++ | 
| A | ncmixer.c | | | 314 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
3 files changed, 369 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -0,0 +1,11 @@
+CPPFLAGS = -DDEBUG
+LDLIBS = -lsndio -lcurses
+OBJ = ncmixer.o
+BIN = ncmixer
+
+all: $(BIN)
+
+clean:
+	rm -f $(OBJ) $(BIN)
+
+.PHONY: all clean
diff --git a/README b/README
@@ -0,0 +1,44 @@
+visual:
+
+   /tmp/ch0=          /tmp/ch1=
+   state: play        state: idle
+   monitor: off       monitor: on
+
+   |||------------+-------------- 
+
+   speed: 10
+
+movement:
+
+    h,l -- move crossfader left/right at set speed
+    j,k -- decrease/increase speed by one second
+    H,L -- snap to leftmost, center, rightmost positions
+    F   -- start auto crossfading at set speed
+
+monitor control:
+
+    1   -- monitor plays channel 1
+    2   -- monitor plays channel 2
+    3   -- monitor plays both
+
+Speed is measured in seconds it takes to do a full crossfade from one side to
+the other.  Movement keys stop auto crossfade and speed changes affect it.
+
+caveats:
+
+The correct way to do movement is to get keyboard press and release events,
+and move the crossfader at the set speed in the meantime.  We emulate this
+assuming the default auto-repeat settings for X: a delay of 660ms and a rate
+of 25Hz:
+
+    xset q | grep repeat
+
+initial gap: 660ms
+subsequent gaps: 1000 / 25 = 40ms
+state machine:
+
+if current key is right:
+    if previous key is not right:
+        start crossfade to right for 660ms
+    if previous key is right and still crossfading:
+        continue crossfade to right for another 40ms
diff --git a/ncmixer.c b/ncmixer.c
@@ -0,0 +1,314 @@
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <curses.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <sndio.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define LEN(x) (sizeof (x) / sizeof *(x))
+#undef MIN
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#undef MAX
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+#define SA struct sockaddr
+#define CH0_NAME "ch0"
+#define CH1_NAME "ch1"
+#define FPS 25
+#define BITS 16
+#define RATE 44100
+#define CHANS 2
+#define NSAMPLES 2048
+
+#ifdef DEBUG
+#define DEBUG_FD 8
+#define DPRINTF(x...) dprintf(DEBUG_FD, x)
+#define DPRINTF_D(x) dprintf(DEBUG_FD, #x "=%d\n", x)
+#define DPRINTF_F(x) dprintf(DEBUG_FD, #x "=%0.2f\n", x)
+#else
+#define DPRINTF
+#define DPRINTF_D(x)
+#define DPRINTF_F(x)
+#endif /* DEBUG */
+
+/* mixer state */
+float xfpos = 0.; /* values in [-1.0, 1.0] */
+int speed = 10;
+int autoxf = 1;
+
+struct sio_hdl *sio_hdl;
+
+/* input pcm data on channel 0 */
+static unsigned short ch0_buf[NSAMPLES];
+static unsigned ch0_nsamples;
+/* input pcm data on channel 1 */
+static unsigned short ch1_buf[NSAMPLES];
+static unsigned ch1_nsamples;
+/* output pcm data */
+static unsigned short out_buf[NSAMPLES];
+static unsigned out_nsamples;
+
+int
+audio_open(void)
+{
+	struct sio_par par;
+
+	sio_hdl = sio_open(SIO_DEVANY, SIO_PLAY, 0);
+	if (sio_hdl == NULL) {
+		warnx("sio_open: failed");
+		return -1;
+	}
+
+	sio_initpar(&par);
+	par.bits = BITS;
+	par.rate = RATE;
+	par.pchan = CHANS;
+	par.sig = 1;
+	par.le = SIO_LE_NATIVE;
+
+	if (sio_setpar(sio_hdl, &par) == 0 ||
+	    sio_getpar(sio_hdl, &par) == 0) {
+		warnx("sio_{set,get}par: failed");
+		goto err0;
+	}
+
+	if (par.bits  != BITS ||
+	    par.rate  != RATE ||
+	    par.pchan != CHANS ||
+	    par.le    != SIO_LE_NATIVE ||
+	    par.sig   != 1) {
+		warnx("unsupported audio params");
+		goto err0;
+	}
+
+	if (sio_start(sio_hdl) == 0) {
+		warnx("sio_start: failed");
+		goto err0;
+	}
+
+	return 0;
+err0:
+	sio_close(sio_hdl);
+	sio_hdl = NULL;
+	return -1;
+}
+
+int
+audio_play(void *buf, int n)
+{
+	return sio_write(sio_hdl, buf, n);
+}
+
+void
+audio_close(void)
+{
+	if (sio_hdl != NULL)
+		sio_close(sio_hdl);
+	sio_hdl = NULL;
+}
+
+/* XXX: curses input function */
+int
+key_cb(int fd)
+{
+	int c;
+
+	c = getch();
+	DPRINTF_D(c);
+	return 0;
+}
+
+int
+ch0_cb(int fd)
+{
+	int n;
+
+	memset(ch0_buf, 0, sizeof(ch0_buf));
+	n = read(fd, ch0_buf, sizeof(ch0_buf));
+	if (n == 0 || n == -1)
+		return -1;
+	ch0_nsamples = n / 2;
+	return 0;
+}
+
+int
+ch1_cb(int fd)
+{
+	int n;
+
+	memset(ch1_buf, 0, sizeof(ch1_buf));
+	n = read(fd, ch1_buf, sizeof(ch1_buf));
+	if (n == 0 || n == -1)
+		return -1;
+	ch1_nsamples = n / 2;
+	return 0;
+}
+
+/* XXX: attenuate channel 0 and channel 1 based on cross-fader */
+void
+attenuate(void)
+{
+}
+
+/* mix channel 0 with channel 1 */
+void
+mix(void)
+{
+	short *ch0 = (short *)ch0_buf;
+	short *ch1 = (short *)ch1_buf;
+	short *out = (short *)out_buf;
+	int i;
+
+	memset(out_buf, 0, sizeof(out_buf));
+	out_nsamples = MAX(ch0_nsamples, ch1_nsamples);
+	for (i = 0; i < out_nsamples; i++) {
+		*out = *ch0 + *ch1;
+		*out /= 2;
+		ch0++, ch1++, out++;
+	}
+}
+
+int
+server_listen(char *name)
+{
+	struct sockaddr_un sun;
+	socklen_t len;
+	int fd;
+
+	fd = socket(AF_UNIX, SOCK_STREAM, 0);
+	if (fd == -1)
+		err(1, "socket");
+	unlink(name);
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	strlcpy(sun.sun_path, name, sizeof(sun.sun_path));
+	len = sizeof(sun);
+	if (bind(fd, (SA *)&sun, len) == -1)
+		err(1, "bind");
+	if (listen(fd, 5) == -1)
+		err(1, "listen");
+	return fd;
+}
+
+void
+loop(void)
+{
+	struct sockaddr_un sun;
+	socklen_t len;
+	struct pollfd pfd[5];
+	int i, nready;
+
+#define CH0_LISTEN 1
+#define CH1_LISTEN 2
+#define CH0_CLIENT 3
+#define CH1_CLIENT 4
+
+	pfd[0].fd = 0;
+	pfd[0].events = POLLIN;
+	pfd[CH0_LISTEN].fd = server_listen(CH0_NAME);
+	pfd[CH0_LISTEN].events = POLLIN;
+	pfd[CH1_LISTEN].fd = server_listen(CH1_NAME);
+	pfd[CH1_LISTEN].events = POLLIN;
+	pfd[CH0_CLIENT].fd = -1; /* ch0 source */
+	pfd[CH0_CLIENT].events = 0;
+	pfd[CH1_CLIENT].fd = -1; /* ch1 source */
+	pfd[CH1_CLIENT].events = 0;
+
+	for (;;) {
+		nready = poll(pfd, LEN(pfd), 1000 / FPS);
+		if (nready == -1)
+			err(1, "poll");
+		if (nready == 0)
+			continue; /* XXX: call ui refresh */
+
+		for (i = 0; i < LEN(pfd); i++)
+			if (pfd[i].revents & POLLERR)
+				errx(1, "bad fd");
+
+		if (pfd[0].revents & POLLIN)
+			key_cb(pfd[0].fd);
+
+		if (pfd[CH0_LISTEN].revents & POLLIN) {
+			len = sizeof(sun);
+			pfd[CH0_CLIENT].fd = accept(pfd[CH0_LISTEN].fd,
+			                            (SA *)&sun, &len);
+			if (pfd[CH0_CLIENT].fd == -1)
+				err(1, "accept");
+			pfd[CH0_CLIENT].events = POLLIN;
+			puts("channel 0 connected");
+		}
+
+		if (pfd[CH1_LISTEN].revents & POLLIN) {
+			len = sizeof(sun);
+			pfd[CH1_CLIENT].fd = accept(pfd[CH1_LISTEN].fd,
+			                            (SA *)&sun, &len);
+			if (pfd[CH1_CLIENT].fd == -1)
+				err(1, "accept");
+			pfd[CH1_CLIENT].events = POLLIN;
+			puts("channel 1 connected");
+		}
+
+		if (pfd[CH0_CLIENT].revents & (POLLIN | POLLHUP)) {
+			if (ch0_cb(pfd[CH0_CLIENT].fd) == -1) {
+				pfd[CH0_CLIENT].fd = -1;
+				pfd[CH0_CLIENT].events = 0;
+				puts("channel 0 disconnected");
+			}
+		}
+
+		if (pfd[CH1_CLIENT].revents & (POLLIN | POLLHUP)) {
+			if (ch1_cb(pfd[CH1_CLIENT].fd) == -1) {
+				pfd[CH1_CLIENT].fd = -1;
+				pfd[CH1_CLIENT].events = 0;
+				puts("channel 1 disconnected");
+			}
+		}
+
+		attenuate();
+		mix();
+		audio_play(out_buf, out_nsamples * 2);
+	}
+}
+
+void
+curses_init(void)
+{
+	char *term;
+
+	if (initscr() == NULL) {
+		term = getenv("TERM");
+		if (term != NULL)
+			errx(1, "error opening terminal: %s", term);
+		else
+			errx(1, "failed to initialize curses");
+	}
+	cbreak();
+	noecho();
+	nonl();
+	intrflush(stdscr, FALSE);
+	keypad(stdscr, TRUE);
+	curs_set(FALSE);
+}
+
+void
+curses_exit(void)
+{
+	endwin();
+}
+
+int
+main(void)
+{
+	curses_init();
+	audio_open();
+	loop();
+	audio_close();
+	curses_exit();
+	return 0;
+}