Skip to content

feat: add optional real-time frequency histogram visualizer#64

Merged
mrazza merged 9 commits into
mainfrom
feat/visuals
May 26, 2026
Merged

feat: add optional real-time frequency histogram visualizer#64
mrazza merged 9 commits into
mainfrom
feat/visuals

Conversation

@mrazza
Copy link
Copy Markdown
Owner

@mrazza mrazza commented May 25, 2026

This pull request introduces an optional, dynamic, real-time frequency histogram/equalizer to the "Now Playing" screen in smoc.

Key Contributions:

  1. Real-Time SoundFlow Spectrum Integration:
    • Integrated SoundFlow's FFT SpectrumAnalyzer and LevelMeterAnalyzer in SoundFlowPlaybackService.
    • Enabled dynamic visualizer activation, ensuring FFT background processing and rendering are completely inactive when hidden, eliminating idle background CPU utilization.
  2. FrequencyHistogramView Component:
    • Designed a custom Terminal.Gui v2 View with high-resolution vertical rendering using 8-level Unicode blocks ( , , , , , , , ).
    • Styled using a premium TrueColor vertical color gradient (Green -> Lime -> Yellow -> Orange -> Red).
    • Applied logarithmic bin-grouping to align with human frequency perception and musical notes.
    • Built smooth attack-decay interpolation for liquid motion.
    • Implemented a procedural wave animation fallback for empty playback states or deterministic unit testing.
  3. Pegging, Spacing & Ghost Artifact Refinements:
    • Excluded the DC offset component (minBin = 1) to completely prevent the leftmost channel from pegging.
    • Set the upper frequency ceiling to cover standard equalizer bounds up to 20 kHz.
    • Explicitly wrote background spaces to the gap columns (col + 1) to ensure perfect vertical bar spacing and prevent any terminal residual artifacts.
    • Reduced the linear scaling factor to 4.5f and optimized edge weighting to at most 70.4% to keep frequency bounds naturally active without capping.
  4. Integration, Lifecycles, and Hotkeys:
    • Toggles visualizer visibility cleanly in the album art viewport using hotkey v/V or the command :np-vis.
    • Fully cleaned up analyzers, timers, and event handlers upon disposal.
  5. Robust Test Suite:
    • Achieved comprehensive test coverage verifying Stopped/Empty states, procedural animations, logarithmic mapping, decay transitions, and hotkey/command behaviors.
    • Fully verified all 366 passing tests with deterministic screenshot golden testing.

@mrazza mrazza requested a review from oca-agent May 25, 2026 21:37
Copy link
Copy Markdown
Collaborator

@oca-agent oca-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SMoC PR #64 Code Review

First of all, outstanding work on this feature! The real-time frequency visualizer looks phenomenal, and the implementation is highly creative. The integration of SoundFlow FFT spectrum data, vertical color gradients, logarithmic binning, and smooth physics-like transitions provides a highly polished premium feel to the TUI.

Here is a senior-level detailed review focusing on performance, correctness, thread safety, rendering fidelity, and test determinism to help elevate this contribution to production grade.


1. Performance & Memory Allocations (High Impact)

Issue: Excessive GC Allocations in Real-Time Audio Update Loop

Inside SoundFlowPlaybackService.cs's SpectrumData property:

  public float[] SpectrumData {
    get {
      if (_spectrumAnalyzer == null) return Array.Empty<float>();
      float[] raw = _spectrumAnalyzer.SpectrumData;
      if (raw == null || raw.Length == 0) return Array.Empty<float>();

      float peak = _levelMeterAnalyzer?.Peak ?? 1.0f;
      float[] scaled = new float[raw.Length]; // <-- Allocates a new array on every access!
      for (int i = 0; i < raw.Length; i++) {
        scaled[i] = raw[i] * peak;
      }
      return scaled;
    }
  }
  • Why it matters: SpectrumData is called every 100ms inside FrequencyHistogramView.UpdateVisualization during active playback. This creates a continuous stream of allocations (~2-4 KB per second) which creates garbage collector pressure over extended music playing sessions.
  • Senior Recommendation: Redesign the interface to allow zero-allocation updates by reusing a single buffer.
    • Option A: Implement a method that copies into an existing buffer: public void GetSpectrumData(Span<float> destination) or public int GetSpectrumData(float[] destination).
    • Option B: Cache and reuse a single internal private array buffer _cachedSpectrumData inside SoundFlowPlaybackService and only resize it if raw.Length changes, returning a read-only span or the same array (since the UI thread copies its values anyway).

2. Bugs & Rendering Fidelity

Issue: Missing Unicode Lower One-Eighth Block Character

In FrequencyHistogramView.cs, the switch expression maps the lowest bar height 1 to a standard ASCII space ' ':

          char blockChar = cellLevel switch {
            1 => ' ', // <-- standard ASCII space U+0020
            2 => '▂', // U+2582
            3 => '▃', // U+2583
            ...
  • Why it matters: Standard ASCII space ' ' is identical to cellLevel <= 0, which means you lose the finest 1/8th height resolution of the bar. It will jump abruptly from 0/8 (completely empty) to 2/8 ().
  • Senior Recommendation: Use the actual Unicode Lower One Eighth Block character ' ' (U+2581) for case 1:
              1 => ' ', // <-- U+2581 Lower One Eighth Block

3. UI Layout & Visibility Dependencies

Issue: Referencing Layout Dimensions of an Invisible View

In NowPlayingView.cs, _histogramView's layout is dynamically bound to _albumArtView:

    _histogramView = new FrequencyHistogramView(playbackQueueService) {
      ...
      Width = Dim.Func((v) => _albumArtView.Frame.Width),
      Height = Dim.Func((v) => _albumArtView.Frame.Height),
      Visible = false
    };

And during visualization toggle:

  private void ToggleVisualization() {
    _showVisualization = !_showVisualization;
    _albumArtView.Visible = !_showVisualization; // <-- Becomes hidden
    _histogramView.Visible = _showVisualization;  // <-- Becomes visible
    ...
  }
  • Why it matters: In Terminal.Gui (and many layout engines), setting a view's visibility to false can bypass its layout pass or collapse its frame size to 0. If this occurs, _histogramView's width and height (which depend on _albumArtView.Frame.Width/Height) can collapse to 0, making the visualizer invisible.
  • Senior Recommendation: Avoid defining layout constraints based on a peer view that will be hidden. Instead:
    • Bind both _albumArtView and _histogramView to a shared helper method, layout configuration, or parent percentage bounds so they resolve independently of each other's visibility.
    • Alternatively, keep _albumArtView visible with 0-opacity/no rendering, or simply bind _histogramView using the exact same Dim.Func logic used for _albumArtView.

4. Test Suite Determinism & Robustness

Issue: Flaky UI Screenshot Tests on Real-Time Timers

The test suite in FrequencyHistogramViewTest.cs uses context.AdvanceTime(TimeSpan.FromMilliseconds(100)) to trigger and test the visualization updates.

  • Why it matters: Real-time timers and main-loop clocks can be highly non-deterministic on different host environments (e.g., local machines vs slow CI/CD containers). Minor timing drifts or scheduling latencies in the event loop can result in a different number of timeout executions, leading to flaky test runs (as seen in our local run where procedural animations yielded different visual heights).
  • Senior Recommendation:
    • Avoid real-time main loops or timing dependencies in unit tests.
    • Instead of invoking the main loop clock via AdvanceTime, expose an internal Update() or UpdateVisualization() method directly on the view (or via a test interface) to allow the test suite to step through visual frames with 100% mathematical determinism.
    • To ensure colors in screenshot goldens are identical across all developer environments, force color-depth settings inside TestInit.cs by explicitly configuring the COLORTERM environment variable:
      Environment.SetEnvironmentVariable("COLORTERM", ""); // Force standard 16-color ANSI output

5. Concurrency Safety

Issue: Concurrency in Spectrum Copying

Audio playback rendering runs on the hardware's separate audio callback thread, while spectrum drawing runs on Terminal.Gui's UI main thread.

  • Why it matters: The raw array reference accessed via _spectrumAnalyzer.SpectrumData might be replaced or mutated concurrently. Under high load or when track changes occur, this can lead to out-of-bounds array reads if the analyzer is reconfigured.
  • Senior Recommendation: Ensure that SoundFlow's spectrum buffer access is thread-safe and properly locked/copied atomically. Since we copy it locally, we are safe from intermediate UI changes, but verifying that the underlying audio framework guarantees safe access to SpectrumData during playback is highly recommended.

oca-agent
oca-agent previously approved these changes May 26, 2026
Copy link
Copy Markdown
Collaborator

@oca-agent oca-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved based on @razza's instruction.

@mrazza mrazza self-assigned this May 26, 2026
Copy link
Copy Markdown
Collaborator

@oca-agent oca-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving PR per user request.

@mrazza mrazza merged commit 76b5a21 into main May 26, 2026
1 check passed
@mrazza mrazza deleted the feat/visuals branch May 26, 2026 02:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants