nncmpp/nncmpp.c

6201 lines
165 KiB
C
Raw Permalink Normal View History

/*
* 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.
2018-10-20 22:13:13 +02:00
#define ATTRIBUTE_TABLE(XX) \
XX( NORMAL, normal, -1, -1, 0 ) \
XX( HIGHLIGHT, highlight, -1, -1, A_BOLD ) \
2018-10-20 22:13:13 +02:00
/* Gauge */ \
XX( ELAPSED, elapsed, -1, -1, A_REVERSE ) \
XX( REMAINS, remains, -1, -1, A_UNDERLINE ) \
2018-10-20 22:13:13 +02:00
/* Tab bar */ \
XX( TAB_BAR, tab_bar, -1, -1, A_REVERSE ) \
XX( TAB_ACTIVE, tab_active, -1, -1, A_BOLD ) \
2018-10-20 22:13:13 +02:00
/* 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.
2018-10-20 22:13:13 +02:00
* 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 ) \
2018-10-20 22:13:13 +02:00
/* 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"
#ifdef WITH_X11
#define LIBERTY_XUI_WANT_X11
#endif // WITH_X11
#include "liberty/liberty-xui.c"
#include <dirent.h>
#include <locale.h>
#include <math.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
2016-10-01 23:43:51 +02:00
#define APP_TITLE PROGRAM_NAME ///< Left top corner
#include "nncmpp-actions.h"
// --- Utilities ---------------------------------------------------------------
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, '"');
}
2016-10-10 16:02:57 +02:00
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);
}
2016-10-12 23:42:57 +02:00
static const char *
xbasename (const char *path)
{
const char *last_slash = strrchr (path, '/');
return last_slash ? last_slash + 1 : path;
}
2021-12-07 20:07:42 +01:00
static char *xstrdup0 (const char *s) { return s ? xstrdup (s) : NULL; }
2016-10-10 08:39:31 +02:00
static char *
latin1_to_utf8 (const char *latin1)
{
2017-06-22 23:17:25 +02:00
struct str converted = str_make ();
2016-10-10 08:39:31 +02:00
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);
}
}
2017-06-05 13:05:33 +02:00
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];
2020-10-10 21:31:31 +02:00
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;
}
2016-10-13 01:48:45 +02:00
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);
2017-06-22 23:17:25 +02:00
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)
{
2019-02-24 01:41:15 +01:00
// 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 ()))
2016-10-10 07:47:40 +02:00
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;
2016-10-10 07:47:40 +02:00
return error_set (e, "%s: %s",
"cURL setup failed", curl_multi_strerror (mres));
}
2017-06-22 23:17:25 +02:00
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 ()))
2016-10-10 07:47:40 +02:00
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);
2016-10-10 07:47:40 +02:00
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"
2016-10-10 07:47:40 +02:00
if ((mres = curl_multi_add_handle (self->multi, easy)))
return error_set (e, "%s", curl_multi_strerror (mres));
self->registered++;
2016-10-10 07:47:40 +02:00
return true;
}
static bool
poller_curl_remove (struct poller_curl *self, CURL *easy, struct error **e)
{
CURLMcode mres;
2016-10-10 07:47:40 +02:00
if ((mres = curl_multi_remove_handle (self->multi, easy)))
return error_set (e, "%s", curl_multi_strerror (mres));
self->registered--;
2016-10-10 07:47:40 +02:00
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)
{
2017-06-22 23:17:25 +02:00
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
};
2017-06-22 23:25:52 +02:00
static struct item_list
item_list_make (void)
{
2017-06-22 23:25:52 +02:00
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])
2017-06-05 20:53:00 +02:00
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
2021-07-08 04:03:48 +02:00
/// 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);
2022-08-23 09:00:20 +02:00
#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
uint32_t sink_candidate; ///< Used while searching for MPD
uint32_t sink; ///< The relevant sink or -1
pa_cvolume sink_volume; ///< Current volume
bool sink_muted; ///< Currently muted?
void (*on_update) (void); ///< Update callback
};
static void
pulse_on_sink_info (pa_context *context, const pa_sink_info *info, int eol,
void *userdata)
{
(void) context;
(void) eol;
struct pulse *self = userdata;
if (info)
{
self->sink_volume = info->volume;
self->sink_muted = !!info->mute;
self->on_update ();
}
}
static void
pulse_update_from_sink (struct pulse *self)
{
if (self->sink == PA_INVALID_INDEX)
return;
pa_operation_unref (pa_context_get_sink_info_by_index
(self->context, self->sink, pulse_on_sink_info, self));
}
static void
pulse_on_sink_input_info (pa_context *context,
const struct pa_sink_input_info *info, int eol, void *userdata)
{
(void) context;
(void) eol;
struct pulse *self = userdata;
if (!info)
{
if ((self->sink = self->sink_candidate) != PA_INVALID_INDEX)
pulse_update_from_sink (self);