From: Richard Kettlewell Date: Sat, 14 Mar 2009 19:03:08 +0000 (+0000) Subject: Merge config aliasing bug fix. X-Git-Tag: 5.0~168 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/985bb670b4e07d35cb1580780253ded2524a342e?hp=f183d30bc19ccc5dafeb7f469b83a8ce045d9419 Merge config aliasing bug fix. --- diff --git a/.bzrignore b/.bzrignore index e5fb493..ec15e8e 100644 --- a/.bzrignore +++ b/.bzrignore @@ -201,3 +201,4 @@ plugins/index.html doc/disorder-choose.8 doc/disorder-choose.8.html config.aux/compile +server/endian diff --git a/CHANGES.html b/CHANGES.html index 3722b61..fe3c687 100644 --- a/CHANGES.html +++ b/CHANGES.html @@ -62,15 +62,29 @@ span.command {
-

Mac OS X

- +

Server

+
-

The device configuration option and --device option - to disorder-playrtp now work. Devices may be specified either - by UID or name. Fixes The device configuration option work under OS X. Devices may + be specified either by UID or name. Fixes Issue 27.

+ +

Confirmation URLs for online registrations are cleaner now.

+ +
+ +

RTP Player

+ +
+ +

There is a new --command option which allows the RTP player + to send audio data to a user-chosen command instead of an audio API. See + the man page for details.

+ +

The --device option to disorder-playrtp now works + under OS X (as above).

diff --git a/README.upgrades b/README.upgrades index c586dd8..cbe8637 100644 --- a/README.upgrades +++ b/README.upgrades @@ -17,6 +17,14 @@ all 1.1.x versions. If you install from .deb files then much of this work is automated. +* 4.x -> 4.4 + +The syntax of confirmation strings for online registrations has changed and old +ones no longer work. This only affects users who registered before the upgrade +but have not yet confirmed their login. You can delete such half-created users +with 'disorder deluser USERNAME' (as an administrative user, for instance as +root on the server) and they can start the registration process again. + * 3.0 -> 4.x If you customized any of the templates, you will pretty much have to start from diff --git a/clients/Makefile.am b/clients/Makefile.am index 5456bd1..1ec55a9 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -25,7 +25,7 @@ AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib disorder_SOURCES=disorder.c authorize.c authorize.h \ ../lib/memgc.c disorder_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ - $(LIBGC) $(LIBGCRYPT) $(LIBPCRE) $(LIBDB) + $(LIBGC) $(LIBGCRYPT) $(LIBPCRE) $(LIBDB) $(LIBPTHREAD) disorder_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a disorderfm_SOURCES=disorderfm.c \ diff --git a/clients/disorder.c b/clients/disorder.c index 8dc6d7f..2466311 100644 --- a/clients/disorder.c +++ b/clients/disorder.c @@ -32,6 +32,7 @@ #include #include #include +#include #include "configuration.h" #include "syscalls.h" @@ -761,9 +762,14 @@ int main(int argc, char **argv) { if(password) config->password = password; if(local) - config->connect.n = 0; + config->connect.af = -1; n = optind; optind = 1; /* for subsequent getopt calls */ + /* gcrypt initialization */ + if(!gcry_check_version(NULL)) + disorder_fatal(0, "gcry_check_version failed"); + gcry_control(GCRYCTL_INIT_SECMEM, 0); + gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); /* accumulate command args */ while(n < argc) { if((i = TABLE_FIND(commands, name, argv[n])) < 0) diff --git a/clients/playrtp.c b/clients/playrtp.c index 03e4ad2..338cbbf 100644 --- a/clients/playrtp.c +++ b/clients/playrtp.c @@ -104,7 +104,7 @@ unsigned minbuffer = 2 * 44100 / 10; /* 0.2 seconds */ /** @brief Buffer high watermark * * We'll only start playing when this many samples are available. */ -static unsigned readahead = 2 * 2 * 44100; +static unsigned readahead = 44100; /* 0.5 seconds */ /** @brief Maximum buffer size * @@ -217,6 +217,7 @@ static const struct option options[] = { #endif { "dump", required_argument, 0, 'r' }, { "command", required_argument, 0, 'e' }, + { "pause-mode", required_argument, 0, 'P' }, { "socket", required_argument, 0, 's' }, { "config", required_argument, 0, 'C' }, { 0, 0, 0, 0 } @@ -492,7 +493,9 @@ static void help(void) { #if HAVE_COREAUDIO_AUDIOHARDWARE_H " --core-audio, -c Use Core Audio to play audio\n" #endif - " --command, -e COMMAND Pipe audio to command\n" + " --command, -e COMMAND Pipe audio to command.\n" + " --pause-mode, -P silence For -e: pauses send silence (default)\n" + " --pause-mode, -P suspend For -e: pauses suspend writes\n" " --help, -h Display usage message\n" " --version, -V Display version number\n" ); @@ -580,6 +583,7 @@ int main(int argc, char **argv) { union any_sockaddr mgroup; const char *dumpfile = 0; pthread_t ltid; + static const int one = 1; static const struct addrinfo prefs = { .ai_flags = AI_PASSIVE, @@ -591,7 +595,7 @@ int main(int argc, char **argv) { mem_init(); if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale"); backend = uaudio_apis[0]; - while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:re:", options, 0)) >= 0) { + while((n = getopt_long(argc, argv, "hVdD:m:b:x:L:R:M:aocC:re:P:", options, 0)) >= 0) { switch(n) { case 'h': help(); case 'V': version("disorder-playrtp"); @@ -615,6 +619,7 @@ int main(int argc, char **argv) { case 's': control_socket = optarg; break; case 'r': dumpfile = optarg; break; case 'e': backend = &uaudio_command; uaudio_set("command", optarg); break; + case 'P': uaudio_set("pause-mode", optarg); break; default: fatal(0, "invalid option"); } } @@ -651,8 +656,14 @@ int main(int argc, char **argv) { res->ai_socktype, res->ai_protocol)) < 0) fatal(errno, "error creating socket"); - /* Stash the multicast group address */ - if((is_multicast = multicast(res->ai_addr))) { + /* Allow multiple listeners */ + xsetsockopt(rtpfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one); + is_multicast = multicast(res->ai_addr); + /* The multicast and unicast/broadcast cases are different enough that they + * are totally split. Trying to find commonality between them causes more + * trouble that it's worth. */ + if(is_multicast) { + /* Stash the multicast group address */ memcpy(&mgroup, res->ai_addr, res->ai_addrlen); switch(res->ai_addr->sa_family) { case AF_INET: @@ -661,24 +672,13 @@ int main(int argc, char **argv) { case AF_INET6: mgroup.in6.sin6_port = 0; break; + default: + fatal(0, "unsupported family %d", (int)res->ai_addr->sa_family); } - } - /* Bind to 0/port */ - switch(res->ai_addr->sa_family) { - case AF_INET: - memset(&((struct sockaddr_in *)res->ai_addr)->sin_addr, 0, - sizeof (struct in_addr)); - break; - case AF_INET6: - memset(&((struct sockaddr_in6 *)res->ai_addr)->sin6_addr, 0, - sizeof (struct in6_addr)); - break; - default: - fatal(0, "unsupported family %d", (int)res->ai_addr->sa_family); - } - if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0) - fatal(errno, "error binding socket to %s", sockname); - if(is_multicast) { + /* Bind to to the multicast group address */ + if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0) + fatal(errno, "error binding socket to %s", format_sockaddr(res->ai_addr)); + /* Add multicast group membership */ switch(mgroup.sa.sa_family) { case PF_INET: mreq.imr_multiaddr = mgroup.in.sin_addr; @@ -697,10 +697,35 @@ int main(int argc, char **argv) { default: fatal(0, "unsupported address family %d", res->ai_family); } + /* Report what we did */ info("listening on %s multicast group %s", format_sockaddr(res->ai_addr), format_sockaddr(&mgroup.sa)); - } else + } else { + /* Bind to 0/port */ + switch(res->ai_addr->sa_family) { + case AF_INET: { + struct sockaddr_in *in = (struct sockaddr_in *)res->ai_addr; + + memset(&in->sin_addr, 0, sizeof (struct in_addr)); + if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0) + fatal(errno, "error binding socket to 0.0.0.0 port %d", + ntohs(in->sin_port)); + break; + } + case AF_INET6: { + struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)res->ai_addr; + + memset(&in6->sin6_addr, 0, sizeof (struct in6_addr)); + break; + } + default: + fatal(0, "unsupported family %d", (int)res->ai_addr->sa_family); + } + if(bind(rtpfd, res->ai_addr, res->ai_addrlen) < 0) + fatal(errno, "error binding socket to %s", format_sockaddr(res->ai_addr)); + /* Report what we did */ info("listening on %s", format_sockaddr(res->ai_addr)); + } len = sizeof rcvbuf; if(getsockopt(rtpfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len) < 0) fatal(errno, "error calling getsockopt SO_RCVBUF"); @@ -763,7 +788,9 @@ int main(int argc, char **argv) { info("Playing..."); next_timestamp = pheap_first(&packets)->timestamp; active = 1; + pthread_mutex_unlock(&lock); backend->activate(); + pthread_mutex_lock(&lock); /* Wait until the buffer empties out */ while(nsamples >= minbuffer || (nsamples > 0 @@ -771,7 +798,9 @@ int main(int argc, char **argv) { pthread_cond_wait(&cond, &lock); } /* Stop playing for a bit until the buffer re-fills */ + pthread_mutex_unlock(&lock); backend->deactivate(); + pthread_mutex_lock(&lock); active = 0; /* Go back round */ } diff --git a/debian/postrm.disorder-server b/debian/postrm.disorder-server index 7ba0933..4cd590a 100755 --- a/debian/postrm.disorder-server +++ b/debian/postrm.disorder-server @@ -30,6 +30,7 @@ cleanup_remove() { rm -f $state/__db.* rm -f $state/noticed.db rm -f $state/search.db + rm -f $state/isearch.db rm -f $state/tags.db rm -f $state/tracks.db } @@ -43,6 +44,7 @@ cleanup_purge() { rm -f $state/prefs.db rm -f $state/schedule.db rm -f $state/users.db + rm -f $state/playlists.db } case "$1" in diff --git a/disobedience/disobedience.c b/disobedience/disobedience.c index e0fa295..dea9d59 100644 --- a/disobedience/disobedience.c +++ b/disobedience/disobedience.c @@ -25,6 +25,7 @@ #include #include #include +#include /* Apologies for the numerous de-consting casts, but GLib et al do not seem to * have heard of const. */ @@ -448,6 +449,11 @@ int main(int argc, char **argv) { } if(!gtkok) fatal(0, "failed to initialize GTK+"); + /* gcrypt initialization */ + if(!gcry_check_version(NULL)) + disorder_fatal(0, "gcry_check_version failed"); + gcry_control(GCRYCTL_INIT_SECMEM, 0); + gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); signal(SIGPIPE, SIG_IGN); init_styles(); load_settings(); diff --git a/disobedience/login.c b/disobedience/login.c index 1c6df79..36850c1 100644 --- a/disobedience/login.c +++ b/disobedience/login.c @@ -73,7 +73,7 @@ static void default_connect(void) { if(config->password) return; /* If we already have a host and/or port that's good too */ - if(config->connect.n) + if(config->connect.af != -1) return; /* If there's a suitable socket that's probably what we wanted */ const char *s = config_get_file("socket"); @@ -84,11 +84,21 @@ static void default_connect(void) { } static const char *get_hostname(void) { - return config->connect.n >= 2 ? config->connect.s[0] : ""; + if(config->connect.af == -1 || !config->connect.address) + return ""; + else + return config->connect.address; } static const char *get_service(void) { - return config->connect.n >= 2 ? config->connect.s[1] : ""; + if(config->connect.af == -1) + return ""; + else { + char *s; + + byte_xasprintf(&s, "%d", config->connect.port); + return s; + } } static const char *get_username(void) { @@ -100,11 +110,13 @@ static const char *get_password(void) { } static void set_hostname(struct config *c, const char *s) { - c->connect.s[0] = (char *)s; + if(c->connect.af == -1) + c->connect.af = AF_UNSPEC; + c->connect.address = xstrdup(s); } static void set_service(struct config *c, const char *s) { - c->connect.s[1] = (char *)s; + c->connect.port = atoi(s); } static void set_username(struct config *c, const char *s) { @@ -131,13 +143,10 @@ static void login_update_config(struct config *c) { size_t n; const gboolean remote = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(lwi_remote)); - if(remote) { - c->connect.n = 2; - c->connect.s = xcalloc(2, sizeof (char *)); - } else { - c->connect.n = 0; - c->connect.s = 0; - } + if(remote) + c->connect.af = AF_UNSPEC; + else + c->connect.af = -1; for(n = 0; n < NLWIS; ++n) if(remote || !(lwis[n].flags & LWI_REMOTE)) lwis[n].set(c, xstrdup(gtk_entry_get_text(GTK_ENTRY(lwi_entry[n])))); @@ -161,10 +170,12 @@ static void login_save_config(void) { "password %s\n", quoteutf8(config->username), quoteutf8(config->password)); - if(rc >= 0 && config->connect.n) - rc = fprintf(fp, "connect %s %s\n", - quoteutf8(config->connect.s[0]), - quoteutf8(config->connect.s[1])); + if(rc >= 0 && config->connect.af != -1) { + char **vec; + + netaddress_format(&config->connect, NULL, &vec); + rc = fprintf(fp, "connect %s %s %s\n", vec[0], vec[1], vec[2]); + } if(rc < 0) { fpopup_msg(GTK_MESSAGE_ERROR, "error writing to %s: %s", tmp, strerror(errno)); @@ -327,7 +338,7 @@ void login_box(void) { } /* Initial settings */ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(lwi_remote), - config->connect.n >= 2); + config->connect.af != -1); lwi_remote_toggled(GTK_TOGGLE_BUTTON(lwi_remote), 0); buttonbox = create_buttons(buttons, NBUTTONS); vbox = gtk_vbox_new(FALSE, 1); diff --git a/doc/disorder-playrtp.1.in b/doc/disorder-playrtp.1.in index ea4c93f..c313ece 100644 --- a/doc/disorder-playrtp.1.in +++ b/doc/disorder-playrtp.1.in @@ -66,7 +66,8 @@ If \fICOMMAND\fR exits it is re-executed; any samples that had been written to the pipe but not processed by the previous instance will be lost. .IP .B \-\-device -is redundant with this option. +is redundant with this option, but you might want to set +.BR \-\-pause\-mode . .IP As an example, .B "-e \(aqcat > dump\(aq" @@ -75,6 +76,12 @@ You could convert it to another format with, for instance: .IP .B "sox -c2 -traw -r44100 -s -w dump dump.wav" .TP +.B \-\-pause\-mode \fIMODE\fR, \fB-P \fIMODE +Set the pause mode for \fB\-\-command\fR to either \fBsilence\fR (the default), in +which pauses are represented by sending silent samples, or \fBsuspend\fR, in which +writes to the subprocess are suspended, requiring it to infer a pause from flow +control. +.TP .B \-\-config \fIPATH\fR, \fB\-C \fIPATH Set the configuration file. The default is diff --git a/doc/disorder_config.5.in b/doc/disorder_config.5.in index d033258..1813152 100644 --- a/doc/disorder_config.5.in +++ b/doc/disorder_config.5.in @@ -275,6 +275,10 @@ Execute a command. This is the default if .B speaker_command is specified, or if no native is available. +.IP +You might want to set +.B pause_mode +with this backend. .TP .B rtp Transmit audio over the network. @@ -293,14 +297,22 @@ See .BR disorder_protocol (5) for more details. .TP -.B broadcast \fIADDRESS\fR \fIPORT\fR +.B broadcast \fR[\fIFAMILY\fR] \fIADDRESS\fR \fIPORT\fR Transmit sound data to \fIADDRESS\fR using UDP port \fIPORT\fR. This implies \fBapi rtp\fR. .IP +\fIFAMILY\fR can be \fB-4\fR or \fB-6\fR to force IPv4 or IPv6, if this is not +implied by \fIADDRESS\fR. +Note that IPv6 is not currently well tested. +.IP See also \fBmulticast_loop\fR and \fBmulticast_ttl\fR. .TP -.B broadcast_from \fIADDRESS\fR \fIPORT\fR +.B broadcast_from \fR[\fIFAMILY\fR] \fIADDRESS\fR \fIPORT\fR Sets the (local) source address used by \fBbroadcast\fR. +.IP +\fIFAMILY\fR can be \fB-4\fR or \fB-6\fR to force IPv4 or IPv6, if this is not +implied by \fIADDRESS\fR. +Note that IPv6 is not currently well tested. .TP .B channel \fICHANNEL\fR The mixer channel that the volume control should use. @@ -309,6 +321,7 @@ For \fBapi oss\fR the possible values are: .RS .TP 8 .B pcm + Output level for the audio device. This is probably what you want and is the default. .TP @@ -409,10 +422,14 @@ reinstated. Specifies the number of recently played tracks to remember (including failed tracks and scratches). .TP -.B listen \fR[\fIHOST\fR] \fISERVICE\fR +.B listen \fR[\fIFAMILY\fR] \fR[\fIHOST\fR] \fISERVICE\fR Listen for connections on the address specified by \fIHOST\fR and port specified by \fISERVICE\fR. -If \fIHOST\fR is omitted then listens on all local addresses. +If \fIHOST\fR is omitted, or is \fB*\fR, then listens on all local addresses. +.IP +\fIFAMILY\fR can be \fB-4\fR or \fB-6\fR to force IPv4 or IPv6, if this is not +implied by \fIHOST\fR. +Note that IPv6 is not currently well tested. .IP Normally the server only listens on a UNIX domain socket. .TP @@ -527,6 +544,19 @@ The maximum days that a track can survive in the database of newly added tracks. The default is 31. .TP +.B pause_mode \fIMODE +Sets the pause mode for the \fBcommand\fR backend. +The possible values are: +.RS +.TP +.B silence +Send silent (0-value) samples when paused. +This is the default. +.TP +.B suspend +Stop writing when paused. +.RE +.TP .B player \fIPATTERN\fR \fIMODULE\fR [\fIOPTIONS.. [\fB\-\-\fR]] \fIARGS\fR... Specifies the player for files matching the glob \fIPATTERN\fR. \fIMODULE\fR specifies which plugin module to use. @@ -712,9 +742,13 @@ These options would normally be used in \fI~\fRUSERNAME\fI/.disorder/passwd\fR or \fIpkgconfdir/config.\fRUSERNAME. .TP -.B connect \fIHOST SERVICE\fR +.B connect \fR[\fIFAMILY\fR] \fIHOST SERVICE\fR Connect to the address specified by \fIHOST\fR and port specified by \fISERVICE\fR. +.IP +\fIFAMILY\fR can be \fB-4\fR or \fB-6\fR to force IPv4 or IPv6, if this is not +implied by \fIHOST\fR. +Note that IPv6 is not currently well tested. .TP .B password \fIPASSWORD\fR Specify password. diff --git a/lib/addr.c b/lib/addr.c index fecc28c..ecbab22 100644 --- a/lib/addr.c +++ b/lib/addr.c @@ -30,6 +30,9 @@ #include "printf.h" #include "addr.h" #include "mem.h" +#include "syscalls.h" +#include "configuration.h" +#include "vector.h" /** @brief Convert a pair of strings to an address * @param a Pointer to string list @@ -92,29 +95,38 @@ struct addrinfo *get_address(const struct stringlist *a, */ int addrinfocmp(const struct addrinfo *a, const struct addrinfo *b) { - const struct sockaddr_in *ina, *inb; - const struct sockaddr_in6 *in6a, *in6b; - if(a->ai_family != b->ai_family) return a->ai_family - b->ai_family; if(a->ai_socktype != b->ai_socktype) return a->ai_socktype - b->ai_socktype; if(a->ai_protocol != b->ai_protocol) return a->ai_protocol - b->ai_protocol; - switch(a->ai_family) { + return sockaddrcmp(a->ai_addr, b->ai_addr); +} + +/** @brief Comparison function for socket addresses + * + * Suitable for qsort(). + */ +int sockaddrcmp(const struct sockaddr *a, + const struct sockaddr *b) { + const struct sockaddr_in *ina, *inb; + const struct sockaddr_in6 *in6a, *in6b; + + if(a->sa_family != b->sa_family) return a->sa_family - b->sa_family; + switch(a->sa_family) { case PF_INET: - ina = (const struct sockaddr_in *)a->ai_addr; - inb = (const struct sockaddr_in *)b->ai_addr; + ina = (const struct sockaddr_in *)a; + inb = (const struct sockaddr_in *)b; if(ina->sin_port != inb->sin_port) return ina->sin_port - inb->sin_port; return ina->sin_addr.s_addr - inb->sin_addr.s_addr; break; case PF_INET6: - in6a = (const struct sockaddr_in6 *)a->ai_addr; - in6b = (const struct sockaddr_in6 *)b->ai_addr; + in6a = (const struct sockaddr_in6 *)a; + in6b = (const struct sockaddr_in6 *)b; if(in6a->sin6_port != in6b->sin6_port) return in6a->sin6_port - in6b->sin6_port; return memcmp(&in6a->sin6_addr, &in6b->sin6_addr, sizeof (struct in6_addr)); default: - error(0, "unsupported protocol family %d", a->ai_protocol); - return memcmp(a->ai_addr, b->ai_addr, a->ai_addrlen); /* kludge */ + fatal(0, "unsupported protocol family %d", a->sa_family); } } @@ -194,6 +206,142 @@ char *format_sockaddr(const struct sockaddr *sa) { } } +/** @brief Parse the text form of a network address + * @param na Where to store result + * @param nvec Number of strings + * @param vec List of strings + * @return 0 on success, -1 on syntax error + */ +int netaddress_parse(struct netaddress *na, + int nvec, + char **vec) { + const char *port; + + na->af = AF_UNSPEC; + if(nvec > 0 && vec[0][0] == '-') { + if(!strcmp(vec[0], "-4")) + na->af = AF_INET; + else if(!strcmp(vec[0], "-6")) + na->af = AF_INET6; + else if(!strcmp(vec[0], "-unix")) + na->af = AF_UNIX; + else if(!strcmp(vec[0], "-")) + na->af = AF_UNSPEC; + else + return -1; + --nvec; + ++vec; + } + if(nvec == 0) + return -1; + /* Possibilities are: + * + * /path/to/unix/socket an AF_UNIX socket + * * PORT any address, specific port + * PORT any address, specific port + * ADDRESS PORT specific address, specific port + */ + if(vec[0][0] == '/' && na->af == AF_UNSPEC) + na->af = AF_UNIX; + if(na->af == AF_UNIX) { + if(nvec != 1) + return -1; + na->address = xstrdup(vec[0]); + na->port = -1; /* makes no sense */ + } else { + switch(nvec) { + case 1: + na->address = NULL; + port = vec[0]; + break; + case 2: + if(!strcmp(vec[0], "*")) + na->address = NULL; + else + na->address = xstrdup(vec[0]); + port = vec[1]; + break; + default: + return -1; + } + if(port[strspn(port, "0123456789")]) + return -1; + long p; + int e = xstrtol(&p, port, NULL, 10); + + if(e) + return -1; + if(p > 65535) + return -1; + na->port = (int)p; + } + return 0; +} + +/** @brief Format a @ref netaddress structure + * @param na Network address to format + * @param nvecp Where to put string count, or NULL + * @param vecp Where to put strings + * + * The formatted form is suitable for passing to netaddress_parse(). + */ +void netaddress_format(const struct netaddress *na, + int *nvecp, + char ***vecp) { + struct vector v[1]; + + vector_init(v); + switch(na->af) { + case AF_UNSPEC: vector_append(v, xstrdup("-")); break; + case AF_INET: vector_append(v, xstrdup("-4")); break; + case AF_INET6: vector_append(v, xstrdup("-6")); break; + case AF_UNIX: vector_append(v, xstrdup("-unix")); break; + } + if(na->address) + vector_append(v, xstrdup(na->address)); + else + vector_append(v, xstrdup("*")); + if(na->port != -1) { + char buffer[64]; + + snprintf(buffer, sizeof buffer, "%d", na->port); + vector_append(v, xstrdup(buffer)); + } + vector_terminate(v); + if(nvecp) + *nvecp = v->nvec; + if(vecp) + *vecp = v->vec; +} + +/** @brief Resolve a network address + * @param na Address structure + * @param passive True if passive (bindable) address is desired + * @param protocol Protocol number desired (e.g. @c IPPROTO_TCP) + * @return List of suitable addresses or NULL + */ +struct addrinfo *netaddress_resolve(const struct netaddress *na, + int passive, + int protocol) { + struct addrinfo *res, hints[1]; + char service[64]; + int rc; + + memset(hints, 0, sizeof hints); + hints->ai_family = na->af; + hints->ai_protocol = protocol; + hints->ai_flags = passive ? AI_PASSIVE : 0; + snprintf(service, sizeof service, "%d", na->port); + rc = getaddrinfo(na->address, service, hints, &res); + if(rc) { + error(0, "getaddrinfo %s %d: %s", + na->address ? na->address : "*", + na->port, gai_strerror(rc)); + return NULL; + } + return res; +} + /* Local Variables: c-basic-offset:2 diff --git a/lib/addr.h b/lib/addr.h index 973733f..90c165e 100644 --- a/lib/addr.h +++ b/lib/addr.h @@ -23,7 +23,23 @@ #include -#include "configuration.h" +struct stringlist; + +/** @brief A network address */ +struct netaddress { + /** @brief Address family + * + * Typically @c AF_UNIX, @c AF_INET, @c AF_INET6 or @c AF_UNSPEC. + * Set to -1 to mean 'no address'. + */ + int af; + + /** @brief Address or NULL for 'any' */ + char *address; + + /** @brief Port number */ + int port; +}; struct addrinfo *get_address(const struct stringlist *a, const struct addrinfo *pref, @@ -31,10 +47,22 @@ struct addrinfo *get_address(const struct stringlist *a, int addrinfocmp(const struct addrinfo *a, const struct addrinfo *b); +int sockaddrcmp(const struct sockaddr *a, + const struct sockaddr *b); int multicast(const struct sockaddr *sa); char *format_sockaddr(const struct sockaddr *sa); +int netaddress_parse(struct netaddress *na, + int nvec, + char **vec); +void netaddress_format(const struct netaddress *na, + int *nvecp, + char ***vecp); +struct addrinfo *netaddress_resolve(const struct netaddress *na, + int passive, + int protocol); + #endif /* ADDR_H */ /* diff --git a/lib/basen.c b/lib/basen.c index 87d2795..7c1f9ec 100644 --- a/lib/basen.c +++ b/lib/basen.c @@ -31,7 +31,7 @@ * @param nwords Length of bignum * @return !v */ -static int zero(const unsigned long *v, int nwords) { +static int zero(const uint32_t *v, int nwords) { int n; for(n = 0; n < nwords && !v[n]; ++n) @@ -47,7 +47,7 @@ static int zero(const unsigned long *v, int nwords) { * * The quotient is stored in @p v. */ -static unsigned divide(unsigned long *v, int nwords, unsigned long m) { +static unsigned divide(uint32_t *v, int nwords, unsigned long m) { unsigned long r = 0, a, b; int n; @@ -66,6 +66,31 @@ static unsigned divide(unsigned long *v, int nwords, unsigned long m) { return r; } +/** @brief Multiple v by m and add a + * @param v Pointer to bigendian bignum + * @param nwords Length of bignum + * @param m Value to multiply by + * @param a Value to add + * @return 0 on success, non-0 on overflow + * + * Does v = m * v + a. + */ +static int mla(uint32_t *v, int nwords, uint32_t m, uint32_t a) { + int n = nwords - 1; + uint32_t carry = a; + + while(n >= 0) { + const uint64_t p = (uint64_t)v[n] * m + carry; + carry = (uint32_t)(p >> 32); + v[n] = (uint32_t)p; + --n; + } + /* If there is still a carry then we overflowed */ + return !!carry; +} + +static const char basen_chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + /** @brief Convert v to a chosen base * @param v Pointer to bigendian bignum (modified!) * @param nwords Length of bignum @@ -75,26 +100,53 @@ static unsigned divide(unsigned long *v, int nwords, unsigned long m) { * @return 0 on success, -1 if the buffer is too small * * Converts @p v to a string in the given base using decimal digits, lower case - * letter sand upper case letters as digits. + * letters and upper case letters as digits. + * + * The inverse of nesab(). */ -int basen(unsigned long *v, +int basen(uint32_t *v, int nwords, char buffer[], size_t bufsize, unsigned base) { size_t i = bufsize; - static const char chars[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; do { if(i <= 1) return -1; /* overflow */ - buffer[--i] = chars[divide(v, nwords, base)]; + buffer[--i] = basen_chars[divide(v, nwords, base)]; } while(!zero(v, nwords)); memmove(buffer, buffer + i, bufsize - i); buffer[bufsize - i] = 0; return 0; } +/** @brief Convert a string back to a large integer in an arbitrary base + * @param v Where to store result as a bigendian bignum + * @param nwords Space available in @p v + * @param s Input string + * @param base Number base (2..62) + * @return 0 on success, non-0 on overflow or invalid input + * + * The inverse of basen(). If the number is much smaller than the buffer then + * the first words will be 0. + */ +int nesab(uint32_t *v, + int nwords, + const char *s, + unsigned base) { + /* Initialize to 0 */ + memset(v, 0, nwords * sizeof *v); + while(*s) { + const char *dp = strchr(basen_chars, *s++); + if(!dp) + return -1; + if(mla(v, nwords, base, dp - basen_chars)) + return -1; + } + return 0; +} + /* Local Variables: c-basic-offset:2 diff --git a/lib/basen.h b/lib/basen.h index 717f2ea..c1a60f0 100644 --- a/lib/basen.h +++ b/lib/basen.h @@ -19,7 +19,7 @@ #ifndef BASEN_H #define BASEN_H -int basen(unsigned long *v, +int basen(uint32_t *v, int nwords, char buffer[], size_t bufsize, @@ -29,6 +29,11 @@ int basen(unsigned long *v, * Returns 0 on success or -1 on overflow. */ +int nesab(uint32_t *v, + int nwords, + const char *s, + unsigned base); + #endif /* BASEN_H */ /* diff --git a/lib/client-common.c b/lib/client-common.c index ced82e4..aa43124 100644 --- a/lib/client-common.c +++ b/lib/client-common.c @@ -45,17 +45,11 @@ socklen_t find_server(struct config *c, struct addrinfo *res = 0; char *name; socklen_t len; - - static const struct addrinfo pref = { - .ai_flags = 0, - .ai_family = PF_INET, - .ai_socktype = SOCK_STREAM, - .ai_protocol = IPPROTO_TCP, - }; - if(c->connect.n) { - res = get_address(&c->connect, &pref, &name); - if(!res) return -1; + if(c->connect.af != -1) { + res = netaddress_resolve(&c->connect, 0, IPPROTO_TCP); + if(!res) + return -1; sa = res->ai_addr; len = res->ai_addrlen; } else { @@ -73,7 +67,7 @@ socklen_t find_server(struct config *c, *sap = xmalloc_noptr(len); memcpy(*sap, sa, len); if(namep) - *namep = name; + *namep = format_sockaddr(sa); if(res) freeaddrinfo(res); return len; diff --git a/lib/configuration.c b/lib/configuration.c index 958ea6c..1fcfb1a 100644 --- a/lib/configuration.c +++ b/lib/configuration.c @@ -489,6 +489,18 @@ static int set_rights(const struct config_state *cs, return 0; } +static int set_netaddress(const struct config_state *cs, + const struct conf *whoami, + int nvec, char **vec) { + struct netaddress *na = ADDRESS(cs->config, struct netaddress); + + if(netaddress_parse(na, nvec, vec)) { + error(0, "%s:%d: invalid network address", cs->path, cs->line); + return -1; + } + return 0; +} + /* free functions */ static void free_none(struct config attribute((unused)) *c, @@ -573,6 +585,13 @@ static void free_transformlist(struct config *c, xfree(tl->t); } +static void free_netaddress(struct config *c, + const struct conf *whoami) { + struct netaddress *na = ADDRESS(c, struct netaddress); + + xfree(na->address); +} + /* configuration types */ static const struct conftype @@ -588,6 +607,7 @@ static const struct conftype type_restrict = { set_restrict, free_none }, type_namepart = { set_namepart, free_namepartlist }, type_transform = { set_transform, free_transformlist }, + type_netaddress = { set_netaddress, free_netaddress }, type_rights = { set_rights, free_none }; /* specific validation routine */ @@ -814,45 +834,6 @@ static int validate_alias(const struct config_state *cs, return 0; } -static int validate_addrport(const struct config_state attribute((unused)) *cs, - int nvec, - char attribute((unused)) **vec) { - switch(nvec) { - case 0: - error(0, "%s:%d: missing address", - cs->path, cs->line); - return -1; - case 1: - error(0, "%s:%d: missing port name/number", - cs->path, cs->line); - return -1; - case 2: - return 0; - default: - error(0, "%s:%d: expected ADDRESS PORT", - cs->path, cs->line); - return -1; - } -} - -static int validate_port(const struct config_state attribute((unused)) *cs, - int nvec, - char attribute((unused)) **vec) { - switch(nvec) { - case 0: - error(0, "%s:%d: missing address", - cs->path, cs->line); - return -1; - case 1: - case 2: - return 0; - default: - error(0, "%s:%d: expected [ADDRESS] PORT", - cs->path, cs->line); - return -1; - } -} - static int validate_algo(const struct config_state attribute((unused)) *cs, int nvec, char **vec) { @@ -890,6 +871,31 @@ static int validate_backend(const struct config_state attribute((unused)) *cs, return 0; } +static int validate_pausemode(const struct config_state attribute((unused)) *cs, + int nvec, + char **vec) { + if(nvec == 1 && (!strcmp(vec[0], "silence") || !strcmp(vec[0], "suspend"))) + return 0; + error(0, "%s:%d: invalid pause mode", cs->path, cs->line); + return -1; +} + +static int validate_destaddr(const struct config_state attribute((unused)) *cs, + int nvec, + char **vec) { + struct netaddress na[1]; + + if(netaddress_parse(na, nvec, vec)) { + error(0, "%s:%d: invalid network address", cs->path, cs->line); + return -1; + } + if(!na->address) { + error(0, "%s:%d: destination address required", cs->path, cs->line); + return -1; + } + return 0; +} + /** @brief Item name and and offset */ #define C(x) #x, offsetof(struct config, x) /** @brief Item name and and offset */ @@ -901,13 +907,13 @@ static const struct conf conf[] = { { C(allow), &type_stringlist_accum, validate_allow }, { C(api), &type_string, validate_backend }, { C(authorization_algorithm), &type_string, validate_algo }, - { C(broadcast), &type_stringlist, validate_addrport }, - { C(broadcast_from), &type_stringlist, validate_addrport }, + { C(broadcast), &type_netaddress, validate_destaddr }, + { C(broadcast_from), &type_netaddress, validate_any }, { C(channel), &type_string, validate_any }, { C(checkpoint_kbyte), &type_integer, validate_non_negative }, { C(checkpoint_min), &type_integer, validate_non_negative }, { C(collection), &type_collections, validate_any }, - { C(connect), &type_stringlist, validate_addrport }, + { C(connect), &type_netaddress, validate_destaddr }, { C(cookie_login_lifetime), &type_integer, validate_positive }, { C(cookie_key_lifetime), &type_integer, validate_positive }, { C(dbversion), &type_integer, validate_positive }, @@ -916,7 +922,7 @@ static const struct conf conf[] = { { C(gap), &type_integer, validate_non_negative }, { C(history), &type_integer, validate_positive }, { C(home), &type_string, validate_isabspath }, - { C(listen), &type_stringlist, validate_port }, + { C(listen), &type_netaddress, validate_any }, { C(lock), &type_boolean, validate_any }, { C(mail_sender), &type_string, validate_any }, { C(mixer), &type_string, validate_any }, @@ -932,6 +938,7 @@ static const struct conf conf[] = { { C(nice_speaker), &type_integer, validate_any }, { C(noticed_history), &type_integer, validate_positive }, { C(password), &type_string, validate_any }, + { C(pause_mode), &type_string, validate_pausemode }, { C(player), &type_stringlist_accum, validate_player }, { C(plugins), &type_string_accum, validate_isdir }, { C(prefsync), &type_integer, validate_positive }, @@ -941,6 +948,7 @@ static const struct conf conf[] = { { C(reminder_interval), &type_integer, validate_positive }, { C(remote_userman), &type_boolean, validate_any }, { C2(restrict, restrictions), &type_restrict, validate_any }, + { C(rtp_delay_threshold), &type_integer, validate_positive }, { C(sample_format), &type_sample_format, validate_sample_format }, { C(scratch), &type_string_accum, validate_isreg }, { C(sendmail), &type_string, validate_isabspath }, @@ -1192,6 +1200,10 @@ static struct config *config_default(void) { default_players[n], "disorder-tracklength", (char *)0)) exit(1); } + c->broadcast.af = -1; + c->broadcast_from.af = -1; + c->listen.af = -1; + c->connect.af = -1; return c; } @@ -1263,7 +1275,7 @@ static void config_postdefaults(struct config *c, if(!c->api) { if(c->speaker_command) c->api = xstrdup("command"); - else if(c->broadcast.n) + else if(c->broadcast.af != -1) c->api = xstrdup("rtp"); else if(config_uaudio_apis) c->api = xstrdup(config_uaudio_apis[0]->name); @@ -1275,7 +1287,7 @@ static void config_postdefaults(struct config *c, if(server) { if(!strcmp(c->api, "command") && !c->speaker_command) fatal(0, "'api command' but speaker_command is not set"); - if((!strcmp(c->api, "rtp")) && !c->broadcast.n) + if((!strcmp(c->api, "rtp")) && c->broadcast.af == -1) fatal(0, "'api rtp' but broadcast is not set"); } /* Override sample format */ diff --git a/lib/configuration.h b/lib/configuration.h index d9af67d..4cb5677 100644 --- a/lib/configuration.h +++ b/lib/configuration.h @@ -26,6 +26,7 @@ #include "speaker-protocol.h" #include "rights.h" +#include "addr.h" struct uaudio; @@ -154,7 +155,7 @@ struct config { long prefsync; /* preflog sync interval */ /** @brief Secondary listen address */ - struct stringlist listen; + struct netaddress listen; /** @brief Alias format string */ const char *alias; @@ -171,6 +172,9 @@ struct config { /** @brief Command execute by speaker to play audio */ const char *speaker_command; + /** @brief Pause mode for command backend */ + const char *pause_mode; + /** @brief Target sample format */ struct stream_header sample_format; @@ -190,7 +194,7 @@ struct config { const char *password; /** @brief Address to connect to */ - struct stringlist connect; + struct netaddress connect; /** @brief Directories to search for web templates */ struct stringlist templates; @@ -230,11 +234,14 @@ struct config { struct transformlist transform; /* path name transformations */ /** @brief Address to send audio data to */ - struct stringlist broadcast; + struct netaddress broadcast; /** @brief Source address for network audio transmission */ - struct stringlist broadcast_from; + struct netaddress broadcast_from; + /** @brief RTP delay threshold */ + long rtp_delay_threshold; + /** @brief TTL for multicast packets */ long multicast_ttl; diff --git a/lib/random.c b/lib/random.c index 3f8f714..d6be3d9 100644 --- a/lib/random.c +++ b/lib/random.c @@ -75,7 +75,7 @@ void random_get(void *ptr, size_t bytes) { /** @brief Return a random ID string */ char *random_id(void) { - unsigned long words[2]; + uint32_t words[2]; char id[128]; random_get(words, sizeof words); diff --git a/lib/uaudio-alsa.c b/lib/uaudio-alsa.c index 2236c91..721639c 100644 --- a/lib/uaudio-alsa.c +++ b/lib/uaudio-alsa.c @@ -26,6 +26,7 @@ #include "mem.h" #include "log.h" #include "uaudio.h" +#include "configuration.h" /** @brief The current PCM handle */ static snd_pcm_t *alsa_pcm; @@ -135,7 +136,8 @@ static void alsa_start(uaudio_callback *callback, alsa_open(); uaudio_thread_start(callback, userdata, alsa_play, 32 / uaudio_sample_size, - 4096 / uaudio_sample_size); + 4096 / uaudio_sample_size, + 0); } static void alsa_stop(void) { @@ -245,6 +247,12 @@ static void alsa_set_volume(int *left, int *right) { *right = to_percent(r); } +static void alsa_configure(void) { + uaudio_set("device", config->device); + uaudio_set("mixer-control", config->mixer); + uaudio_set("mixer-channel", config->channel); +} + const struct uaudio uaudio_alsa = { .name = "alsa", .options = alsa_options, @@ -256,6 +264,7 @@ const struct uaudio uaudio_alsa = { .close_mixer = alsa_close_mixer, .get_volume = alsa_get_volume, .set_volume = alsa_set_volume, + .configure = alsa_configure }; #endif diff --git a/lib/uaudio-command.c b/lib/uaudio-command.c index 012185a..2c610ff 100644 --- a/lib/uaudio-command.c +++ b/lib/uaudio-command.c @@ -34,6 +34,7 @@ #include "mem.h" #include "wstat.h" #include "uaudio.h" +#include "configuration.h" /** @brief Pipe to subprocess */ static int command_fd; @@ -43,6 +44,7 @@ static pid_t command_pid; static const char *const command_options[] = { "command", + "pause-mode", NULL }; @@ -114,13 +116,23 @@ static size_t command_play(void *buffer, size_t nsamples) { static void command_start(uaudio_callback *callback, void *userdata) { + const char *pausemode = uaudio_get("pause-mode", "silence"); + unsigned flags = 0; + + if(!strcmp(pausemode, "silence")) + flags |= UAUDIO_THREAD_FAKE_PAUSE; + else if(!strcmp(pausemode, "suspend")) + ; + else + fatal(0, "unknown pause mode '%s'", pausemode); command_open(); uaudio_schedule_init(); uaudio_thread_start(callback, userdata, command_play, uaudio_channels, - 4096 / uaudio_sample_size); + 4096 / uaudio_sample_size, + flags); } static void command_stop(void) { @@ -137,13 +149,19 @@ static void command_deactivate(void) { uaudio_thread_deactivate(); } +static void command_configure(void) { + uaudio_set("command", config->speaker_command); + uaudio_set("pause-mode", config->pause_mode); +} + const struct uaudio uaudio_command = { .name = "command", .options = command_options, .start = command_start, .stop = command_stop, .activate = command_activate, - .deactivate = command_deactivate + .deactivate = command_deactivate, + .configure = command_configure, }; /* diff --git a/lib/uaudio-coreaudio.c b/lib/uaudio-coreaudio.c index 81a5711..5f28e67 100644 --- a/lib/uaudio-coreaudio.c +++ b/lib/uaudio-coreaudio.c @@ -26,6 +26,7 @@ #include "mem.h" #include "log.h" #include "syscalls.h" +#include "configuration.h" /** @brief Callback to request sample data */ static uaudio_callback *coreaudio_callback; @@ -182,13 +183,18 @@ static void coreaudio_deactivate(void) { coreaudio_fatal(status, "AudioDeviceStop"); } +static void coreaudio_configure(void) { + uaudio_set("device", config->device); +} + const struct uaudio uaudio_coreaudio = { .name = "coreaudio", .options = coreaudio_options, .start = coreaudio_start, .stop = coreaudio_stop, .activate = coreaudio_activate, - .deactivate = coreaudio_deactivate + .deactivate = coreaudio_deactivate, + .configure = coreaudio_configure, }; #endif diff --git a/lib/uaudio-oss.c b/lib/uaudio-oss.c index 081e1eb..6d902cb 100644 --- a/lib/uaudio-oss.c +++ b/lib/uaudio-oss.c @@ -32,6 +32,7 @@ #include "mem.h" #include "log.h" #include "uaudio.h" +#include "configuration.h" #ifndef AFMT_U16_NE # if BYTE_ORDER == BIG_ENDIAN @@ -127,13 +128,15 @@ static void oss_start(uaudio_callback *callback, /* Very specific buffer size requirements here apparently */ uaudio_thread_start(callback, userdata, oss_play, 4608 / uaudio_sample_size, - 4608 / uaudio_sample_size); + 4608 / uaudio_sample_size, + 0); #else /* We could SNDCTL_DSP_GETBLKSIZE but only when the device is already open, * which is kind of inconvenient. We go with 1-4Kbyte for now. */ uaudio_thread_start(callback, userdata, oss_play, 32 / uaudio_sample_size, - 4096 / uaudio_sample_size); + 4096 / uaudio_sample_size, + 0); #endif } @@ -195,6 +198,12 @@ static void oss_set_volume(int *left, int *right) { } } +static void oss_configure(void) { + uaudio_set("device", config->device); + uaudio_set("mixer-device", config->mixer); + uaudio_set("mixer-channel", config->channel); +} + const struct uaudio uaudio_oss = { .name = "oss", .options = oss_options, @@ -206,6 +215,7 @@ const struct uaudio uaudio_oss = { .close_mixer = oss_close_mixer, .get_volume = oss_get_volume, .set_volume = oss_set_volume, + .configure = oss_configure, }; #endif diff --git a/lib/uaudio-rtp.c b/lib/uaudio-rtp.c index 0060940..bb03be4 100644 --- a/lib/uaudio-rtp.c +++ b/lib/uaudio-rtp.c @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -36,6 +38,7 @@ #include "addr.h" #include "ifreq.h" #include "timeval.h" +#include "configuration.h" /** @brief Bytes to send per network packet * @@ -82,6 +85,50 @@ static const char *const rtp_options[] = { NULL }; +static void rtp_get_netconfig(const char *af, + const char *addr, + const char *port, + struct netaddress *na) { + char *vec[3]; + + vec[0] = uaudio_get(af, NULL); + vec[1] = uaudio_get(addr, NULL); + vec[2] = uaudio_get(port, NULL); + if(!*vec) + na->af = -1; + else + if(netaddress_parse(na, 3, vec)) + fatal(0, "invalid RTP address"); +} + +static void rtp_set_netconfig(const char *af, + const char *addr, + const char *port, + const struct netaddress *na) { + uaudio_set(af, NULL); + uaudio_set(addr, NULL); + uaudio_set(port, NULL); + if(na->af != -1) { + int nvec; + char **vec; + + netaddress_format(na, &nvec, &vec); + if(nvec > 0) { + uaudio_set(af, vec[0]); + xfree(vec[0]); + } + if(nvec > 1) { + uaudio_set(addr, vec[1]); + xfree(vec[1]); + } + if(nvec > 2) { + uaudio_set(port, vec[2]); + xfree(vec[2]); + } + xfree(vec); + } +} + static size_t rtp_play(void *buffer, size_t nsamples) { struct rtp_header header; struct iovec vec[2]; @@ -126,52 +173,31 @@ static size_t rtp_play(void *buffer, size_t nsamples) { static void rtp_open(void) { struct addrinfo *res, *sres; - static const struct addrinfo pref = { - .ai_flags = 0, - .ai_family = PF_INET, - .ai_socktype = SOCK_DGRAM, - .ai_protocol = IPPROTO_UDP, - }; - static const struct addrinfo prefbind = { - .ai_flags = AI_PASSIVE, - .ai_family = PF_INET, - .ai_socktype = SOCK_DGRAM, - .ai_protocol = IPPROTO_UDP, - }; static const int one = 1; int sndbuf, target_sndbuf = 131072; socklen_t len; - char *sockname, *ssockname; - struct stringlist dst, src; + struct netaddress dst[1], src[1]; /* Get configuration */ - dst.n = 2; - dst.s = xcalloc(2, sizeof *dst.s); - dst.s[0] = uaudio_get("rtp-destination", NULL); - dst.s[1] = uaudio_get("rtp-destination-port", NULL); - src.n = 2; - src.s = xcalloc(2, sizeof *dst.s); - src.s[0] = uaudio_get("rtp-source", NULL); - src.s[1] = uaudio_get("rtp-source-port", NULL); - if(!dst.s[0]) - fatal(0, "'rtp-destination' not set"); - if(!dst.s[1]) - fatal(0, "'rtp-destination-port' not set"); - if(src.s[0]) { - if(!src.s[1]) - fatal(0, "'rtp-source-port' not set"); - src.n = 2; - } else - src.n = 0; + rtp_get_netconfig("rtp-destination-af", + "rtp-destination", + "rtp-destination-port", + dst); + rtp_get_netconfig("rtp-source-af", + "rtp-source", + "rtp-source-port", + src); rtp_delay_threshold = atoi(uaudio_get("rtp-delay-threshold", "1000")); /* ...microseconds */ /* Resolve addresses */ - res = get_address(&dst, &pref, &sockname); - if(!res) exit(-1); - if(src.n) { - sres = get_address(&src, &prefbind, &ssockname); - if(!sres) exit(-1); + res = netaddress_resolve(dst, 0, IPPROTO_UDP); + if(!res) + exit(-1); + if(src->af != -1) { + sres = netaddress_resolve(src, 1, IPPROTO_UDP); + if(!sres) + exit(-1); } else sres = 0; /* Create the socket */ @@ -206,7 +232,7 @@ static void rtp_open(void) { fatal(0, "unsupported address family %d", res->ai_family); } info("multicasting on %s TTL=%d loop=%s", - sockname, ttl, loop ? "yes" : "no"); + format_sockaddr(res->ai_addr), ttl, loop ? "yes" : "no"); } else { struct ifaddrs *ifs; @@ -225,9 +251,10 @@ static void rtp_open(void) { if(ifs) { if(setsockopt(rtp_fd, SOL_SOCKET, SO_BROADCAST, &one, sizeof one) < 0) fatal(errno, "error setting SO_BROADCAST on broadcast socket"); - info("broadcasting on %s (%s)", sockname, ifs->ifa_name); + info("broadcasting on %s (%s)", + format_sockaddr(res->ai_addr), ifs->ifa_name); } else - info("unicasting on %s", sockname); + info("unicasting on %s", format_sockaddr(res->ai_addr)); } /* Enlarge the socket buffer */ len = sizeof sndbuf; @@ -247,9 +274,11 @@ static void rtp_open(void) { /* We might well want to set additional broadcast- or multicast-related * options here */ if(sres && bind(rtp_fd, sres->ai_addr, sres->ai_addrlen) < 0) - fatal(errno, "error binding broadcast socket to %s", ssockname); + fatal(errno, "error binding broadcast socket to %s", + format_sockaddr(sres->ai_addr)); if(connect(rtp_fd, res->ai_addr, res->ai_addrlen) < 0) - fatal(errno, "error connecting broadcast socket to %s", sockname); + fatal(errno, "error connecting broadcast socket to %s", + format_sockaddr(res->ai_addr)); } static void rtp_start(uaudio_callback *callback, @@ -278,7 +307,8 @@ static void rtp_start(uaudio_callback *callback, rtp_play, 256 / uaudio_sample_size, (NETWORK_BYTES - sizeof(struct rtp_header)) - / uaudio_sample_size); + / uaudio_sample_size, + 0); } static void rtp_stop(void) { @@ -296,13 +326,30 @@ static void rtp_deactivate(void) { uaudio_thread_deactivate(); } +static void rtp_configure(void) { + char buffer[64]; + + rtp_set_netconfig("rtp-destination-af", + "rtp-destination", + "rtp-destination-port", &config->broadcast); + rtp_set_netconfig("rtp-source-af", + "rtp-source", + "rtp-source-port", &config->broadcast_from); + snprintf(buffer, sizeof buffer, "%ld", config->multicast_ttl); + uaudio_set("multicast-ttl", buffer); + uaudio_set("multicast-loop", config->multicast_loop ? "yes" : "no"); + snprintf(buffer, sizeof buffer, "%ld", config->rtp_delay_threshold); + uaudio_set("delay-threshold", buffer); +} + const struct uaudio uaudio_rtp = { .name = "rtp", .options = rtp_options, .start = rtp_start, .stop = rtp_stop, .activate = rtp_activate, - .deactivate = rtp_deactivate + .deactivate = rtp_deactivate, + .configure = rtp_configure, }; /* diff --git a/lib/uaudio-thread.c b/lib/uaudio-thread.c index cd0d727..6bed4a8 100644 --- a/lib/uaudio-thread.c +++ b/lib/uaudio-thread.c @@ -68,6 +68,9 @@ static pthread_t uaudio_collect_thread; /** @brief Playing thread ID */ static pthread_t uaudio_play_thread; +/** @brief Flags */ +static unsigned uaudio_thread_flags; + static uaudio_callback *uaudio_thread_collect_callback; static uaudio_playcallback *uaudio_thread_play_callback; static void *uaudio_thread_userdata; @@ -104,7 +107,8 @@ static void *uaudio_collect_thread_fn(void attribute((unused)) *arg) { /* We are definitely active now */ uaudio_thread_collecting = 1; pthread_cond_broadcast(&uaudio_thread_cond); - while(uaudio_thread_activated) { + while(uaudio_thread_activated + || (uaudio_thread_flags & UAUDIO_THREAD_FAKE_PAUSE)) { if(uaudio_buffers_used() < UAUDIO_THREAD_BUFFERS - 1) { /* At least one buffer is available. We release the lock while * collecting data so that other already-filled buffers can be played @@ -115,12 +119,17 @@ static void *uaudio_collect_thread_fn(void attribute((unused)) *arg) { /* Keep on trying until we get the minimum required amount of data */ b->nsamples = 0; - while(b->nsamples < uaudio_thread_min) { - b->nsamples += uaudio_thread_collect_callback - ((char *)b->samples - + b->nsamples * uaudio_sample_size, - uaudio_thread_max - b->nsamples, - uaudio_thread_userdata); + if(uaudio_thread_activated) { + while(b->nsamples < uaudio_thread_min) { + b->nsamples += uaudio_thread_collect_callback + ((char *)b->samples + + b->nsamples * uaudio_sample_size, + uaudio_thread_max - b->nsamples, + uaudio_thread_userdata); + } + } else if(uaudio_thread_flags & UAUDIO_THREAD_FAKE_PAUSE) { + memset(b->samples, 0, uaudio_thread_min * uaudio_sample_size); + b->nsamples += uaudio_thread_min; } pthread_mutex_lock(&uaudio_thread_lock); /* Advance to next buffer */ @@ -199,13 +208,15 @@ void uaudio_thread_start(uaudio_callback *callback, void *userdata, uaudio_playcallback *playcallback, size_t min, - size_t max) { + size_t max, + unsigned flags) { int e; uaudio_thread_collect_callback = callback; uaudio_thread_userdata = userdata; uaudio_thread_play_callback = playcallback; uaudio_thread_min = min; uaudio_thread_max = max; + uaudio_thread_flags = flags; uaudio_thread_started = 1; for(int n = 0; n < UAUDIO_THREAD_BUFFERS; ++n) uaudio_buffers[n].samples = xcalloc_noptr(uaudio_thread_max, @@ -253,9 +264,10 @@ void uaudio_thread_deactivate(void) { pthread_mutex_lock(&uaudio_thread_lock); uaudio_thread_activated = 0; pthread_cond_broadcast(&uaudio_thread_cond); - - while(uaudio_thread_collecting || uaudio_buffers_used()) - pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + if(!(uaudio_thread_flags & UAUDIO_THREAD_FAKE_PAUSE)) { + while(uaudio_thread_collecting || uaudio_buffers_used()) + pthread_cond_wait(&uaudio_thread_cond, &uaudio_thread_lock); + } pthread_mutex_unlock(&uaudio_thread_lock); } diff --git a/lib/uaudio.c b/lib/uaudio.c index 2f6b67c..e793045 100644 --- a/lib/uaudio.c +++ b/lib/uaudio.c @@ -24,6 +24,7 @@ #include "uaudio.h" #include "hash.h" #include "mem.h" +#include "log.h" /** @brief Options for chosen uaudio API */ static hash *uaudio_options; @@ -50,6 +51,11 @@ size_t uaudio_sample_size; /** @brief Set a uaudio option */ void uaudio_set(const char *name, const char *value) { + if(!value) { + if(uaudio_options) + hash_remove(uaudio_options, name); + return; + } if(!uaudio_options) uaudio_options = hash_new(sizeof(char *)); value = xstrdup(value); @@ -58,10 +64,12 @@ void uaudio_set(const char *name, const char *value) { /** @brief Get a uaudio option */ char *uaudio_get(const char *name, const char *default_value) { - const char *value = (uaudio_options ? - *(char **)hash_find(uaudio_options, name) - : default_value); - return value ? xstrdup(value) : NULL; + if(!uaudio_options) + return default_value ? xstrdup(default_value) : 0; + char **valuep = hash_find(uaudio_options, name); + if(!valuep) + return default_value ? xstrdup(default_value) : 0; + return xstrdup(*valuep); } /** @brief Set sample format diff --git a/lib/uaudio.h b/lib/uaudio.h index 20bfbbc..d6376c8 100644 --- a/lib/uaudio.h +++ b/lib/uaudio.h @@ -34,6 +34,15 @@ extern size_t uaudio_sample_size; * @param max_samples How many samples to supply * @param userdata As passed to uaudio_open() * @return Number of samples filled + * + * This function should not block if possible (better to fill the buffer with + * 0s) and should definitely not block indefinitely. This great caution with + * any locks or syscalls! In particular avoid it taking a lock that may be + * held while any of the @ref uaudio members are called. + * + * If it's more convenient, it's OK to return less than the maximum number of + * samples (including 0) provided you expect to be called again for more + * samples immediately. */ typedef size_t uaudio_callback(void *buffer, size_t max_samples, @@ -114,6 +123,9 @@ struct uaudio { * 0 is silent and 100 is maximum volume. */ void (*set_volume)(int *left, int *right); + + /** @brief Set configuration */ + void (*configure)(void); }; @@ -124,7 +136,17 @@ void uaudio_thread_start(uaudio_callback *callback, void *userdata, uaudio_playcallback *playcallback, size_t min, - size_t max); + size_t max, + unsigned flags); + +/** @brief Fake pauses + * + * This flag is used for audio backends that cannot sensibly be paused. + * The thread support code will supply silence while deactivated in this + * case. + */ +#define UAUDIO_THREAD_FAKE_PAUSE 0x00000001 + void uaudio_thread_stop(void); void uaudio_thread_activate(void); void uaudio_thread_deactivate(void); diff --git a/libtests/t-basen.c b/libtests/t-basen.c index 715490c..3cbc044 100644 --- a/libtests/t-basen.c +++ b/libtests/t-basen.c @@ -18,12 +18,22 @@ #include "test.h" static void test_basen(void) { - unsigned long v[64]; + uint32_t v[64]; char buffer[1024]; v[0] = 999; insist(basen(v, 1, buffer, sizeof buffer, 10) == 0); check_string(buffer, "999"); + memset(v, 0xFF, sizeof v); + insist(nesab(v, 1, buffer, 10) == 0); + check_integer(v[0], 999); + check_integer(v[1], 0xFFFFFFFF); + insist(nesab(v, 4, buffer, 10) == 0); + check_integer(v[0], 0); + check_integer(v[1], 0); + check_integer(v[2], 0); + check_integer(v[3], 999); + check_integer(v[4], 0xFFFFFFFF); v[0] = 1+2*7+3*7*7+4*7*7*7; insist(basen(v, 1, buffer, sizeof buffer, 7) == 0); @@ -35,6 +45,24 @@ static void test_basen(void) { v[3] = 0x0C0D0E0F; insist(basen(v, 4, buffer, sizeof buffer, 256) == 0); check_string(buffer, "123456789abcdef"); + memset(v, 0xFF, sizeof v); + insist(nesab(v, 4, buffer, 256) == 0); + check_integer(v[0], 0x00010203); + check_integer(v[1], 0x04050607); + check_integer(v[2], 0x08090A0B); + check_integer(v[3], 0x0C0D0E0F); + check_integer(v[4], 0xFFFFFFFF); + memset(v, 0xFF, sizeof v); + insist(nesab(v, 8, buffer, 256) == 0); + check_integer(v[0], 0); + check_integer(v[1], 0); + check_integer(v[2], 0); + check_integer(v[3], 0); + check_integer(v[4], 0x00010203); + check_integer(v[5], 0x04050607); + check_integer(v[6], 0x08090A0B); + check_integer(v[7], 0x0C0D0E0F); + check_integer(v[8], 0xFFFFFFFF); v[0] = 0x00010203; v[1] = 0x04050607; @@ -42,6 +70,24 @@ static void test_basen(void) { v[3] = 0x0C0D0E0F; insist(basen(v, 4, buffer, sizeof buffer, 16) == 0); check_string(buffer, "102030405060708090a0b0c0d0e0f"); + memset(v, 0xFF, sizeof v); + insist(nesab(v, 4, buffer, 16) == 0); + check_integer(v[0], 0x00010203); + check_integer(v[1], 0x04050607); + check_integer(v[2], 0x08090A0B); + check_integer(v[3], 0x0C0D0E0F); + check_integer(v[4], 0xFFFFFFFF); + memset(v, 0xFF, sizeof v); + insist(nesab(v, 8, buffer, 16) == 0); + check_integer(v[0], 0); + check_integer(v[1], 0); + check_integer(v[2], 0); + check_integer(v[3], 0); + check_integer(v[4], 0x00010203); + check_integer(v[5], 0x04050607); + check_integer(v[6], 0x08090A0B); + check_integer(v[7], 0x0C0D0E0F); + check_integer(v[8], 0xFFFFFFFF); v[0] = 0x00010203; v[1] = 0x04050607; diff --git a/plugins/Makefile.am b/plugins/Makefile.am index ae3f8eb..7852152 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -25,7 +25,7 @@ notify_la_LDFLAGS=-module disorder_tracklength_la_SOURCES=tracklength.c mad.c madshim.h ../lib/wav.h ../lib/wav.c disorder_tracklength_la_LDFLAGS=-module -disorder_tracklength_la_LIBADD=$(LIBVORBISFILE) $(LIBMAD) $(LIBFLAC) +disorder_tracklength_la_LIBADD=$(LIBVORBISFILE) $(LIBMAD) $(LIBFLAC) -lm fs_la_SOURCES=fs.c fs_la_LDFLAGS=-module diff --git a/server/Makefile.am b/server/Makefile.am index 7dfe99d..5ed5def 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -19,7 +19,7 @@ sbin_PROGRAMS=disorderd disorder-deadlock disorder-rescan disorder-dump \ disorder-speaker disorder-decode disorder-normalize \ disorder-stats disorder-dbupgrade disorder-choose -noinst_PROGRAMS=trackname +noinst_PROGRAMS=trackname endian AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib @@ -39,7 +39,8 @@ disorder_deadlock_DEPENDENCIES=../lib/libdisorder.a disorder_speaker_SOURCES=speaker.c disorder_speaker_LDADD=$(LIBOBJS) ../lib/libdisorder.a \ - $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(COREAUDIO) + $(LIBASOUND) $(LIBPCRE) $(LIBICONV) $(LIBGCRYPT) $(COREAUDIO) \ + $(LIBPTHREAD) disorder_speaker_DEPENDENCIES=../lib/libdisorder.a disorder_decode_SOURCES=decode.c disorder-server.h @@ -116,12 +117,12 @@ check-help: all ./disorder-choose --version > /dev/null # My sox doesn't know MP3 or FLAC unfortunately -check-decode: disorder-decode disorder-normalize +check-decode: disorder-decode disorder-normalize endian echo "speaker_backend network" > config echo "broadcast 127.255.255.255 discard" > config ./disorder-decode ${top_srcdir}/sounds/scratch.ogg | \ ./disorder-normalize --config config > decoded.raw - oggdec -b 16 -e 1 -R -s 1 -o oggdec.raw ${top_srcdir}/sounds/scratch.ogg + oggdec -b 16 -e `./endian` -R -s 1 -o oggdec.raw ${top_srcdir}/sounds/scratch.ogg cmp decoded.raw oggdec.raw sox ${top_srcdir}/sounds/scratch.ogg scratch.wav ./disorder-decode scratch.wav | \ diff --git a/server/disorderd.c b/server/disorderd.c index 72a8e3d..74fc07e 100644 --- a/server/disorderd.c +++ b/server/disorderd.c @@ -209,7 +209,10 @@ int main(int argc, char **argv) { fix_path(); srand(time(0)); /* don't start the same every time */ /* gcrypt initialization */ + if(!gcry_check_version(NULL)) + disorder_fatal(0, "gcry_check_version failed"); gcry_control(GCRYCTL_INIT_SECMEM, 1); + gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); /* make sure we can't have more than FD_SETSIZE files open (event.c does * check but this provides an additional line of defence) */ if(getrlimit(RLIMIT_NOFILE, rl) < 0) diff --git a/server/endian.c b/server/endian.c new file mode 100644 index 0000000..2e4b152 --- /dev/null +++ b/server/endian.c @@ -0,0 +1,20 @@ +#include +#include + +int main(void) { +#if WORDS_BIGENDIAN + puts("1"); +#else + puts("0"); +#endif + return 0; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/server/server.c b/server/server.c index 345c2ca..8bd23e7 100644 --- a/server/server.c +++ b/server/server.c @@ -17,13 +17,18 @@ */ #include "disorder-server.h" +#include "basen.h" #ifndef NONCE_SIZE # define NONCE_SIZE 16 #endif #ifndef CONFIRM_SIZE -# define CONFIRM_SIZE 10 +/** @brief Size of nonce in confirmation string in 32-bit words + * + * 64 bits gives 11 digits (in base 62). + */ +# define CONFIRM_SIZE 2 #endif int volume_left, volume_right; /* last known volume */ @@ -829,7 +834,7 @@ static int c_volume(struct conn *c, sink_writes(ev_writer_sink(c->w), "550 error accessing mixer\n"); return 1; } - api->set_volume(&l, &r); + (set ? api->set_volume : api->get_volume)(&l, &r); sink_printf(ev_writer_sink(c->w), "252 %d %d\n", l, r); if(l != volume_left || r != volume_right) { volume_left = l; @@ -1100,9 +1105,12 @@ static int c_rtp_address(struct conn *c, char attribute((unused)) **vec, int attribute((unused)) nvec) { if(api == &uaudio_rtp) { + char **addr; + + netaddress_format(&config->broadcast, NULL, &addr); sink_printf(ev_writer_sink(c->w), "252 %s %s\n", - quoteutf8(config->broadcast.s[0]), - quoteutf8(config->broadcast.s[1])); + quoteutf8(addr[1]), + quoteutf8(addr[2])); } else sink_writes(ev_writer_sink(c->w), "550 No RTP\n"); return 1; @@ -1319,30 +1327,23 @@ static int c_users(struct conn *c, return 1; /* completed */ } -/** @brief Base64 mapping table for confirmation strings - * - * This is used with generic_to_base64() and generic_base64(). We cannot use - * the MIME table as that contains '+' and '=' which get quoted when - * URL-encoding. (The CGI still does the URL encoding but it is desirable to - * avoid it being necessary.) - */ -static const char confirm_base64_table[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/.*"; - static int c_register(struct conn *c, char **vec, int attribute((unused)) nvec) { - char *buf, *cs; - size_t bufsize; - int offset; - - /* The confirmation string is base64(username;nonce) */ - bufsize = strlen(vec[0]) + CONFIRM_SIZE + 2; - buf = xmalloc_noptr(bufsize); - offset = byte_snprintf(buf, bufsize, "%s;", vec[0]); - gcry_randomize(buf + offset, CONFIRM_SIZE, GCRY_STRONG_RANDOM); - cs = generic_to_base64((uint8_t *)buf, offset + CONFIRM_SIZE, - confirm_base64_table); + char *cs; + uint32_t nonce[CONFIRM_SIZE]; + char nonce_str[(32 * CONFIRM_SIZE) / 5 + 1]; + + /* The confirmation string is username/base62(nonce). The confirmation + * process will pick the username back out to identify them but the _whole_ + * string is used as the confirmation string. Base 62 means we used only + * letters and digits, minimizing the chance of the URL being mispasted. */ + gcry_randomize(nonce, sizeof nonce, GCRY_STRONG_RANDOM); + if(basen(nonce, CONFIRM_SIZE, nonce_str, sizeof nonce_str, 62)) { + error(0, "buffer too small encoding confirmation string"); + sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n"); + } + byte_xasprintf(&cs, "%s/%s", vec[0], nonce_str); if(trackdb_adduser(vec[0], vec[1], config->default_rights, vec[2], cs)) sink_writes(ev_writer_sink(c->w), "550 Cannot create user\n"); else @@ -1353,7 +1354,6 @@ static int c_register(struct conn *c, static int c_confirm(struct conn *c, char **vec, int attribute((unused)) nvec) { - size_t nuser; char *user, *sep; rights_type rights; const char *host; @@ -1363,12 +1363,12 @@ static int c_confirm(struct conn *c, sink_writes(ev_writer_sink(c->w), "530 Authentication failure\n"); return 1; } - if(!(user = generic_base64(vec[0], &nuser, confirm_base64_table)) - || !(sep = memchr(user, ';', nuser))) { + /* Picking the LAST / means we don't (here) rule out slashes in usernames. */ + if(!(sep = strrchr(vec[0], '/'))) { sink_writes(ev_writer_sink(c->w), "550 Malformed confirmation string\n"); return 1; } - *sep = 0; + user = xstrndup(vec[0], sep - vec[0]); if(trackdb_confirm(user, vec[0], &rights)) sink_writes(ev_writer_sink(c->w), "550 Incorrect confirmation string\n"); else { @@ -1850,6 +1850,7 @@ int server_start(ev_source *ev, int pf, l->pf = pf; if(ev_listen(ev, fd, listen_callback, l, "server listener")) exit(EXIT_FAILURE); + info("listening on %s", name); return fd; } diff --git a/server/speaker.c b/server/speaker.c index 6a212dd..b41c94f 100644 --- a/server/speaker.c +++ b/server/speaker.c @@ -41,6 +41,11 @@ * obvious way. If the callback finds itself required to play when there is no * playing track it returns dead air. * + * To implement gapless playback, the server is notified that a track has + * finished slightly early. @ref SM_PLAY is therefore allowed to arrive while + * the previous track is still playing provided an early @ref SM_FINISHED has + * been sent for it. + * * @b Encodings. The encodings supported depend entirely on the uaudio backend * chosen. See @ref uaudio.h, etc. * @@ -79,6 +84,8 @@ #include #include #include +#include +#include #include "configuration.h" #include "syscalls.h" @@ -94,6 +101,12 @@ /** @brief Maximum number of FDs to poll for */ #define NFDS 1024 +/** @brief Number of bytes before end of track to send SM_FINISHED + * + * Generally set to 1 second. + */ +static size_t early_finish; + /** @brief Track structure * * Known tracks are kept in a linked list. Usually there will be at most two @@ -131,6 +144,14 @@ struct track { * out life not playable. */ int playable; + + /** @brief Set when finished + * + * This is set when we've notified the server that the track is finished. + * Once this has happened (typically very late in the track's lifetime) the + * track cannot be paused or cancelled. + */ + int finished; /** @brief Input buffer * @@ -142,9 +163,13 @@ struct track { /** @brief Lock protecting data structures * * This lock protects values shared between the main thread and the callback. - * It is needed e.g. if changing @ref playing or if modifying buffer pointers. - * It is not needed to add a new track, to read values only modified in the - * same thread, etc. + * + * It is held 'all' the time by the main thread, the exceptions being when + * called activate/deactivate callbacks and when calling (potentially) slow + * system calls (in particular poll(), where in fact the main thread will spend + * most of its time blocked). + * + * The callback holds it when it's running. */ static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; @@ -153,11 +178,17 @@ static struct track *tracks; /** @brief Playing track, or NULL * - * This means the DESIRED playing track. It does not reflect any other state - * (e.g. activation of uaudio backend). + * This means the track the speaker process intends to play. It does not + * reflect any other state (e.g. activation of uaudio backend). */ static struct track *playing; +/** @brief Pending playing track, or NULL + * + * This means the track the server wants the speaker to play. + */ +static struct track *pending_playing; + /** @brief Array of file descriptors for poll() */ static struct pollfd fds[NFDS]; @@ -272,7 +303,6 @@ static int speaker_fill(struct track *t) { t->id, t->eof, t->used)); if(t->eof) return -1; - pthread_mutex_lock(&lock); if(t->used < sizeof t->buffer) { /* there is room left in the buffer */ where = (t->start + t->used) % sizeof t->buffer; @@ -307,7 +337,6 @@ static int speaker_fill(struct track *t) { rc = 0; } } - pthread_mutex_unlock(&lock); return rc; } @@ -315,10 +344,15 @@ static int speaker_fill(struct track *t) { * * We want to play audio if there is a current track; and it is not paused; and * it is playable according to the rules for @ref track::playable. + * + * We don't allow tracks to be paused if we've already told the server we've + * finished them; that would cause such tracks to survive much longer than the + * few samples they're supposed to, with report() remaining silent for the + * duration. */ static int playable(void) { return playing - && !paused + && (!paused || playing->finished) && playing->playable; } @@ -327,15 +361,17 @@ static void report(void) { struct speaker_message sm; if(playing) { + /* Had better not send a report for a track that the server thinks has + * finished, that would be confusing. */ + if(playing->finished) + return; memset(&sm, 0, sizeof sm); sm.type = paused ? SM_PAUSED : SM_PLAYING; strcpy(sm.id, playing->id); - pthread_mutex_lock(&lock); sm.data = playing->played / (uaudio_rate * uaudio_channels); - pthread_mutex_unlock(&lock); speaker_send(1, &sm); + time(&last_report); } - time(&last_report); } /** @brief Add a file descriptor to the set to poll() for @@ -401,6 +437,10 @@ static size_t speaker_callback(void *buffer, if(!provided_samples) { memset(buffer, 0, max_bytes); provided_samples = max_samples; + if(playing) + info("%zu samples silence, playing->used=%zu", provided_samples, playing->used); + else + info("%zu samples silence, playing=NULL", provided_samples); } pthread_mutex_unlock(&lock); return provided_samples; @@ -413,13 +453,14 @@ static void mainloop(void) { int n, fd, stdin_slot, timeout, listen_slot, sigpipe_slot; /* Keep going while our parent process is alive */ + pthread_mutex_lock(&lock); while(getppid() != 1) { int force_report = 0; fdno = 0; - /* By default we will wait up to a second before thinking about current - * state. */ - timeout = 1000; + /* By default we will wait up to half a second before thinking about + * current state. */ + timeout = 500; /* Always ready for commands from the main server. */ stdin_slot = addfd(0, POLLIN); /* Also always ready for inbound connections */ @@ -445,9 +486,11 @@ static void mainloop(void) { } else t->slot = -1; } - sigpipe_slot = addfd(sigpipe[1], POLLIN); + sigpipe_slot = addfd(sigpipe[0], POLLIN); /* Wait for something interesting to happen */ + pthread_mutex_unlock(&lock); n = poll(fds, fdno, timeout); + pthread_mutex_lock(&lock); if(n < 0) { if(errno == EINTR) continue; fatal(errno, "error calling poll"); @@ -493,18 +536,32 @@ static void mainloop(void) { * this won't be the case, so we don't bother looping around to pick them * all up. */ n = speaker_recv(0, &sm); - /* TODO */ if(n > 0) + /* As a rule we don't send success replies to most commands - we just + * force the regular status update to be sent immediately rather than + * on schedule. */ switch(sm.type) { case SM_PLAY: - if(playing) - fatal(0, "got SM_PLAY but already playing something"); + /* SM_PLAY is only allowed if the server reasonably believes that + * nothing is playing */ + if(playing) { + /* If finished isn't set then the server can't believe that this + * track has finished */ + if(!playing->finished) + fatal(0, "got SM_PLAY but already playing something"); + /* If pending_playing is set then the server must believe that that + * is playing */ + if(pending_playing) + fatal(0, "got SM_PLAY but have a pending playing track"); + } t = findtrack(sm.id, 1); D(("SM_PLAY %s fd %d", t->id, t->fd)); if(t->fd == -1) error(0, "cannot play track because no connection arrived"); - playing = t; - force_report = 1; + pending_playing = t; + /* If nothing is currently playing then we'll switch to the pending + * track below so there's no point distinguishing the situations + * here. */ break; case SM_PAUSE: D(("SM_PAUSE")); @@ -520,11 +577,15 @@ static void mainloop(void) { D(("SM_CANCEL %s", sm.id)); t = removetrack(sm.id); if(t) { - pthread_mutex_lock(&lock); - if(t == playing) { - /* scratching the playing track */ + if(t == playing || t == pending_playing) { + /* Scratching the track that the server believes is playing, + * which might either be the actual playing track or a pending + * playing track */ sm.type = SM_FINISHED; - playing = 0; + if(t == playing) + playing = 0; + else + pending_playing = 0; } else { /* Could be scratching the playing track before it's quite got * going, or could be just removing a track from the queue. We @@ -536,7 +597,6 @@ static void mainloop(void) { } strcpy(sm.id, t->id); destroy(t); - pthread_mutex_unlock(&lock); } else { /* Probably scratching the playing track well before it's got * going, but could indicate a bug, so we log this as an error. */ @@ -569,29 +629,47 @@ static void mainloop(void) { read(sigpipe[0], buffer, sizeof buffer); } - if(playing && playing->used == 0 && playing->eof) { - /* The playing track is done. Tell the server, and destroy it. */ + /* Send SM_FINISHED when we're near the end of the track. + * + * This is how we implement gapless play; we hope that the SM_PLAY from the + * server arrives before the remaining bytes of the track play out. + */ + if(playing + && playing->eof + && !playing->finished + && playing->used <= early_finish) { memset(&sm, 0, sizeof sm); sm.type = SM_FINISHED; strcpy(sm.id, playing->id); speaker_send(1, &sm); + playing->finished = 1; + } + /* When the track is actually finished, deconfigure it */ + if(playing && playing->eof && !playing->used) { removetrack(playing->id); - pthread_mutex_lock(&lock); destroy(playing); playing = 0; - pthread_mutex_unlock(&lock); - /* The server will presumalby send as an SM_PLAY by return */ + } + /* Act on the pending SM_PLAY */ + if(!playing && pending_playing) { + playing = pending_playing; + pending_playing = 0; + force_report = 1; } /* Impose any state change required by the above */ if(playable()) { if(!activated) { activated = 1; + pthread_mutex_unlock(&lock); backend->activate(); + pthread_mutex_lock(&lock); } } else { if(activated) { activated = 0; + pthread_mutex_unlock(&lock); backend->deactivate(); + pthread_mutex_lock(&lock); } } /* If we've not reported our state for a second do so now. */ @@ -651,6 +729,11 @@ int main(int argc, char **argv) { info("set RLIM_NOFILE to %lu", (unsigned long)rl->rlim_cur); } else info("RLIM_NOFILE is %lu", (unsigned long)rl->rlim_cur); + /* gcrypt initialization */ + if(!gcry_check_version(NULL)) + disorder_fatal(0, "gcry_check_version failed"); + gcry_control(GCRYCTL_INIT_SECMEM, 0); + gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); /* create a pipe between the backend callback and the poll() loop */ xpipe(sigpipe); nonblock(sigpipe[0]); @@ -659,9 +742,12 @@ int main(int argc, char **argv) { config->sample_format.channels, config->sample_format.bits, config->sample_format.bits != 8); + early_finish = uaudio_sample_size * uaudio_channels * uaudio_rate; /* TODO other parameters! */ backend = uaudio_find(config->api); /* backend-specific initialization */ + if(backend->configure) + backend->configure(); backend->start(speaker_callback, NULL); /* create the socket directory */ byte_xasprintf(&dir, "%s/speaker", config->home); diff --git a/server/state.c b/server/state.c index a0ea660..9a93dc9 100644 --- a/server/state.c +++ b/server/state.c @@ -23,8 +23,15 @@ static const char *current_unix; static int current_unix_fd; -static struct addrinfo *current_listen_addrinfo; -static int current_listen_fd; +/** @brief TCP listener definition */ +struct listener { + struct listener *next; + struct sockaddr *sa; + int fd; +}; + +/** @brief Current listeners */ +static struct listener *listeners; /** @brief Current audio API */ const struct uaudio *api; @@ -38,18 +45,17 @@ void quit(ev_source *ev) { exit(0); } +static struct sockaddr *copy_sockaddr(const struct addrinfo *addr) { + struct sockaddr *sa = xmalloc_noptr(addr->ai_addrlen); + memcpy(sa, addr->ai_addr, addr->ai_addrlen); + return sa; +} + static void reset_socket(ev_source *ev) { const char *new_unix; - struct addrinfo *res; + struct addrinfo *res, *r; + struct listener *l, **ll; struct sockaddr_un sun; - char *name; - - static const struct addrinfo pref = { - .ai_flags = AI_PASSIVE, - .ai_family = PF_INET, - .ai_socktype = SOCK_STREAM, - .ai_protocol = IPPROTO_TCP, - }; /* unix first */ new_unix = config_get_file("socket"); @@ -80,28 +86,42 @@ static void reset_socket(ev_source *ev) { } /* get the new listen config */ - if(config->listen.n) - res = get_address(&config->listen, &pref, &name); + if(config->listen.af != -1) + res = netaddress_resolve(&config->listen, 1, IPPROTO_TCP); else res = 0; - if((res && !current_listen_addrinfo) - || (current_listen_addrinfo - && (!res - || addrinfocmp(res, current_listen_addrinfo)))) { - /* something has to change */ - if(current_listen_addrinfo) { - /* delete the old listener */ - server_stop(ev, current_listen_fd); - freeaddrinfo(current_listen_addrinfo); - current_listen_addrinfo = 0; + /* Close any current listeners that aren't required any more */ + ll = &listeners; + while((l = *ll)) { + for(r = res; r; r = r->ai_next) + if(!sockaddrcmp(r->ai_addr, l->sa)) + break; + if(!r) { + /* Didn't find a match, remove this one */ + server_stop(ev, l->fd); + *ll = l->next; + } else { + /* This address is still wanted */ + ll = &l->next; } - if(res) { - /* start the new listener */ - if((current_listen_fd = server_start(ev, res->ai_family, res->ai_addrlen, - res->ai_addr, name)) >= 0) { - current_listen_addrinfo = res; - res = 0; + } + + /* Open any new listeners that are required */ + for(r = res; r; r = r->ai_next) { + for(l = listeners; l; l = l->next) + if(!sockaddrcmp(r->ai_addr, l->sa)) + break; + if(!l) { + /* Didn't find a match, need a new listener */ + int fd = server_start(ev, r->ai_family, r->ai_addrlen, r->ai_addr, + format_sockaddr(r->ai_addr)); + if(fd >= 0) { + l = xmalloc(sizeof *l); + l->next = listeners; + l->sa = copy_sockaddr(r); + l->fd = fd; + listeners = l; } } } @@ -135,6 +155,8 @@ int reconfigure(ev_source *ev, int reload) { /* We only allow for upgrade at startup */ trackdb_open(TRACKDB_CAN_UPGRADE); api = uaudio_find(config->api); + if(api->configure) + api->configure(); if(api->open_mixer) api->open_mixer(); if(need_another_rescan) diff --git a/tests/dtest.py b/tests/dtest.py index 1c62dc5..d2bb201 100644 --- a/tests/dtest.py +++ b/tests/dtest.py @@ -190,7 +190,7 @@ tracklength *.mp3 disorder-tracklength tracklength *.ogg disorder-tracklength tracklength *.wav disorder-tracklength tracklength *.flac disorder-tracklength -api network +api rtp broadcast 127.0.0.1 %d broadcast_from 127.0.0.1 %d mail_sender no.such.user.sorry@greenend.org.uk