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>
36 #include "statusbar.h"
38 #include "screen_switcher.h"
42 using Global::MainStartY
;
43 using Global::MainHeight
;
45 Visualizer
*myVisualizer
;
51 // toColor: a scaling function for coloring. For numbers 0 to max this function returns
52 // a coloring from the lowest color to the highest, and colors will not loop from 0 to max.
53 NC::Color
toColor(size_t number
, size_t max
, bool wrap
= true)
55 const auto colors_size
= Config
.visualizer_colors
.size();
56 const auto index
= (number
* colors_size
) / max
;
57 return Config
.visualizer_colors
[
58 wrap
? index
% colors_size
: std::min(index
, colors_size
-1)
64 Visualizer::Visualizer()
65 : Screen(NC::Window(0, MainStartY
, COLS
, MainHeight
, "", NC::Color::Default
, NC::Border::None
))
68 m_samples
= 44100/fps
;
69 if (Config
.visualizer_in_stereo
)
72 m_fftw_results
= m_samples
/2+1;
73 m_freq_magnitudes
.resize(m_fftw_results
);
74 m_fftw_input
= static_cast<double *>(fftw_malloc(sizeof(double)*m_samples
));
75 m_fftw_output
= static_cast<fftw_complex
*>(fftw_malloc(sizeof(fftw_complex
)*m_fftw_results
));
76 m_fftw_plan
= fftw_plan_dft_r2c_1d(m_samples
, m_fftw_input
, m_fftw_output
, FFTW_ESTIMATE
);
77 # endif // HAVE_FFTW3_H
80 void Visualizer::switchTo()
82 SwitchTo::execute(this);
85 m_timer
= boost::posix_time::from_time_t(0);
89 void Visualizer::resize()
91 size_t x_offset
, width
;
92 getWindowResizeParams(x_offset
, width
);
93 w
.resize(width
, MainHeight
);
94 w
.moveTo(x_offset
, MainStartY
);
98 std::wstring
Visualizer::title()
100 return L
"Music visualizer";
103 void Visualizer::update()
108 // PCM in format 44100:16:1 (for mono visualization) and
109 // 44100:16:2 (for stereo visualization) is supported.
110 int16_t buf
[m_samples
];
111 ssize_t data
= read(m_fifo
, buf
, sizeof(buf
));
112 if (data
< 0) // no data available in fifo
115 if (m_output_id
!= -1 && Global::Timer
- m_timer
> Config
.visualizer_sync_interval
)
117 Mpd
.DisableOutput(m_output_id
);
119 Mpd
.EnableOutput(m_output_id
);
120 m_timer
= Global::Timer
;
123 void (Visualizer::*draw
)(int16_t *, ssize_t
, size_t, size_t);
124 void (Visualizer::*drawStereo
)(int16_t *, int16_t *, ssize_t
, size_t);
126 if (Config
.visualizer_type
== VisualizerType::Spectrum
)
128 draw
= &Visualizer::DrawFrequencySpectrum
;
129 drawStereo
= &Visualizer::DrawFrequencySpectrumStereo
;
132 # endif // HAVE_FFTW3_H
133 if (Config
.visualizer_type
== VisualizerType::WaveFilled
)
135 draw
= &Visualizer::DrawSoundWaveFill
;
136 drawStereo
= &Visualizer::DrawSoundWaveFillStereo
;
138 else if (Config
.visualizer_type
== VisualizerType::Ellipse
)
140 //Ellipse only works with stereo
141 draw
= &Visualizer::DrawSoundWave
;
142 drawStereo
= &Visualizer::DrawSoundEllipseStereo
;
146 draw
= &Visualizer::DrawSoundWave
;
147 drawStereo
= &Visualizer::DrawSoundWaveStereo
;
150 const ssize_t samples_read
= data
/sizeof(int16_t);
151 std::for_each(buf
, buf
+samples_read
, [](int16_t &sample
) {
152 int32_t tmp
= sample
* Config
.visualizer_sample_multiplier
;
153 if (tmp
< std::numeric_limits
<int16_t>::min())
154 sample
= std::numeric_limits
<int16_t>::min();
155 else if (tmp
> std::numeric_limits
<int16_t>::max())
156 sample
= std::numeric_limits
<int16_t>::max();
162 if (Config
.visualizer_in_stereo
)
164 auto chan_samples
= samples_read
/2;
165 int16_t buf_left
[chan_samples
], buf_right
[chan_samples
];
166 for (ssize_t i
= 0, j
= 0; i
< samples_read
; i
+= 2, ++j
)
168 buf_left
[j
] = buf
[i
];
169 buf_right
[j
] = buf
[i
+1];
171 size_t half_height
= w
.getHeight()/2;
173 (this->*drawStereo
)(buf_left
, buf_right
, chan_samples
, half_height
);
177 (this->*draw
)(buf
, samples_read
, 0, w
.getHeight());
182 int Visualizer::windowTimeout()
184 if (m_fifo
>= 0 && Status::State::player() == MPD::psPlay
)
187 return Screen
<WindowType
>::windowTimeout();
190 void Visualizer::spacePressed()
192 std::string visualizerName
;
193 if (Config
.visualizer_type
== VisualizerType::Wave
)
195 Config
.visualizer_type
= VisualizerType::WaveFilled
;
196 visualizerName
= "sound wave filled";
198 else if (Config
.visualizer_type
== VisualizerType::WaveFilled
&& Config
.visualizer_in_stereo
)
200 Config
.visualizer_type
= VisualizerType::Ellipse
;
201 visualizerName
= "sound ellipse";
204 else if (Config
.visualizer_type
== VisualizerType::Ellipse
|| Config
.visualizer_type
== VisualizerType::WaveFilled
)
206 Config
.visualizer_type
= VisualizerType::Spectrum
;
207 visualizerName
= "frequency spectrum";
209 # endif // HAVE_FFTW3_H
212 Config
.visualizer_type
= VisualizerType::Wave
;
213 visualizerName
= "sound wave";
216 Statusbar::printf("Visualization type: %1%", visualizerName
.c_str());
219 void Visualizer::DrawSoundWaveStereo(int16_t *buf_left
, int16_t *buf_right
, ssize_t samples
, size_t height
)
221 DrawSoundWave(buf_left
, samples
, 0, height
);
222 DrawSoundWave(buf_right
, samples
, height
, w
.getHeight() - height
);
225 void Visualizer::DrawSoundWaveFillStereo(int16_t *buf_left
, int16_t *buf_right
, ssize_t samples
, size_t height
)
227 DrawSoundWaveFill(buf_left
, samples
, 0, height
);
228 DrawSoundWaveFill(buf_right
, samples
, height
, w
.getHeight() - height
);
231 // DrawSoundEllipseStereo: This visualizer only works in stereo. The colors form concentric
232 // rings originating from the center (width/2, height/2). For any given point, the width is
233 // scaled with the left channel and height is scaled with the right channel. For example,
234 // if a song is entirely in the right channel, then it would just be a vertical line.
236 // Since every font/terminal is different, the visualizer is never a perfect circle. This
237 // visualizer assume the font height is twice the length of the font's width. If the font
238 // is skinner or wider than this, instead of a circle it will be an ellipse.
239 void Visualizer::DrawSoundEllipseStereo(int16_t *buf_left
, int16_t *buf_right
, ssize_t samples
, size_t height
)
241 const long width
= w
.getWidth()/2;
243 // Makes the radius of the color circle proportional to max of height or width.
244 // Divide by colors size so that there are multiple color rings instead of just a few.
245 const long scaledRadius
= std::max(pow(width
,2), pow(height
,2))/pow(Config
.visualizer_colors
.size(),2);
246 for (size_t i
= 0; i
< samples
; ++i
)
248 long x
= width
+ ((double) buf_left
[i
] * 2 * ((double)width
/ 65536.0));
249 long y
= height
+ ((double) buf_right
[i
] * 2 * ((double)height
/ 65536.0));
251 // The arguments to the toColor function roughly follow a circle equation where
252 // the center is not centered around (0,0). For example (x - w)^2 + (y-h)+2 = r^2
253 // centers the circle around the point (w,h). Because fonts are not all the same
254 // size, this will not always generate a perfect circle.
255 w
<< toColor(pow((x
- width
)*1, 2) + pow((y
- ((long)height
)) * 2,2), scaledRadius
)
257 << Config
.visualizer_chars
[1]
262 // DrawSoundWaveFill: This visualizer is very similar to DrawSoundWave, but instead of
263 // a single line the entire height is filled. In stereo mode, the top half of the screen
264 // is dedicated to the right channel, the bottom the left channel.
265 void Visualizer::DrawSoundWaveFill(int16_t *buf
, ssize_t samples
, size_t y_offset
, size_t height
)
267 // if right channel is drawn, bars descend from the top to the bottom
268 const bool flipped
= y_offset
> 0;
269 const size_t win_width
= w
.getWidth();
270 const int samples_per_column
= samples
/win_width
;
272 // too little samples
273 if (samples_per_column
== 0)
277 for (size_t x
= 0; x
< win_width
; ++x
)
280 // calculate mean from the relevant points
281 for (int j
= 0; j
< samples_per_column
; ++j
)
282 point_y
+= buf
[x
*samples_per_column
+j
];
283 point_y
/= samples_per_column
;
284 // normalize it to fit the screen
285 point_y
= std::abs(point_y
);
286 point_y
*= height
/ 32768.0;
288 for (int32_t j
= 0; j
< point_y
; ++j
)
290 size_t y
= flipped
? y_offset
+j
: y_offset
+height
-j
-1;
292 << toColor(j
, height
)
293 << Config
.visualizer_chars
[1]
299 void Visualizer::DrawSoundWave(int16_t *buf
, ssize_t samples
, size_t y_offset
, size_t height
)
301 const size_t half_height
= height
/2;
302 const size_t base_y
= y_offset
+half_height
;
303 const size_t win_width
= w
.getWidth();
304 const int samples_per_column
= samples
/win_width
;
306 // too little samples
307 if (samples_per_column
== 0)
310 auto draw_point
= [&](size_t x
, int32_t y
) {
311 w
<< NC::XY(x
, base_y
+y
)
312 << toColor(std::abs(y
), half_height
, false)
313 << Config
.visualizer_chars
[0]
317 int32_t point_y
, prev_point_y
= 0;
318 for (size_t x
= 0; x
< win_width
; ++x
)
321 // calculate mean from the relevant points
322 for (int j
= 0; j
< samples_per_column
; ++j
)
323 point_y
+= buf
[x
*samples_per_column
+j
];
324 point_y
/= samples_per_column
;
325 // normalize it to fit the screen
326 point_y
*= height
/ 65536.0;
328 draw_point(x
, point_y
);
330 // if the gap between two consecutive points is too big,
331 // intermediate values are needed for the wave to be watchable.
332 if (x
> 0 && std::abs(prev_point_y
-point_y
) > 1)
334 const int32_t half
= (prev_point_y
+point_y
)/2;
335 if (prev_point_y
< point_y
)
337 for (auto y
= prev_point_y
; y
< point_y
; ++y
)
338 draw_point(x
-(y
< half
), y
);
342 for (auto y
= prev_point_y
; y
> point_y
; --y
)
343 draw_point(x
-(y
> half
), y
);
346 prev_point_y
= point_y
;
351 void Visualizer::DrawFrequencySpectrumStereo(int16_t *buf_left
, int16_t *buf_right
, ssize_t samples
, size_t height
)
353 DrawFrequencySpectrum(buf_left
, samples
, 0, height
);
354 DrawFrequencySpectrum(buf_right
, samples
, height
, w
.getHeight() - height
);
357 void Visualizer::DrawFrequencySpectrum(int16_t *buf
, ssize_t samples
, size_t y_offset
, size_t height
)
359 // if right channel is drawn, bars descend from the top to the bottom
360 const bool flipped
= y_offset
> 0;
362 // copy samples to fftw input array
363 for (unsigned i
= 0; i
< m_samples
; ++i
)
364 m_fftw_input
[i
] = i
< samples
? buf
[i
] : 0;
365 fftw_execute(m_fftw_plan
);
367 // count magnitude of each frequency and scale it to fit the screen
368 for (size_t i
= 0; i
< m_fftw_results
; ++i
)
369 m_freq_magnitudes
[i
] = sqrt(
370 m_fftw_output
[i
][0]*m_fftw_output
[i
][0]
371 + m_fftw_output
[i
][1]*m_fftw_output
[i
][1]
374 const size_t win_width
= w
.getWidth();
375 // cut bandwidth a little to achieve better look
376 const double bins_per_bar
= m_fftw_results
/win_width
* 7/10;
378 size_t bar_bound_height
;
379 for (size_t x
= 0; x
< win_width
; ++x
)
382 for (int j
= 0; j
< bins_per_bar
; ++j
)
383 bar_height
+= m_freq_magnitudes
[x
*bins_per_bar
+j
];
384 // buff higher frequencies
385 bar_height
*= log2(2 + x
);
386 // moderately normalize the heights
387 bar_height
= pow(bar_height
, 0.5);
389 bar_bound_height
= std::min(std::size_t(bar_height
/bins_per_bar
), height
);
390 for (size_t j
= 0; j
< bar_bound_height
; ++j
)
392 size_t y
= flipped
? y_offset
+j
: y_offset
+height
-j
-1;
394 << toColor(j
, height
)
395 << Config
.visualizer_chars
[1]
400 #endif // HAVE_FFTW3_H
402 void Visualizer::SetFD()
404 if (m_fifo
< 0 && (m_fifo
= open(Config
.visualizer_fifo_path
.c_str(), O_RDONLY
| O_NONBLOCK
)) < 0)
405 Statusbar::printf("Couldn't open \"%1%\" for reading PCM data: %2%",
406 Config
.visualizer_fifo_path
, strerror(errno
)
410 void Visualizer::ResetFD()
415 void Visualizer::FindOutputID()
418 if (!Config
.visualizer_output_name
.empty())
421 Mpd
.GetOutputs([this, &idx
](MPD::Output output
) {
422 if (output
.name() == Config
.visualizer_output_name
)
426 if (m_output_id
== -1)
427 Statusbar::printf("There is no output named \"%s\"", Config
.visualizer_output_name
);
431 #endif // ENABLE_VISUALIZER
433 /* vim: set tabstop=4 softtabstop=4 shiftwidth=4 noexpandtab : */