Compare commits
28 Commits
f4999a63a5
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba86961ba5
|
|||
|
0cdb4989e5
|
|||
|
6de940fe96
|
|||
|
6bd8c1db2f
|
|||
|
56efe9c6a9
|
|||
|
8a17e674f8
|
|||
|
bd0ee66c19
|
|||
|
6f6efe077b
|
|||
|
ee5c41b2bf
|
|||
|
9a67e076a9
|
|||
|
53fbb3dec1
|
|||
|
267598643a
|
|||
|
fba1210e9f
|
|||
|
30777e8fd3
|
|||
|
353174ee3c
|
|||
|
2d641d087f
|
|||
|
20c8385f2e
|
|||
|
fa4443a3ce
|
|||
|
14ba637d4b
|
|||
|
66bc3f1c2c
|
|||
|
0646cea126
|
|||
|
a439a56ee9
|
|||
|
120a11ca1b
|
|||
|
7e531e95c5
|
|||
|
0335443b22
|
|||
|
70ff29e3d5
|
|||
|
ba122b7672
|
|||
|
456fab5b11
|
32
.clang-format
Normal file
32
.clang-format
Normal 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
2
.gitignore
vendored
@@ -7,3 +7,5 @@
|
||||
/nncmpp.files
|
||||
/nncmpp.creator*
|
||||
/nncmpp.includes
|
||||
/nncmpp.cflags
|
||||
/nncmpp.cxxflags
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required (VERSION 3.0)
|
||||
project (nncmpp VERSION 0.9.0 LANGUAGES C)
|
||||
project (nncmpp VERSION 1.1.1 LANGUAGES C)
|
||||
|
||||
# Moar warnings
|
||||
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
|
||||
@@ -21,38 +21,59 @@ include (AddThreads)
|
||||
find_package (Termo QUIET NO_MODULE)
|
||||
option (USE_SYSTEM_TERMO
|
||||
"Don't compile our own termo library, use the system one" ${Termo_FOUND})
|
||||
|
||||
if (USE_SYSTEM_TERMO)
|
||||
if (NOT Termo_FOUND)
|
||||
message (FATAL_ERROR "System termo library not found")
|
||||
endif (NOT Termo_FOUND)
|
||||
endif ()
|
||||
else ()
|
||||
# We don't want the library to install, but EXCLUDE_FROM_ALL ignores tests
|
||||
add_subdirectory (termo EXCLUDE_FROM_ALL)
|
||||
# We don't have many good choices when we don't want to install it and want
|
||||
# to support older versions of CMake; this is a relatively clean approach
|
||||
# (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).
|
||||
file (WRITE ${PROJECT_BINARY_DIR}/CTestCustom.cmake
|
||||
"execute_process (COMMAND ${CMAKE_COMMAND} --build termo)")
|
||||
|
||||
# We don't have many good choices; this is a relatively clean approach
|
||||
# (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
|
||||
DIRECTORY termo INCLUDE_DIRECTORIES)
|
||||
set (Termo_LIBRARIES termo-static)
|
||||
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}
|
||||
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS})
|
||||
link_directories (${curl_LIBRARY_DIRS})
|
||||
${Ncursesw_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS} ${curl_INCLUDE_DIRS}
|
||||
${fftw_INCLUDE_DIRS})
|
||||
link_directories (${curl_LIBRARY_DIRS} ${fftw_LIBRARY_DIRS})
|
||||
|
||||
# Configuration
|
||||
include (CheckFunctionExists)
|
||||
set (CMAKE_REQUIRED_LIBRARIES ${Ncursesw_LIBRARIES})
|
||||
CHECK_FUNCTION_EXISTS ("resizeterm" HAVE_RESIZETERM)
|
||||
|
||||
if ("${CMAKE_SYSTEM_NAME}" MATCHES "BSD")
|
||||
# Need this for SIGWINCH in FreeBSD and OpenBSD respectively;
|
||||
# our POSIX version macros make it undefined
|
||||
add_definitions (-D__BSD_VISIBLE=1 -D_BSD_SOURCE=1)
|
||||
elseif (APPLE)
|
||||
add_definitions (-D_DARWIN_C_SOURCE)
|
||||
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
|
||||
configure_file (${PROJECT_SOURCE_DIR}/config.h.in
|
||||
${PROJECT_BINARY_DIR}/config.h)
|
||||
@@ -61,27 +82,31 @@ include_directories (${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR})
|
||||
# Build the main executable and link it
|
||||
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
|
||||
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})
|
||||
|
||||
# Installation
|
||||
include (GNUInstallDirs)
|
||||
install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
|
||||
install (DIRECTORY contrib DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME})
|
||||
|
||||
# Generate documentation from program help
|
||||
find_program (HELP2MAN_EXECUTABLE help2man)
|
||||
if (NOT HELP2MAN_EXECUTABLE)
|
||||
message (FATAL_ERROR "help2man not found")
|
||||
# Generate documentation from text markup
|
||||
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
|
||||
if (NOT ASCIIDOCTOR_EXECUTABLE)
|
||||
message (FATAL_ERROR "asciidoctor not found")
|
||||
endif ()
|
||||
|
||||
foreach (page ${PROJECT_NAME})
|
||||
set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
|
||||
list (APPEND project_MAN_PAGES "${page_output}")
|
||||
add_custom_command (OUTPUT ${page_output}
|
||||
COMMAND ${HELP2MAN_EXECUTABLE} -N
|
||||
"${PROJECT_BINARY_DIR}/${page}" -o ${page_output}
|
||||
DEPENDS ${page}
|
||||
COMMAND ${ASCIIDOCTOR_EXECUTABLE} -b manpage
|
||||
-a release-version=${PROJECT_VERSION}
|
||||
"${PROJECT_SOURCE_DIR}/${page}.adoc"
|
||||
-o "${page_output}"
|
||||
DEPENDS ${page}.adoc
|
||||
COMMENT "Generating man page for ${page}" VERBATIM)
|
||||
endforeach ()
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
39
NEWS
39
NEWS
@@ -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)
|
||||
|
||||
* Initial release
|
||||
|
||||
44
README.adoc
44
README.adoc
@@ -22,11 +22,16 @@ Packages
|
||||
Regular releases are sporadic. git master should be stable enough. You can get
|
||||
a package with the latest development version from Archlinux's AUR.
|
||||
|
||||
Building and Running
|
||||
--------------------
|
||||
Build dependencies: CMake, pkg-config, help2man, liberty (included),
|
||||
Documentation
|
||||
-------------
|
||||
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) +
|
||||
Runtime dependencies: ncursesw, libunistring, cURL
|
||||
Runtime dependencies: ncursesw, libunistring, cURL, fftw3 (optional)
|
||||
|
||||
$ git clone --recursive https://git.janouch.name/p/nncmpp.git
|
||||
$ 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
|
||||
# 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
|
||||
----------------
|
||||
This application aspires to be as close to a GUI as possible. It expects you
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#define PROGRAM_VERSION "${PROJECT_VERSION}"
|
||||
|
||||
#cmakedefine HAVE_RESIZETERM
|
||||
#cmakedefine WITH_FFTW
|
||||
|
||||
#endif // ! CONFIG_H
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ colors = {
|
||||
|
||||
selection = "231 202"
|
||||
multiselect = "231 88"
|
||||
defocused = "231 250"
|
||||
directory = "16 231 bold"
|
||||
|
||||
incoming = "28"
|
||||
|
||||
113
nncmpp.adoc
Normal file
113
nncmpp.adoc
Normal 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
640
nncmpp.c
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 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
|
||||
* purpose with or without fee is hereby granted.
|
||||
@@ -39,6 +39,8 @@
|
||||
* 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 ) \
|
||||
@@ -76,15 +78,13 @@ enum
|
||||
#include <math.h>
|
||||
#include <locale.h>
|
||||
#include <termios.h>
|
||||
#ifndef TIOCGWINSZ
|
||||
#include <sys/ioctl.h>
|
||||
#endif // ! TIOCGWINSZ
|
||||
|
||||
// ncurses is notoriously retarded for input handling, we need something
|
||||
// different if only to receive mouse events reliably.
|
||||
//
|
||||
// 2020 update: ncurses is mostly reliable now but rxvt-unicode needs to start
|
||||
// supporting 1006, or ncurses needs to start supporting the 1015 mode.
|
||||
// 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"
|
||||
|
||||
@@ -93,6 +93,13 @@ enum
|
||||
|
||||
#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
|
||||
|
||||
// --- Utilities ---------------------------------------------------------------
|
||||
@@ -101,7 +108,7 @@ enum
|
||||
static void
|
||||
update_curses_terminal_size (void)
|
||||
{
|
||||
#if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ)
|
||||
#if defined HAVE_RESIZETERM && defined TIOCGWINSZ
|
||||
struct winsize 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;
|
||||
}
|
||||
|
||||
// --- 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 -------------------------------------------------------------
|
||||
|
||||
// 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 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 *tabs; ///< All other tabs
|
||||
@@ -673,6 +976,13 @@ static struct app_context
|
||||
int gauge_offset; ///< Offset to the gauge or -1
|
||||
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 poller_idle refresh_event; ///< Refresh the screen
|
||||
|
||||
@@ -682,6 +992,7 @@ static struct app_context
|
||||
struct poller_timer tk_timer; ///< termo timeout timer
|
||||
bool locale_is_utf8; ///< The locale is Unicode
|
||||
bool use_partial_boxes; ///< Use Unicode box drawing chars
|
||||
bool focused; ///< Whether the terminal has focus
|
||||
|
||||
struct attrs attrs[ATTRIBUTE_COUNT];
|
||||
}
|
||||
@@ -741,10 +1052,28 @@ static struct config_schema g_config_settings[] =
|
||||
{ .name = "password",
|
||||
.comment = "Password to use for MPD authentication",
|
||||
.type = CONFIG_ITEM_STRING },
|
||||
|
||||
// NOTE: this is unused--in theory we could allow manual metadata adjustment
|
||||
{ .name = "root",
|
||||
.comment = "Where all the files MPD is playing are located",
|
||||
.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:
|
||||
// - when MPD stalls on retrieving audio data, we keep ticking
|
||||
// - 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.config = config_make ();
|
||||
g.streams = strv_make ();
|
||||
g.enqueue = strv_make ();
|
||||
g.playlist = item_list_make ();
|
||||
|
||||
g.playback_info = str_map_make (free);
|
||||
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,
|
||||
// since the locale name is canonicalized by locale_charset().
|
||||
// Note that non-Unicode locales are handled pretty inefficiently.
|
||||
@@ -908,6 +1243,9 @@ app_init_context (void)
|
||||
// TODO: make this configurable
|
||||
g.use_partial_boxes = g.locale_is_utf8;
|
||||
|
||||
// Presumably, although not necessarily; unsure if queryable at all
|
||||
g.focused = true;
|
||||
|
||||
app_init_attributes ();
|
||||
}
|
||||
|
||||
@@ -916,9 +1254,9 @@ app_init_terminal (void)
|
||||
{
|
||||
TERMO_CHECK_VERSION;
|
||||
if (!(g.tk = termo_new (STDIN_FILENO, NULL, 0)))
|
||||
abort ();
|
||||
exit_fatal ("failed to set up the terminal");
|
||||
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...
|
||||
if (start_color () == ERR
|
||||
@@ -947,8 +1285,18 @@ app_free_context (void)
|
||||
mpd_client_free (&g.client);
|
||||
str_map_free (&g.playback_info);
|
||||
strv_free (&g.streams);
|
||||
strv_free (&g.enqueue);
|
||||
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);
|
||||
|
||||
config_free (&g.config);
|
||||
@@ -1210,6 +1558,21 @@ app_draw_header (void)
|
||||
g.tabs_offset = g.header_height;
|
||||
LIST_FOR_EACH (struct tab, iter, g.tabs)
|
||||
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]);
|
||||
|
||||
const char *header = g.active_tab->header;
|
||||
@@ -1336,11 +1699,13 @@ app_draw_view (void)
|
||||
|
||||
bool override_colors = true;
|
||||
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 &&
|
||||
((item_index >= tab->item_mark && item_index <= tab->item_selected)
|
||||
|| (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
|
||||
override_colors = false;
|
||||
|
||||
@@ -2221,6 +2586,14 @@ app_init_bindings (const char *keymap,
|
||||
static bool
|
||||
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;
|
||||
if (g.editor.line)
|
||||
{
|
||||
@@ -2228,7 +2601,7 @@ app_process_termo_event (termo_key_t *event)
|
||||
sizeof *binding, app_binding_cmp)))
|
||||
return app_editor_process_action (binding->action);
|
||||
if (event->type != TERMO_TYPE_KEY || event->modifiers != 0)
|
||||
return false;
|
||||
return handled;
|
||||
|
||||
line_editor_insert (&g.editor, event->code.codepoint);
|
||||
app_invalidate ();
|
||||
@@ -2247,7 +2620,7 @@ app_process_termo_event (termo_key_t *event)
|
||||
if (app_goto_tab ((n == 0 ? 10 : n) - 1))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return handled;
|
||||
}
|
||||
|
||||
// --- Current tab -------------------------------------------------------------
|
||||
@@ -3405,6 +3778,143 @@ debug_tab_init (void)
|
||||
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 -----------------------------------------------------------
|
||||
|
||||
static void
|
||||
@@ -3465,6 +3975,10 @@ mpd_update_playback_state (void)
|
||||
if (!strcmp (state, "play")) g.state = PLAYER_PLAYING;
|
||||
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,
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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
|
||||
mpd_on_password_response (const struct mpd_response *response,
|
||||
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;
|
||||
|
||||
if (response->success)
|
||||
{
|
||||
mpd_request_info ();
|
||||
library_tab_reload (NULL);
|
||||
}
|
||||
mpd_on_ready ();
|
||||
else
|
||||
{
|
||||
print_error ("%s: %s",
|
||||
@@ -3716,10 +4278,7 @@ mpd_on_connected (void *user_data)
|
||||
mpd_client_add_task (c, mpd_on_password_response, NULL);
|
||||
}
|
||||
else
|
||||
{
|
||||
mpd_request_info ();
|
||||
library_tab_reload (NULL);
|
||||
}
|
||||
mpd_on_ready ();
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -3736,6 +4295,8 @@ mpd_on_failure (void *user_data)
|
||||
mpd_update_playback_state ();
|
||||
current_tab_update ();
|
||||
info_tab_update ();
|
||||
|
||||
spectrum_discard_fifo ();
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -4016,6 +4577,28 @@ app_init_poller_events (void)
|
||||
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
|
||||
main (int argc, char *argv[])
|
||||
{
|
||||
@@ -4028,7 +4611,8 @@ main (int argc, char *argv[])
|
||||
};
|
||||
|
||||
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;
|
||||
while ((c = opt_handler_get (&oh)) != -1)
|
||||
@@ -4051,12 +4635,6 @@ main (int argc, char *argv[])
|
||||
|
||||
argc -= optind;
|
||||
argv += optind;
|
||||
|
||||
if (argc)
|
||||
{
|
||||
opt_handler_usage (&oh, stderr);
|
||||
exit (EXIT_FAILURE);
|
||||
}
|
||||
opt_handler_free (&oh);
|
||||
|
||||
// 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");
|
||||
|
||||
app_init_context ();
|
||||
app_init_enqueue (argv, argc);
|
||||
app_load_configuration ();
|
||||
app_init_terminal ();
|
||||
signals_setup_handlers ();
|
||||
app_init_poller_events ();
|
||||
app_init_terminal ();
|
||||
|
||||
g_normal_keys = app_init_bindings ("normal",
|
||||
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_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;
|
||||
while (g.polling)
|
||||
poller_run (&g.poller);
|
||||
|
||||
2
termo
2
termo
Submodule termo updated: f7912a8ce7...8265f075b1
Reference in New Issue
Block a user