ncmixer

ncurses audio mixer for DJ'ing
git clone git://git.2f30.org/ncmixer.git
Log | Files | Refs | README | LICENSE

commit 2deb6d14d8d3ae89e0c8acb4f51009b9cd99fe7c
Author: lostd <lostd@2f30.org>
Date:   Thu,  2 Jun 2016 12:12:24 +0100

Initial import

Diffstat:
Makefile | 11+++++++++++
README | 44++++++++++++++++++++++++++++++++++++++++++++
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; +}