3.0.0 — Modulation, deep analysis, MilkDrop, mapping, transitions
CAYADEV Visualizer 3.0.0 is the largest release the project has had. It adds a
modulation matrix, deep audio analysis, a real MilkDrop preset engine,
projection mapping, scene transitions, recording, text and lyrics, a broadcast
layout family, and a rebuilt spectrum engine — and it fixes several things that
were quietly broken.
New engines
Modulation matrix. LFOs, envelope followers, sample-and-hold and random,
routed to any value in the configuration. Eight LFO shapes, rate in Hz or in
beat divisions locked to the detected tempo, curve shaping, per-route smoothing
and slew limiting, and eight assignable macro knobs. LFO phase is computed from
the draw clock rather than accumulated, so offline export stays deterministic
even when frames are skipped.
Deep audio analysis. Musical key and chord from a constant-Q chroma vector
computed with a Goertzel filterbank — FFT bins cannot resolve the bass register
at 2048 samples. YIN pitch tracking, harmonic/percussive separation, per-band
onset detection, spectral descriptors, loudness, dynamics, true peak, stereo
width and correlation. Every measurement is available as a modulation source.
MilkDrop. The preset language actually runs: tokeniser, parser, compilation
to JavaScript closures, the variable pool, and per_frame / per_pixel
equations driving a real warp mesh with feedback. .milk import handles
multi-file packs and reports compile errors per file. No preset text reaches
generated code — identifiers become pool indices — and a fuzz test asserts a
preset cannot smuggle JavaScript.
Projection mapping. Corner pin as a true homography, with the denominator
written into gl_Position.w so the texture stays perspective-correct. Mesh warp
on a Catmull-Rom grid that passes through its control points. Per-output crop,
colour correction and polygon masks, alignment grids and test patterns, and soft
edge blending whose curves are tested to sum to exactly 1 across the overlap.
Scene transitions. 18 of them — crossfade, dissolve, wipes, slides, luma
wipe, glitch, zoom, blur, flash — and they can be switched off. They fire on a
change of scene, not on a change of any setting, so dragging a slider never
starts one.
Recording. One-key capture of the live output as it appears, with the live
audio, modulation, transitions and effects. MP4, WebM, GIF and PNG. GIF uses
two-pass palette generation because one pass bands visibly. PNG snapshot up to
4×.
Text and lyrics. Audio-reactive typography with per-character response, LRC
and SRT import with format detection and enhanced word timings, a timing editor
that writes back to LRC, and now-playing metadata.
Rebuilt spectrum engine
Bars are measured rather than guessed. Four frequency scales (logarithmic,
linear, mel, bark), a decibel amplitude scale with a settable floor, separate
attack and release ballistics that are frame-rate independent, neighbour spread,
symmetric profile smoothing, and a dB-per-octave tilt.
This came out of a real defect: several of the leftmost bars rose and fell as
one. Every bar was given an integer range of FFT bins on a logarithmic split,
and at 2048 samples and 48 kHz a bin spans 23.4 Hz — starting from 30 Hz with 64
bars, the first five bars all resolved to bin 1 and read the same number. Bands
narrower than a bin are now interpolated at their centre frequency.
Music video layouts
Eight restrained layouts for release videos and official channels, deliberately
separate from the club and VJ material: a limited bar count, calm colour, the
logo beside the bars rather than behind them, and the track and artist beneath.
Bars gained real placement — width, horizontal position, height and baseline,
each a fraction of the frame — so the block can sit in a corner, across the
centre, or as a thin strip. All four default to the previous behaviour exactly.
More of everything
- Visualizer modes: 32 → 48, including 14 generative modes.
- Backgrounds: 19 → 31.
- Post-FX: 15 → 40, orderable, audio-bindable, and available per layer.
- Formulas: 35 → 98, plus 13 solids — platonic, geodesic, L-systems and IFS
clouds. - Built-in shaders: 5 → 42, all compiled on a real GPU by the self-test.
- Templates: 72 in nine groups.
- Layer masks, groups with a single fader, solo/mute/lock, and an A/B
crossfader on an equal-power curve.
Fixes
Video files in the media layer never played. The path sat in the URL's host
position, and a Windows path carries a colon and backslashes, neither legal in a
host. Chromium refused to build the request at all, so it never reached the
protocol handler and no error surfaced. The address now carries the path in the
path component, serving honours Range requests — required for seeking and for
MP4s whose index sits at the end — and the OBS overlay is fixed too.
The topmost layer was drawn at the bottom. The array is bottom-first by
design, but the panel enumerated it forwards. Only the presentation is reversed;
reversing the array would have shifted layer groups, the crossfader and mask
targeting, which all index into it.
3D attractors rendered off-screen. Framing came from hand-written constants
that were simply wrong — Lorenz declared a centre of [0, 0, -25] when its
measured centroid is [0, 0, +26]. Framing is now measured from the system's own
bounding box, and a test asserts every attractor lands inside the view volume.
Reset to defaults was missing on every panel added this cycle, because those
panels declared no configuration roots. They do now, and the self-test verifies
the declared roots exist.
The screenshot generator and the self-test opened the webcam if the saved
settings happened to have the media layer on. Camera access is now refused
outright in automation mode, at the permission layer, and the self-test asserts
it stays refused.
Fixes after the first 3.0.0 build
Record and Snapshot could not be pressed. The DOM helper set every property
with setAttribute, so disabled: false became disabled="false" — and in
HTML a boolean attribute disables an element by being present, whatever its
value. Both buttons were therefore dead from the moment they gained a disabled
state, as was the MilkDrop .milk import button. Boolean attributes are now
written by presence, and the self-test asserts it against a real DOM.
The blackout fade did nothing until it ended. Engaging blackout turns every
layer off, which left the layer stack resolving to an empty list and drawing a
fully transparent frame. The crossfade was mixing the outgoing scene into
nothing, which looks like nothing: the old scene stayed at full brightness for
the whole blackout duration and then snapped to black at the last moment.
Measured across a one-second fade, mean luminance was flat at 127.8 and then
0.0; it now ramps 127.8 → 103 → 77 → 50 → 22 → 4 → 0. An empty stack falls back
to the composition's black ground. Transparent broadcast output is the one
exception and still composites with alpha 0, so OBS overlays are unaffected.
Blackout from the phone remote did nothing when the layer stack was on. The
remote wrote background.type and visualizer.type directly, but with the
layer stack enabled the scene is drawn from the layer list, so neither value
reached the output — the panic button failed in the one situation it exists for.
The remote now sends an action and the panel performs the blackout, which is
where the single real implementation and its restore state already live. The
remote's allowed-path list stays narrow; a phone still cannot rewrite layer data.
Scenes and colour presets could not be reordered. Items sat in creation
order, and the only way to change it was to delete and recreate them. All three
lists — the scene dock, the scene list in the Library card, and the colour
presets — now have a drag handle. The handle is what is draggable rather than
the whole row, because each row carries a text input for its name and a
draggable container stops text selection inside it. The handle is also a button,
so the arrow keys reorder while it has focus; drag and drop on its own would
leave reordering impossible without a mouse.
Snapshot never worked, and recording produced an empty file. Snapshot redrew
the scene and passed SVPreview.lastAudio or window.SVAudioCore as the audio
frame; neither has ever existed in this project, so every snapshot drew with
audio === undefined and the visualizer modes threw on their first read. It now
captures the frame already on screen instead of redrawing it.
Recording read the live preview surface directly, and that surface is resized
whenever the preview relayouts — which the record card itself causes, since its
status line changes the card's height. Resizing a canvas with an active
captureStream ends the track, so MediaRecorder collected nothing and ffmpeg
reported "streams received no packets" over an empty container, after the user
had already chosen a filename. The scene is now copied each frame onto a
separate canvas whose size never changes during a capture, and the stream comes
from that.
That canvas also fixes the resolution: capture used to be the size of the
preview surface, so snapshots came out around 600 px wide and the scale slider
only enlarged them without adding detail. There is now a resolution setting —
1080p by default, up to 4K, plus portrait, square, and "preview size" — applied
to both recording and snapshot, with the aspect ratio preserved and the
remainder left black. The source is still the preview, so scaling up adds no
detail; Video Export remains the way to render at genuine high resolution.
Eleven untranslated strings in the English interface, including composed
labels such as "Visualizer · Bars", "+ Sprites" and "1. Threshold" — these are
now split at their separator and translated part by part rather than needing a
dictionary entry per combination.
The blackout self-test reported a failure that was not real. It asserted the
restored background was "not solid" rather than comparing against the scene as
it stood before blackout, and the test scene legitimately had a solid
background. It now compares against the actual pre-blackout state.
Frame rate
The frame rate now defaults to Match Display, which is what the setting has
always described as recommended. Note this for new installations on high refresh
hardware: the previous default capped at 120 FPS, so on a 240 Hz display a fresh
install will now draw 240 frames per second where it used to draw 120. Existing
settings are untouched. The setting also shows the refresh rate it detected, so
you can see it took effect, and records that the panel preview is separately
capped at 45 FPS and is not the place to judge smoothness.
Quality
623 unit tests, all passing, plus a GPU self-test that draws every registered
mode, background, effect, shader, formula and transition on a real GPU, scans
the English interface for untranslated strings, and checks the camera stays
closed. Config migration from 1.3 and 2.0 loads without losing a value.
What is not here
The timeline and clip deck are v3.1.0. Native senders (Spout, Syphon, NDI) are
v3.1.1. Per-application audio capture is v3.1.2. The broader, much faster video
export is v3.1.3. The broadcast layout editor is v3.1.4. Redundancy and frame
sync are v3.2.0. ROADMAP.md carries the detail, and an honest list of what is
deliberately not done and why.