ncmixer

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

commit 771b8b9b697d38ae23d548c5a1e143981ea4c90e
parent 855e07f7b31a17f1da0aa95d39032a5d230722c9
Author: sin <sin@2f30.org>
Date:   Tue, 25 Oct 2016 18:11:46 +0100

Add initial implementation of shout output

To use the shout output invoke mixerd as follows:
mixerd shout://user:pass@host:port/mountpoint.mp3

Diffstat:
MMakefile | 5+++--
Mmixerd.c | 359++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
2 files changed, 321 insertions(+), 43 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,7 +1,8 @@ VERSION = 0.1 -CPPFLAGS = -DDEBUG +CPPFLAGS = -DDEBUG -I/usr/local/include CFLAGS = -Wall -g -LDLIBS = -lsndio -lcurses -lm +LDFLAGS = -L/usr/local/lib +LDLIBS = -lsndio -lcurses -lm -lshout -lmp3lame DISTFILES = arg.h mixerd.c ncmixerc.c proto.h LICENSE Makefile README patches NCMIXERC_OBJ = ncmixerc.o NCMIXERC_BIN = ncmixerc diff --git a/mixerd.c b/mixerd.c @@ -16,6 +16,9 @@ #include <syslog.h> #include <unistd.h> +#include <lame/lame.h> +#include <shout/shout.h> + #include "arg.h" #include "proto.h" @@ -28,10 +31,12 @@ #define CTL_SOCK_PATH "/tmp/mixerctl" #define CH0_SOCK_PATH "/tmp/mixer0" #define CH1_SOCK_PATH "/tmp/mixer1" -#define MASTER_DEV "snd/0" -#define MONITOR_DEV "rsnd/1" +#define MASTER_DEV "sndio://snd/0" +#define MONITOR_DEV "sndio://rsnd/1" +#define NOUTPUTS 2 #define BITS 16 #define BPS (BITS / 8) +#define BITRATE 320 /* for lame */ #define RATE 44100 #define CHANS 2 #define NSAMPLES 128 @@ -63,22 +68,40 @@ struct input { short buf[NSAMPLES]; short attenuated_buf[NSAMPLES]; int nsamples; - int mon_on; - float level; + int mon_on; /* whether input should be directed to monitor */ + float level; /* channel level in the internal [0.0, 1.0] */ } inputs[] = { { .listenfd = -1, .clifd = -1, .level = 1.0f }, { .listenfd = -1, .clifd = -1, .level = 1.0f } }; struct output { - char *name; - struct sio_hdl *sio_hdl; short buf[NSAMPLES]; int nsamples; + void *enc; /* lame handle, only useful for the shout output */ + void *hdl; /* shout/sndio handle */ + void *arg; /* shout/sndio args, see {shout,sndio}_arg below */ + int (*open)(struct output *); + int (*play)(struct output *); + void (*close)(struct output *); int (*mix)(struct output *, struct input *, struct input *); -} outputs[] = { - { .name = MASTER_DEV, .mix = mix_master }, - { .name = MONITOR_DEV, .mix = mix_monitor } +} outputs[NOUTPUTS]; + +struct sio_arg { + char *devname; +} sio_arg[NOUTPUTS]; + +struct shout_arg { + char *user; + char *pw; + char *host; + char *port; + char *mount; +} shout_arg[NOUTPUTS]; + +int (*mixers[NOUTPUTS])(struct output *, struct input *, struct input *) = { + mix_master, + mix_monitor }; void @@ -117,15 +140,200 @@ log_err(const char *msg, ...) exit(1); } -struct sio_hdl * -audio_open(char *devname) +/* initialize lame to be used with libshout */ +int +encoder_open(struct output *out) +{ + if ((out->enc = lame_init()) == NULL) { + log_msg(LOG_WARNING, "failed to initialize lame"); + return -1; + } + + if (lame_set_brate(out->enc, BITRATE) == -1) { + log_msg(LOG_WARNING, "failed to set lame bitrate"); + goto err0; + } + + if (lame_set_num_channels(out->enc, CHANS) == -1) { + log_msg(LOG_WARNING, "failed to set lame channels"); + goto err0; + } + + if (lame_set_in_samplerate(out->enc, RATE) == -1) { + log_msg(LOG_WARNING, "failed to set lame input sample rate"); + goto err0; + } + + if (lame_set_out_samplerate(out->enc, RATE) == -1) { + log_msg(LOG_WARNING, "failed to set lame output sample rate"); + goto err0; + } + + if (lame_init_params(out->enc) == -1) { + log_msg(LOG_WARNING, "failed to set lame params"); + goto err0; + } + + return 0; +err0: + lame_close(out->enc); + out->enc = NULL; + return -1; +} + +void +encoder_close(struct output *out) +{ + if (out->enc != NULL) { + lame_close(out->enc); + out->enc = NULL; + } +} + +int +shout_audio_open(struct output *out) +{ + struct shout_arg *shout_arg = out->arg; + shout_t *hdl; + char tmp[32]; + + if (encoder_open(out) == -1) + return -1; + + shout_init(); + + if ((hdl = shout_new()) == NULL) + goto err0; + + strlcpy(tmp, shout_arg->user, sizeof(tmp)); + if (shout_arg->user[0] == '\0') + strlcpy(tmp, "source", sizeof(tmp)); + if (shout_set_user(hdl, tmp) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set user: %s", + shout_get_error(hdl)); + goto err1; + } + + if (shout_set_password(hdl, shout_arg->pw) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set password: %s", + shout_get_error(hdl)); + goto err1; + } + + if (shout_set_host(hdl, shout_arg->host) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set host: %s", + shout_get_error(hdl)); + goto err1; + } + + if (shout_set_port(hdl, atoi(shout_arg->port)) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set port: %s", + shout_get_error(hdl)); + goto err1; + } + + strlcpy(tmp, shout_arg->mount, sizeof(tmp)); + if (shout_arg->mount[0] != '/') { + strlcpy(tmp, "/", sizeof(tmp)); + strlcat(tmp, shout_arg->mount, sizeof(tmp)); + } + if (shout_set_mount(hdl, tmp) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set mountpoint: %s", + shout_get_error(hdl)); + goto err1; + } + + if (shout_set_protocol(hdl, SHOUT_PROTOCOL_HTTP) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set protocol: %s", + shout_get_error(hdl)); + goto err1; + } + + /* XXX: should be configurable */ + if (shout_set_format(hdl, SHOUT_FORMAT_MP3) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set format: %s", + shout_get_error(hdl)); + goto err1; + } + + snprintf(tmp, sizeof(tmp), "%d", BITRATE); + if (shout_set_audio_info(hdl, SHOUT_AI_BITRATE, tmp) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set bitrate: %s", + shout_get_error(hdl)); + goto err1; + } + + snprintf(tmp, sizeof(tmp), "%d", RATE); + if (shout_set_audio_info(hdl, SHOUT_AI_SAMPLERATE, tmp) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set sample rate: %s", + shout_get_error(hdl)); + goto err1; + } + + snprintf(tmp, sizeof(tmp), "%d", CHANS); + if (shout_set_audio_info(hdl, SHOUT_AI_CHANNELS, tmp) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to set channels: %s", + shout_get_error(hdl)); + goto err1; + } + + if (shout_open(hdl) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to connect: %s", + shout_get_error(hdl)); + goto err1; + } + + out->hdl = hdl; + return 0; +err1: + shout_free(hdl); + out->hdl = NULL; +err0: + encoder_close(out); + return -1; +} + +int +shout_audio_play(struct output *out) +{ + unsigned char mp3buf[2 * out->nsamples + 7200]; /* worst-case scenario */ + int n; + + n = lame_encode_buffer_interleaved(out->enc, out->buf, + out->nsamples / CHANS, + mp3buf, sizeof(mp3buf)); + if (shout_send(out->hdl, mp3buf, n) != SHOUTERR_SUCCESS) { + log_msg(LOG_WARNING, "failed to send data: %s", + shout_get_error(out->hdl)); + return -1; + } + shout_sync(out->hdl); + return out->nsamples * BPS; +} + +void +shout_audio_close(struct output *out) +{ + if (out->hdl != NULL) { + shout_close(out->hdl); + shout_free(out->hdl); + shout_shutdown(); + out->hdl = NULL; + } + + encoder_close(out); +} + +int +sio_audio_open(struct output *out) { - struct sio_hdl *sio_hdl; + struct sio_arg *sio_arg = out->arg; + struct sio_hdl *hdl; struct sio_par par; - sio_hdl = sio_open(devname, SIO_PLAY, 0); - if (sio_hdl == NULL) - return NULL; + hdl = sio_open(sio_arg->devname, SIO_PLAY, 0); + if (hdl == NULL) + return -1; sio_initpar(&par); par.bits = BITS; @@ -135,12 +343,12 @@ audio_open(char *devname) par.le = SIO_LE_NATIVE; par.appbufsz = RATE * BUFFER_TIME_MS / 1000; - if (sio_setpar(sio_hdl, &par) == 0) { + if (sio_setpar(hdl, &par) == 0) { log_msg(LOG_WARNING, "%s: failed to set params", devname); goto err0; } - if (sio_getpar(sio_hdl, &par) == 0) { + if (sio_getpar(hdl, &par) == 0) { log_msg(LOG_WARNING, "%s: failed to get params", devname); goto err0; } @@ -165,35 +373,38 @@ audio_open(char *devname) goto err0; } - if (sio_start(sio_hdl) == 0) { + if (sio_start(hdl) == 0) { log_msg(LOG_WARNING, "%s: failed to start audio device", devname); goto err0; } - return sio_hdl; + out->hdl = hdl; + return 0; err0: - sio_close(sio_hdl); - return NULL; + sio_close(hdl); + out->hdl = NULL; + return -1; } int -audio_play(struct sio_hdl *sio_hdl, void *buf, int len) +sio_audio_play(struct output *out) { int n; - n = sio_write(sio_hdl, buf, len); - if (n == 0 && sio_eof(sio_hdl) != 0) + n = sio_write(out->hdl, out->buf, out->nsamples * BPS); + if (n == 0 && sio_eof(out->hdl) != 0) return -1; return n; } void -audio_close(struct sio_hdl **sio_hdl) +sio_audio_close(struct output *out) { - if (*sio_hdl != NULL) - sio_close(*sio_hdl); - *sio_hdl = NULL; + if (out->hdl != NULL) { + sio_close(out->hdl); + out->hdl = NULL; + } } int @@ -423,7 +634,7 @@ msg_handler(int fd) msg.m_output_index); return 0; } - msg.m_output_on = outputs[msg.m_output_index].sio_hdl != NULL; + msg.m_output_on = outputs[msg.m_output_index].hdl != NULL; tx_reply = 1; break; case SET_AIR: @@ -588,18 +799,87 @@ loop(void) for (i = 0; i < LEN(outputs); i++) { out = &outputs[i]; /* open output device on demand */ - if (out->sio_hdl == NULL) { - out->sio_hdl = audio_open(out->name); - if (out->sio_hdl == NULL) - continue; - } + if (out->hdl == NULL && out->open(out) == -1) + continue; out->mix(out, &inputs[0], &inputs[1]); - ret = audio_play(out->sio_hdl, out->buf, - out->nsamples * BPS); + ret = out->play(out); if (ret == -1) - audio_close(&out->sio_hdl); + out->close(out); + } + } +} + +void +map_sndio_output(int idx, char *uri) +{ + struct output *out = &outputs[idx]; + struct sio_arg *arg = &sio_arg[idx]; + + arg->devname = uri; + out->arg = arg; + out->open = sio_audio_open; + out->play = sio_audio_play; + out->close = sio_audio_close; + out->mix = mixers[idx]; +} + +void +map_shout_output(int idx, char *uri) +{ + struct output *out = &outputs[idx]; + struct shout_arg *arg = &shout_arg[idx]; + char *p; + + /* dumb URI parser, all fields are mandatory */ +#define EXTRACT_FIELD(field, sep) do { \ + if ((p = strchr(uri, sep)) == NULL) \ + errx(1, "URI is malformed"); \ + *p++ = '\0'; \ + arg->field = uri; \ + uri = p; \ +} while (0) + + EXTRACT_FIELD(user, ':'); + EXTRACT_FIELD(pw, '@'); + EXTRACT_FIELD(host, ':'); + EXTRACT_FIELD(port, '/'); + EXTRACT_FIELD(mount, '\0'); + + out->arg = arg; + out->open = shout_audio_open; + out->play = shout_audio_play; + out->close = shout_audio_close; + out->mix = mixers[idx]; +} + +void +map_output(int idx, char *uri) +{ + int i; + struct { + char *scheme; + void (*fn)(int, char *); + } uris[] = { + { .scheme = "sndio://", map_sndio_output }, + { .scheme = "shout://", map_shout_output } + }; + + for (i = 0; i < LEN(uris); i++) { + char *scheme = uris[i].scheme; + if (strncmp(uri, scheme, strlen(scheme)) == 0) { + uris[i].fn(idx, uri + strlen(scheme)); + break; } } + if (i == LEN(uris)) + errx(1, "invalid output"); +} + +void +map_outputs(int argc, char *argv[]) +{ + map_output(0, argc > 0 ? argv[0] : MASTER_DEV); + map_output(1, argc > 1 ? argv[1] : MONITOR_DEV); } void @@ -620,10 +900,7 @@ main(int argc, char *argv[]) usage(); } ARGEND - if (argc > 0) - outputs[0].name = argv[0]; - if (argc > 1) - outputs[1].name = argv[1]; + map_outputs(argc, argv); if (!debug && daemon(0, 0) == -1) err(1, "daemon"); signal(SIGPIPE, SIG_IGN);