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:
M | Makefile | | | 5 | +++-- |
M | mixerd.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);