Use log scale for visualizer (#397)

* Visualizer: use log scale

- log scale frequency and gain
- Hamming windowing
- improve reading from fifo

* Fix Visualizer memory leaks

* Visualizer: use Blackman window

* Visualizer: support DFT zero padding

* Visualizer: support fractional height bars, fix fifo read

* Revert "Fix Visualizer memory leaks"

This reverts commit 5c6509d2b8ed985a4928f681217dc8616d053ace.

* Visualizer: fix fifo read again

* Visualizer: add cubic interpolation option

* Visualizer: Expose more config options, add docs for config options

* Visualizer: Use reverse video text for stereo visualizer smooth look

* Visualizer: use FormattedColor to for reverse-video

* Visualizer: change some config options for spectrum

* Fix build fftw disabled

* Visualizer: use [0,5] interval for dft_size config option
This commit is contained in:
Evan Chang
2020-12-13 08:13:45 -05:00
committed by GitHub
parent 750e7ff59d
commit 47b3baf93c
6 changed files with 338 additions and 59 deletions

View File

@@ -94,6 +94,31 @@
##
#visualizer_color = 41, 83, 119, 155, 185, 215, 209, 203, 197, 161
#
##
## Note: The next few visualization options apply to the spectrum visualizer
##
#
## Use unicode block characters for a smoother, more continuous look.
## This will override the visualizer_look option. With transparent terminals
## and visualizer_in_stereo set, artifacts may be visible on the bottom half of
## the visualization.
#
#visualizer_spectrum_smooth_look = yes
#
## A value between 0 and 5 inclusive. Specifying a larger value makes the
## visualizer look at a larger slice of time, which results less jumpy
## visualizer output.
#
#visualizer_spectrum_dft_size = 2
#
## Left-most frequency of visualizer in Hz, must be less than HZ MAX
#
#visualizer_spectrum_hz_min = 20
#
## Right-most frequency of visualizer in Hz, must be greater than HZ MIN
#
#visualizer_spectrum_hz_max = 20000
#
##### system encoding #####
##
## ncmpcpp should detect your charset encoding but if it failed to do so, you

View File

@@ -99,6 +99,21 @@ Defines visualizer's look (string has to be exactly 2 characters long: first one
.B visualizer_color = COLORS
Comma separated list of colors to be used in music visualization.
.TP
.B visualizer_autoscale = yes/no
Automatically scale visualizer size.
.TP
.B visualizer_spectrum_smooth_look = yes/no
For spectrum visualizer, use unicode block characters for a smoother, more continuous look. This will override the visualizer_look option. With transparent terminals and visualizer_in_stereo set, artifacts may be visible on the bottom half of the visualization.
.TP
.B visualizer_spectrum_dft_size = NUMBER
For spectrum visualizer, a value between 0 and 5 inclusive. Specifying a larger value makes the visualizer look at a larger slice of time, which results less jumpy visualizer output.
.TP
.B visualizer_spectrum_hz_min = Hz
For spectrum visualizer, left-most frequency of visualizer, must be less than HZ MAX.
.TP
.B visualizer_spectrum_hz_max = Hz
For spectrum visualizer, right-most frequency of visualizer, must be greater than HZ MIN.
.TP
.B system_encoding = ENCODING
If you use encoding other than utf8, set it in order to handle utf8 encoded strings properly.
.TP

View File

@@ -22,6 +22,7 @@
#ifdef ENABLE_VISUALIZER
#include <algorithm>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/math/constants/constants.hpp>
#include <cerrno>
@@ -30,6 +31,7 @@
#include <fstream>
#include <limits>
#include <fcntl.h>
#include <cassert>
#include "global.h"
#include "settings.h"
@@ -39,6 +41,7 @@
#include "screens/screen_switcher.h"
#include "status.h"
#include "enums.h"
#include "utility/wide_string.h"
using Samples = std::vector<int16_t>;
@@ -50,6 +53,7 @@ Visualizer *myVisualizer;
namespace {
const int fps = 25;
const uint32_t MIN_DFT_SIZE = 14;
// toColor: a scaling function for coloring. For numbers 0 to max this function
// returns a coloring from the lowest color to the highest, and colors will not
@@ -67,17 +71,39 @@ const NC::FormattedColor &toColor(size_t number, size_t max, bool wrap = true)
Visualizer::Visualizer()
: Screen(NC::Window(0, MainStartY, COLS, MainHeight, "", NC::Color::Default, NC::Border()))
# ifdef HAVE_FFTW3_H
,
DFT_NONZERO_SIZE(1 << Config.visualizer_spectrum_dft_size),
DFT_TOTAL_SIZE(Config.visualizer_spectrum_dft_size >= MIN_DFT_SIZE ? 1 << (Config.visualizer_spectrum_dft_size) : 1<<MIN_DFT_SIZE),
DYNAMIC_RANGE(100),
HZ_MIN(Config.visualizer_spectrum_hz_min),
HZ_MAX(Config.visualizer_spectrum_hz_max),
GAIN(0),
SMOOTH_CHARS(ToWString("▁▂▃▄▅▆▇█"))
#endif
{
ResetFD();
m_samples = 44100/fps;
if (Config.visualizer_in_stereo)
m_samples *= 2;
# ifdef HAVE_FFTW3_H
m_fftw_results = m_samples/2+1;
m_read_samples = DFT_NONZERO_SIZE;
# else
m_read_samples = 44100 / fps;
# endif // HAVE_FFTW3_H
if (Config.visualizer_in_stereo)
m_read_samples *= 2;
m_sample_buffer.resize(m_read_samples);
m_temp_sample_buffer.resize(m_read_samples);
memset(m_sample_buffer.data(), 0, m_sample_buffer.size()*sizeof(int16_t));
memset(m_temp_sample_buffer.data(), 0, m_sample_buffer.size()*sizeof(int16_t));
# ifdef HAVE_FFTW3_H
m_fftw_results = DFT_TOTAL_SIZE/2+1;
m_freq_magnitudes.resize(m_fftw_results);
m_fftw_input = static_cast<double *>(fftw_malloc(sizeof(double)*m_samples));
m_fftw_input = static_cast<double *>(fftw_malloc(sizeof(double)*DFT_TOTAL_SIZE));
memset(m_fftw_input, 0, sizeof(double)*DFT_TOTAL_SIZE);
m_fftw_output = static_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex)*m_fftw_results));
m_fftw_plan = fftw_plan_dft_r2c_1d(m_samples, m_fftw_input, m_fftw_output, FFTW_ESTIMATE);
m_fftw_plan = fftw_plan_dft_r2c_1d(DFT_TOTAL_SIZE, m_fftw_input, m_fftw_output, FFTW_ESTIMATE);
m_dft_logspace.reserve(500);
m_bar_heights.reserve(100);
# endif // HAVE_FFTW3_H
}
@@ -88,6 +114,11 @@ void Visualizer::switchTo()
SetFD();
m_timer = boost::posix_time::from_time_t(0);
drawHeader();
memset(m_sample_buffer.data(), 0, m_sample_buffer.size()*sizeof(int16_t));
# ifdef HAVE_FFTW3_H
GenLogspace();
m_bar_heights.reserve(w.getWidth());
# endif // HAVE_FFTW3_H
}
void Visualizer::resize()
@@ -97,6 +128,10 @@ void Visualizer::resize()
w.resize(width, MainHeight);
w.moveTo(x_offset, MainStartY);
hasToBeResized = 0;
# ifdef HAVE_FFTW3_H
GenLogspace();
m_bar_heights.reserve(w.getWidth());
# endif // HAVE_FFTW3_H
}
std::wstring Visualizer::title()
@@ -111,12 +146,20 @@ void Visualizer::update()
// PCM in format 44100:16:1 (for mono visualization) and
// 44100:16:2 (for stereo visualization) is supported.
Samples samples(m_samples);
ssize_t data = read(m_fifo, samples.data(),
samples.size() * sizeof(Samples::value_type));
if (data < 0) // no data available in fifo
const int buf_size = sizeof(int16_t)*m_read_samples;
ssize_t data = read(m_fifo, m_temp_sample_buffer.data(), buf_size);
if (data <= 0) // no data available in fifo
return;
{
// create int8_t pointers for arithmetic
int8_t *const sdata = (int8_t *)m_sample_buffer.data();
int8_t *const temp_sdata = (int8_t *)m_temp_sample_buffer.data();
int8_t *const sdata_end = sdata + buf_size;
memmove(sdata, sdata + data, buf_size - data);
memcpy(sdata_end - data, temp_sdata, data);
}
if (m_output_id != -1 && Global::Timer - m_timer > Config.visualizer_sync_interval)
{
Mpd.DisableOutput(m_output_id);
@@ -130,6 +173,7 @@ void Visualizer::update()
# ifdef HAVE_FFTW3_H
if (Config.visualizer_type == VisualizerType::Spectrum)
{
m_read_samples = DFT_NONZERO_SIZE;
draw = &Visualizer::DrawFrequencySpectrum;
drawStereo = &Visualizer::DrawFrequencySpectrumStereo;
}
@@ -137,23 +181,28 @@ void Visualizer::update()
# endif // HAVE_FFTW3_H
if (Config.visualizer_type == VisualizerType::WaveFilled)
{
m_read_samples = 44100 / fps;
draw = &Visualizer::DrawSoundWaveFill;
drawStereo = &Visualizer::DrawSoundWaveFillStereo;
}
else if (Config.visualizer_type == VisualizerType::Ellipse)
{
m_read_samples = 44100 / fps;
draw = &Visualizer::DrawSoundEllipse;
drawStereo = &Visualizer::DrawSoundEllipseStereo;
}
else
{
m_read_samples = 44100 / fps;
draw = &Visualizer::DrawSoundWave;
drawStereo = &Visualizer::DrawSoundWaveStereo;
}
m_sample_buffer.resize(m_read_samples);
m_temp_sample_buffer.resize(m_read_samples);
const ssize_t samples_read = data/sizeof(int16_t);
if (Config.visualizer_autoscale) {
m_auto_scale_multiplier += 1.0/fps;
for (auto &sample : samples)
for (auto &sample : m_sample_buffer)
{
double scale = std::numeric_limits<int16_t>::min();
scale /= sample;
@@ -161,7 +210,7 @@ void Visualizer::update()
if (scale < m_auto_scale_multiplier)
m_auto_scale_multiplier = scale;
}
for (auto &sample : samples)
for (auto &sample : m_sample_buffer)
{
int32_t tmp = sample;
if (m_auto_scale_multiplier <= 50.0) // limit the auto scale
@@ -173,16 +222,17 @@ void Visualizer::update()
else
sample = tmp;
}
}
w.clear();
if (Config.visualizer_in_stereo)
{
auto chan_samples = samples_read/2;
auto chan_samples = m_read_samples/2;
int16_t buf_left[chan_samples], buf_right[chan_samples];
for (ssize_t i = 0, j = 0; i < samples_read; i += 2, ++j)
for (ssize_t i = 0, j = 0; i < m_read_samples; i += 2, ++j)
{
buf_left[j] = samples[i];
buf_right[j] = samples[i+1];
buf_left[j] = m_sample_buffer[i];
buf_right[j] = m_sample_buffer[i+1];
}
size_t half_height = w.getHeight()/2;
@@ -190,7 +240,7 @@ void Visualizer::update()
}
else
{
(this->*draw)(samples.data(), samples_read, 0, w.getHeight());
(this->*draw)(m_sample_buffer.data(), m_read_samples, 0, w.getHeight());
}
w.refresh();
}
@@ -393,42 +443,111 @@ void Visualizer::DrawFrequencySpectrum(int16_t *buf, ssize_t samples, size_t y_o
// If right channel is drawn, bars descend from the top to the bottom.
const bool flipped = y_offset > 0;
// copy samples to fftw input array
for (unsigned i = 0; i < m_samples; ++i)
m_fftw_input[i] = i < samples ? buf[i] : 0;
// copy samples to fftw input array and apply Hamming window
ApplyWindow(m_fftw_input, buf, samples);
fftw_execute(m_fftw_plan);
// Count magnitude of each frequency and scale it to fit the screen.
// Count magnitude of each frequency and normalize
for (size_t i = 0; i < m_fftw_results; ++i)
m_freq_magnitudes[i] = sqrt(
m_fftw_output[i][0]*m_fftw_output[i][0]
+ m_fftw_output[i][1]*m_fftw_output[i][1]
)/2e4*height;
) / (DFT_NONZERO_SIZE);
const size_t win_width = w.getWidth();
// Cut bandwidth a little to achieve better look.
const double bins_per_bar = m_fftw_results/win_width * 7/10;
double bar_height;
size_t bar_bound_height;
double prev_bar_height = 0;
size_t cur_bin = 0;
while (cur_bin < m_fftw_results && Bin2Hz(cur_bin) < m_dft_logspace[0])
{
++cur_bin;
}
m_bar_heights.clear();
for (size_t x = 0; x < win_width; ++x)
{
bar_height = 0;
for (int j = 0; j < bins_per_bar; ++j)
bar_height += m_freq_magnitudes[x*bins_per_bar+j];
// Buff higher frequencies.
bar_height *= log2(2 + x) * 100.0/win_width;
// Moderately normalize the heights.
bar_height = pow(bar_height, 0.5);
double bar_height = 0;
bar_bound_height = std::min(std::size_t(bar_height/bins_per_bar), height);
for (size_t j = 0; j < bar_bound_height; ++j)
// accumulate bins
size_t count = 0;
// check right bound
while (cur_bin < m_fftw_results && Bin2Hz(cur_bin) < m_dft_logspace[x])
{
// check left bound if not first index
if (x == 0 || Bin2Hz(cur_bin) >= m_dft_logspace[x-1])
{
bar_height += m_freq_magnitudes[cur_bin];
++count;
}
++cur_bin;
}
if (count == 0)
continue;
// average bins
bar_height /= count;
prev_bar_height = bar_height;
// log scale bar heights
bar_height = (20 * log10(bar_height) + DYNAMIC_RANGE + GAIN) / DYNAMIC_RANGE;
// Scale bar height between 0 and height
bar_height = bar_height > 0 ? bar_height * height : 0;
bar_height = bar_height > height ? height : bar_height;
m_bar_heights.emplace_back(std::make_pair(x, bar_height));
}
size_t h_idx = 0;
for (size_t x = 0; x < win_width; ++x)
{
const size_t &i = m_bar_heights[h_idx].first;
const double &bar_height = m_bar_heights[h_idx].second;
double h = 0;
if (x == i) {
// this data point exists
h = bar_height;
if (h_idx < m_bar_heights.size()-1)
++h_idx;
} else {
// data point does not exist, need to interpolate
h = Interpolate(x, h_idx);
}
for (size_t j = 0; j < h; ++j)
{
size_t y = flipped ? y_offset+j : y_offset+height-j-1;
auto c = toColor(j, height);
auto color = toColor(j, height);
std::wstring ch;
bool reverse = false;
// select character to draw
if (Config.visualizer_spectrum_smooth_look) {
// smooth
const size_t &size = SMOOTH_CHARS.size();
const int idx = static_cast<int>(size*h) % size;
if (j < h-1 || idx == size-1) {
// full height
ch = SMOOTH_CHARS[size-1];
} else {
// fractional height
if (flipped) {
ch = SMOOTH_CHARS[size-idx-2];
color = NC::FormattedColor(color.color(), {NC::Format::Reverse});
} else {
ch = SMOOTH_CHARS[idx];
}
}
} else {
// default, non-smooth
ch = Config.visualizer_chars[1];
}
// draw character on screen
w << NC::XY(x, y)
<< c
<< Config.visualizer_chars[1]
<< NC::FormattedColor::End<>(c);
<< color
<< ch
<< NC::FormattedColor::End<>(color);
}
}
}
@@ -438,6 +557,86 @@ void Visualizer::DrawFrequencySpectrumStereo(int16_t *buf_left, int16_t *buf_rig
DrawFrequencySpectrum(buf_left, samples, 0, height);
DrawFrequencySpectrum(buf_right, samples, height, w.getHeight() - height);
}
double Visualizer::Interpolate(size_t x, size_t h_idx)
{
const double &x_next = m_bar_heights[h_idx].first;
const double &h_next = m_bar_heights[h_idx].second;
double dh = 0;
if (h_idx == 0) {
// no data points on left, linear extrap
if (h_idx < m_bar_heights.size()-1) {
const double &x_next2 = m_bar_heights[h_idx+1].first;
const double &h_next2 = m_bar_heights[h_idx+1].second;
dh = (h_next2 - h_next) / (x_next2 - x_next);
}
return h_next - dh*(x_next-x);
} else if (h_idx == 1) {
// one data point on left, linear interp
const double &x_prev = m_bar_heights[h_idx-1].first;
const double &h_prev = m_bar_heights[h_idx-1].second;
dh = (h_next - h_prev) / (x_next - x_prev);
return h_next - dh*(x_next-x);
} else if (h_idx < m_bar_heights.size()-1) {
// two data points on both sides, cubic interp
// see https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Interpolation_on_an_arbitrary_interval
const double &x_prev2 = m_bar_heights[h_idx-2].first;
const double &h_prev2 = m_bar_heights[h_idx-2].second;
const double &x_prev = m_bar_heights[h_idx-1].first;
const double &h_prev = m_bar_heights[h_idx-1].second;
const double &x_next2 = m_bar_heights[h_idx+1].first;
const double &h_next2 = m_bar_heights[h_idx+1].second;
const double m0 = (h_prev - h_prev2) / (x_prev - x_prev2);
const double m1 = (h_next2 - h_next) / (x_next2 - x_next);
const double t = (x - x_prev) / (x_next - x_prev);
const double h00 = 2*t*t*t - 3*t*t + 1;
const double h10 = t*t*t - 2*t*t + t;
const double h01 = -2*t*t*t + 3*t*t;
const double h11 = t*t*t - t*t;
return h00*h_prev + h10*(x_next-x_prev)*m0 + h01*h_next + h11*(x_next-x_prev)*m1;
}
// less than two data points on right, no interp, should never happen unless VERY low DFT size
return h_next;
}
void Visualizer::ApplyWindow(double *output, int16_t *input, ssize_t samples)
{
// Use Blackman window for low sidelobes and fast sidelobe rolloff
// don't care too much about mainlobe width
const double alpha = 0.16;
const double a0 = (1 - alpha) / 2;
const double a1 = 0.5;
const double a2 = alpha / 2;
const double pi = boost::math::constants::pi<double>();
for (unsigned i = 0; i < samples; ++i)
{
double window = a0 - a1*cos(2*pi*i/(DFT_NONZERO_SIZE-1)) + a2*cos(4*pi*i/(DFT_NONZERO_SIZE-1));
output[i] = window * input[i] / INT16_MAX;
}
}
double Visualizer::Bin2Hz(size_t bin)
{
return bin*44100/DFT_TOTAL_SIZE;
}
// Generate log-scaled vector of frequencies from HZ_MIN to HZ_MAX
void Visualizer::GenLogspace()
{
// Calculate number of extra bins needed between 0 HZ and HZ_MIN
const size_t win_width = w.getWidth();
const size_t left_bins = (log10(HZ_MIN) - win_width*log10(HZ_MIN)) / (log10(HZ_MIN) - log10(HZ_MAX));
// Generate logspaced frequencies
m_dft_logspace.resize(win_width);
const double log_scale = log10(HZ_MAX) / (left_bins + m_dft_logspace.size() - 1);
for (int i = left_bins; i < m_dft_logspace.size() + left_bins; ++i) {
m_dft_logspace[i - left_bins] = pow(10, i * log_scale);
}
}
#endif // HAVE_FFTW3_H
/**********************************************************************/

View File

@@ -71,19 +71,34 @@ private:
# ifdef HAVE_FFTW3_H
void DrawFrequencySpectrum(int16_t *, ssize_t, size_t, size_t);
void DrawFrequencySpectrumStereo(int16_t *, int16_t *, ssize_t, size_t);
void ApplyWindow(double *, int16_t *, ssize_t);
void GenLogspace();
double Bin2Hz(size_t);
double Interpolate(size_t, size_t);
# endif // HAVE_FFTW3_H
int m_output_id;
boost::posix_time::ptime m_timer;
int m_fifo;
size_t m_samples;
size_t m_read_samples;
std::vector<int16_t> m_sample_buffer;
std::vector<int16_t> m_temp_sample_buffer;
double m_auto_scale_multiplier;
# ifdef HAVE_FFTW3_H
size_t m_fftw_results;
double *m_fftw_input;
fftw_complex *m_fftw_output;
fftw_plan m_fftw_plan;
const uint32_t DFT_NONZERO_SIZE;
const uint32_t DFT_TOTAL_SIZE;
const double DYNAMIC_RANGE;
const double HZ_MIN;
const double HZ_MAX;
const double GAIN;
const std::wstring SMOOTH_CHARS;
std::vector<double> m_dft_logspace;
std::vector<std::pair<size_t, double>> m_bar_heights;
std::vector<double> m_freq_magnitudes;
# endif // HAVE_FFTW3_H

View File

@@ -291,6 +291,26 @@ bool Configuration::read(const std::vector<std::string> &config_paths, bool igno
boundsCheck<std::wstring::size_type>(result.size(), 2, 2);
return result;
});
p.add("visualizer_autoscale", &visualizer_autoscale, "yes", yes_no);
p.add("visualizer_spectrum_smooth_look", &visualizer_spectrum_smooth_look, "yes", yes_no);
p.add("visualizer_spectrum_dft_size", &visualizer_spectrum_dft_size,
"12", [](std::string v) {
uint32_t result = verbose_lexical_cast<uint32_t>(v);
boundsCheck<uint32_t>(result, 0, 5);
return result + 12;
});
p.add("visualizer_spectrum_hz_min", &visualizer_spectrum_hz_min,
"20", [](std::string v) {
auto result = verbose_lexical_cast<double>(v);
lowerBoundCheck<double>(result, 1);
return result;
});
p.add("visualizer_spectrum_hz_max", &visualizer_spectrum_hz_max,
"20000", [](std::string v) {
auto result = verbose_lexical_cast<double>(v);
lowerBoundCheck<double>(result, Config.visualizer_spectrum_hz_min+1);
return result;
});
p.add("visualizer_color", &visualizer_colors,
"blue, cyan, green, yellow, magenta, red", list_of<NC::FormattedColor>);
p.add("system_encoding", &system_encoding, "", [](std::string encoding) {

View File

@@ -82,6 +82,11 @@ struct Configuration
std::string lastfm_preferred_language;
std::wstring progressbar;
std::wstring visualizer_chars;
bool visualizer_autoscale;
bool visualizer_spectrum_smooth_look;
uint32_t visualizer_spectrum_dft_size;
double visualizer_spectrum_hz_min;
double visualizer_spectrum_hz_max;
std::string pattern;