visualizer: scale automatically if multiplier is 1
[ncmpcpp.git] / src / visualizer.cpp
blob6d10d36792c815c8da69a8af5a93d5fc8556d0dd
1 /***************************************************************************
2 * Copyright (C) 2008-2014 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include "visualizer.h"
23 #ifdef ENABLE_VISUALIZER
25 #include <boost/date_time/posix_time/posix_time.hpp>
26 #include <boost/math/constants/constants.hpp>
27 #include <cerrno>
28 #include <cmath>
29 #include <cstring>
30 #include <fstream>
31 #include <limits>
32 #include <fcntl.h>
34 #include "global.h"
35 #include "settings.h"
36 #include "status.h"
37 #include "statusbar.h"
38 #include "title.h"
39 #include "screen_switcher.h"
40 #include "status.h"
41 #include "enums.h"
43 using Global::MainStartY;
44 using Global::MainHeight;
46 Visualizer *myVisualizer;
48 namespace {
50 const int fps = 25;
52 // toColor: a scaling function for coloring. For numbers 0 to max this function returns
53 // a coloring from the lowest color to the highest, and colors will not loop from 0 to max.
54 const NC::Color &toColor(size_t number, size_t max, bool wrap = true)
56 const auto colors_size = Config.visualizer_colors.size();
57 const auto index = (number * colors_size) / max;
58 return Config.visualizer_colors[
59 wrap ? index % colors_size : std::min(index, colors_size-1)
65 Visualizer::Visualizer()
66 : Screen(NC::Window(0, MainStartY, COLS, MainHeight, "", NC::Color::Default, NC::Border()))
68 ResetFD();
69 m_samples = 44100/fps;
70 if (Config.visualizer_in_stereo)
71 m_samples *= 2;
72 # ifdef HAVE_FFTW3_H
73 m_fftw_results = m_samples/2+1;
74 m_freq_magnitudes.resize(m_fftw_results);
75 m_fftw_input = static_cast<double *>(fftw_malloc(sizeof(double)*m_samples));
76 m_fftw_output = static_cast<fftw_complex *>(fftw_malloc(sizeof(fftw_complex)*m_fftw_results));
77 m_fftw_plan = fftw_plan_dft_r2c_1d(m_samples, m_fftw_input, m_fftw_output, FFTW_ESTIMATE);
78 # endif // HAVE_FFTW3_H
81 void Visualizer::switchTo()
83 SwitchTo::execute(this);
84 w.clear();
85 SetFD();
86 m_timer = boost::posix_time::from_time_t(0);
87 drawHeader();
90 void Visualizer::resize()
92 size_t x_offset, width;
93 getWindowResizeParams(x_offset, width);
94 w.resize(width, MainHeight);
95 w.moveTo(x_offset, MainStartY);
96 hasToBeResized = 0;
99 std::wstring Visualizer::title()
101 return L"Music visualizer";
104 void Visualizer::update()
106 if (m_fifo < 0)
107 return;
109 // PCM in format 44100:16:1 (for mono visualization) and
110 // 44100:16:2 (for stereo visualization) is supported.
111 int16_t buf[m_samples];
112 ssize_t data = read(m_fifo, buf, sizeof(buf));
113 if (data < 0) // no data available in fifo
114 return;
116 if (m_output_id != -1 && Global::Timer - m_timer > Config.visualizer_sync_interval)
118 Mpd.DisableOutput(m_output_id);
119 usleep(50000);
120 Mpd.EnableOutput(m_output_id);
121 m_timer = Global::Timer;
124 void (Visualizer::*draw)(int16_t *, ssize_t, size_t, size_t);
125 void (Visualizer::*drawStereo)(int16_t *, int16_t *, ssize_t, size_t);
126 # ifdef HAVE_FFTW3_H
127 if (Config.visualizer_type == VisualizerType::Spectrum)
129 draw = &Visualizer::DrawFrequencySpectrum;
130 drawStereo = &Visualizer::DrawFrequencySpectrumStereo;
132 else
133 # endif // HAVE_FFTW3_H
134 if (Config.visualizer_type == VisualizerType::WaveFilled)
136 draw = &Visualizer::DrawSoundWaveFill;
137 drawStereo = &Visualizer::DrawSoundWaveFillStereo;
139 else if (Config.visualizer_type == VisualizerType::Ellipse)
141 draw = &Visualizer::DrawSoundEllipse;
142 drawStereo = &Visualizer::DrawSoundEllipseStereo;
144 else
146 draw = &Visualizer::DrawSoundWave;
147 drawStereo = &Visualizer::DrawSoundWaveStereo;
150 const ssize_t samples_read = data/sizeof(int16_t);
151 if (Config.visualizer_sample_multiplier == 1.0)
153 m_auto_scale_multiplier += 1.0/fps;
154 std::for_each(buf, buf+samples_read, [this](int16_t &sample) {
155 double scale = std::numeric_limits<int16_t>::min();
156 scale /= sample;
157 scale = fabs(scale);
158 if (scale < m_auto_scale_multiplier)
159 m_auto_scale_multiplier = scale;
162 std::for_each(buf, buf+samples_read, [this](int16_t &sample) {
163 int32_t tmp = sample;
164 if (Config.visualizer_sample_multiplier != 1.0)
165 tmp *= Config.visualizer_sample_multiplier;
166 else if (m_auto_scale_multiplier <= 50.0) // limit the auto scale
167 tmp *= m_auto_scale_multiplier;
168 if (tmp < std::numeric_limits<int16_t>::min())
169 sample = std::numeric_limits<int16_t>::min();
170 else if (tmp > std::numeric_limits<int16_t>::max())
171 sample = std::numeric_limits<int16_t>::max();
172 else
173 sample = tmp;
176 w.clear();
177 if (Config.visualizer_in_stereo)
179 auto chan_samples = samples_read/2;
180 int16_t buf_left[chan_samples], buf_right[chan_samples];
181 for (ssize_t i = 0, j = 0; i < samples_read; i += 2, ++j)
183 buf_left[j] = buf[i];
184 buf_right[j] = buf[i+1];
186 size_t half_height = w.getHeight()/2;
188 (this->*drawStereo)(buf_left, buf_right, chan_samples, half_height);
190 else
192 (this->*draw)(buf, samples_read, 0, w.getHeight());
194 w.refresh();
197 int Visualizer::windowTimeout()
199 if (m_fifo >= 0 && Status::State::player() == MPD::psPlay)
200 return 1000/fps;
201 else
202 return Screen<WindowType>::windowTimeout();
205 /**********************************************************************/
207 void Visualizer::DrawSoundWave(int16_t *buf, ssize_t samples, size_t y_offset, size_t height)
209 const size_t half_height = height/2;
210 const size_t base_y = y_offset+half_height;
211 const size_t win_width = w.getWidth();
212 const int samples_per_column = samples/win_width;
214 // too little samples
215 if (samples_per_column == 0)
216 return;
218 auto draw_point = [&](size_t x, int32_t y) {
219 w << NC::XY(x, base_y+y)
220 << toColor(std::abs(y), half_height, false)
221 << Config.visualizer_chars[0]
222 << NC::Color::End;
225 int32_t point_y, prev_point_y = 0;
226 for (size_t x = 0; x < win_width; ++x)
228 point_y = 0;
229 // calculate mean from the relevant points
230 for (int j = 0; j < samples_per_column; ++j)
231 point_y += buf[x*samples_per_column+j];
232 point_y /= samples_per_column;
233 // normalize it to fit the screen
234 point_y *= height / 65536.0;
236 draw_point(x, point_y);
238 // if the gap between two consecutive points is too big,
239 // intermediate values are needed for the wave to be watchable.
240 if (x > 0 && std::abs(prev_point_y-point_y) > 1)
242 const int32_t half = (prev_point_y+point_y)/2;
243 if (prev_point_y < point_y)
245 for (auto y = prev_point_y; y < point_y; ++y)
246 draw_point(x-(y < half), y);
248 else
250 for (auto y = prev_point_y; y > point_y; --y)
251 draw_point(x-(y > half), y);
254 prev_point_y = point_y;
258 void Visualizer::DrawSoundWaveStereo(int16_t *buf_left, int16_t *buf_right, ssize_t samples, size_t height)
260 DrawSoundWave(buf_left, samples, 0, height);
261 DrawSoundWave(buf_right, samples, height, w.getHeight() - height);
264 /**********************************************************************/
266 // DrawSoundWaveFill: This visualizer is very similar to DrawSoundWave, but instead of
267 // a single line the entire height is filled. In stereo mode, the top half of the screen
268 // is dedicated to the right channel, the bottom the left channel.
269 void Visualizer::DrawSoundWaveFill(int16_t *buf, ssize_t samples, size_t y_offset, size_t height)
271 // if right channel is drawn, bars descend from the top to the bottom
272 const bool flipped = y_offset > 0;
273 const size_t win_width = w.getWidth();
274 const int samples_per_column = samples/win_width;
276 // too little samples
277 if (samples_per_column == 0)
278 return;
280 int32_t point_y;
281 for (size_t x = 0; x < win_width; ++x)
283 point_y = 0;
284 // calculate mean from the relevant points
285 for (int j = 0; j < samples_per_column; ++j)
286 point_y += buf[x*samples_per_column+j];
287 point_y /= samples_per_column;
288 // normalize it to fit the screen
289 point_y = std::abs(point_y);
290 point_y *= height / 32768.0;
292 for (int32_t j = 0; j < point_y; ++j)
294 size_t y = flipped ? y_offset+j : y_offset+height-j-1;
295 w << NC::XY(x, y)
296 << toColor(j, height)
297 << Config.visualizer_chars[1]
298 << NC::Color::End;
303 void Visualizer::DrawSoundWaveFillStereo(int16_t *buf_left, int16_t *buf_right, ssize_t samples, size_t height)
305 DrawSoundWaveFill(buf_left, samples, 0, height);
306 DrawSoundWaveFill(buf_right, samples, height, w.getHeight() - height);
309 /**********************************************************************/
311 // draws the sound wave as an ellipse with origin in the center of the screen
312 void Visualizer::DrawSoundEllipse(int16_t *buf, ssize_t samples, size_t, size_t height)
314 const size_t half_width = w.getWidth()/2;
315 const size_t half_height = height/2;
317 // make it so that the loop goes around the ellipse exactly once
318 const double deg_multiplier = 2*boost::math::constants::pi<double>()/samples;
320 int32_t x, y;
321 double radius, max_radius;
322 for (ssize_t i = 0; i < samples; ++i)
324 x = half_width * std::cos(i*deg_multiplier);
325 y = half_height * std::sin(i*deg_multiplier);
326 max_radius = sqrt(x*x + y*y);
328 // calculate the distance of the sample from the center,
329 // where 0 is the center of the ellipse and 1 is its border
330 radius = std::abs(buf[i]);
331 radius /= 32768.0;
333 // appropriately scale the position
334 x *= radius;
335 y *= radius;
337 w << NC::XY(half_width + x, half_height + y)
338 << toColor(sqrt(x*x + y*y), max_radius, false)
339 << Config.visualizer_chars[0]
340 << NC::Color::End;
344 // DrawSoundEllipseStereo: This visualizer only works in stereo. The colors form concentric
345 // rings originating from the center (width/2, height/2). For any given point, the width is
346 // scaled with the left channel and height is scaled with the right channel. For example,
347 // if a song is entirely in the right channel, then it would just be a vertical line.
349 // Since every font/terminal is different, the visualizer is never a perfect circle. This
350 // visualizer assume the font height is twice the length of the font's width. If the font
351 // is skinner or wider than this, instead of a circle it will be an ellipse.
352 void Visualizer::DrawSoundEllipseStereo(int16_t *buf_left, int16_t *buf_right, ssize_t samples, size_t half_height)
354 const size_t width = w.getWidth();
355 const size_t left_half_width = width/2;
356 const size_t right_half_width = width - left_half_width;
357 const size_t top_half_height = half_height;
358 const size_t bottom_half_height = w.getHeight() - half_height;
360 // Makes the radius of each ring be approximately 2 cells wide.
361 const int32_t radius = 2*Config.visualizer_colors.size();
362 int32_t x, y;
363 for (ssize_t i = 0; i < samples; ++i)
365 x = buf_left[i]/32768.0 * (buf_left[i] < 0 ? left_half_width : right_half_width);
366 y = buf_right[i]/32768.0 * (buf_right[i] < 0 ? top_half_height : bottom_half_height);
368 // The arguments to the toColor function roughly follow a circle equation where
369 // the center is not centered around (0,0). For example (x - w)^2 + (y-h)+2 = r^2
370 // centers the circle around the point (w,h). Because fonts are not all the same
371 // size, this will not always generate a perfect circle.
372 w << toColor(sqrt(x*x + 4*y*y), radius)
373 << NC::XY(left_half_width + x, top_half_height + y)
374 << Config.visualizer_chars[1]
375 << NC::Color::End;
379 /**********************************************************************/
381 #ifdef HAVE_FFTW3_H
382 void Visualizer::DrawFrequencySpectrum(int16_t *buf, ssize_t samples, size_t y_offset, size_t height)
384 // if right channel is drawn, bars descend from the top to the bottom
385 const bool flipped = y_offset > 0;
387 // copy samples to fftw input array
388 for (unsigned i = 0; i < m_samples; ++i)
389 m_fftw_input[i] = i < samples ? buf[i] : 0;
390 fftw_execute(m_fftw_plan);
392 // count magnitude of each frequency and scale it to fit the screen
393 for (size_t i = 0; i < m_fftw_results; ++i)
394 m_freq_magnitudes[i] = sqrt(
395 m_fftw_output[i][0]*m_fftw_output[i][0]
396 + m_fftw_output[i][1]*m_fftw_output[i][1]
397 )/2e4*height;
399 const size_t win_width = w.getWidth();
400 // cut bandwidth a little to achieve better look
401 const double bins_per_bar = m_fftw_results/win_width * 7/10;
402 double bar_height;
403 size_t bar_bound_height;
404 for (size_t x = 0; x < win_width; ++x)
406 bar_height = 0;
407 for (int j = 0; j < bins_per_bar; ++j)
408 bar_height += m_freq_magnitudes[x*bins_per_bar+j];
409 // buff higher frequencies
410 bar_height *= log2(2 + x) * 100.0/win_width;
411 // moderately normalize the heights
412 bar_height = pow(bar_height, 0.5);
414 bar_bound_height = std::min(std::size_t(bar_height/bins_per_bar), height);
415 for (size_t j = 0; j < bar_bound_height; ++j)
417 size_t y = flipped ? y_offset+j : y_offset+height-j-1;
418 w << NC::XY(x, y)
419 << toColor(j, height)
420 << Config.visualizer_chars[1]
421 << NC::Color::End;
426 void Visualizer::DrawFrequencySpectrumStereo(int16_t *buf_left, int16_t *buf_right, ssize_t samples, size_t height)
428 DrawFrequencySpectrum(buf_left, samples, 0, height);
429 DrawFrequencySpectrum(buf_right, samples, height, w.getHeight() - height);
431 #endif // HAVE_FFTW3_H
433 /**********************************************************************/
435 void Visualizer::ToggleVisualizationType()
437 switch (Config.visualizer_type)
439 case VisualizerType::Wave:
440 Config.visualizer_type = VisualizerType::WaveFilled;
441 break;
442 case VisualizerType::WaveFilled:
443 # ifdef HAVE_FFTW3_H
444 Config.visualizer_type = VisualizerType::Spectrum;
445 # else
446 Config.visualizer_type = VisualizerType::Ellipse;
447 # endif // HAVE_FFTW3_H
448 break;
449 # ifdef HAVE_FFTW3_H
450 case VisualizerType::Spectrum:
451 Config.visualizer_type = VisualizerType::Ellipse;
452 break;
453 # endif // HAVE_FFTW3_H
454 case VisualizerType::Ellipse:
455 Config.visualizer_type = VisualizerType::Wave;
456 break;
458 Statusbar::printf("Visualization type: %1%", Config.visualizer_type);
461 void Visualizer::SetFD()
463 if (m_fifo < 0 && (m_fifo = open(Config.visualizer_fifo_path.c_str(), O_RDONLY | O_NONBLOCK)) < 0)
464 Statusbar::printf("Couldn't open \"%1%\" for reading PCM data: %2%",
465 Config.visualizer_fifo_path, strerror(errno)
469 void Visualizer::ResetFD()
471 m_fifo = -1;
474 void Visualizer::FindOutputID()
476 m_output_id = -1;
477 if (!Config.visualizer_output_name.empty())
479 for (MPD::OutputIterator out = Mpd.GetOutputs(), end; out != end; ++out)
481 if (out->name() == Config.visualizer_output_name)
483 m_output_id = out->id();
484 break;
487 if (m_output_id == -1)
488 Statusbar::printf("There is no output named \"%s\"", Config.visualizer_output_name);
492 void Visualizer::ResetAutoScaleMultiplier()
494 m_auto_scale_multiplier = std::numeric_limits<double>::infinity();
497 #endif // ENABLE_VISUALIZER
499 /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab : */