diff --git a/.gitignore b/.gitignore index 04dde468..b12f07be 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ missing **/.deps/ src/ncmpcpp stamp-h1 +compile_commands.json diff --git a/src/enums.cpp b/src/enums.cpp index 5ff08031..40fdc29e 100644 --- a/src/enums.cpp +++ b/src/enums.cpp @@ -184,6 +184,9 @@ std::ostream &operator<<(std::ostream& os, VisualizerType vt) case VisualizerType::Spectrum: os << "frequency spectrum"; break; + case VisualizerType::Graph: + os << "cava graph"; + break; # endif // HAVE_FFTW3_H case VisualizerType::Ellipse: os << "sound ellipse"; @@ -203,6 +206,8 @@ std::istream &operator>>(std::istream& is, VisualizerType &vt) # ifdef HAVE_FFTW3_H else if (svt == "spectrum") vt = VisualizerType::Spectrum; + else if (svt == "graph") + vt = VisualizerType::Graph; # endif // HAVE_FFTW3_H else if (svt == "ellipse") vt = VisualizerType::Ellipse; diff --git a/src/enums.h b/src/enums.h index 0c3245ed..8e51cd18 100644 --- a/src/enums.h +++ b/src/enums.h @@ -49,6 +49,7 @@ enum class VisualizerType { WaveFilled, # ifdef HAVE_FFTW3_H Spectrum, + Graph, # endif // HAVE_FFTW3_H Ellipse }; diff --git a/src/screens/visualizer.cpp b/src/screens/visualizer.cpp index 66fc546d..e71d8968 100644 --- a/src/screens/visualizer.cpp +++ b/src/screens/visualizer.cpp @@ -565,6 +565,139 @@ void Visualizer::DrawFrequencySpectrumStereo(const int16_t *buf_left, const int1 DrawFrequencySpectrum(buf_right, samples, height, w.getHeight() - height); } +/**********************************************************************/ + +/**********************************************************************/ + +void Visualizer::DrawSoundGraph(const int16_t *buf, ssize_t samples, + size_t y_offset, size_t height) { + const bool flipped = false; + + const double bar_width = 2; + const double gap_width = 0.05; + const double stride = bar_width + gap_width; + + // 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 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]) / + (DFT_NONZERO_SIZE); + + m_bar_heights.clear(); + const size_t win_width = w.getWidth(); + + size_t cur_bin = 0; + size_t bar_index = 0; + + for (size_t x = 0; x + bar_width <= win_width; x += stride, ++bar_index) { + double bar_height = 0; + size_t count = 0; + + // accumulate bins for this bar + // accumulate bins for this bar — always ensure at least one bin +size_t bin_limit = (bar_index + 1 < m_dft_freqspace.size()) + ? m_dft_freqspace[bar_index + 1] + : m_fftw_results; + +while (cur_bin < bin_limit) { + bar_height += m_freq_magnitudes[cur_bin]; + ++count; + ++cur_bin; +} + + + // safe average or fallback + if (count > 0) + bar_height /= count; + + + // scale bar height + double scaled_height = bar_height; + if (Config.visualizer_spectrum_log_scale_y) { + scaled_height = + (20 * log10(bar_height) + DYNAMIC_RANGE + GAIN) / DYNAMIC_RANGE; + } else { + scaled_height *= pow(10, 0.5 + GAIN / 20); // lower sensitivity + scaled_height *= 0.7 + log2(1 + x) / 10; // mild high-frequency boost + scaled_height = pow(scaled_height, 0.8); // moderate compression + } + + // multiply by screen height + scaled_height *= w.getHeight(); + + // enforce minimum and maximum heights + // if (scaled_height < 5.0) + // scaled_height = 5.0; + // if (scaled_height > w.getHeight()) + // scaled_height = (double)w.getHeight(); + + m_bar_heights.emplace_back(x, scaled_height); + } + + // draw bars + for (size_t bar = 0; bar < m_bar_heights.size(); ++bar) { + const size_t x = m_bar_heights[bar].first; + const double bar_height = m_bar_heights[bar].second; + + for (size_t bw = 0; bw < bar_width; ++bw) { + size_t draw_x = x + bw; + if (draw_x >= win_width) + continue; + + for (size_t j = 0; j < bar_height; ++j) { + size_t y = flipped ? y_offset + j : y_offset + height - j - 1; + auto color = toColor(j, height, false); + std::wstring ch; + + if (Config.visualizer_spectrum_smooth_look) { + const size_t size = SMOOTH_CHARS.size(); + const size_t idx = static_cast(size * bar_height) % size; + if (j < bar_height - 1 || idx == size - 1) { + ch = SMOOTH_CHARS[size - 1]; + } else { + if (flipped) { + if (Config.visualizer_spectrum_smooth_look_legacy_chars) + ch = SMOOTH_CHARS_FLIPPED[idx]; + else { + ch = SMOOTH_CHARS[size - idx - 2]; + color = + NC::FormattedColor(color.color(), {NC::Format::Reverse}); + } + } else { + ch = SMOOTH_CHARS[idx]; + } + } + } else { + ch = Config.visualizer_chars[1]; + } + + w << NC::XY(x, y) << color << ch << NC::FormattedColor::End<>(color); + } + } + } +} + + +void Visualizer::DrawSoundGraphStereo(const int16_t *buf_left, + const int16_t *buf_right, ssize_t samples, + size_t height) { + // DrawSoundGraph(buf_left, samples, 0, height); // Draws Top + std::vector mono(samples); + + for (ssize_t i = 0; i < samples; ++i) { + // wont scale amplitude dynamically + int32_t mixed = + static_cast(buf_left[i]) + static_cast(buf_right[i]); + mono[i] = static_cast(mixed / 2); + } + DrawSoundGraph(mono.data(), samples, height, + w.getHeight() - height); // Draws bottom +} + double Visualizer::InterpolateCubic(size_t x, size_t h_idx) { const double x_next = m_bar_heights[h_idx].first; @@ -722,49 +855,54 @@ void Visualizer::InitVisualization() { size_t rendered_samples = 0; switch (Config.visualizer_type) - { - case VisualizerType::Wave: - // Guarantee integral amount of samples per column. - rendered_samples = ceil(44100.0 / Config.visualizer_fps / w.getWidth()); - rendered_samples *= w.getWidth(); - // Slow the scolling 10 times to make it watchable. - rendered_samples *= 10; - draw = &Visualizer::DrawSoundWave; - drawStereo = &Visualizer::DrawSoundWaveStereo; - break; - case VisualizerType::WaveFilled: - // Guarantee integral amount of samples per column. - rendered_samples = ceil(44100.0 / Config.visualizer_fps / w.getWidth()); - rendered_samples *= w.getWidth(); - // Slow the scolling 10 times to make it watchable. - rendered_samples *= 10; - draw = &Visualizer::DrawSoundWaveFill; - drawStereo = &Visualizer::DrawSoundWaveFillStereo; - break; -# ifdef HAVE_FFTW3_H - case VisualizerType::Spectrum: - rendered_samples = DFT_NONZERO_SIZE; - draw = &Visualizer::DrawFrequencySpectrum; - drawStereo = &Visualizer::DrawFrequencySpectrumStereo; - break; -# endif // HAVE_FFTW3_H - case VisualizerType::Ellipse: - // Keep constant amount of samples on the screen regardless of fps. - rendered_samples = 44100 / 30; - draw = &Visualizer::DrawSoundEllipse; - drawStereo = &Visualizer::DrawSoundEllipseStereo; - break; - } - if (Config.visualizer_in_stereo) - rendered_samples *= 2; - m_rendered_samples.resize(rendered_samples); - - // Keep 500ms worth of samples in the incoming buffer. - size_t buffered_samples = 44100.0 / 2; - if (Config.visualizer_in_stereo) - buffered_samples *= 2; - m_incoming_samples.resize(buffered_samples); - m_buffered_samples.resize(buffered_samples); + { + case VisualizerType::Wave: + // Guarantee integral amount of samples per column. + rendered_samples = ceil(44100.0 / Config.visualizer_fps / w.getWidth()); + rendered_samples *= w.getWidth(); + // Slow the scolling 10 times to make it watchable. + rendered_samples *= 10; + draw = &Visualizer::DrawSoundWave; + drawStereo = &Visualizer::DrawSoundWaveStereo; + break; + case VisualizerType::WaveFilled: + // Guarantee integral amount of samples per column. + rendered_samples = ceil(44100.0 / Config.visualizer_fps / w.getWidth()); + rendered_samples *= w.getWidth(); + // Slow the scolling 10 times to make it watchable. + rendered_samples *= 10; + draw = &Visualizer::DrawSoundWaveFill; + drawStereo = &Visualizer::DrawSoundWaveFillStereo; + break; +#ifdef HAVE_FFTW3_H + case VisualizerType::Spectrum: + rendered_samples = DFT_NONZERO_SIZE; + draw = &Visualizer::DrawFrequencySpectrum; + drawStereo = &Visualizer::DrawFrequencySpectrumStereo; + break; + case VisualizerType::Graph: + rendered_samples = DFT_NONZERO_SIZE; + draw = &Visualizer::DrawSoundGraph; + drawStereo = &Visualizer::DrawSoundGraphStereo; + break; +#endif // HAVE_FFTW3_H + case VisualizerType::Ellipse: + // Keep constant amount of samples on the screen regardless of fps. + rendered_samples = 44100 / 30; + draw = &Visualizer::DrawSoundEllipse; + drawStereo = &Visualizer::DrawSoundEllipseStereo; + break; + } + if (Config.visualizer_in_stereo) + rendered_samples *= 2; + m_rendered_samples.resize(rendered_samples); + + // Keep 500ms worth of samples in the incoming buffer. + size_t buffered_samples = 44100.0 / 2; + if (Config.visualizer_in_stereo) + buffered_samples *= 2; + m_incoming_samples.resize(buffered_samples); + m_buffered_samples.resize(buffered_samples); } /**********************************************************************/ @@ -790,27 +928,30 @@ void Visualizer::ToggleVisualizationType() { switch (Config.visualizer_type) { - case VisualizerType::Wave: - Config.visualizer_type = VisualizerType::WaveFilled; - break; - case VisualizerType::WaveFilled: -# ifdef HAVE_FFTW3_H - Config.visualizer_type = VisualizerType::Spectrum; -# else - Config.visualizer_type = VisualizerType::Ellipse; -# endif // HAVE_FFTW3_H - break; -# ifdef HAVE_FFTW3_H - case VisualizerType::Spectrum: - Config.visualizer_type = VisualizerType::Ellipse; - break; -# endif // HAVE_FFTW3_H - case VisualizerType::Ellipse: - Config.visualizer_type = VisualizerType::Wave; - break; - } - InitVisualization(); - Statusbar::printf("Visualization type: %1%", Config.visualizer_type); + case VisualizerType::Wave: + Config.visualizer_type = VisualizerType::WaveFilled; + break; + case VisualizerType::WaveFilled: +#ifdef HAVE_FFTW3_H + Config.visualizer_type = VisualizerType::Spectrum; +#else + Config.visualizer_type = VisualizerType::Ellipse; +#endif + break; +#ifdef HAVE_FFTW3_H + case VisualizerType::Spectrum: + Config.visualizer_type = VisualizerType::Graph; // ← cycle to bars/Graph + break; + case VisualizerType::Graph: + Config.visualizer_type = VisualizerType::Ellipse; // ← next after Graph + break; +#endif + case VisualizerType::Ellipse: + Config.visualizer_type = VisualizerType::Wave; + break; + } + InitVisualization(); + Statusbar::printf("Visualization type: %1%", Config.visualizer_type); } void Visualizer::OpenDataSource() diff --git a/src/screens/visualizer.h b/src/screens/visualizer.h index 3a7a65ba..c0696dd7 100644 --- a/src/screens/visualizer.h +++ b/src/screens/visualizer.h @@ -74,6 +74,8 @@ struct Visualizer: Screen, Tabbable # ifdef HAVE_FFTW3_H void DrawFrequencySpectrum(const int16_t *, ssize_t, size_t, size_t); void DrawFrequencySpectrumStereo(const int16_t *, const int16_t *, ssize_t, size_t); + void DrawSoundGraph(const int16_t *buf, ssize_t samples, size_t y_offset, size_t height); + void DrawSoundGraphStereo(const int16_t *buf_left, const int16_t *buf_right, ssize_t samples, size_t height); void ApplyWindow(double *, const int16_t *, ssize_t); void GenLogspace(); void GenLinspace(); diff --git a/src/settings.cpp b/src/settings.cpp index ddde73e0..2be5fd70 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -235,7 +235,7 @@ bool Configuration::read(const std::vector &config_paths, bool igno p.add("visualizer_in_stereo", &visualizer_in_stereo, "yes", yes_no); p.add("visualizer_type", &visualizer_type, #ifdef HAVE_FFTW3_H - "spectrum" + "graph" #else "ellipse" #endif