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;
+}