9 Commits

Author SHA1 Message Date
ee5c41b2bf README: update dependencies 2021-10-21 09:21:08 +02:00
9a67e076a9 Bump version, update NEWS 2021-10-21 09:16:51 +02:00
53fbb3dec1 Fix the line editor/spectrum analyser interaction
The updater assumed the terminal cursor was invisible.
2021-10-21 09:13:07 +02:00
267598643a Add program arguments to MPD's current playlist
I was tired of using `mpv --no-video`, this is a bit better.

It's all rather quirky, but very little code is involved.

I've added a few related TODO entries.
2021-09-07 06:35:24 +02:00
fba1210e9f Clean up connection initialisation
Also, do not set up the spectrum visualiser before a password is sent.

It would look a bit weird to have it run but display "Disconnected",
even though technically, it would probably work.
2021-09-06 21:48:27 +02:00
30777e8fd3 Improve terminal initialisation
Don't just abort() on failures, print a proper error message.

Also, set up ncurses as late as possible.  This should be alright wrt.
signal handlers according to ncurses code, as well as XSI:

> Curses implementations may provide for special handling of
> the SIGINT, SIGQUIT and SIGTSTP signals if their disposition
> is SIG_DFL at the time initscr is called ...

termo blocks job control, so SIGTSTP is not a concern at all.
2021-09-06 21:30:03 +02:00
353174ee3c Spetrum analyser: expand my favourite comment 2021-07-09 20:08:53 +02:00
2d641d087f Spectrum analyser: add some useful comments 2021-07-09 06:25:48 +02:00
20c8385f2e Spectrum analyser: optimise the x:16:2 case
nncmpp CPU usage went from 2 to 1.7 percent, a 15% improvement.

Sort of worth it, given that it's a constant load.

The assembly certainly looks nicer.
2021-07-08 19:14:26 +02:00
5 changed files with 132 additions and 33 deletions

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.0) cmake_minimum_required (VERSION 3.0)
project (nncmpp VERSION 1.0.0 LANGUAGES C) project (nncmpp VERSION 1.1.0 LANGUAGES C)
# Moar warnings # Moar warnings
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC) if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)

4
NEWS
View File

@@ -1,10 +1,12 @@
1.1.0 (20xx-xx-xx) 1.1.0 (2021-10-21)
* Now requesting and processing terminal de/focus events, * Now requesting and processing terminal de/focus events,
using a new "defocused" attribute for selected rows using a new "defocused" attribute for selected rows
* Made it possible to show a spectrum visualiser when built against FFTW * Made it possible to show a spectrum visualiser when built against FFTW
* Any program arguments are now added to MPD's current playlist
1.0.0 (2020-11-05) 1.0.0 (2020-11-05)

View File

@@ -31,7 +31,7 @@ Building
-------- --------
Build dependencies: CMake, pkg-config, asciidoctor, liberty (included), Build dependencies: CMake, pkg-config, asciidoctor, liberty (included),
termo (included) + termo (included) +
Runtime dependencies: ncursesw, libunistring, cURL Runtime dependencies: ncursesw, libunistring, cURL, fftw3 (optional)
$ git clone --recursive https://git.janouch.name/p/nncmpp.git $ git clone --recursive https://git.janouch.name/p/nncmpp.git
$ mkdir nncmpp/build $ mkdir nncmpp/build

View File

@@ -10,7 +10,7 @@ nncmpp - terminal-based MPD client
Synopsis Synopsis
-------- --------
*nncmpp* [_OPTION_]... *nncmpp* [_OPTION_]... [_URL_ | _PATH_]...
Description Description
----------- -----------
@@ -19,6 +19,10 @@ you with an overview of all key bindings and the actions they're assigned to.
Individual tabs can be switched to either using the mouse or by pressing *M-1* Individual tabs can be switched to either using the mouse or by pressing *M-1*
through *M-9*, corresponding to the order they appear in. through *M-9*, corresponding to the order they appear in.
As a convenience utility, any program arguments are added to the MPD queue.
Note that to add files outside the database, you need to connect to MPD using
a socket file.
Options Options
------- -------
*-d*, *--debug*:: *-d*, *--debug*::

151
nncmpp.c
View File

@@ -569,6 +569,9 @@ item_list_resize (struct item_list *self, size_t len)
// --- Spectrum analyzer ------------------------------------------------------- // --- Spectrum analyzer -------------------------------------------------------
// See http://www.zytrax.com/tech/audio/equalization.html
// for a good write-up about this problem domain
#ifdef WITH_FFTW #ifdef WITH_FFTW
struct spectrum struct spectrum
@@ -638,13 +641,13 @@ spectrum_decode_8 (struct spectrum *s, int sample)
{ {
size_t n = s->useful_bins; size_t n = s->useful_bins;
float *data = s->data + n; float *data = s->data + n;
int8_t *p = (int8_t *) s->buffer + sample * n * s->channels; for (int8_t *p = (int8_t *) s->buffer + sample * n * s->channels;
while (n--) n--; p += s->channels)
{ {
int32_t acc = 0; int32_t acc = 0;
for (int ch = 0; ch < s->channels; ch++) for (int ch = 0; ch < s->channels; ch++)
acc += *p++; acc += p[ch];
*data++ = (float) acc / -INT8_MIN / s->channels; *data++ = (float) acc / s->channels / -INT8_MIN;
} }
} }
@@ -653,16 +656,25 @@ spectrum_decode_16 (struct spectrum *s, int sample)
{ {
size_t n = s->useful_bins; size_t n = s->useful_bins;
float *data = s->data + n; float *data = s->data + n;
int16_t *p = (int16_t *) s->buffer + sample * n * s->channels; for (int16_t *p = (int16_t *) s->buffer + sample * n * s->channels;
while (n--) n--; p += s->channels)
{ {
int32_t acc = 0; int32_t acc = 0;
for (int ch = 0; ch < s->channels; ch++) for (int ch = 0; ch < s->channels; ch++)
acc += *p++; acc += p[ch];
*data++ = (float) acc / -INT16_MIN / s->channels; *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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - Spectrum analysis - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *spectrum_bars[] = static const char *spectrum_bars[] =
@@ -781,7 +793,8 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
// having the amount of bins be strictly a factor of Nyquist / 20 (stemming // 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, // from the equation 20 = Nyquist / bins). Since log2(44100 / 2 / 20) > 10,
// it would be fairly expensive, and somewhat slowly updating. Always. // it would be fairly expensive, and somewhat slowly updating. Always.
// (Note that you can increase window overlap to get smoother framerates.) // (Note that you can increase window overlap to get smoother framerates,
// but it would remain laggy.)
double audible_ratio = s->sampling_rate / 2. / 20000; double audible_ratio = s->sampling_rate / 2. / 20000;
s->bins = ceil (necessary_bins * MAX (audible_ratio, 1)); s->bins = ceil (necessary_bins * MAX (audible_ratio, 1));
s->useful_bins = s->bins / 2; s->useful_bins = s->bins / 2;
@@ -800,9 +813,14 @@ spectrum_init (struct spectrum *s, char *format, int bars, struct error **e)
if (s->samples < 1) if (s->samples < 1)
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 == 8) s->decode = spectrum_decode_8;
if (s->bits == 16) s->decode = spectrum_decode_16; 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_size = s->samples * s->useful_bins * s->bits / 8 * s->channels;
s->buffer = xcalloc (1, s->buffer_size); s->buffer = xcalloc (1, s->buffer_size);
@@ -944,6 +962,7 @@ static struct app_context
struct config config; ///< Program configuration struct config config; ///< Program configuration
struct strv streams; ///< List of "name NUL URI NUL" struct strv streams; ///< List of "name NUL URI NUL"
struct strv enqueue; ///< Items to enqueue once connected
struct tab *help_tab; ///< Special help tab struct tab *help_tab; ///< Special help tab
struct tab *tabs; ///< All other tabs struct tab *tabs; ///< All other tabs
@@ -1206,6 +1225,7 @@ app_init_context (void)
g.client = mpd_client_make (&g.poller); g.client = mpd_client_make (&g.poller);
g.config = config_make (); g.config = config_make ();
g.streams = strv_make (); g.streams = strv_make ();
g.enqueue = strv_make ();
g.playlist = item_list_make (); g.playlist = item_list_make ();
g.playback_info = str_map_make (free); g.playback_info = str_map_make (free);
@@ -1236,9 +1256,9 @@ app_init_terminal (void)
{ {
TERMO_CHECK_VERSION; TERMO_CHECK_VERSION;
if (!(g.tk = termo_new (STDIN_FILENO, NULL, 0))) if (!(g.tk = termo_new (STDIN_FILENO, NULL, 0)))
abort (); exit_fatal ("failed to set up the terminal");
if (!initscr () || nonl () == ERR) if (!initscr () || nonl () == ERR)
abort (); exit_fatal ("failed to set up the terminal");
// By default we don't use any colors so they're not required... // By default we don't use any colors so they're not required...
if (start_color () == ERR if (start_color () == ERR
@@ -1267,6 +1287,7 @@ app_free_context (void)
mpd_client_free (&g.client); mpd_client_free (&g.client);
str_map_free (&g.playback_info); str_map_free (&g.playback_info);
strv_free (&g.streams); strv_free (&g.streams);
strv_free (&g.enqueue);
item_list_free (&g.playlist); item_list_free (&g.playlist);
#ifdef WITH_FFTW #ifdef WITH_FFTW
@@ -3768,9 +3789,15 @@ spectrum_redraw (void)
// let's hack around it in this case // let's hack around it in this case
if (g.spectrum_row != -1) if (g.spectrum_row != -1)
{ {
// Don't mess up the line editor caret, when it's shown
int last_x, last_y;
getyx (stdscr, last_y, last_x);
attrset (APP_ATTR (TAB_BAR)); attrset (APP_ATTR (TAB_BAR));
mvaddstr (g.spectrum_row, g.spectrum_column, g.spectrum.spectrum); mvaddstr (g.spectrum_row, g.spectrum_column, g.spectrum.spectrum);
attrset (0); attrset (0);
move (last_y, last_x);
refresh (); refresh ();
} }
else else
@@ -4168,6 +4195,57 @@ mpd_queue_reconnect (void)
poller_timer_set (&g.connect_event, 5 * 1000); poller_timer_set (&g.connect_event, 5 * 1000);
} }
// On an error, MPD discards the rest of our enqueuing commands--work it around
static void mpd_enqueue_step (size_t start_offset);
static void
mpd_on_enqueue_response (const struct mpd_response *response,
const struct strv *data, void *user_data)
{
(void) data;
intptr_t start_offset = (intptr_t) user_data;
if (response->success)
strv_reset (&g.enqueue);
else
{
// Their addition may also overflow, but YOLO
hard_assert (start_offset >= 0 && response->list_offset >= 0);
print_error ("%s: %s", response->message_text,
g.enqueue.vector[start_offset + response->list_offset]);
mpd_enqueue_step (start_offset + response->list_offset + 1);
}
}
static void
mpd_enqueue_step (size_t start_offset)
{
struct mpd_client *c = &g.client;
if (start_offset >= g.enqueue.len)
{
strv_reset (&g.enqueue);
return;
}
// TODO: might want to consider using addid and autoplaying
mpd_client_list_begin (c);
for (size_t i = start_offset; i < g.enqueue.len; i++)
mpd_client_send_command (c, "add", g.enqueue.vector[i], NULL);
mpd_client_list_end (c);
mpd_client_add_task (c, mpd_on_enqueue_response, (void *) start_offset);
mpd_client_idle (c, 0);
}
static void
mpd_on_ready (void)
{
mpd_request_info ();
library_tab_reload (NULL);
spectrum_setup_fifo ();
mpd_enqueue_step (0);
}
static void static void
mpd_on_password_response (const struct mpd_response *response, mpd_on_password_response (const struct mpd_response *response,
const struct strv *data, void *user_data) const struct strv *data, void *user_data)
@@ -4177,10 +4255,7 @@ mpd_on_password_response (const struct mpd_response *response,
struct mpd_client *c = &g.client; struct mpd_client *c = &g.client;
if (response->success) if (response->success)
{ mpd_on_ready ();
mpd_request_info ();
library_tab_reload (NULL);
}
else else
{ {
print_error ("%s: %s", print_error ("%s: %s",
@@ -4203,12 +4278,7 @@ mpd_on_connected (void *user_data)
mpd_client_add_task (c, mpd_on_password_response, NULL); mpd_client_add_task (c, mpd_on_password_response, NULL);
} }
else else
{ mpd_on_ready ();
mpd_request_info ();
library_tab_reload (NULL);
}
spectrum_setup_fifo ();
} }
static void static void
@@ -4507,6 +4577,28 @@ app_init_poller_events (void)
g.refresh_event.dispatcher = app_on_refresh; g.refresh_event.dispatcher = app_on_refresh;
} }
static void
app_init_enqueue (char *argv[], int argc)
{
// TODO: MPD is unwilling to play directories, so perhaps recurse ourselves
char cwd[4096] = "";
for (int i = 0; i < argc; i++)
{
// This is a super-trivial method of URL detection, however anything
// contaning the scheme and authority delimiters in a sequence is most
// certainly not a filesystem path, and thus it will work as expected.
// Error handling may be done by MPD.
const char *path_or_URL = argv[i];
if (*path_or_URL == '/' || strstr (path_or_URL, "://"))
strv_append (&g.enqueue, path_or_URL);
else if (!*cwd && !getcwd (cwd, sizeof cwd))
exit_fatal ("getcwd: %s", strerror (errno));
else
strv_append_owned (&g.enqueue,
xstrdup_printf ("%s/%s", cwd, path_or_URL));
}
}
int int
main (int argc, char *argv[]) main (int argc, char *argv[])
{ {
@@ -4519,7 +4611,8 @@ main (int argc, char *argv[])
}; };
struct opt_handler oh = struct opt_handler oh =
opt_handler_make (argc, argv, opts, NULL, "Terminal-based MPD client."); opt_handler_make (argc, argv, opts,
"[URL | PATH]...", "Terminal-based MPD client.");
int c; int c;
while ((c = opt_handler_get (&oh)) != -1) while ((c = opt_handler_get (&oh)) != -1)
@@ -4542,12 +4635,6 @@ main (int argc, char *argv[])
argc -= optind; argc -= optind;
argv += optind; argv += optind;
if (argc)
{
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
opt_handler_free (&oh); opt_handler_free (&oh);
// We only need to convert to and from the terminal encoding // We only need to convert to and from the terminal encoding
@@ -4555,10 +4642,11 @@ main (int argc, char *argv[])
print_warning ("failed to set the locale"); print_warning ("failed to set the locale");
app_init_context (); app_init_context ();
app_init_enqueue (argv, argc);
app_load_configuration (); app_load_configuration ();
app_init_terminal ();
signals_setup_handlers (); signals_setup_handlers ();
app_init_poller_events (); app_init_poller_events ();
app_init_terminal ();
g_normal_keys = app_init_bindings ("normal", g_normal_keys = app_init_bindings ("normal",
g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len); g_normal_defaults, N_ELEMENTS (g_normal_defaults), &g_normal_keys_len);
@@ -4578,6 +4666,11 @@ main (int argc, char *argv[])
app_prepend_tab (current_tab_init ()); app_prepend_tab (current_tab_init ());
app_switch_tab ((g.help_tab = help_tab_init ())); app_switch_tab ((g.help_tab = help_tab_init ()));
// TODO: the help tab should be the default for new users only,
// so provide a configuration option to flip this
if (argc)
app_switch_tab (&g_current_tab);
g.polling = true; g.polling = true;
while (g.polling) while (g.polling)
poller_run (&g.poller); poller_run (&g.poller);