Skip to content

feat(modes): Stereo Trails, each band rises from its place in the stereo image - #732

Merged
CaYatur merged 4 commits into
CaYatur:mainfrom
Barbaror4:feat/stereo-trails-mode
Oct 11, 2026
Merged

CaYatur merged 4 commits into
CaYatur:mainfrom
Barbaror4:feat/stereo-trails-mode

Conversation

@Barbaror4

@Barbaror4 Barbaror4 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Refs #638

What

A new visualizer, Stereo Trails (stereotrails, Stereo İzler), in Particles & Events. It is a fountain: every frequency band launches its own particles.

  • Launch angle: where the band sits between the left and right channel.
  • Scatter: how diffuse the band is.
  • Count and height: the band's level.

A centred kick draws an upright column. A guitar panned left bends its arcs to the left. A wide reverb opens like a fan. Mono input stands straight up, which is the correct reading.

Why

#638 asks for more original modes. Since #566 every frame carries real left and right channels, but only the Goniometer and the Level Meter read them, and both are meters. This turns the stereo image into a visual mode.

How

src/shared/stereo-bands.js (new, pure arithmetic, Node-tested) runs one band-pass biquad per band over timeL/timeR and keeps ΣL², ΣR² and ΣLR:

  • pan = (ΣR² − ΣL²) / (ΣR² + ΣL²). It is exact at hard left, centre and hard right, and reads sin(p·π/2) under constant-power panning.
  • spread = 1 − correlation. A single panned source reads 0 wherever it sits; decorrelated or anti-phase sound reads 1. analysis.js's width is deliberately not reused: it gives a hard-panned mono guitar and a wide reverb the same value (1).
  • Silence and 8-bit quantisation noise read as centred.
  • Cost: about 0.07 ms per frame for 8 bands, with no allocation per frame.

The mode (in generative2.js, where the #638 batches live):

  • Levels come from audio.getBars, so every control the catalog flags reveal does something: bar count (band count), frequency range, the whole Spectrum Measurement group, line width, amplitude and the secondary colour.
  • Band edges for the stereo measurement come from SVSpectrum.bandEdges, so with a mel or bark scale the level and the pan come from the same band.
  • Pan curve: pan maps to angle through |p|^0.6. Most bands in a real mix sit near the centre (±0.05 to 0.25 in the demo signal), and a linear map left a subtle stereo image upright.
  • Drawing: source-over, not lighter, because additive blending washed out on the default light background. Butt caps, not round: Iris Xe, 1080p, uncapped bench gave 118 and 92 fps with round caps against 124 and 118 with butt.
  • Floor is the bottom edge of the canvas. A Height slider (visualizer.trailHeight) keeps today's peak at 100% and can carry the arcs off the top of the frame, up to 300%.
  • Paint rate is capped at 120 Hz. Physics still steps every frame. At 60 Hz and 75 Hz every frame still paints, so the picture is unchanged. See the before/after below.
  • Housekeeping: a fixed pool of 1800 particles, a seeded RNG (deterministic for export), and one stroke per band × rising/falling bucket.

Wiring:

  • the catalog entry and English label;
  • stereo-bands.js loaded on all four pages that draw modes (visualizer, overlay, exporter, admin);
  • the README mode count and lists (EN and TR);
  • the test-count claims;
  • docs/screenshots/modes-visualizer.jpg, regenerated from the catalogue (60 modes, including Stereo Trails).

Performance of the follow-up fix

Uncapped, the full-screen trail and glow were submitted hundreds of times a second, faster than the GPU finished them. That backlog was charged to the next scene. Painting is now capped at 120 Hz. The cap is only inside StereoTrails.draw.

Same machine (RTX 5070 Laptop, 1080p, npm run bench, 1 s warm-up, 3 s measured). The pair in each row is Bars immediately before Stereo Trails, then Bars immediately after it.

Bars before Stereo Trails Bars after
Before the cap 614.7 fps, p95 2.6 ms, max 19.7 ms 916.2 fps, p95 2.2 ms 380.5 fps, p95 2.9 ms, max 124.4 ms
After the cap 663.3 fps, p95 2.5 ms, max 18.3 ms 1438.9 fps, p95 1.3 ms 665.3 fps, p95 2.4 ms, max 16.9 ms

With vsync on, after the cap, Stereo Trails and the following Flow Field both hold 75.0 fps, p95 13.4 ms. The Iris Xe vsync figures below are from the original commit and still describe the mode while it is on screen.

Checks

  • npm test: 2942 pass, 17 of them new (the original 16, plus the height-slider test).
  • tests/stereo-bands.test.js, the measurement: checked against known-answer signals (panned tones, mono, constant-power pan law, anti-phase, decorrelated tones, silence, byte vs float input, mel edges).
  • tests/stereo-bands.test.js, the mode: the real mode file runs in a vm with a recording canvas. The tests check that left, right and mono sound lean the right way, that diffuse sound fans out, that each band sits in its own place, that subtle stereo visibly leans, that levels come from getBars, that height 1 matches the unset default and 2.5 lifts the peak by more than one screen, that output is deterministic, and that every page loads the helper.
  • Mutations: 10 of 10 caught (pan sign, tilt sign, spread formula, quiet gate, scatter, pan curve, custom edges, level source, Math.random, missing script tag).
  • npm run smoke: PASS on 2026-10-11, exit 0, isolated --user-data-dir. 60 visualizer modes drawn (including stereotrails), 41 backgrounds, 40/40 effects, 18/18 transitions. The real profile was not used.
  • npm run bench, vsync on, 1080p, Iris Xe (original commit): 58.2 / 60.0 / 58.4 fps with p95 16.9 ms over three runs. In the same runs the Goniometer read 59.4 / 60.0 and Bouncing Balls 60.0.

For the reviewer

  • The uncapped follow-on cost is fixed. The table above is the before and after. Treat the uncapped Stereo Trails fps as CPU-side: paints are limited to 120 Hz, so that number is not a GPU cost.
  • The mode sheet is regenerated. docs/screenshots/modes-visualizer.jpg shows 60 modes.
  • Comments follow the codebase. They are in Turkish, and new files carry the project's copyright header, which tests/copyright-headers.test.js requires.
  • Timing is your call. This is a new feature during the v3.1.5 stabilisation in Stabilisation before v3.1.5 #695. It is fine to hold it until after the release.

🤖 Generated with Claude Code

…reo image

Refs CaYatur#638

A new visualizer in Particles & Events. It is a fountain: every frequency
band launches its own particles. The launch angle comes from where the band
sits between the left and right channel, the scatter from how diffuse it
is, and the count and height from the band's level. A centred kick draws an
upright column, a guitar panned left bends its arcs to the left, a wide
reverb opens like a fan. Mono input stands straight up, which is correct.

The stereo measurement is new: src/shared/stereo-bands.js runs a band-pass
biquad per band over timeL/timeR and keeps sum(L^2), sum(R^2), sum(LR):
- pan = (R^2 - L^2) / (R^2 + L^2), exact at hard left, centre, hard right
  (sin(p*pi/2) under constant-power panning);
- spread = 1 - correlation: a single panned source is 0 wherever it sits,
  decorrelated or anti-phase sound is 1. analysis.js's width gives a
  hard-panned mono guitar and a wide reverb the same value (1), so it is
  deliberately not reused here.
Silence and 8-bit quantisation noise read as centred. 0.07 ms per frame
for 8 bands in Node; no allocation per frame.

Levels come from audio.getBars, so every control the catalog flags reveal
works: bar count (band count), frequency range, the whole Spectrum
Measurement group, line width, amplitude and the secondary colour. The
stereo bands use the spectrum's own edges (SVSpectrum.bandEdges), so with a
mel or bark scale the level and the pan come from the same band.

Drawn with source-over and butt caps: additive blending washed out on the
default light background, and round caps cost frames on integrated
graphics (Iris Xe, 1080p, uncapped: 118/92 fps with round, 124/118 butt).

Checks:
- npm test: 2941 pass (16 new); doc counts updated.
- tests/stereo-bands.test.js runs the real mode file in a vm with a
  recording canvas: left/right/mono lean, diffuse sound fans out, each
  band in its own place, levels from getBars, deterministic output, and
  every page loads the helper. 10 of 10 mutations caught.
- npm run smoke (isolated --user-data-dir): PASS, 60 visualizer modes drawn.
- npm run bench, vsync on, 1080p on Iris Xe: 58.2 / 60.0 / 58.4 fps,
  p95 16.9 ms in three runs (Goniometer 59.4 / 60.0, Bouncing Balls 60.0).
- The mode sheet in docs/screenshots is not regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cayadraft

cayadraft Bot commented Oct 11, 2026

Copy link
Copy Markdown

@CaYatur

CaYaDraft — pull request

Summary
feat(modes): Stereo Trails, each band rises from its place in the stereo image

Files
README.md (modified, +8/-7)
@@ -7,7 +7,7 @@

Free, open-source music visualizer and VJ software for every screen you own

Turn whatever your computer is playing — Spotify, YouTube, a DAW, a DJ set, a game — into
-audio-reactive visuals on one display or ten. A real MilkDrop engine, 59 visualizer modes,
+audio-reactive visuals on one display or ten. A real MilkDrop engine, 60 visualizer modes,
layers a…

README.tr.md (modified, +8/-7)
@@ -7,7 +7,7 @@

Sahip olduğunuz her ekran için ücretsiz, açık kaynak müzik görselleştirici ve VJ yazılımı

Bilgisayarınızda ne çalıyorsa — Spotify, YouTube, bir DAW, bir DJ seti, bir oyun — bir ekranda ya
-da on ekranda sese tepki veren görsellere dönüştürür. Gerçek bir MilkDrop motoru, **59
+da on ekranda sese tepki veren görsellere dönüştürür. Gerçek bir MilkDrop motoru, **60
gö…

ROADMAP.md (modified, +1/-1)
@@ -129,7 +129,7 @@ npm test
npm start -- --smoke


-- **2925 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
+- **2941 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
  105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at
  that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 1103
  on `main` since.

src/admin/index.html (modified, +1/-0)
@@ -272,6 +272,7 @@ <h3>Sahneler</h3>
    <script src="../shared/lyrics.js"></script>
    <script src="../shared/analysis.js"></script>
    <script src="../shared/spectrum.js"></script>
+    <script src="../shared/stereo-bands.js"></script>
    <script src="../shared/modulation.js"></script>
    <script src="../shared/timeline.js"></script>
    <script src="../shared/timeline-edit.js"></scr…

src/exporter/index.html (modified, +1/-0)
@@ -69,6 +69,7 @@
    <script src="../shared/lyrics.js"></script>
    <script src="../shared/analysis.js"></script>
    <script src="../shared/spectrum.js"></script>
+    <script src="../shared/stereo-bands.js"></script>
    <script src="../shared/modulation.js"></script>
    <script src="../shared/timeline.js"></script>
    <script src="../shared/nowplaying.js"></script>

src/shared/i18n.js (modified, +1/-0)
@@ -559,6 +559,7 @@
    'Kardioid': 'Cardioid',
    'Vuruş Pedleri': 'Beat Pads',
    'Zıplayan Toplar': 'Bouncing Balls',
+    'Stereo İzler': 'Stereo Trails',
    'Seviye Ölçer (PPM)': 'Level Meter (PPM)',
    'Su Altı': 'Underwater',
    'Işık Hüzmesi': 'Light Shafts',

src/shared/mode-catalog.js (…

**Note**
The local model is unavailable, so this is only the title and file list.

---
Stopping here. I will not merge, push, or change this pull request until you ask.

`/caya explain` describes the diff. `/caya plan` previews the next edit. `/caya revise` adds a commit.

<!-- cayadraft -->

The fountain sat at 94% of the canvas, so a full screen left a dead band under the launch line. The floor is now the bottom edge.

trailHeight scales those same arcs. 1 keeps today's peak. Up to 3 lets them leave the top of the frame and fall back. The slider sits on the layer and on the classic visualizer card, and the MIDI/OSC target uses the same range.
The sheet is drawn from the catalogue and now holds 60 modes, including Stereo Trails. That mode waits with the other trail builders so its tile is not an empty first frame.
Uncapped, the full-screen trail and glow were submitted hundreds of times a second, faster than the GPU finished them. That backlog was charged to the following scene. Painting is now capped at 120 Hz. Physics still steps every frame, and 60 Hz and 75 Hz displays still paint every frame.

On this machine, uncapped 1080p Bars read 663 fps before Stereo Trails and 380 fps after. After the cap it reads 663 fps before and 665 fps after. With vsync, Stereo Trails and the next scene both hold 75 fps.

CaYatur commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Ready to merge.

Checked 619580c:

  • CI is green: unit tests on Ubuntu and Windows, Node 20 and 22, plus the syntax check. Merge state is CLEAN and the branch does not conflict with main.
  • docs/screenshots/modes-visualizer.jpg was regenerated from the catalogue. It shows 60 modes, including Stereo Trails.
  • The fountain floor sits on the bottom edge. The Height slider keeps today's peak at 100% and can carry the arcs off the top of the frame, up to 300%.
  • The uncapped follow-on cost is fixed. Painting is capped at 120 Hz, so a backlog is no longer charged to the next scene. At 60 Hz and 75 Hz every frame still paints, so the picture is unchanged. Measured here, uncapped 1080p: Bars read 663 fps before Stereo Trails and 665 fps after. With vsync, Stereo Trails and the following Flow Field both hold 75 fps.
  • The cap is only inside StereoTrails.draw. The other modes are unchanged.

The description still says 2941 tests. The tree and the README badges say 2942, after the height test. That is not a code blocker.

@CaYatur
CaYatur merged commit 6a1900d into CaYatur:main Oct 11, 2026
5 checks passed
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