|
|
|
/*
|
|
|
|
* nncmpp -- the MPD client you never knew you needed
|
|
|
|
*
|
|
|
|
* Copyright (c) 2016 - 2023, Přemysl Eric Janouch <p@janouch.name>
|
|
|
|
*
|
|
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
* purpose with or without fee is hereby granted.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
|
|
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
|
|
|
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
|
|
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
// We "need" to have an enum for attributes before including liberty.
|
|
|
|
// Avoiding colours in the defaults here in order to support dumb terminals.
|
|
|
|
#define ATTRIBUTE_TABLE(XX) \
|
|
|
|
XX( NORMAL, normal, -1, -1, 0 ) \
|
|
|
|
XX( HIGHLIGHT, highlight, -1, -1, A_BOLD ) \
|
|
|
|
/* Gauge */ \
|
|
|
|
XX( ELAPSED, elapsed, -1, -1, A_REVERSE ) \
|
|
|
|
XX( REMAINS, remains, -1, -1, A_UNDERLINE ) \
|
|
|
|
/* Tab bar */ \
|
|
|
|
XX( TAB_BAR, tab_bar, -1, -1, A_REVERSE ) \
|
|
|
|
XX( TAB_ACTIVE, tab_active, -1, -1, A_UNDERLINE ) \
|
|
|
|
/* Listview */ \
|
|
|
|
XX( HEADER, header, -1, -1, A_UNDERLINE ) \
|
|
|
|
XX( EVEN, even, -1, -1, 0 ) \
|
|
|
|
XX( ODD, odd, -1, -1, 0 ) \
|
|
|
|
XX( DIRECTORY, directory, -1, -1, 0 ) \
|
|
|
|
XX( SELECTION, selection, -1, -1, A_REVERSE ) \
|
|
|
|
/* Cyan is good with both black and white.
|
|
|
|
* Can't use A_REVERSE because bold'd be bright.
|
|
|
|
* Unfortunately ran out of B&W attributes. */ \
|
|
|
|
XX( MULTISELECT, multiselect, -1, 6, 0 ) \
|
|
|
|
/* This ought to be indicative enough. */ \
|
|
|
|
XX( DEFOCUSED, defocused, -1, -1, A_UNDERLINE ) \
|
|
|
|
XX( SCROLLBAR, scrollbar, -1, -1, 0 ) \
|
|
|
|
/* These are for debugging only */ \
|
|
|
|
XX( WARNING, warning, 3, -1, 0 ) \
|
|
|
|
XX( ERROR, error, 1, -1, 0 ) \
|
|
|
|
XX( INCOMING, incoming, 2, -1, 0 ) \
|
|
|
|
XX( OUTGOING, outgoing, 4, -1, 0 )
|
|
|
|
|
|
|
|
enum
|
|
|
|
{
|
|
|
|
#define XX(name, config, fg_, bg_, attrs_) ATTRIBUTE_ ## name,
|
|
|
|
ATTRIBUTE_TABLE (XX)
|
|
|
|
#undef XX
|
|
|
|
ATTRIBUTE_COUNT
|
|
|
|
};
|
|
|
|
|
|
|
|
// My battle-tested C framework acting as a GLib replacement. Its one big
|
|
|
|
// disadvantage is missing support for i18n but that can eventually be added
|
|
|
|
// as an optional feature. Localised applications look super awkward, though.
|
|
|
|
|
|
|
|
// User data for logger functions to enable formatted logging
|
|
|
|
#define print_fatal_data ((void *) ATTRIBUTE_ERROR)
|
|
|
|
#define print_error_data ((void *) ATTRIBUTE_ERROR)
|
|
|
|
#define print_warning_data ((void *) ATTRIBUTE_WARNING)
|
|
|
|
|
|
|
|
#define LIBERTY_WANT_POLLER
|
|
|
|
#define LIBERTY_WANT_ASYNC
|
|
|
|
#define LIBERTY_WANT_PROTO_HTTP
|
|
|
|
#define LIBERTY_WANT_PROTO_MPD
|
|
|
|
#include "liberty/liberty.c"
|
|
|
|
#include "liberty/liberty-tui.c"
|
|
|
|
|
|
|
|
#define HAVE_LIBERTY
|
|
|
|
#include "line-editor.c"
|
|
|
|
|
|
|
|
#include <dirent.h>
|
|
|
|
#include <locale.h>
|
|
|
|
#include <math.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
#include <termios.h>
|
|
|
|
|
|
|
|
// ncurses is notoriously retarded for input handling, we need something
|
|
|
|
// different if only to receive mouse events reliably.
|
|
|
|
//
|
|
|
|
// 2021 update: ncurses is mostly reliable now, though rxvt-unicode only
|
|
|
|
// supports the 1006 mode that ncurses also supports mode starting with 9.25.
|
|
|
|
#include "termo.h"
|
|
|
|
|
|
|
|
// We need cURL to extract links from Internet stream playlists. It'd be way
|
|
|
|
// too much code to do this all by ourselves, and there's nothing better around.
|
|
|
|
#include <curl/curl.h>
|
|
|
|
|
|
|
|
// The spectrum analyser requires a DFT transform. The FFTW library is fairly
|
|
|
|
// efficient, and doesn't have a requirement on the number of bins.
|
|
|
|
#ifdef WITH_FFTW
|
|
|
|
#include <fftw3.h>
|
|
|
|
#endif // WITH_FFTW
|
|
|
|
|
|
|
|
// Remote MPD control needs appropriate volume controls.
|
|
|
|
#ifdef WITH_PULSE
|
|
|
|
#include "liberty/liberty-pulse.c"
|
|
|
|
#include <pulse/context.h>
|
|
|
|
#include <pulse/error.h>
|
|
|
|
#include <pulse/introspect.h>
|
|
|
|
#include <pulse/subscribe.h>
|
|
|
|
#include <pulse/sample.h>
|
|
|
|
#endif // WITH_PULSE
|
|
|
|
|
|
|
|
// Elementary port of the TUI to X11.
|
|
|
|
#ifdef WITH_X11
|
|
|
|
#include <X11/Xatom.h>
|
|
|
|
#include <X11/Xlib.h>
|
|
|
|
#include <X11/keysym.h>
|
|
|
|
#include <X11/XKBlib.h>
|
|
|
|
#include <X11/Xft/Xft.h>
|
|
|
|
#endif // WITH_X11
|
|
|
|
|
|
|
|
#define APP_TITLE PROGRAM_NAME ///< Left top corner
|
|
|
|
|
|
|
|
#include "nncmpp-actions.h"
|
|
|
|
|
|
|
|
// --- Utilities ---------------------------------------------------------------
|
|
|
|
|
|
|
|
static int64_t
|
|
|
|
clock_msec (clockid_t clock)
|
|
|
|
{
|
|
|
|
struct timespec tp;
|
|
|
|
hard_assert (clock_gettime (clock, &tp) != -1);
|
|
|
|
return (int64_t) tp.tv_sec * 1000 + (int64_t) tp.tv_nsec / 1000000;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
shell_quote (const char *str, struct str *output)
|
|
|
|
{
|
|
|
|
// See SUSv3 Shell and Utilities, 2.2.3 Double-Quotes
|
|
|
|
str_append_c (output, '"');
|
|
|
|
for (const char *p = str; *p; p++)
|
|
|
|
{
|
|
|
|
if (strchr ("`$\"\\", *p))
|
|
|
|
str_append_c (output, '\\');
|
|
|
|
str_append_c (output, *p);
|
|
|
|
}
|
|
|
|
str_append_c (output, '"');
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
xstrtoul_map (const struct str_map *map, const char *key, unsigned long *out)
|
|
|
|
{
|
|
|
|
const char *field = str_map_find (map, key);
|
|
|
|
return field && xstrtoul (out, field, 10);
|
|
|
|
}
|
|
|
|
|
|
|
|
static const char *
|
|
|
|
xbasename (const char *path)
|
|
|
|
{
|
|
|
|
const char *last_slash = strrchr (path, '/');
|
|
|
|
return last_slash ? last_slash + 1 : path;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *xstrdup0 (const char *s) { return s ? xstrdup (s) : NULL; }
|
|
|
|
|
|
|
|
static char *
|
|
|
|
latin1_to_utf8 (const char *latin1)
|
|
|
|
{
|
|
|
|
struct str converted = str_make ();
|
|
|
|
while (*latin1)
|
|
|
|
{
|
|
|
|
uint8_t c = *latin1++;
|
|
|
|
if (c < 0x80)
|
|
|
|
str_append_c (&converted, c);
|
|
|
|
else
|
|
|
|
{
|
|
|
|
str_append_c (&converted, 0xC0 | (c >> 6));
|
|
|
|
str_append_c (&converted, 0x80 | (c & 0x3F));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return str_steal (&converted);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
str_enforce_utf8 (struct str *self)
|
|
|
|
{
|
|
|
|
if (!utf8_validate (self->str, self->len))
|
|
|
|
{
|
|
|
|
char *sanitized = latin1_to_utf8 (self->str);
|
|
|
|
str_reset (self);
|
|
|
|
str_append (self, sanitized);
|
|
|
|
free (sanitized);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
cstr_uncapitalize (char *s)
|
|
|
|
{
|
|
|
|
if (isupper (s[0]) && islower (s[1]))
|
|
|
|
s[0] = tolower_ascii (s[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
print_curl_debug (CURL *easy, curl_infotype type, char *data, size_t len,
|
|
|
|
void *ud)
|
|
|
|
{
|
|
|
|
(void) easy;
|
|
|
|
(void) ud;
|
|
|
|
(void) type;
|
|
|
|
|
|
|
|
char copy[len + 1];
|
|
|
|
for (size_t i = 0; i < len; i++)
|
|
|
|
{
|
|
|
|
uint8_t c = data[i];
|
|
|
|
copy[i] = !iscntrl_ascii (c) || c == '\n' ? c : '.';
|
|
|
|
}
|
|
|
|
copy[len] = '\0';
|
|
|
|
|
|
|
|
char *next;
|
|
|
|
for (char *p = copy; p; p = next)
|
|
|
|
{
|
|
|
|
if ((next = strchr (p, '\n')))
|
|
|
|
*next++ = '\0';
|
|
|
|
if (!*p)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (!utf8_validate (p, strlen (p)))
|
|
|
|
{
|
|
|
|
char *fixed = latin1_to_utf8 (p);
|
|
|
|
print_debug ("cURL: %s", fixed);
|
|
|
|
free (fixed);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
print_debug ("cURL: %s", p);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
|
|
mpd_parse_kv (char *line, char **value)
|
|
|
|
{
|
|
|
|
char *key = mpd_client_parse_kv (line, value);
|
|
|
|
if (!key) print_debug ("%s: %s", "erroneous MPD output", line);
|
|
|
|
return key;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
mpd_read_time (const char *value, int *sec, int *optional_msec)
|
|
|
|
{
|
|
|
|
if (!value)
|
|
|
|
return;
|
|
|
|
|
|
|
|
char *end = NULL;
|
|
|
|
long n = strtol (value, &end, 10);
|
|
|
|
if (n < 0 || (*end && *end != '.'))
|
|
|
|
return;
|
|
|
|
|
|
|
|
int msec = 0;
|
|
|
|
if (*end == '.')
|
|
|
|
{
|
|
|
|
// In practice, MPD always uses three decimal digits
|
|
|
|
size_t digits = strspn (++end, "0123456789");
|
|
|
|
if (end[digits])
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (digits--) msec += (*end++ - '0') * 100;
|
|
|
|
if (digits--) msec += (*end++ - '0') * 10;
|
|
|
|
if (digits--) msec += *end++ - '0';
|
|
|
|
}
|
|
|
|
|
|
|
|
*sec = MIN (INT_MAX, n);
|
|
|
|
if (optional_msec)
|
|
|
|
*optional_msec = msec;
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- cURL async wrapper ------------------------------------------------------
|
|
|
|
|
|
|
|
// You are meant to subclass this structure, no user_data pointers needed
|
|
|
|
struct poller_curl_task;
|
|
|
|
|
|
|
|
/// Receives notification for finished transfers
|
|
|
|
typedef void (*poller_curl_done_fn)
|
|
|
|
(CURLMsg *msg, struct poller_curl_task *task);
|
|
|
|
|
|
|
|
struct poller_curl_task
|
|
|
|
{
|
|
|
|
CURL *easy; ///< cURL easy interface handle
|
|
|
|
char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
|
|
|
|
poller_curl_done_fn on_done; ///< Done callback
|
|
|
|
};
|
|
|
|
|
|
|
|
struct poller_curl_fd
|
|
|
|
{
|
|
|
|
LIST_HEADER (struct poller_curl_fd)
|
|
|
|
struct poller_fd fd; ///< Poller FD
|
|
|
|
};
|
|
|
|
|
|
|
|
struct poller_curl
|
|
|
|
{
|
|
|
|
struct poller *poller; ///< Parent poller
|
|
|
|
struct poller_timer timer; ///< cURL timer
|
|
|
|
CURLM *multi; ///< cURL multi interface handle
|
|
|
|
struct poller_curl_fd *fds; ///< List of all FDs
|
|
|
|
|
|
|
|
// TODO: also make sure to dispose of them at the end of the program
|
|
|
|
|
|
|
|
int registered; ///< Number of attached easy handles
|
|
|
|
};
|
|
|
|
|
|
|
|
static void
|
|
|
|
poller_curl_collect (struct poller_curl *self, curl_socket_t s, int ev_bitmask)
|
|
|
|
{
|
|
|
|
int running = 0;
|
|
|
|
CURLMcode res;
|
|
|
|
// XXX: ignoring errors, in particular CURLM_CALL_MULTI_PERFORM
|
|
|
|
if ((res = curl_multi_socket_action (self->multi, s, ev_bitmask, &running)))
|
|
|
|
print_debug ("cURL: %s", curl_multi_strerror (res));
|
|
|
|
|
|
|
|
CURLMsg *msg;
|
|
|
|
while ((msg = curl_multi_info_read (self->multi, &running)))
|
|
|
|
if (msg->msg == CURLMSG_DONE)
|
|
|
|
{
|
|
|
|
struct poller_curl_task *task = NULL;
|
|
|
|
hard_assert (!curl_easy_getinfo
|
|
|
|
(msg->easy_handle, CURLINFO_PRIVATE, &task));
|
|
|
|
task->on_done (msg, task);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
poller_curl_on_socket (const struct pollfd *pfd, void *user_data)
|
|
|
|
{
|
|
|
|
int mask = 0;
|
|
|
|
if (pfd->revents & POLLIN) mask |= CURL_CSELECT_IN;
|
|
|
|
if (pfd->revents & POLLOUT) mask |= CURL_CSELECT_OUT;
|
|
|
|
if (pfd->revents & POLLERR) mask |= CURL_CSELECT_ERR;
|
|
|
|
poller_curl_collect (user_data, pfd->fd, mask);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
poller_curl_on_socket_action (CURL *easy, curl_socket_t s, int what,
|
|
|
|
void *user_data, void *socket_data)
|
|
|
|
{
|
|
|
|
(void) easy;
|
|
|
|
struct poller_curl *self = user_data;
|
|
|
|
|
|
|
|
struct poller_curl_fd *fd;
|
|
|
|
if (!(fd = socket_data))
|
|
|
|
{
|
|
|
|
set_cloexec (s);
|
|
|
|
|
|
|
|
fd = xmalloc (sizeof *fd);
|
|
|
|
LIST_PREPEND (self->fds, fd);
|
|
|
|
|
|
|
|
fd->fd = poller_fd_make (self->poller, s);
|
|
|
|
fd->fd.dispatcher = poller_curl_on_socket;
|
|
|
|
fd->fd.user_data = self;
|
|
|
|
curl_multi_assign (self->multi, s, fd);
|
|
|
|
}
|
|
|
|
if (what == CURL_POLL_REMOVE)
|
|
|
|
{
|
|
|
|
// Some annoying cURL bug. Never trust libraries.
|
|
|
|
fd->fd.closed = fcntl(fd->fd.fd, F_GETFL) < 0 && errno == EBADF;
|
|
|
|
|
|
|
|
poller_fd_reset (&fd->fd);
|
|
|
|
LIST_UNLINK (self->fds, fd);
|
|
|
|
free (fd);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
short events = 0;
|
|
|
|
if (what == CURL_POLL_IN) events = POLLIN;
|
|
|
|
if (what == CURL_POLL_OUT) events = POLLOUT;
|
|
|
|
if (what == CURL_POLL_INOUT) events = POLLIN | POLLOUT;
|
|
|
|
poller_fd_set (&fd->fd, events);
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
poller_curl_on_timer (void *user_data)
|
|
|
|
{
|
|
|
|
poller_curl_collect (user_data, CURL_SOCKET_TIMEOUT, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
poller_curl_on_timer_change (CURLM *multi, long timeout_ms, void *user_data)
|
|
|
|
{
|
|
|
|
(void) multi;
|
|
|
|
struct poller_curl *self = user_data;
|
|
|
|
|
|
|
|
if (timeout_ms < 0)
|
|
|
|
poller_timer_reset (&self->timer);
|
|
|
|
else
|
|
|
|
poller_timer_set (&self->timer, timeout_ms);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static bool
|
|
|
|
poller_curl_init (struct poller_curl *self, struct poller *poller,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
memset (self, 0, sizeof *self);
|
|
|
|
if (!(self->multi = curl_multi_init ()))
|
|
|
|
return error_set (e, "cURL setup failed");
|
|
|
|
|
|
|
|
CURLMcode mres;
|
|
|
|
if ((mres = curl_multi_setopt (self->multi,
|
|
|
|
CURLMOPT_SOCKETFUNCTION, poller_curl_on_socket_action))
|
|
|
|
|| (mres = curl_multi_setopt (self->multi,
|
|
|
|
CURLMOPT_TIMERFUNCTION, poller_curl_on_timer_change))
|
|
|
|
|| (mres = curl_multi_setopt (self->multi, CURLMOPT_SOCKETDATA, self))
|
|
|
|
|| (mres = curl_multi_setopt (self->multi, CURLMOPT_TIMERDATA, self)))
|
|
|
|
{
|
|
|
|
curl_multi_cleanup (self->multi);
|
|
|
|
self->multi = NULL;
|
|
|
|
return error_set (e, "%s: %s",
|
|
|
|
"cURL setup failed", curl_multi_strerror (mres));
|
|
|
|
}
|
|
|
|
|
|
|
|
self->timer = poller_timer_make ((self->poller = poller));
|
|
|
|
self->timer.dispatcher = poller_curl_on_timer;
|
|
|
|
self->timer.user_data = self;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
poller_curl_free (struct poller_curl *self)
|
|
|
|
{
|
|
|
|
curl_multi_cleanup (self->multi);
|
|
|
|
poller_timer_reset (&self->timer);
|
|
|
|
|
|
|
|
LIST_FOR_EACH (struct poller_curl_fd, iter, self->fds)
|
|
|
|
{
|
|
|
|
poller_fd_reset (&iter->fd);
|
|
|
|
free (iter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
/// Initialize a task with a new easy instance that can be used with the poller
|
|
|
|
static bool
|
|
|
|
poller_curl_spawn (struct poller_curl_task *task, struct error **e)
|
|
|
|
{
|
|
|
|
CURL *easy;
|
|
|
|
if (!(easy = curl_easy_init ()))
|
|
|
|
return error_set (e, "cURL setup failed");
|
|
|
|
|
|
|
|
// We already take care of SIGPIPE, and native DNS timeouts are only
|
|
|
|
// a problem for people without the AsynchDNS feature.
|
|
|
|
//
|
|
|
|
// Unfortunately, cURL doesn't allow custom callbacks for DNS.
|
|
|
|
// The most we could try is parse out the hostname and provide an address
|
|
|
|
// override for it using CURLOPT_RESOLVE. Or be our own SOCKS4A/5 proxy.
|
|
|
|
|
|
|
|
CURLcode res;
|
|
|
|
if ((res = curl_easy_setopt (easy, CURLOPT_NOSIGNAL, 1L))
|
|
|
|
|| (res = curl_easy_setopt (easy, CURLOPT_ERRORBUFFER, task->curl_error))
|
|
|
|
|| (res = curl_easy_setopt (easy, CURLOPT_PRIVATE, task)))
|
|
|
|
{
|
|
|
|
curl_easy_cleanup (easy);
|
|
|
|
return error_set (e, "%s", curl_easy_strerror (res));
|
|
|
|
}
|
|
|
|
|
|
|
|
task->easy = easy;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
poller_curl_add (struct poller_curl *self, CURL *easy, struct error **e)
|
|
|
|
{
|
|
|
|
CURLMcode mres;
|
|
|
|
// "CURLMOPT_TIMERFUNCTION [...] will be called from within this function"
|
|
|
|
if ((mres = curl_multi_add_handle (self->multi, easy)))
|
|
|
|
return error_set (e, "%s", curl_multi_strerror (mres));
|
|
|
|
self->registered++;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
poller_curl_remove (struct poller_curl *self, CURL *easy, struct error **e)
|
|
|
|
{
|
|
|
|
CURLMcode mres;
|
|
|
|
if ((mres = curl_multi_remove_handle (self->multi, easy)))
|
|
|
|
return error_set (e, "%s", curl_multi_strerror (mres));
|
|
|
|
self->registered--;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- Compact map -------------------------------------------------------------
|
|
|
|
|
|
|
|
// MPD provides us with a hefty amount of little key-value maps. The overhead
|
|
|
|
// of str_map for such constant (string -> string) maps is too high and it's
|
|
|
|
// much better to serialize them (mainly cache locality and memory efficiency).
|
|
|
|
//
|
|
|
|
// This isn't intended to be reusable and has case insensitivity built-in.
|
|
|
|
|
|
|
|
typedef uint8_t *compact_map_t; ///< Compacted (string -> string) map
|
|
|
|
|
|
|
|
static compact_map_t
|
|
|
|
compact_map (struct str_map *map)
|
|
|
|
{
|
|
|
|
struct str s = str_make ();
|
|
|
|
struct str_map_iter iter = str_map_iter_make (map);
|
|
|
|
|
|
|
|
char *value;
|
|
|
|
static const size_t zero = 0, alignment = sizeof zero;
|
|
|
|
while ((value = str_map_iter_next (&iter)))
|
|
|
|
{
|
|
|
|
size_t entry_len = iter.link->key_length + 1 + strlen (value) + 1;
|
|
|
|
size_t padding_len = (alignment - entry_len % alignment) % alignment;
|
|
|
|
entry_len += padding_len;
|
|
|
|
|
|
|
|
str_append_data (&s, &entry_len, sizeof entry_len);
|
|
|
|
str_append_printf (&s, "%s%c%s%c", iter.link->key, 0, value, 0);
|
|
|
|
str_append_data (&s, &zero, padding_len);
|
|
|
|
}
|
|
|
|
str_append_data (&s, &zero, sizeof zero);
|
|
|
|
return (compact_map_t) str_steal (&s);
|
|
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
|
|
compact_map_find (compact_map_t data, const char *needle)
|
|
|
|
{
|
|
|
|
size_t entry_len;
|
|
|
|
while ((entry_len = *(size_t *) data))
|
|
|
|
{
|
|
|
|
data += sizeof entry_len;
|
|
|
|
if (!strcasecmp_ascii (needle, (const char *) data))
|
|
|
|
return (char *) data + strlen (needle) + 1;
|
|
|
|
data += entry_len;
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
struct item_list
|
|
|
|
{
|
|
|
|
compact_map_t *items; ///< Compacted (string -> string) maps
|
|
|
|
size_t len; ///< Length
|
|
|
|
size_t alloc; ///< Allocated items
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct item_list
|
|
|
|
item_list_make (void)
|
|
|
|
{
|
|
|
|
struct item_list self = {};
|
|
|
|
self.items = xcalloc (sizeof *self.items, (self.alloc = 16));
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
item_list_free (struct item_list *self)
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < self->len; i++)
|
|
|
|
free (self->items[i]);
|
|
|
|
free (self->items);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
item_list_set (struct item_list *self, int i, struct str_map *item)
|
|
|
|
{
|
|
|
|
if (i < 0 || (size_t) i >= self->len)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
free (self->items[i]);
|
|
|
|
self->items[i] = compact_map (item);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static compact_map_t
|
|
|
|
item_list_get (struct item_list *self, int i)
|
|
|
|
{
|
|
|
|
if (i < 0 || (size_t) i >= self->len || !self->items[i])
|
|
|
|
return NULL;
|
|
|
|
return self->items[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
item_list_resize (struct item_list *self, size_t len)
|
|
|
|
{
|
|
|
|
// Make the allocated array big enough but not too large
|
|
|
|
size_t new_alloc = self->alloc;
|
|
|
|
while (new_alloc < len)
|
|
|
|
new_alloc <<= 1;
|
|
|
|
while ((new_alloc >> 1) >= len
|
|
|
|
&& (new_alloc - len) >= 1024)
|
|
|
|
new_alloc >>= 1;
|
|
|
|
|
|
|
|
for (size_t i = len; i < self->len; i++)
|
|
|
|
free (self->items[i]);
|
|
|
|
if (new_alloc != self->alloc)
|
|
|
|
self->items = xreallocarray (self->items,
|
|
|
|
sizeof *self->items, (self->alloc = new_alloc));
|
|
|
|
for (size_t i = self->len; i < len; i++)
|
|
|
|
self->items[i] = NULL;
|
|
|
|
|
|
|
|
self->len = len;
|
|
|
|
}
|
|
|
|
|
|
|
|
// --- Spectrum analyzer -------------------------------------------------------
|
|
|
|
|
|
|
|
// See http://www.zytrax.com/tech/audio/equalization.html
|
|
|
|
// for a good write-up about this problem domain
|
|
|
|
|
|
|
|
#ifdef WITH_FFTW
|
|
|
|
|
|
|
|
struct spectrum
|
|
|
|
{
|
|
|
|
int sampling_rate; ///< Number of samples per seconds
|
|
|
|
int channels; ///< Number of sampled channels
|
|
|
|
int bits; ///< Number of bits per sample
|
|
|
|
int bars; ///< Number of output vertical bars
|
|
|
|
|
|
|
|
int bins; ///< Number of DFT bins
|
|
|
|
int useful_bins; ///< Bins up to the Nyquist frequency
|
|
|
|
int samples; ///< Number of windows to average
|
|
|
|
float accumulator_scale; ///< Scaling factor for accum. values
|
|
|
|
int *top_bins; ///< Top DFT bin index for each bar
|
|
|
|
char *rendered; ///< String buffer for the "render"
|
|
|
|
float *spectrum; ///< The "render" as normalized floats
|
|
|
|
|
|
|
|
void *buffer; ///< Input buffer
|
|
|
|
size_t buffer_len; ///< Input buffer fill level
|
|
|
|
size_t buffer_size; ///< Input buffer size
|
|
|
|
|
|
|
|
/// Decode the respective part of the buffer into the second half of data
|
|
|
|
void (*decode) (struct spectrum *, int sample);
|
|
|
|
|
|
|
|
float *data; ///< Normalized audio data
|
|
|
|
float *window; ///< Sampled window function
|
|
|
|
float *windowed; ///< data * window
|
|
|
|
fftwf_complex *out; ///< DFT output
|
|
|
|
fftwf_plan p; ///< DFT plan/FFTW configuration
|
|
|
|
float *accumulator; ///< Accumulated powers of samples
|
|
|
|
};
|
|
|
|
|
|
|
|
// - - Windows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
// Out: float[n] of 0..1
|
|
|
|
static void
|
|
|
|
window_hann (float *coefficients, size_t n)
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < n; i++)
|
|
|
|
{
|
|
|
|
float sine = sin (M_PI * i / n);
|
|
|
|
coefficients[i] = sine * sine;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// In: float[n] of -1..1, float[n] of 0..1; out: float[n] of -1..1
|
|
|
|
static void
|
|
|
|
window_apply (const float *in, const float *coefficients, float *out, size_t n)
|
|
|
|
{
|
|
|
|
for (size_t i = 0; i < n; i++)
|
|
|
|
out[i] = in[i] * coefficients[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
// In: float[n] of 0..1; out: float 0..n, describing the coherent gain
|
|
|
|
static float
|
|
|
|
window_coherent_gain (const float *in, size_t n)
|
|
|
|
{
|
|
|
|
float sum = 0;
|
|
|
|
for (size_t i = 0; i < n; i++)
|
|
|
|
sum += in[i];
|
|
|
|
return sum;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - Decoding - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static void
|
|
|
|
spectrum_decode_8 (struct spectrum *s, int sample)
|
|
|
|
{
|
|
|
|
size_t n = s->useful_bins;
|
|
|
|
float *data = s->data + n;
|
|
|
|
for (int8_t *p = (int8_t *) s->buffer + sample * n * s->channels;
|
|
|
|
n--; p += s->channels)
|
|
|
|
{
|
|
|
|
int32_t acc = 0;
|
|
|
|
for (int ch = 0; ch < s->channels; ch++)
|
|
|
|
acc += p[ch];
|
|
|
|
*data++ = (float) acc / s->channels / -INT8_MIN;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
spectrum_decode_16 (struct spectrum *s, int sample)
|
|
|
|
{
|
|
|
|
size_t n = s->useful_bins;
|
|
|
|
float *data = s->data + n;
|
|
|
|
for (int16_t *p = (int16_t *) s->buffer + sample * n * s->channels;
|
|
|
|
n--; p += s->channels)
|
|
|
|
{
|
|
|
|
int32_t acc = 0;
|
|
|
|
for (int ch = 0; ch < s->channels; ch++)
|
|
|
|
acc += p[ch];
|
|
|
|
*data++ = (float) acc / s->channels / -INT16_MIN;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
spectrum_decode_16_2 (struct spectrum *s, int sample)
|
|
|
|
{
|
|
|
|
size_t n = s->useful_bins;
|
|
|
|
float *data = s->data + n;
|
|
|
|
for (int16_t *p = (int16_t *) s->buffer + sample * n * 2; n--; p += 2)
|
|
|
|
*data++ = ((int32_t) p[0] + p[1]) / 2. / -INT16_MIN;
|
|
|
|
}
|
|
|
|
|
|
|
|
// - - Spectrum analysis - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
|
|
|
|
static const char *spectrum_bars[] =
|
|
|
|
{ " ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" };
|
|
|
|
|
|
|
|
/// Assuming the input buffer is full, updates the rendered spectrum
|
|
|
|
static void
|
|
|
|
spectrum_sample (struct spectrum *s)
|
|
|
|
{
|
|
|
|
memset (s->accumulator, 0, sizeof *s->accumulator * s->useful_bins);
|
|
|
|
|
|
|
|
// Credit for the algorithm goes to Audacity's /src/SpectrumAnalyst.cpp,
|
|
|
|
// apparently Welch's method
|
|
|
|
for (int sample = 0; sample < s->samples; sample++)
|
|
|
|
{
|
|
|
|
// We use 50% overlap and start with data from the last run (if any)
|
|
|
|
memmove (s->data, s->data + s->useful_bins,
|
|
|
|
sizeof *s->data * s->useful_bins);
|
|
|
|
s->decode (s, sample);
|
|
|
|
|
|
|
|
window_apply (s->data, s->window, s->windowed, s->bins);
|
|
|
|
fftwf_execute (s->p);
|
|
|
|
|
|
|
|
for (int bin = 0; bin < s->useful_bins; bin++)
|
|
|
|
{
|
|
|
|
// out[0][0] is the DC component, not useful to us
|
|
|
|
float re = s->out[bin + 1][0];
|
|
|
|
float im = s->out[bin + 1][1];
|
|
|
|
s->accumulator[bin] += re * re + im * im;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int last_bin = 0;
|
|
|
|
char *p = s->rendered;
|
|
|
|
for (int bar = 0; bar < s->bars; bar++)
|
|
|
|
{
|
|
|
|
int top_bin = s->top_bins[bar];
|
|
|
|
|
|
|
|
// Think of this as accumulating energies within bands,
|
|
|
|
// so that it matches our non-linear hearing--there's no averaging.
|
|
|
|
// For more precision, we could employ an "equal loudness contour".
|
|
|
|
float acc = 0;
|
|
|
|
for (int bin = last_bin; bin < top_bin; bin++)
|
|
|
|
acc += s->accumulator[bin];
|
|
|
|
|
|
|
|
last_bin = top_bin;
|
|
|
|
float db = 10 * log10f (acc * s->accumulator_scale);
|
|
|
|
if (db > 0)
|
|
|
|
db = 0;
|
|
|
|
|
|
|
|
// Assuming decibels are always negative (i.e., properly normalized).
|
|
|
|
// The division defines the cutoff: 8 * 7 = 56 dB of range.
|
|
|
|
int height = N_ELEMENTS (spectrum_bars) - 1 + (int) (db / 7);
|
|
|
|
p += strlen (strcpy (p, spectrum_bars[MAX (height, 0)]));
|
|
|
|
|
|
|
|
// Even with slightly the higher display resolutions provided by X11,
|
|
|
|
// 60 dB roughly covers the useful range.
|
|
|
|
s->spectrum[bar] = MAX (0, 1 + db / 60);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool
|
|
|
|
spectrum_init (struct spectrum *s, char *format, int bars, int fps,
|
|
|
|
struct error **e)
|
|
|
|
{
|
|
|
|
errno = 0;
|
|
|
|
|
|
|
|
long sampling_rate, bits, channels;
|
|
|
|
if (!format
|
|
|
|
|| (sampling_rate = strtol (format, &format, 10), *format++ != ':')
|
|
|
|
|| (bits = strtol (format, &format, 10), *format++ != ':')
|
|
|
|
|| (channels = strtol (format, &format, 10), *format)
|
|
|
|
|| errno != 0)
|
|
|
|
return error_set (e, "invalid format, expected RATE:BITS:CHANNELS");
|
|
|
|
|
|
|
|
if (sampling_rate < 20000 || sampling_rate > INT_MAX)
|
|
|
|
return error_set (e, "unsupported sampling rate (%ld)", sampling_rate);
|
|
|
|
if (bits != 8 && bits != 16)
|
|
|
|
return error_set (e, "unsupported bit count (%ld)", bits);
|
|
|
|
if (channels < 1 || channels > INT_MAX)
|
|
|
|
return error_set (e, "no channels to sample (%ld)", channels);
|
|
|
|
if (bars < 1 || bars > 12)
|
|
|
|
return error_set (e, "requested too few or too many bars (%d)", bars);
|
|
|
|
|
|
|
|
// All that can fail henceforth is memory allocation
|
|
|
|
*s = (struct spectrum)
|
|
|
|
{
|
|
|
|
.sampling_rate = sampling_rate,
|
|
|
|
.bits = bits,
|
|
|
|
.channels = channels,
|
|
|
|
.bars = bars,
|
|
|
|
};
|
|
|
|
|
|
|
|
// The number of bars is always smaller than that of the samples (bins).
|
|
|
|
// Let's start with the equation of the top FFT bin to use for a given bar:
|
|
|
|
// top_bin = (num_bins + 1) ^ (bar / num_bars) - 1
|
|
|
|
// N.b. if we didn't subtract, the power function would make this ≥ 1.
|
|
|
|
// N.b. we then also need to extend the range by the same amount.
|
|
|
|
//
|
|
|
|
// We need the amount of bins for the first bar to be at least one:
|
|
|
|
// 1 ≤ (num_bins + 1) ^ (1 / num_bars) - 1
|
|
|
|
//
|
|
|
|
// Solving with Wolfram Alpha gives us:
|
|
|
|
// num_bins ≥ (2 ^ num_bars) - 1 [for y > 0]
|
|
|
|
//
|
|
|
|
// And we need to remember that half of the FFT bins are useless/missing--
|
|
|
|
// FFTW skips useless points past the Nyquist frequency.
|
|
|
|
int necessary_bins = 2 << s->bars;
|
|
|
|
|
|
|
|
// Discard frequencies above 20 kHz, which take up a constant ratio
|
|
|
|
// of all bins, given by the sampling rate. A more practical/efficient
|
|
|
|
// solution would be to just handle 96/192/... kHz rates as bitshifts.
|
|
|
|
//
|
|
|
|
// Filtering out sub-20 Hz frequencies would be even more wasteful than
|
|
|
|
// this wild DFT size, so we don't even try. While we may just shift
|
|
|
|
// the lowest used bin easily within the extra range provided by this
|
|
|
|
// extension (the Nyquist is usually above 22 kHz, and it hardly matters
|
|
|
|
// if we go a bit beyond 20 kHz in the last bin), for a small number of bars
|
|
|
|
// the first bin already includes audible frequencies, and even for larger
|
|
|
|
// numbers it wouldn't be too accurate. An exact solution would require
|
|
|
|
// having the amount of bins be strictly a factor of Nyquist / 20 (stemming
|
|
|
|
// from the equation 20 = Nyquist / bins). Since log2(44100 / 2 / 20) > 10,
|
|
|
|
// it would be fairly expensive, and somewhat slowly updating. Always.
|
|
|
|
// (Note that you can increase window overlap to get smoother framerates,
|
|
|
|
// but it would remain laggy.)
|
|
|
|
double audible_ratio = s->sampling_rate / 2. / 20000;
|
|
|
|
s->bins = ceil (necessary_bins * MAX (audible_ratio, 1));
|
|
|
|
s->useful_bins = s->bins / 2;
|
|
|
|
|
|
|
|
int used_bins = necessary_bins / 2;
|
|
|
|
s->rendered = xcalloc (sizeof *s->rendered, s->bars * 3 + 1);
|
|
|
|
s->spectrum = xcalloc (sizeof *s->spectrum, s->bars);
|
|
|
|
s->top_bins = xcalloc (sizeof *s->top_bins, s->bars);
|
|
|
|
for (int bar = 0; bar < s->bars; bar++)
|
|
|
|
{
|
|
|
|
int top_bin = floor (pow (used_bins + 1, (bar + 1.) / s->bars)) - 1;
|
|
|
|
s->top_bins[bar] = MIN (top_bin, used_bins);
|
|
|
|
}
|
|
|
|
|
|
|
|
s->samples = s->sampling_rate / s->bins * 2 / MAX (fps, 1);
|
|
|
|
if (s->samples < 1)
|
|
|
|
s->samples = 1;
|
|
|
|
|
|
|
|
// XXX: we average the channels but might want to average the DFT results
|
|
|
|
if (s->bits == 8) s->decode = spectrum_decode_8;
|
|
|
|
if (s->bits == 16) s->decode = spectrum_decode_16;
|
|
|
|
|
|
|
|
// Micro-optimize to achieve some piece of mind; it's weak but measurable
|
|
|
|
if (s->bits == 16 && s->channels == 2)
|
|
|
|
s->decode = spectrum_decode_16_2;
|
|
|
|
|
|
|
|
s->buffer_size = s->samples * s->useful_bins * s->bits / 8 * s->channels;
|
|
|
|
s->buffer = xcalloc (1, s->buffer_size);
|
|
|
|
|
|
|
|
// Prepare the window
|
|
|
|
s->window = xcalloc (sizeof *s->window, s->bins);
|
|
|
|
window_hann (s->window, s->bins);
|
|
|
|
|
|
|
|
// Multiply by 2 for only using half of the DFT's result, then adjust to
|
|
|
|
// the total energy of the window. Both squared, because the accumulator
|
|
|
|
// contains squared values. Compute the average, and convert to decibels.
|
|
|
|
// See also the mildly confusing https://dsp.stackexchange.com/a/14945.
|
|
|
|
float coherent_gain = window_coherent_gain (s->window, s->bins);
|
|
|
|
s->accumulator_scale = 2 * 2 / coherent_gain / coherent_gain / s->samples;
|
|
|
|
|
|
|
|
s->data = xcalloc (sizeof *s->data, s->bins);
|
|
|
|
s->windowed = fftw_malloc (sizeof *s->windowed * s->bins);
|
|
|
|
s->out = fftw_malloc (sizeof *s->out * (s->useful_bins + 1));
|
|
|
|
s->p = fftwf_plan_dft_r2c_1d (s->bins, s->windowed, s->out, FFTW_MEASURE);
|
|
|
|
s->accumulator = xcalloc (sizeof *s->accumulator, s->useful_bins);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
spectrum_free (struct spectrum *s)
|
|
|
|
{
|
|
|
|
free (s->accumulator);
|
|
|
|
fftwf_destroy_plan (s->p);
|
|
|
|
fftw_free (s->out);
|
|
|
|
fftw_free (s->windowed);
|
|
|
|
free (s->data);
|
|
|
|
free (s->window);
|
|
|
|
#if 0
|
|
|
|
// We don't particularly want to discard wisdom.
|
|
|
|
fftwf_cleanup ();
|
|
|
|
#endif
|
|
|
|
|
|
|
|
free (s->rendered);
|
|
|
|
free (s->spectrum);
|
|
|
|
free (s->top_bins);
|
|
|
|
free (s->buffer);
|
|
|
|
|
|
|
|
memset (s, 0, sizeof *s);
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // WITH_FFTW
|
|
|
|
|
|
|
|
// --- PulseAudio --------------------------------------------------------------
|
|
|
|
|
|
|
|
#ifdef WITH_PULSE
|
|
|
|
|
|
|
|
struct pulse
|
|
|
|
{
|
|
|
|
struct poller_timer make_context; ///< Event to establish connection
|
|
|
|
pa_mainloop_api *api; ///< PulseAudio event loop proxy
|
|
|
|
pa_context *context; ///< PulseAudio connection context
|
|