diff options
-rw-r--r-- | doc/config | 25 | ||||
-rw-r--r-- | doc/ncmpcpp.1 | 15 | ||||
-rw-r--r-- | src/screens/visualizer.cpp | 315 | ||||
-rw-r--r-- | src/screens/visualizer.h | 17 | ||||
-rw-r--r-- | src/settings.cpp | 20 | ||||
-rw-r--r-- | src/settings.h | 5 |
6 files changed, 338 insertions, 59 deletions
@@ -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 diff --git a/doc/ncmpcpp.1 b/doc/ncmpcpp.1 index 2fa8703c..6a8acbfa 100644 --- a/doc/ncmpcpp.1 +++ b/doc/ncmpcpp.1 @@ -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 diff --git a/src/screens/visualizer.cpp b/src/screens/visualizer.cpp index e3621612..f1a1a8a4 100644 --- a/src/screens/visualizer.cpp +++ b/src/screens/visualizer.cpp @@ -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; +# ifdef HAVE_FFTW3_H + m_read_samples = DFT_NONZERO_SIZE; +# else + m_read_samples = 44100 / fps; +# endif // HAVE_FFTW3_H if (Config.visualizer_in_stereo) - m_samples *= 2; + 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 = m_samples/2+1; + 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,52 +181,58 @@ 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); - m_auto_scale_multiplier += 1.0/fps; - for (auto &sample : samples) - { - double scale = std::numeric_limits<int16_t>::min(); - scale /= sample; - scale = fabs(scale); - if (scale < m_auto_scale_multiplier) - m_auto_scale_multiplier = scale; - } - for (auto &sample : samples) - { - int32_t tmp = sample; - if (m_auto_scale_multiplier <= 50.0) // limit the auto scale - tmp *= m_auto_scale_multiplier; - if (tmp < std::numeric_limits<int16_t>::min()) - sample = std::numeric_limits<int16_t>::min(); - else if (tmp > std::numeric_limits<int16_t>::max()) - sample = std::numeric_limits<int16_t>::max(); - else - sample = tmp; + if (Config.visualizer_autoscale) { + m_auto_scale_multiplier += 1.0/fps; + for (auto &sample : m_sample_buffer) + { + double scale = std::numeric_limits<int16_t>::min(); + scale /= sample; + scale = fabs(scale); + if (scale < m_auto_scale_multiplier) + m_auto_scale_multiplier = scale; + } + for (auto &sample : m_sample_buffer) + { + int32_t tmp = sample; + if (m_auto_scale_multiplier <= 50.0) // limit the auto scale + tmp *= m_auto_scale_multiplier; + if (tmp < std::numeric_limits<int16_t>::min()) + sample = std::numeric_limits<int16_t>::min(); + else if (tmp > std::numeric_limits<int16_t>::max()) + sample = std::numeric_limits<int16_t>::max(); + 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) + { + double bar_height = 0; + + // 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) { - 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); - - bar_bound_height = std::min(std::size_t(bar_height/bins_per_bar), height); - for (size_t j = 0; j < bar_bound_height; ++j) + 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 /**********************************************************************/ diff --git a/src/screens/visualizer.h b/src/screens/visualizer.h index 40e191ad..7fb7aa56 100644 --- a/src/screens/visualizer.h +++ b/src/screens/visualizer.h @@ -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 diff --git a/src/settings.cpp b/src/settings.cpp index 45b264bb..fe43e883 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -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) { diff --git a/src/settings.h b/src/settings.h index f31ba748..af0dd18f 100644 --- a/src/settings.h +++ b/src/settings.h @@ -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; |