1 /***************************************************************************
2 * Copyright (C) 2008-2014 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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. *
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. *
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>
37 #include "statusbar.h"
39 #include "screen_switcher.h"
43 using Global::MainStartY
;
44 using Global::MainHeight
;
46 Visualizer
*myVisualizer
;
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()))
69 m_samples
= 44100/fps
;
70 if (Config
.visualizer_in_stereo
)
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);
86 m_timer
= boost::posix_time::from_time_t(0);
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
);
99 std::wstring
Visualizer::title()
101 return L
"Music visualizer";
104 void Visualizer::update()
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
116 if (m_output_id
!= -1 && Global::Timer
- m_timer
> Config
.visualizer_sync_interval
)
118 Mpd
.DisableOutput(m_output_id
);
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);
127 if (Config
.visualizer_type
== VisualizerType::Spectrum
)
129 draw
= &Visualizer::DrawFrequencySpectrum
;
130 drawStereo
= &Visualizer::DrawFrequencySpectrumStereo
;
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
;
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();
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();
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
);
192 (this->*draw
)(buf
, samples_read
, 0, w
.getHeight());
197 int Visualizer::windowTimeout()
199 if (m_fifo
>= 0 && Status::State::player() == MPD::psPlay
)
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)
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]
225 int32_t point_y
, prev_point_y
= 0;
226 for (size_t x
= 0; x
< win_width
; ++x
)
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
);
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)
281 for (size_t x
= 0; x
< win_width
; ++x
)
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;
296 << toColor(j
, height
)
297 << Config
.visualizer_chars
[1]
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
;
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
]);
333 // appropriately scale the position
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]
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();
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]
379 /**********************************************************************/
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]
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;
403 size_t bar_bound_height
;
404 for (size_t x
= 0; x
< win_width
; ++x
)
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;
419 << toColor(j
, height
)
420 << Config
.visualizer_chars
[1]
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
;
442 case VisualizerType::WaveFilled
:
444 Config
.visualizer_type
= VisualizerType::Spectrum
;
446 Config
.visualizer_type
= VisualizerType::Ellipse
;
447 # endif // HAVE_FFTW3_H
450 case VisualizerType::Spectrum
:
451 Config
.visualizer_type
= VisualizerType::Ellipse
;
453 # endif // HAVE_FFTW3_H
454 case VisualizerType::Ellipse
:
455 Config
.visualizer_type
= VisualizerType::Wave
;
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()
474 void Visualizer::FindOutputID()
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();
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 : */