28 Commits

Author SHA1 Message Date
ba86961ba5 Bump version, update NEWS 2021-11-04 14:19:50 +01:00
0cdb4989e5 Bump termo 2021-11-04 14:16:25 +01:00
6de940fe96 Do not beep on focus changes 2021-11-04 13:24:15 +01:00
6bd8c1db2f CMakeLists.txt: fix macOS build 2021-11-02 17:17:32 +01:00
56efe9c6a9 Update .gitignore 2021-10-30 03:35:49 +02:00
8a17e674f8 CMakeLists.txt: clean up 2021-10-30 03:02:00 +02:00
bd0ee66c19 Add clang-format configuration 2021-10-30 03:02:00 +02:00
6f6efe077b CMakeLists.txt: synchronize with sdtui 2021-10-27 19:48:47 +02:00
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
fa4443a3ce Rectify an obsolete comment 2021-07-08 04:33:03 +02:00
14ba637d4b Expand the last comment once again 2021-07-08 04:32:53 +02:00
66bc3f1c2c Expand the comment on spectrum frequency filtering 2021-07-05 23:42:51 +02:00
0646cea126 Silence a compiler warning
The statement can be eliminated, then it suggests braces.
2021-07-05 01:26:16 +02:00
a439a56ee9 Add an optional spectrum visualiser
This is really more of a demo.  It's doable, just rather ugly.

It would deserve some further tuning, if anyone cared enough.
2021-07-05 01:10:46 +02:00
120a11ca1b Update a comment about mouse modes
We might even depend on termo now more than is stated.
2021-07-04 10:23:37 +02:00
7e531e95c5 Process focus events
Should help prevent accidents in other windows.
2021-06-29 05:28:54 +02:00
0335443b22 Bump version, update NEWS 2020-11-05 01:47:18 +01:00
70ff29e3d5 Add a real manual page
Closes #3
2020-11-05 01:47:06 +01:00
ba122b7672 Minor clarifications 2020-11-05 01:47:05 +01:00
456fab5b11 CMakeLists.txt: install the contrib directory 2020-11-05 01:47:05 +01:00
11 changed files with 858 additions and 87 deletions

32
.clang-format Normal file
View File

@@ -0,0 +1,32 @@
# clang-format is fairly limited, and these rules are approximate:
# - array initializers can get terribly mangled with clang-format 12.0,
# - sometimes it still aligns with space characters,
# - struct name NL { NL ... NL } NL name; is unachievable.
BasedOnStyle: GNU
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
BreakBeforeBraces: Allman
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignConsecutiveMacros: Consecutive
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
IndentGotoLabels: false
# IncludeCategories has some potential, but it may also break the build.
# Note that the documentation says the value should be "Never".
SortIncludes: false
# This is a compromise, it generally works out aesthetically better.
BinPackArguments: false
# Unfortunately, this can't be told to align to column 40 or so.
SpacesBeforeTrailingComments: 2
# liberty-specific macro body wrappers.
MacroBlockBegin: "BLOCK_START"
MacroBlockEnd: "BLOCK_END"
ForEachMacros: ["LIST_FOR_EACH"]

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@
/nncmpp.files /nncmpp.files
/nncmpp.creator* /nncmpp.creator*
/nncmpp.includes /nncmpp.includes
/nncmpp.cflags
/nncmpp.cxxflags

View File

@@ -1,5 +1,5 @@
cmake_minimum_required (VERSION 3.0) cmake_minimum_required (VERSION 3.0)
project (nncmpp VERSION 0.9.0 LANGUAGES C) project (nncmpp VERSION 1.1.1 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)
@@ -21,38 +21,59 @@ include (AddThreads)
find_package (Termo QUIET NO_MODULE) find_package (Termo QUIET NO_MODULE)
option (USE_SYSTEM_TERMO option (USE_SYSTEM_TERMO
"Don't compile our own termo library, use the system one" ${Termo_FOUND}) "Don't compile our own termo library, use the system one" ${Termo_FOUND})
if (USE_SYSTEM_TERMO) if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND) if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found") message (FATAL_ERROR "System termo library not found")
endif (NOT Termo_FOUND) endif ()
else () else ()
# We don't want the library to install, but EXCLUDE_FROM_ALL ignores tests
add_subdirectory (termo EXCLUDE_FROM_ALL) add_subdirectory (termo EXCLUDE_FROM_ALL)
# We don't have many good choices when we don't want to install it and want file (WRITE ${PROJECT_BINARY_DIR}/CTestCustom.cmake
# to support older versions of CMake; this is a relatively clean approach "execute_process (COMMAND ${CMAKE_COMMAND} --build termo)")
# (other possibilities: setting a variable in the parent scope, using a
# cache variable, writing a special config file with build paths in it and # We don't have many good choices; this is a relatively clean approach
# including it here, or setting a custom property on the targets). # (other possibilities: setting a variable in the parent scope, using
# a cache variable, writing a special config file with build paths in it
# and including it here, or setting a custom property on the targets)
get_directory_property (Termo_INCLUDE_DIRS get_directory_property (Termo_INCLUDE_DIRS
DIRECTORY termo INCLUDE_DIRECTORIES) DIRECTORY termo INCLUDE_DIRECTORIES)
set (Termo_LIBRARIES termo-static) set (Termo_LIBRARIES termo-static)
endif () endif ()
pkg_check_modules (fftw fftw3 fftw3f)
option (WITH_FFTW "Use FFTW to enable spectrum visualisation" ${fftw_FOUND})
if (WITH_FFTW)
if (NOT fftw_FOUND)
message (FATAL_ERROR "FFTW not found")
endif ()
endif ()
include_directories (${Unistring_INCLUDE_DIRS} include_directories (${Unistring_INCLUDE_DIRS}
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS}) ${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS}
link_directories (${curl_LIBRARY_DIRS}) ${fftw_INCLUDE_DIRS})
link_directories (${curl_LIBRARY_DIRS} ${fftw_LIBRARY_DIRS})
# Configuration # Configuration
include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD") if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively; # Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
# our POSIX version macros make it undefined # our POSIX version macros make it undefined
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1) add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
elseif (APPLE)
add_definitions (-D_DARWIN_C_SOURCE)
endif () endif ()
include (CheckFunctionExists)
set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
# -lm may or may not be a part of libc
foreach (extra m)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
list (APPEND extra_libraries ${extra_lib_${extra}})
endif ()
endforeach ()
# Generate a configuration file # Generate a configuration file
configure_file (${PROJECT_SOURCE_DIR}/config.h.in configure_file (${PROJECT_SOURCE_DIR}/config.h.in
${PROJECT_BINARY_DIR}/config.h) ${PROJECT_BINARY_DIR}/config.h)
@@ -61,27 +82,31 @@ include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
# Build the main executable and link it # Build the main executable and link it
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c) add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES} target_link_libraries (${PROJECT_NAME} ${Unistring_LIBRARIES}
${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES}) ${Ncursesw_LIBRARIES} termo-static ${curl_LIBRARIES}
${fftw_LIBRARIES} ${extra_libraries})
add_threads (${PROJECT_NAME}) add_threads (${PROJECT_NAME})
# Installation # Installation
include (GNUInstallDirs) include (GNUInstallDirs)
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
# Generate documentation from program help # Generate documentation from text markup
find_program (HELP2MAN_EXECUTABLE help2man) find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
if (NOT HELP2MAN_EXECUTABLE) if (NOT ASCIIDOCTOR_EXECUTABLE)
message (FATAL_ERROR "help2man not found") message (FATAL_ERROR "asciidoctor not found")
endif () endif ()
foreach (page ${PROJECT_NAME}) foreach (page ${PROJECT_NAME})
set (page_output "${PROJECT_BINARY_DIR}/${page}.1") set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
list (APPEND project_MAN_PAGES "${page_output}") list (APPEND project_MAN_PAGES "${page_output}")
add_custom_command (OUTPUT ${page_output} add_custom_command (OUTPUT ${page_output}
COMMAND ${HELP2MAN_EXECUTABLE} -N COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
"${PROJECT_BINARY_DIR}/${page}" -o ${page_output} -a release-version=${PROJECT_VERSION}
DEPENDS ${page} "${PROJECT_SOURCE_DIR}/${page}.adoc"
-o "${page_output}"
DEPENDS ${page}.adoc
COMMENT "Generating man page for ${page}" VERBATIM) COMMENT "Generating man page for ${page}" VERBATIM)
endforeach () endforeach ()

View File

@@ -1,4 +1,4 @@
Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name> Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted. purpose with or without fee is hereby granted.

39
NEWS
View File

@@ -1,3 +1,42 @@
1.1.1 (2021-11-04)
* Terminal focus in/out events no longer ring the terminall bell
* Made mouse work in non-rxvt terminals with recent xterm terminfo
1.1.0 (2021-10-21)
* Now requesting and processing terminal de/focus events,
using a new "defocused" attribute for selected rows
* 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)
* Coming with a real manual page instead of a help2man-generated stub
* Added a mode to poll MPD for the elapsed time, enabled by default,
fixing two cases of improper tracking
* Started showing song duration in the library
* Added C-PgUp/PgDown and C-Left/Right bindings to iterate tabs
* Added VIM-like C-y and C-e bindings for scrolling
* Added Windows Explorer-like M-Up binding to go up a directory
* Worked around a cURL bug crashing the application
* Fixed handling of direct SHOUTcast streams
* Miscellaneous little fixes
0.9.0 (2018-11-02) 0.9.0 (2018-11-02)
* Initial release * Initial release

View File

@@ -22,11 +22,16 @@ Packages
Regular releases are sporadic. git master should be stable enough. You can get Regular releases are sporadic. git master should be stable enough. You can get
a package with the latest development version from Archlinux's AUR. a package with the latest development version from Archlinux's AUR.
Building and Running Documentation
-------------------- -------------
Build dependencies: CMake, pkg-config, help2man, liberty (included), See the link:nncmpp.adoc[man page] for information about usage.
The rest of this README will concern itself with externalities.
Building
--------
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
@@ -43,37 +48,6 @@ Or you can try telling CMake to make a package for you. For Debian it is:
$ cpack -G DEB $ cpack -G DEB
# dpkg -i nncmpp-*.deb # dpkg -i nncmpp-*.deb
Having the program installed, create a configuration file and run it.
Configuration
-------------
Create _~/.config/nncmpp/nncmpp.conf_ with contents like the following:
....
settings = {
address = "localhost:6600"
password = "<your password>"
root = "~/Music"
}
colors = {
normal = ""
highlight = "bold"
elapsed = "reverse"
remains = "ul"
tab_bar = "reverse"
tab_active = "ul"
even = ""
odd = ""
selection = "reverse"
multiselect = "-1 6"
scrollbar = ""
}
streams = {
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
}
....
Terminal caveats Terminal caveats
---------------- ----------------
This application aspires to be as close to a GUI as possible. It expects you This application aspires to be as close to a GUI as possible. It expects you

View File

@@ -5,6 +5,7 @@
#define PROGRAM_VERSION "${PROJECT_VERSION}" #define PROGRAM_VERSION "${PROJECT_VERSION}"
#cmakedefine HAVE_RESIZETERM #cmakedefine HAVE_RESIZETERM
#cmakedefine WITH_FFTW
#endif // ! CONFIG_H #endif // ! CONFIG_H

View File

@@ -12,6 +12,7 @@ colors = {
selection = "231 202" selection = "231 202"
multiselect = "231 88" multiselect = "231 88"
defocused = "231 250"
directory = "16 231 bold" directory = "16 231 bold"
incoming = "28" incoming = "28"

113
nncmpp.adoc Normal file
View File

@@ -0,0 +1,113 @@
nncmpp(1)
=========
:doctype: manpage
:manmanual: nncmpp Manual
:mansource: nncmpp {release-version}
Name
----
nncmpp - terminal-based MPD client
Synopsis
--------
*nncmpp* [_OPTION_]... [_URL_ | _PATH_]...
Description
-----------
*nncmpp* is a terminal-based GUI-like MPD client. On start up it will welcome
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*
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
-------
*-d*, *--debug*::
Adds a "Debug" tab showing all MPD communication and other information
that help debug various issues.
*-h*, *--help*::
Display a help message and exit.
*-V*, *--version*::
Output version information and exit.
Configuration
-------------
Unless you run MPD on a remote machine, on an unusual port, or protected by
a password, the client doesn't need a configuration file to work. It is,
however, likely that you'll want to customize the looks or add some streams.
You can start off with the following snippet:
....
settings = {
address = "localhost:6600"
password = "<your password>"
root = "~/Music"
}
colors = {
normal = ""
highlight = "bold"
elapsed = "reverse"
remains = "ul"
tab_bar = "reverse"
tab_active = "ul"
even = ""
odd = ""
selection = "reverse"
multiselect = "-1 6"
defocused = "ul"
scrollbar = ""
}
streams = {
"dnbradio.com" = "http://www.dnbradio.com/hi.m3u"
"BassDrive.com" = "http://bassdrive.com/v2/streams/BassDrive.pls"
}
....
Terminal attributes are accepted in a format similar to that of *git-config*(1),
only named colours aren't supported. The distribution contains example colour
schemes in the _contrib_ directory.
// TODO: it seems like liberty should contain an includable snippet about
// the format, which could form a part of nncmpp.conf(5).
Spectrum visualiser
-------------------
When built against the FFTW library, *nncmpp* can make use of MPD's "fifo"
output plugin to show the audio spectrum. This has some caveats, namely that
it may not be properly synchronized, only one instance of a client can read from
a given named pipe at a time, it will cost you some CPU time, and finally you'll
need to set it up manually to match your MPD configuration, e.g.:
....
settings = {
...
spectrum_path = "~/.mpd/mpd.fifo" # "path"
spectrum_format = "44100:16:2" # "format" (samplerate:bits:channels)
spectrum_bars = 8 # beware of exponential complexity
...
}
....
The sample rate should be greater than 40 kHz, the number of bits 8 or 16,
and the number of channels doesn't matter, as they're simply averaged together.
Files
-----
*nncmpp* follows the XDG Base Directory Specification.
_~/.config/nncmpp/nncmpp.conf_::
The configuration file.
Reporting bugs
--------------
Use https://git.janouch.name/p/nncmpp to report bugs, request features,
or submit pull requests.
See also
--------
*mpd*(1)

640
nncmpp.c
View File

@@ -1,7 +1,7 @@
/* /*
* nncmpp -- the MPD client you never knew you needed * nncmpp -- the MPD client you never knew you needed
* *
* Copyright (c) 2016 - 2020, Přemysl Eric Janouch <p@janouch.name> * Copyright (c) 2016 - 2021, Přemysl Eric Janouch <p@janouch.name>
* *
* Permission to use, copy, modify, and/or distribute this software for any * Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted. * purpose with or without fee is hereby granted.
@@ -39,6 +39,8 @@
* Can't use A_REVERSE because bold'd be bright. * Can't use A_REVERSE because bold'd be bright.
* Unfortunately ran out of B&W attributes. */ \ * Unfortunately ran out of B&W attributes. */ \
XX( MULTISELECT, multiselect, -1, 6, 0 ) \ 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 ) \ XX( SCROLLBAR, scrollbar, -1, -1, 0 ) \
/* These are for debugging only */ \ /* These are for debugging only */ \
XX( WARNING, warning, 3, -1, 0 ) \ XX( WARNING, warning, 3, -1, 0 ) \
@@ -76,15 +78,13 @@ enum
#include <math.h> #include <math.h>
#include <locale.h> #include <locale.h>
#include <termios.h> #include <termios.h>
#ifndef TIOCGWINSZ
#include <sys/ioctl.h> #include <sys/ioctl.h>
#endif // ! TIOCGWINSZ
// ncurses is notoriously retarded for input handling, we need something // ncurses is notoriously retarded for input handling, we need something
// different if only to receive mouse events reliably. // different if only to receive mouse events reliably.
// //
// 2020 update: ncurses is mostly reliable now but rxvt-unicode needs to start // 2021 update: ncurses is mostly reliable now, though rxvt-unicode only
// supporting 1006, or ncurses needs to start supporting the 1015 mode. // supports the 1006 mode that ncurses also supports mode starting with 9.25.
#include "termo.h" #include "termo.h"
@@ -93,6 +93,13 @@ enum
#include <curl/curl.h> #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
#define APP_TITLE PROGRAM_NAME ///< Left top corner #define APP_TITLE PROGRAM_NAME ///< Left top corner
// --- Utilities --------------------------------------------------------------- // --- Utilities ---------------------------------------------------------------
@@ -101,7 +108,7 @@ enum
static void static void
update_curses_terminal_size (void) update_curses_terminal_size (void)
{ {
#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ) #if defined HAVE_RESIZETERM && defined TIOCGWINSZ
struct winsize size; struct winsize size;
if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size))
{ {
@@ -558,6 +565,301 @@ item_list_resize (struct item_list *self, size_t len)
self->len = len; 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 *spectrum; ///< String buffer for the "render"
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->spectrum;
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: 9 * 7 = 63 dB of range.
int height = N_ELEMENTS (spectrum_bars) - 1 + (int) (db / 7);
p += strlen (strcpy (p, spectrum_bars[MAX (height, 0)]));
}
}
static bool
spectrum_init (struct spectrum *s, char *format, int bars, 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->spectrum = xcalloc (sizeof *s->spectrum, s->bars * 3 + 1);
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);
}
// Limit updates to 30 times per second to limit CPU load
s->samples = s->sampling_rate / s->bins * 2 / 30;
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);
free (s->spectrum);
free (s->top_bins);
free (s->buffer);
memset (s, 0, sizeof *s);
}
#endif // WITH_FFTW
// --- Application ------------------------------------------------------------- // --- Application -------------------------------------------------------------
// Function names are prefixed mostly because of curses which clutters the // Function names are prefixed mostly because of curses which clutters the
@@ -658,6 +960,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
@@ -673,6 +976,13 @@ static struct app_context
int gauge_offset; ///< Offset to the gauge or -1 int gauge_offset; ///< Offset to the gauge or -1
int gauge_width; ///< Width of the gauge, if present int gauge_width; ///< Width of the gauge, if present
#ifdef WITH_FFTW
struct spectrum spectrum; ///< Spectrum analyser
int spectrum_fd; ///< FIFO file descriptor (non-blocking)
int spectrum_column, spectrum_row; ///< Position for fast refresh
struct poller_fd spectrum_event; ///< FIFO watcher
#endif // WITH_FFTW
struct line_editor editor; ///< Line editor struct line_editor editor; ///< Line editor
struct poller_idle refresh_event; ///< Refresh the screen struct poller_idle refresh_event; ///< Refresh the screen
@@ -682,6 +992,7 @@ static struct app_context
struct poller_timer tk_timer; ///< termo timeout timer struct poller_timer tk_timer; ///< termo timeout timer
bool locale_is_utf8; ///< The locale is Unicode bool locale_is_utf8; ///< The locale is Unicode
bool use_partial_boxes; ///< Use Unicode box drawing chars bool use_partial_boxes; ///< Use Unicode box drawing chars
bool focused; ///< Whether the terminal has focus
struct attrs attrs[ATTRIBUTE_COUNT]; struct attrs attrs[ATTRIBUTE_COUNT];
} }
@@ -741,10 +1052,28 @@ static struct config_schema g_config_settings[] =
{ .name = "password", { .name = "password",
.comment = "Password to use for MPD authentication", .comment = "Password to use for MPD authentication",
.type = CONFIG_ITEM_STRING }, .type = CONFIG_ITEM_STRING },
// NOTE: this is unused--in theory we could allow manual metadata adjustment
{ .name = "root", { .name = "root",
.comment = "Where all the files MPD is playing are located", .comment = "Where all the files MPD is playing are located",
.type = CONFIG_ITEM_STRING }, .type = CONFIG_ITEM_STRING },
#ifdef WITH_FFTW
{ .name = "spectrum_path",
.comment = "Visualizer feed path to a FIFO audio output",
.type = CONFIG_ITEM_STRING },
// MPD's "outputs" command doesn't include this information
{ .name = "spectrum_format",
.comment = "Visualizer feed data format",
.type = CONFIG_ITEM_STRING,
.default_ = "\"44100:16:2\"" },
// 10 is about the useful limit, then it gets too computationally expensive
{ .name = "spectrum_bars",
.comment = "Number of computed audio spectrum bars",
.type = CONFIG_ITEM_INTEGER,
.default_ = "8" },
#endif // WITH_FFTW
// Disabling this minimises MPD traffic and has the following caveats: // Disabling this minimises MPD traffic and has the following caveats:
// - when MPD stalls on retrieving audio data, we keep ticking // - when MPD stalls on retrieving audio data, we keep ticking
// - when the "play" succeeds in ACTION_MPD_REPLACE for the same item as // - when the "play" succeeds in ACTION_MPD_REPLACE for the same item as
@@ -894,11 +1223,17 @@ 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);
g.playback_info.key_xfrm = tolower_ascii_strxfrm; g.playback_info.key_xfrm = tolower_ascii_strxfrm;
#ifdef WITH_FFTW
g.spectrum_fd = -1;
g.spectrum_row = g.spectrum_column = -1;
#endif // WITH_FFTW
// This is also approximately what libunistring does internally, // This is also approximately what libunistring does internally,
// since the locale name is canonicalized by locale_charset(). // since the locale name is canonicalized by locale_charset().
// Note that non-Unicode locales are handled pretty inefficiently. // Note that non-Unicode locales are handled pretty inefficiently.
@@ -908,6 +1243,9 @@ app_init_context (void)
// TODO: make this configurable // TODO: make this configurable
g.use_partial_boxes = g.locale_is_utf8; g.use_partial_boxes = g.locale_is_utf8;
// Presumably, although not necessarily; unsure if queryable at all
g.focused = true;
app_init_attributes (); app_init_attributes ();
} }
@@ -916,9 +1254,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
@@ -947,8 +1285,18 @@ 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
spectrum_free (&g.spectrum);
if (g.spectrum_fd != -1)
{
poller_fd_reset (&g.spectrum_event);
xclose (g.spectrum_fd);
}
#endif // WITH_FFTW
line_editor_free (&g.editor); line_editor_free (&g.editor);
config_free (&g.config); config_free (&g.config);
@@ -1210,6 +1558,21 @@ app_draw_header (void)
g.tabs_offset = g.header_height; g.tabs_offset = g.header_height;
LIST_FOR_EACH (struct tab, iter, g.tabs) LIST_FOR_EACH (struct tab, iter, g.tabs)
row_buffer_append (&buf, iter->name, attrs[iter == g.active_tab]); row_buffer_append (&buf, iter->name, attrs[iter == g.active_tab]);
#ifdef WITH_FFTW
// This seems like the most reasonable, otherwise unoccupied space
if (g.spectrum_fd != -1)
{
// Find some space and remember where it was, for fast refreshes
row_buffer_ellipsis (&buf, COLS - g.spectrum.bars - 1);
row_buffer_align (&buf, COLS - g.spectrum.bars, attrs[false]);
g.spectrum_row = g.header_height;
g.spectrum_column = buf.total_width;
row_buffer_append (&buf, g.spectrum.spectrum, attrs[false]);
}
#endif // WITH_FFTW
app_flush_header (&buf, attrs[false]); app_flush_header (&buf, attrs[false]);
const char *header = g.active_tab->header; const char *header = g.active_tab->header;
@@ -1336,11 +1699,13 @@ app_draw_view (void)
bool override_colors = true; bool override_colors = true;
if (item_index == tab->item_selected) if (item_index == tab->item_selected)
row_attrs = APP_ATTR (SELECTION); row_attrs = g.focused
? APP_ATTR (SELECTION) : APP_ATTR (DEFOCUSED);
else if (tab->item_mark > -1 && else if (tab->item_mark > -1 &&
((item_index >= tab->item_mark && item_index <= tab->item_selected) ((item_index >= tab->item_mark && item_index <= tab->item_selected)
|| (item_index >= tab->item_selected && item_index <= tab->item_mark))) || (item_index >= tab->item_selected && item_index <= tab->item_mark)))
row_attrs = APP_ATTR (MULTISELECT); row_attrs = g.focused
? APP_ATTR (MULTISELECT) : APP_ATTR (DEFOCUSED);
else else
override_colors = false; override_colors = false;
@@ -2221,6 +2586,14 @@ app_init_bindings (const char *keymap,
static bool static bool
app_process_termo_event (termo_key_t *event) app_process_termo_event (termo_key_t *event)
{ {
bool handled = false;
if ((handled = event->type == TERMO_TYPE_FOCUS))
{
g.focused = !!event->code.focused;
app_invalidate ();
// Senseless fall-through
}
struct binding dummy = { *event, 0, 0 }, *binding; struct binding dummy = { *event, 0, 0 }, *binding;
if (g.editor.line) if (g.editor.line)
{ {
@@ -2228,7 +2601,7 @@ app_process_termo_event (termo_key_t *event)
sizeof *binding, app_binding_cmp))) sizeof *binding, app_binding_cmp)))
return app_editor_process_action (binding->action); return app_editor_process_action (binding->action);
if (event->type != TERMO_TYPE_KEY || event->modifiers != 0) if (event->type != TERMO_TYPE_KEY || event->modifiers != 0)
return false; return handled;
line_editor_insert (&g.editor, event->code.codepoint); line_editor_insert (&g.editor, event->code.codepoint);
app_invalidate (); app_invalidate ();
@@ -2247,7 +2620,7 @@ app_process_termo_event (termo_key_t *event)
if (app_goto_tab ((n == 0 ? 10 : n) - 1)) if (app_goto_tab ((n == 0 ? 10 : n) - 1))
return true; return true;
} }
return false; return handled;
} }
// --- Current tab ------------------------------------------------------------- // --- Current tab -------------------------------------------------------------
@@ -3405,6 +3778,143 @@ debug_tab_init (void)
return super; return super;
} }
// --- Spectrum analyser -------------------------------------------------------
#ifdef WITH_FFTW
static void
spectrum_redraw (void)
{
// A full refresh would be too computationally expensive,
// let's hack around it in this case
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));
mvaddstr (g.spectrum_row, g.spectrum_column, g.spectrum.spectrum);
attrset (0);
move (last_y, last_x);
refresh ();
}
else
app_invalidate ();
}
// When any problem occurs with the FIFO, we'll just give up on it completely
static void
spectrum_discard_fifo (void)
{
if (g.spectrum_fd != -1)
{
poller_fd_reset (&g.spectrum_event);
xclose (g.spectrum_fd);
g.spectrum_fd = -1;
spectrum_free (&g.spectrum);
g.spectrum_row = g.spectrum_column = -1;
app_invalidate ();
}
}
static void
spectrum_on_fifo_readable (const struct pollfd *pfd, void *user_data)
{
(void) user_data;
struct spectrum *s = &g.spectrum;
bool update = false;
ssize_t n;
restart:
while ((n = read (pfd->fd,
s->buffer + s->buffer_len, s->buffer_size - s->buffer_len)) > 0)
if ((s->buffer_len += n) == s->buffer_size)
{
update = true;
spectrum_sample (s);
s->buffer_len = 0;
}
if (!n)
spectrum_discard_fifo ();
else if (errno == EINTR)
goto restart;
else if (errno != EAGAIN)
{
print_error ("spectrum: %s", strerror (errno));
spectrum_discard_fifo ();
}
else if (update)
spectrum_redraw ();
}
// When playback is stopped, we need to feed the analyser some zeroes ourselves.
// We could also just hide it. Hard to say which is simpler or better.
static void
spectrum_clear (void)
{
if (g.spectrum_fd != -1)
{
struct spectrum *s = &g.spectrum;
memset (s->buffer, 0, s->buffer_size);
spectrum_sample (s);
spectrum_sample (s);
s->buffer_len = 0;
spectrum_redraw ();
}
}
static void
spectrum_setup_fifo (void)
{
const char *spectrum_path =
get_config_string (g.config.root, "settings.spectrum_path");
const char *spectrum_format =
get_config_string (g.config.root, "settings.spectrum_format");
struct config_item *spectrum_bars =
config_item_get (g.config.root, "settings.spectrum_bars", NULL);
if (!spectrum_path)
return;
struct error *e = NULL;
char *path = resolve_filename
(spectrum_path, resolve_relative_config_filename);
if (!path)
print_error ("spectrum: %s", "FIFO path could not be resolved");
else if (!g.locale_is_utf8)
print_error ("spectrum: %s", "UTF-8 locale required");
else if (!spectrum_init (&g.spectrum,
(char *) spectrum_format, spectrum_bars->value.integer, &e))
{
print_error ("spectrum: %s", e->message);
error_free (e);
}
else if ((g.spectrum_fd = open (path, O_RDONLY | O_NONBLOCK)) == -1)
{
print_error ("spectrum: %s: %s", path, strerror (errno));
spectrum_free (&g.spectrum);
}
else
{
g.spectrum_event = poller_fd_make (&g.poller, g.spectrum_fd);
g.spectrum_event.dispatcher = spectrum_on_fifo_readable;
poller_fd_set (&g.spectrum_event, POLLIN);
}
free (path);
}
#else // ! WITH_FFTW
#define spectrum_setup_fifo()
#define spectrum_clear()
#define spectrum_discard_fifo()
#endif // ! WITH_FFTW
// --- MPD interface ----------------------------------------------------------- // --- MPD interface -----------------------------------------------------------
static void static void
@@ -3465,6 +3975,10 @@ mpd_update_playback_state (void)
if (!strcmp (state, "play")) g.state = PLAYER_PLAYING; if (!strcmp (state, "play")) g.state = PLAYER_PLAYING;
if (!strcmp (state, "pause")) g.state = PLAYER_PAUSED; if (!strcmp (state, "pause")) g.state = PLAYER_PAUSED;
} }
if (g.state == PLAYER_STOPPED)
{
spectrum_clear ();
}
// Values in "time" are always rounded. "elapsed", introduced in MPD 0.16, // Values in "time" are always rounded. "elapsed", introduced in MPD 0.16,
// is in millisecond precision and "duration" as well, starting with 0.20. // is in millisecond precision and "duration" as well, starting with 0.20.
@@ -3681,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)
@@ -3690,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",
@@ -3716,10 +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);
}
} }
static void static void
@@ -3736,6 +4295,8 @@ mpd_on_failure (void *user_data)
mpd_update_playback_state (); mpd_update_playback_state ();
current_tab_update (); current_tab_update ();
info_tab_update (); info_tab_update ();
spectrum_discard_fifo ();
} }
static void static void
@@ -4016,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[])
{ {
@@ -4028,7 +4611,8 @@ main (int argc, char *argv[])
}; };
struct opt_handler oh = struct opt_handler oh =
opt_handler_make (argc, argv, opts, NULL, "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)
@@ -4051,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
@@ -4064,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);
@@ -4087,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);

2
termo

Submodule termo updated: f7912a8ce7...8265f075b1