Version 1.0.0-beta.187
If you are using UV, please contact me asap! This repo is going to move. DJUnicornTears on X/Gmail.
If you're running a live stream or recorded set with Unicorn Viz and upload it somewhere, let us know and we'll repost it.
A fullscreen OpenGL 3.3 demoscene visualizer written in Python 3.11+, designed for Linux (Wayland-first, X11 fallback). Renders audio-reactive effects, classic CP437 ANSI art, live audio input capture via PipeWire/ALSA, and MIDI control.
If you are using UV, please contact me! djunicorntears on X/Gmail.
- Primary and stable repository: https://github.com/djunicorntears/unicorn-viz
- Development and unstable repository: https://github.com/idometeor/unicorn-viz (developers only)
If the embed does not render in your Markdown viewer, watch it here: https://rumble.com/v79jc7e-unicorn-viz-beta-made-by-me.html
Perfect for: live music performances, streaming/recording with OBS, BBS nostalgia, synth/electronic art.
Get the current release bundle (a folder with manifest.json, the packages, and
install.sh), then:
./install.sh --from . # system libs (sudo once), bundled Python runtime, app, menu entry
unicorn-viz --self-test # verifies the install without opening a window
unicorn-viz # goThe installer never touches your system Python: it bundles its own runtime under
~/.local/share/unicorn-viz/. ./install.sh --help lists --prefix,
--no-desktop, --uninstall, and more. Releases are signed — the installer
verifies the signature when release-key.asc is present (see SECURITY.md).
sudo dnf install ./unicorn-viz-*.rpm # Fedora / RHEL family
sudo apt install ./unicorn-viz_*.deb # Ubuntu 24.04 / Debian 12
unicorn-viz --self-testBoth bundle the Python runtime and all dependencies; only system libraries
(SDL2, Mesa, PipeWire, ALSA, PortAudio) come from your distro. ffmpeg is
recommended (recording) but optional.
cd packaging/flatpak
flatpak-builder --user --install --force-clean build-dir io.unicornviz.UnicornViz.yml
flatpak run io.unicornviz.UnicornViz --self-testUnzip UnicornViz-Portable-<version>-win-x64.zip anywhere and run
unicorn-viz.cmd (or unicorn-viz.cmd --self-test first). The build is
unsigned for now: on first run choose More info → Run anyway in SmartScreen.
A proper installer (UnicornViz-Setup-<version>.exe) is produced by CI from
packaging/windows/UnicornViz.iss.
git clone https://github.com/iDoMeteor/unicorn-viz && cd unicorn-viz
./tools/install_linux.sh # distro deps + bundled runtime + .venv (--system-python to opt out)
./run.sh.venv/bin/python -m unicornviz \
--windowed \
--width 2560 --height 1440 \
--mode random \
--transition shuffle \
--effect-duration 30 \
--reactivity 2.5 \
--audio-device spotify \
--log-level DEBUG- 43 audio-reactive GPU effects — a lean audio-analysis core (spectrum, spectrogram, waveforms, centroid) plus themed category packs: psychedelic (plasma, kaleidoscope), retro (copper bars, ANSI viewer, fractal zoom, dali, escher, van gogh), particles (starfield, fireworks, particle storm), vector (3D cube, vector, disco ball), cosmic (cosmos, black hole cathedral, alien invasion), tech (tron grid, cyber war, hacker terminal), immersive (tunnel, wormhole, cathedral of bass), and more
- Standalone drop-in effects — image / video / texture showcases, USD sim showcase, ProjectM Milkdrop preset host, unicorn tears; plus a system-wide webcam overlay (all tested and working; currently private repositories)
- Audio reactivity — all effects respond to bass/mid/treble/beat in real time via FFT analysis
- Effect startup randomization — every effect produces a visually distinct appearance each activation; parameters like palette, speed, intensity, zoom vary automatically to prevent repetition
- Live audio capture — startup prefers the current system default input, with fast in-app source switching when needed (
Ctrl+Shift+A) - Real-time FFT + beat intelligence — profile-aware onset weighting, adaptive MAD thresholding, ACF tempo lane scoring with BPM priors, phase-lock confidence tracking, and drop/build energy slope analysis for musically coherent automation
- Per-effect audio reactivity override — constrain or amplify responsiveness per effect via config
- Sophisticated beat reasoning — not just onset triggers; Auto VJ continuously fuses BPM lock confidence, phase/downbeat coherence, energy slope, and drop scoring to decide when to build, impact, and climax
- Context-aware automation — profile-specific behavior (Chill/Normie/Raver), cooldown governance, and tag-aware effect selection keep transitions intentional instead of random thrash
- Continuous control surfaces — post-FX hue shift and rotation are handled like knobs with incremental slew behavior for smoother, performance-friendly motion
- Keyboard shortcuts — effect navigation, pause/resume, fullscreen, recording, and 40+ more commands
- MIDI support (optional) — CC parameter mapping, note-to-action binding, dynamic CC→parameter routing
- Randomization toggles with armed state — F6/F7/Alt+Z globally randomize speed/reactivity/zoom; armed modes persist across effects even when unsupported, with per-effect override ranges
- Effect-local parameter overrides — define custom min/max bounds for randomization, speed, zoom, and reactivity per effect in config
- Splash screen — music-reactive animated splash with integrity check
- ANSI art viewer — CP437 BBS art rendering with CRT phosphor shader; cycle between personal & downloaded ACiD pack
- Fullscreen + multi-monitor — Wayland-native, X11 fallback; single/span/mirror display modes
- OBS streaming integration — designed for live capture (24/7 recording, RTMP streaming, scene recording)
- Webcam overlay — system-wide camera PiP with 5+ visual treatments, switchable via hotkeys or MIDI
- Screenshot & screen recording — direct PNG capture and per-session video recording with codec options
- Window cursor control — hide by default (show with Ctrl) or keep visible via config flag
- Internal render scale — reduce from 1.0x for performance headroom on heavy shaders; adjust manually with comma/period
- Per-effect config overrides — speed, intensity, amplitude, zoom, parameters tuned per effect or globally
- Fullscreen mode selection — standard, borderless, or exclusive fullscreen via config
- Auto-discovered effects — drop
.pyfiles inunicornviz/effects/ordrop-ins/*/; they load at startup - Drop-in architecture — submodule-based private drop-ins with independent repositories; core remains decoupled
- Effect randomization requirements — built-in per-instance RNG with automatic startup randomization for all visual parameters
- GLSL 3.3+ shaders — moderngl 5.x bindings, SDF helpers, palette functions, transform feedback for procedural geometry
- Configuration-driven workflow — exhaustive
config.full.example.tomltemplate documents all effect parameters, hotkey ranges, audio settings
| Key | Action |
|---|---|
N / → |
Next effect |
P / ← |
Previous effect |
1–9, Shift+1–0, Ctrl+1–0 |
Jump to effect |
u / U / Ctrl+U |
Splash / Unicorn Tears / screen burst |
a / Shift+A / Ctrl+A |
ACiD art / ANSI art / audio source |
, / . / < / > |
Res scale down / up / MIN / MAX |
Ctrl+, / Ctrl+. |
Res scale reset |
Space |
Pause / resume |
F |
Fullscreen |
T |
Toggle auto-advance |
I |
Invert colours |
V |
Toggle recording |
S |
Screenshot |
Tab |
Legacy HUD panel |
H |
Help overlay |
ESC |
Quit |
KP 1–9 |
Camera PiP position |
KP 0 / KP . |
Camera fullscreen / hide |
KP - / + |
Camera PiP size |
KP / / * |
Webcam effect prev / next |
KP Enter |
Webcam effect auto-cycle |
See User Guide for full hotkey list.
- Documentation Index — Canonical map of all project docs, ownership, and maintenance workflow
- User Guide — Installation, running, keyboard/MIDI controls, audio setup, effects reference, troubleshooting
- Configuration Reference — All
config.tomlsettings - Effect Settings Reference — Every effect's tweakable config variables
- Developer Guide — Architecture, effects API, GLSL conventions, contributing
- Drop-in Documentation Registry — Per-drop-in docs coverage and deep-dive links
unicorn-viz/
├── unicornviz/ # Main package
│ ├── effects/ # 20+ visual effects (auto-discovered)
│ ├── audio/ # Audio capture + FFT + beat detection
│ ├── ansi/ # ANSI art parser + CP437 font
│ ├── app.py # Main loop + SDL2/moderngl
│ ├── config.py # Config loader
│ ├── hotkeys.py # Keyboard + MIDI dispatch
│ ├── midi.py # MIDI input
│ ├── overlays.py # HUD rendering
│ ├── playlist.py # Effect sequencer
│ └── splash.py # Animated splash screen
├── assets/
│ └── ansi/ # BBS art (generated + ACiD pack)
├── docs/ # Documentation
├── tools/ # Standalone utilities
├── config.toml # Runtime configuration
├── requirements.txt # Python dependencies
└── run.sh # Launch script
| Dependency | Minimum | Purpose |
|---|---|---|
| Python | 3.11 | tomllib standard library |
| SDL2 | 2.0.18 | Window & input |
| OpenGL | 3.3 | GPU rendering |
| PipeWire/ALSA | any | Audio loopback capture |
The following machine is the current baseline used for pre-release testing. For stable live use, target equivalent or better specs.
| Component | Minimum Recommended |
|---|---|
| OS | Fedora Linux 37 (or equivalent modern Linux distro) |
| Kernel | Linux 6.5.x |
| CPU | Intel Core i7-8809G class CPU (4 cores / 8 threads) |
| RAM | 32 GB system memory |
| GPU | AMD Radeon RX Vega M GH class GPU |
| VRAM | 4 GB dedicated video memory |
| OpenGL | OpenGL 4.6 core profile (Mesa 23.x or newer) |
Python packages:
moderngl >= 5.11— OpenGL wrapperpysdl2,pysdl2-dll— SDL2 bindingsnumpy,scipy— Numerics & signal processingsounddevice >= 0.4— Audio I/Oopencv-python-headless >= 4.9(optional) — Camera overlay and video clipspython-rtmidi >= 1.5(optional) — MIDI controlPillow— Screenshots
Create a custom config.toml or edit the default one:
[window]
fullscreen = false
width = 1920
height = 1080
[demo]
effect_duration = 20 # auto-advance every 20 seconds
mode = "sequential" # or "random"
[audio]
device = "" # "" = no hint; start on system default input
prefer_default_input = true # startup source policy when device=""
reactivity = 1.0 # 0.5x to 5.0x audio sensitivitySee Configuration Reference for all options.
For a fully documented, exhaustive template including all effect overrides, see:
config.full.example.toml
Runtime logs are written to logs/ automatically, with one timestamped file per run.
You can control verbosity from config or CLI:
.venv/bin/python -m unicornviz --log-level DEBUG- Linux installer script (distro-aware):
./tools/install_linux.sh
- Containerized compatibility matrix (Ubuntu/Debian/Fedora):
./tools/compat_matrix.sh
- CI workflow for the matrix:
.github/workflows/compat-matrix.yml
- Packaging evaluation artifacts:
packaging/flatpak/io.unicornviz.UnicornViz.ymlpackaging/snap/snapcraft.yaml
Unicorn Viz startup source selection works like this:
- System default input first — safest startup behavior for mixed live rigs
- Automatic fallback candidates — tried only when the active source stays silent
- Manual override anytime — open the source selector (
Ctrl+Shift+A) to pick another input
By default, Unicorn Viz starts on Audio Spectrum ([playlist].start_effect = "Audio Spectrum") specifically so you can immediately verify that the analyzer is receiving signal before moving into other effects.
On PipeWire: audio sources are captured via monitor sinks. On ALSA: loopback setup required.
For OBS streaming: the app will capture Spotify/Firefox/system audio, not OBS's own monitor feed — this prevents feedback loops.
See User Guide § Audio Setup for detailed troubleshooting.
All effects are auto-discovered and audio-reactive. Audio analyzers live in the
core package; every other visual lives in a themed category pack (its own
private repo, wired in as a submodule). Each effect carries a canonical category
tag as its first tag. Key bindings are not listed here — press H in-app for
the live hotkey overlay (the single source of truth). See
docs/drop-ins.md for the full drop-in registry.
Core — audio analyzers (unicornviz/effects/):
| Effect | Tags | Description |
|---|---|---|
| Audio Spectrum | analyzer, visualizer | FFT frequency bars + oscilloscope waveform |
| Audio Spectrogram | analyzer, visualizer, spectrogram | Reactive scrolling frequency-history heatmap |
| Audio Tracks | analyzer, visualizer, timeline | DAW-style horizontal scrolling track lanes |
| Audio Waveforms | analyzer, visualizer, oscilloscope | Multi-style oscilloscope visualizer |
| Audio Centroid | analyzer, spectrum, reactive | Spectral-brightness (centroid) visualizer |
| Audio Sine | analyzer, neon, lasers | Textless neon wave-lasers with crossing squash |
psychedelic-01:
| Effect | Tags | Description |
|---|---|---|
| Plasma | psychedelic, classic | Classic sin/cos colour-field shader with palette drift |
| Kaleidoscope | psychedelic, classic | View through a rotating optical kaleidoscope |
| Psychedelic | psychedelic, shader | Animated palette-sine neon field |
particles-01:
| Effect | Tags | Description |
|---|---|---|
| Starfield | particles, classic | Cinematic multi-layer star field with warp-speed mode |
| Fireworks | particles, classic, celebration | Cinematic night-sky pyrotechnic display |
| Particle Storm | particles | GPU transform-feedback particle system |
retro-01:
| Effect | Tags | Description |
|---|---|---|
| Copper Bars | retro, classic, amiga | Amiga-style horizontal raster/colour bars |
| ANSI Viewer | retro, ansi, classic | Scrolling CP437 .ANS art with CRT phosphor shader |
| Fractal Zoom | retro, psychedelic | Deep Mandelbrot voyage with nebula interior shading |
| Escher | retro, art, optical | Impossible-style tiling corridor illusion |
| Dali | retro, art, surreal | Surreal melting forms and mirage-like distortions |
| Van Gogh | retro, art, shader | Painterly swirling night-sky brushstroke field |
feature-01:
| Effect | Tags | Description |
|---|---|---|
| Hexy Stars | feature, stars, hex | Reactive pulsing hex/octa star field |
| Rainbow Trance | feature, mythic, crystal | Prismatic pyramid field with rainbow sky rays |
| Metaballs | feature, classic | GLSL SDF orbs that attract and repel |
vector-01:
| Effect | Tags | Description |
|---|---|---|
| 3D Cube | vector, classic, 3d | Solid neon rotating cube with Phong shading |
| Vector | vector, classic, demoscene, 3d | Classic demoscene spinning wireframe polyhedra |
| Disco Ball | vector, disco, raymarching, reflections | Raymarched mirror-tile ball with rotating spot beams |
cosmic-01:
| Effect | Tags | Description |
|---|---|---|
| Cosmos | cosmic, shader | Nebula clouds with star field and warp streaks |
| Black Hole Cathedral | cosmic, shader, cathedral | Gothic mega-structure orbiting an accretion lens |
| Wavey Gravy | cosmic, sci-fi, shader | Pulsating organic landscape with bioluminescent veins |
| Alien Invasion | cosmic, sci-fi, procedural | UFO fleets descending with atmospheric probing beams |
tech-01:
| Effect | Tags | Description |
|---|---|---|
| Tron Grid | tech, tron, laser, grid, raymarching, neon, sci-fi | First-person laser-grid corridor; bass scales grid, beats trigger shockwaves |
| Cyber War | tech, network, cyberpunk | Real-time digital battle-map with AI node attacks |
| Hacker Terminal | tech, cyberpunk, glitch | Animated shell/log streams with audio-reactive glitch |
| Hacker Terminal 2.0 | tech, cyberpunk, glitch, text | Real CP437 scrolling text streams with glitch transitions |
| Threat Matrix | tech, cyberpunk, hacker, network, matrix | War-room IDS threat map: sector heat grid, trace-route packets, radar sweep, beat-driven breach waves |
immersive-01:
| Effect | Tags | Description |
|---|---|---|
| Tunnel | immersive, classic | Fully procedural infinite rotating tunnel |
| Wormhole | immersive, crystal, prism, lattice | Refractive prism-lattice flight down a shifting light corridor |
| Cathedral of Bass | immersive, bass, shader, raver, cathedral | Layered, room-shaking flight down an infinite nave |
games-01:
| Effect | Tags | Description |
|---|---|---|
| Breakout | games, arcade | Audio-reactive arcade brick-breaker |
holiday-01:
| Effect | Tags | Description |
|---|---|---|
| America 250 | holiday, america, patriotic, fireworks | Over-the-top bicentennial-plus-50 celebration |
Standalone effect drop-ins (one effect per repo):
| Effect | Source | Tags | Description |
|---|---|---|---|
| Image Showcase | images-01 |
media, images, slideshow | Image directory with Ken Burns motion and multiple presentation styles |
| Video Clips | video-clips-01 |
media, videos, slideshow | Audio-reactive clip montage with per-activation directory-group selection |
| Video Player | videos-01 |
media, videos, player | Plays whole videos with their own audio (ffpyplayer), letterboxed; manual-only |
| Texture Showcase | textures-01 |
media, textures, modern | Ken Burns pan/zoom with reactive colour grade and beat fade |
| Sim Showcase | sims-01 |
media, 3d, usd, simulation | USD scene carousel with camera orbits (robotics/galaxy scenes) |
| ProjectM Presets | projectm-01 |
projectm, milkdrop | libprojectM Milkdrop preset host with 1000+ community presets |
| Unicorn Tears | unicorn-tears-01 |
psychedelic | Prismatic iridescent teardrops falling through a star-field |
System & overlay drop-ins (not playlist effects):
| Drop-in | Purpose |
|---|---|
webcam-01 |
System-wide camera PiP overlay with 5+ visual treatments |
multi-head-01 |
Multi-monitor display controller (single / span / mirror modes) |
streaming-01 |
RTMP streaming controller (when available) |
Black screen?
- Check OpenGL version:
glxinfo | grep "OpenGL version" - Try X11:
SDL_VIDEODRIVER=x11 ./run.sh
No audio?
- Check devices:
python3 -c "import sounddevice as sd; print([d['name'] for d in sd.query_devices()])" - Check PipeWire:
pw-dump | grep "monitor"
Crash on startup?
- Ensure Python 3.11+:
python3 --version - Verify virtualenv:
.venv/bin/python --version
See User Guide § Troubleshooting for more.
To write a new effect:
- Create
unicornviz/effects/my_effect.py - Subclass
BaseEffect, setNAME = "My Effect" - Implement
_init(),update(),render(),destroy() - It's auto-discovered at runtime
See Developer Guide § Writing a New Effect.
Unicorn Viz is MIT licensed — see LICENSE.
Third-party dependencies keep their own licenses; the full texts ship in
THIRD_PARTY_LICENSES.md, which is generated by
tools/gen_third_party_licenses.py and installed by every packaging target.
Regenerate it whenever requirements.txt pins change.
Some bundled assets are not covered by the project's MIT license: the ANSi
art in assets/ansi/acid/ remains the copyright of its individual artists (see
assets/ansi/README.md for credits). Restricted asset
packs — NVIDIA Omniverse USD characters, MilkDrop presets — are deliberately
not redistributed here; each operator fetches their own. Licensing posture for
the whole project is reviewed in
docs/audits/2026-08-08-licensing-audit.md.
Issues and PRs welcome. See Developer Guide § Contributing for code standards.
Happy demoscene visualizing! 🦄✨
-
1.0.0-beta.199 — The
audio_toggleaction is gone (owner call: it replayed the same key aseq). It is removed from the hotkey table, the help labels and the generic MIDI default note map (that note now playseq). A saved hotkey override, a[midi.note_map]entry, a MIDI Learn binding or a controller profile that still names it is mapped toeqwith a one-time log line instead of an error; midi-controllers-01 0.11.1 rebinds every pad that wasaudio_toggletoeqso no pad goes dead. -
1.0.0-beta.198 — Three core fixes. Transitions are visible with Candy Frame, Rainbow Nova or the Grand Finale overlay on: during a scene transition those overlays drew the outgoing effect over the blend (Candy Frame replaces the whole picture), so the old scene stayed on screen until it snapped to the new one; they now draw over the blend. A larger audio capture block no longer blinds the analyzer: with a block bigger than twice the FFT bands (the documented 2048 xrun remedy at the default 512 bands) the FFT only saw the older half of each block, through half a window, so the newest audio never reached the spectrum, flux or onset detector; it now analyzes the newest samples. Runtime overrides no longer corrupt shared config: setting an override (the config menu's next-launch settings) changed the module-level defaults and the parsed config.toml data, so later configs read the override and
file_value()returned it. -
1.0.0-beta.197 — Two more UI fixes. The audio and MIDI selector panels no longer run off the screen when there are many devices (PipeWire lists a lot once monitors are counted, Windows lists each device once per audio API): the panel is clamped to the window, scrolls around the selected row, and shows an
n/Nposition. Rebinding a hotkey frees its old key and checks real conflicts: after you move an action off its default key (say Delete), that key no longer fires the action, unless another action is still bound to it; and the capture screen now refuses keys that the help documents for core or any drop-in (such asVfor recording), not just the thirty-odd named actions. -
1.0.0-beta.196 — Two visible fixes. Speed changes no longer hard-cut the picture: the
=/-/ Ctrl+=/ Ctrl+-hotkeys, random speed (F6, Alt+=, and each scene change while armed), the G reset, a MIDI knob mapped to speed and the config editor's speed row now all keep the shader's phase continuous, the way the API's speed setter already did (34 effects computed time as time-times-speed and jumped to an unrelated moment). Non-ASCII text is readable: overlay text used a 7-bit mask on its glyph table, so·drew as7,—and→as blanks andéasi; now accented letters draw as their base letter (Beyoncé, Ólafur), punctuation as an ASCII stand-in (—as-,→as>,·as.), and anything else as?. -
1.0.0-beta.195 — Follow-up to the drop-in crash containment: an overlay (Candy Frame, Rainbow Nova, Grand Finale) that keeps failing and gets switched off no longer leaves the picture shrunk inside a frame that is not being drawn; the scene is drawn plainly and full-screen instead. And when an overlay fails mid-frame, the graphics state it may have left changed (blending, depth, scissor, bound target) is reset before the next one runs.
-
1.0.0-beta.194 — Stopping a recording or a stream no longer freezes the show. Pressing
Vto end a long recording used to hold the visuals until ffmpeg finished rewriting the file (tens of seconds for a long set), and a stalled encoder or dead stream endpoint could hang the app for good because closing ffmpeg's pipe waited on a write that never returned. Both now stop instantly and finish in the background (killing a wedged ffmpeg first, bounded at every step); exit waits at most a minute for the file. Resizing the window or toggling fullscreen while recording now rotates to a new, separately named file once the size has settled (not once per resize event), the old file finishes in the background, and the new file is recorded at the new size (before, it kept the old geometry). streaming-01 0.9.1. -
1.0.0-beta.193 — A misbehaving drop-in can no longer take the show down. Exceptions in the config editor's keys, MIDI actions, the per-frame drop-in overlays (webcam, dancing unicorn, Rainbow Nova, Grand Finale, Candy Frame, music-video layer, streaming write) and the end of an effect transition are now contained and logged once (then at most every 30 seconds); a per-frame step that fails 30 frames in a row is switched off for the session. If the effect the show starts on fails to build (a shader that does not compile on your driver), the next effect starts instead of the app aborting. The audio analysis thread survives any exception and keeps the audio-reactive values live (it resets the analyzer after a long run of failures). Switching audio sources is serialized between the operator selector and the silence fallback, and pressing Esc on the splash screen shuts down once instead of twice.
-
1.0.0-beta.192 — Installed builds get the patched free-threading builds of moderngl, glcontext, python-rtmidi, sphn and OpenCV (Linux and Windows; published as the
wheelhouse-cp314t-patched-2026-10-02release), so importing them no longer turns the GIL back on, and the known thread-safety bugs in them are fixed. Bundle builds keep only the newest build of each wheel. -
1.0.0-beta.191 — A damaged settings file can no longer wipe your settings. If
global_state.json(every menu setting),config_profiles.jsonorpresets.jsonis truncated, mangled or unreadable, it is now moved aside as<name>.corrupt-<timestamp>with its bytes intact, an ERROR says where, and the app starts from defaults instead of silently saving an empty file over it (and if it cannot be moved aside, saves to that file are blocked for the session). Quitting is also more robust: every shutdown step is isolated, so one failure (a dead audio process, a GL error) can no longer skip the settings save or the window teardown. -
1.0.0-beta.190 — Recording works on stock Fedora. Fedora's
ffmpeg-freehas no libx264, and the recorder used to fall back to it unconditionally, so with no working hardware encoder recording failed.codec = "auto"now tries libx264, then libopenh264 (bitrate-controlled), then MPEG-4, each with a real test encode, and logs which it chose; if the ffmpeg has no H.264 encoder at all it says how to get one (the full ffmpeg from RPM Fusion, or Fedora's openh264 repository). Audio falls back from aac to opus, mp3 or ac3 the same way, and each encoder gets its own quality setting. -
1.0.0-beta.189 — The Windows portable bundle moves to free-threaded Python 3.14 (W1, 2026-09-30 audit), so mixer tracks, stems and sampler pads can load on installed Windows builds. Windows builds of the GL, MIDI, OpenCV and stem-separation packages that PyPI lacks for free-threaded 3.14 were made on GitHub Actions and are published as the
wheelhouse-cp314t-win-2026-10-01release (with their license notices, including the LGPL FFmpeg plugin in the OpenCV wheel); the bundle build checks every one against a checksum file committed in the repository.build_windows_portable.sh --runtime-flavor gilstill builds the old 3.11 bundle. Not yet run on a Windows machine. -
1.0.0-beta.188 — Fixed a crash of the audio process (the mixer's engine host) when a track loads: it segfaulted, the app fell back to running the engine inside the main process for the rest of the session (seen in real sessions on 2026-09-26 and 2026-09-27). A deck's old track or stems were unmapped from shared memory two seconds after the swap while another thread could still be reading them. They are now unmapped only once nothing references the array any more.
-
1.0.0-beta.187 — The Linux installers put CPU-only PyTorch under the mixer's stem separation instead of the default CUDA build: the mixer dependency set is about 1.4 GB instead of 6 GB, and demucs runs on the CPU either way. Set
UV_TORCH_CUDA=1to get the CUDA build. -
1.0.0-beta.186 — The main process now logs garbage-collection pauses: a WARNING for any collection over 15 ms (same threshold as the mixer's) and a DEBUG summary of runs, total and max time per generation once a minute. On a free-threaded Python
gc.freeze()only halves a full pass instead of removing it, so this is how a 3.14t session shows what it really pays. -
1.0.0-beta.185 — Installed builds move to free-threaded Python 3.14 and finally get the drop-ins' dependencies (W1/W2, 2026-09-30 audit). The bundled runtime can now be the python-build-standalone 3.14 free-threaded build (
fetch_runtime.sh --flavor ft), which fixes theSharedMemory(track=)break that stopped every mixer track, stem and sampler load on the old 3.11. It is checksum-pinned, and a runtime with no checksum is now refused instead of installed with a warning. Linux installs (one-liner, clone installer, rpm/deb) pick it automatically when the release carries our prebuiltwheelhouse/(moderngl, glcontext, python-rtmidi, OpenCV). Every installer also installs each drop-in's own requirements (PyAV, mutagen, python-vlc, ably, usd-core and so on); one that has no build for the interpreter is skipped with a warning instead of failing the install, and mediapipe goes in with--no-depsso it can't overwrite OpenCV. The Windows bundle stays on 3.11 until its Windows wheels are built (build_windows_portable.sh --runtime-flavor ftis ready for that day). -
1.0.0-beta.184 — OpenCV is optional.
--self-testreports a missingcv2as a warning instead of a failure, so a clean free-threaded Python install (whereopencv-python-headlesshas no wheel yet) passes the install check; core never imported it, and the webcam and video-clips drop-ins already guard their imports. -
1.0.0-beta.183 — Startup now says which interpreter it is running and whether the GIL is on. On a free-threaded Python the main process logs one INFO line naming the modules that turned the GIL back on (expected for now: moderngl, glcontext, python-rtmidi) and a WARNING for any other; the audio helper logs a WARNING if its GIL is on, since it imports none of them. The audio helper has been run on free-threaded 3.14 with the GIL off (plan W1).
-
1.0.0-beta.182 — Ctrl+C in a terminal shuts down cleanly (G3, 2026-09-30 audit): the audio and mixer helper processes ignore the terminal's group SIGINT and stop when the app tells them to, and a helper that is already gone no longer aborts teardown, so runtime state, webcam and relay state, MIDI close and GL/SDL teardown always run.
-
1.0.0-beta.181 —
--effect-durationworks again: the built-in default was the int60while the flag parses a float, so every launch example using the flag failed config validation at startup (G1, 2026-09-30 audit). An integereffect_durationin config.toml is still accepted. -
1.0.0-beta.180 — The config menu is reorganized. Performance now holds only the settings that cost frames. A new System tab has display, MIDI, the audio engine, folders and paths, and the Spotify client ID. A new Streaming tab has RTMP, video out and the chat username. Auto VJ gets its own tab with every Auto VJ setting in sections (General, Detector, Director, USER mood, Recommender, HUD, Logs and training). Your saved values come along.
-
1.0.0-beta.179 — Auto VJ settings get menu rows (auto-vj-01 rc.154): Auto VJ on/off, beat tracker engine, onset envelope, HUD BPM smoothing, the director's snap and phrase-wait rows, and the USER-mood drop thresholds and cruise post-FX slots. Visuals › HUD detail gains Production HUD. Values set in config.toml move over as usual.
-
1.0.0-beta.178 — projectM and Image Showcase settings get menu rows. Visuals › projectM: blend between presets, lock preset, start presets clean, opening preset, auto-exclude flat presets, flat-color grace time (apply the next time the effect loads). Performance: preload images at startup, preset warm-up frames, libprojectM path, extra preset folders, preset texture folders. Rows show only when the effect is installed; values set in config.toml move over as usual.
-
1.0.0-beta.177 — Effect parameters set in config.toml move into your profile. The first time each effect loads, its
[effects.<Effect>]values for the sliders the Effects tab edits (speed, zoom, reactivity, ...) are copied into the active profile once; after that the menu is the source of truth. Values you already changed in the menu are kept. -
1.0.0-beta.176 — The last config.toml settings in the audit get menu rows. Recording: Save folder (next recording). Visuals: ANSI art folder. Performance: Display mode and Display (live, and the app starts in them; applied before multi-head is built) and MIDI device / MIDI preset (next launch; AUTO/NONE are real choices and an unplugged device stays selectable). Drop-ins: media folder (media-01 0.31.0), V4L2 device (video-out-01 0.7.0), chat position and username (chat-01 0.7.0). Values set in config.toml move over as usual.
-
1.0.0-beta.175 — Reverts beta.174's DJ mixer Drop-ins switch and its early startup read: per the owner,
[dj_mixer] enabledstays config.toml / boot-level only. Nothing had migrated (beta.174 was never run). -
1.0.0-beta.174 — DJ mixer joins the config editor's Drop-ins tab (RESTART; the mixer's own settings stay in its ⚙ SETTINGS). It is applied before the app resolves its boot profile, like the Logging tab, so
--mixersees the menu's choice. -
1.0.0-beta.173 — Drop-in rows on the Logging tab are remembered and replayed like Performance rows, and migrate from config.toml the same way, so every logging option can live on one tab: Auto VJ's decision log, log folder, training logs and corpus files and detector log (auto-vj-01 rc.152), and Spotify's corpus file (spotify-01 rc.10) join core's own.
-
1.0.0-beta.172 — Logging tab and text rows in the config menu. A new Logging tab holds log level, log folder, per-frame perf logging (moved from Performance), crash dump file and stall dump; logging is set up before the app, so
__main__lays these over config.toml itself (a--log-levelflag still wins). New text and secret row kinds: click or Enter to type (real text input, on only while a field is open), Enter or click-away to save, Escape to cancel; secrets show SET / NOT SET with a SHOW toggle and never pre-fill a hidden value. Drop-in rows can be text or secret (Spotify client ID and corpus file, streaming endpoint). Secret-looking keys are masked in the startup override log. -
1.0.0-beta.171 — More settings move into the config menu. New Drop-ins tab of load/start switches (RESTART): Candy Frame, chat, color grade (+ on at start), Control Room, keystroke log, lyrics, media, Spotify (+ Web API), streaming, video out (+ on at start, V4L2). Visuals gains HUD detail (detector BPM, profile score, recommended profile; still hidden in Auto VJ production mode) and Random look min/max ranges for the speed/reactivity/zoom hotkeys. Values set in config.toml move over on the next start. A dotted key like
[spotify.web_api] enablednow overrides only its leaf, never the whole table. Adding a tab no longer switches which tab is open. -
1.0.0-beta.170 — config.toml values move into the menu. At the end of startup, every menu row whose setting config.toml actually sets (never a built-in default) gets that value copied into its saved setting once, if nothing is saved yet; the menu wins from then on and the startup log lists what moved. Core restart rows and live rows with a config twin migrate; drop-in rows opt in with
'config': '<section>.<key>'. NewConfig.file_value(). Also fixes beta.169's first boot: with profiles saved but none remembered as active, the running settings become "default" instead of an arbitrary saved profile being loaded; rows added since a profile was written are caught up into it. -
1.0.0-beta.169 — Config editor profiles save as you go. There is always an active profile ("default" on first run, shown orange), every Audio/Visuals row and per-effect parameter change writes through to it (debounced, flushed at shutdown), and it loads at the end of startup, so menu settings survive a restart with no explicit save. Save writes to the active profile; a new name in NAME creates and activates one. Clicking another profile marks it purple, with its border and LOAD pulsing cyan until loaded. LOAD applies the selected profile. Deleting the active profile loads the next one instead of overwriting it. First step of moving settings out of config.toml into the menu (docs/audits/2026-09-24-config-menu-coverage-audit.md).
-
1.0.0-beta.168 — Performance tab rows for the audio process. Audio process toggles
[audio] processand Audio process Python picks[audio] process_python: APP PYTHON, FREE-THREADED (only when the helper interpreter is installed at<XDG data>/unicorn-viz/venv-ft), or CUSTOM for a hand-set path, which opening the menu never overwrites. Both are RESTART rows, remembered and laid over config.toml at the next launch; the free-threaded interpreter is never chosen by default. An empty interpreter (APP PYTHON) now counts as a real override, so it can win over a hand-set path. -
1.0.0-beta.167 —
unicornviz.workers: a spawn-context process pool for pure batch jobs from path-loaded drop-in modules (WorkerPool.submit(module_path, func, *args)), workers at +10 niceness;UNICORNVIZ_WORKERSoverrides the default (tests use 0 = in-thread). First user: dj-mixer-01 0.219.0's ANLZ runs. -
1.0.0-beta.166 —
remote_objects.adopt_array(): in a helper process, owners move big buffers into shared memory as they adopt them, so publishing them to the main process costs no second copy (the helper was holding every track and stem twice). Retired segments are unmapped after 2 s instead of 10. -
1.0.0-beta.165 —
gpu2dstreamed rasters:TextureAtlas.stream_entry(key, image)/DrawList.stream_image(key, image, x, y)redraw a live preview through one reused atlas region instead of a new entry per refresh (which filled the atlas in ~25 frames); only the newest pixels per region are uploaded. For the Control Room's program preview. -
1.0.0-beta.164 —
NowPlayingHub.active()evaluates its sources once per ~frame and hands each caller its own copy (the overlay, auto-vj and the Control Room each re-ran every source snapshot: ~6% of the main thread live). -
1.0.0-beta.163 — Audio process tuning from a live profile: a waited call publishes only its own object (the helper's command thread spent 62% re-publishing everything, device lists included); device lists refresh about once a second (
Policy.cold_derive); reactivity is set main-side (auto-vj drifts it every frame and the helper never reads it); the claimed-device list is pushed only when it changes. -
1.0.0-beta.162 — Capture and analysis run in the audio process (
[audio] process, default on;unicornviz/audio/process.py): a realAudioManagerin the helper, the app's one a shadow reading the analysis frame the helper publishes every tick; onsets stream as a sequence-numbered log; capture state reaches the runtime store through a relay (one writer); dj-mixer-01 0.218.0 joins the same process viavj_api.audio_process_host(); helper death restarts capture in-process on the next frame.AudioManageritself: the silence fallback now ticks on the analysis thread, never the render thread (the 2026-09-22 lockup path), onsets are a cursor-read log, andaudio_snapshot()is the one locked read.remote_objectsgains value objects, event streams, per-tick derived values, factories loaded into a running helper, resync on attach, and tolerant unpickling. -
1.0.0-beta.161 — Gen-2 GC pauses cut from 100-200 ms to ~1 ms.
unicornviz.gc_tuning.freeze_heap()moves the long-lived heap out of the cyclic collector's reach; the app calls it once after startup (one collection first, before the first frame) and dj-mixer-01 0.217.1 again after its library prewarm. Measured on 3.14.6 with a 2M-object heap: 124 ms -> 1 ms. -
1.0.0-beta.160 —
unicornviz.remote_objects: run an object graph in a helper process, drive it through shadows. The helper (python -m unicornviz.remote_objects, interpreter selectable, never imports moderngl) builds the real objects and publishes their state as deltas; the main process swaps its own instances to shadow subclasses (declared read-only calls local, everything else forwarded, writes sequence-acknowledged so faders never flick back). Objects and bound methods cross as references, large arrays through shared memory, host-only facts as derived values; slow calls run off the command thread; helper death is reported, not hung. First user: dj-mixer-01 0.217.0 (audio engine process). -
1.0.0-beta.159 — GPU 2D draw lists for second-window UIs (
unicornviz.gpu2d): a Pillow-ImageDraw-compatible recorder, a text/image texture atlas and a raw-GL instanced SDF renderer, plusSecondaryGLWindow.present_gpu_frame(). Axis-aligned shapes, lines and text are pixel-exact with Pillow; curves are anti-aliased. ~23x cheaper than a Pillow raster at 2,500 calls, ~100x less upload. First user: dj-mixer-01 0.216.0. -
1.0.0-beta.158 — Audio analysis ~35% cheaper per block.
Analyzer.process()computed the 64 perceptual band means (and the low-band replacements) as ~80 tiny.mean()calls, ~46% of the analysis thread under the GIL; they are now two running-sum reads. Beats, onsets and every detector-facing field are bit-identical on three real tracks (34k blocks);bandsdiffers by <= 2.4e-7 (float32 rounding). -
1.0.0-beta.157 — Operator panels gain hot pads and in-place rename.
PanelContent.padsis a row of equal-width tiles above a panel's buttons for picking one of several things;PanelButton.rename_actionmakes a pad or button renamable by double-click (Enter sendson_action(rename_action, {'payload': ..., 'text': ...})). Both are optional fields, so existing panels are unchanged. First user: webcam-01 1.8.0's per-camera pads (drawn by control-room-01 0.22.0). -
1.0.0-beta.156 — Fix a main-thread lockup in the audio-capture silence fallback.
App.run()now calls_refresh_claimed_audio_devices()every frame, right beforeaudio_manager.get_audio_data()— previously that refresh only ran when the operator opened the manual audio-source selector, so a session that never opened it left the claimed-device list permanently empty. With nothing claimed,capture.py's_device_is_claimed()guard (built for exactly this collision) was a no-op: the automatic silence-based fallback switched onto the DJ mixer's own hardware (DDJ-REV1: USB Audio/DDJ-REV1 Analog Surround 4.0), and the underlying blockingsounddevicestream-open call hung the render thread indefinitely (2026-09-22, diagnosed from a faulthandler dump after a force-quit). The refresh itself does no device I/O, so calling it every frame is cheap. -
1.0.0-beta.155 —
Appgains per-window monitor assignment for the two operator windows, driving multi-head-01's new Displays-page "MOVE MIXER HERE" / "MOVE CONTROL ROOM HERE" buttons:set_control_room_display()/control_room_display_index()(persists through the runtime store, closes and reopens the window if it's currently open — control-room-01 0.21.0 reads the store key back at construction) andset_mixer_display()/mixer_display_index()(delegates toDjMixerController.set_display_index(), dj-mixer-01 0.215.0, which owns its own window lifecycle). Both exposed throughVJApias thin pass-throughs, per the Public Runtime Surface Rules. Also fixes multi-head-01's Displays page RESET reverting to staleconfig.tomlinstead of the active exclude set, and makes SAVE apply excludes to the live window immediately instead of requiring a restart (multi-head-01 1.1.0-rc.3). Closes outdocs/planning/multihead-control-room-surface-2026-09-21.md. -
1.0.0-beta.154 — Reverts beta.153's Windows registry write (
AppCompatFlags\Layers/ "Disable fullscreen optimizations"). Owner tested that exact setting by hand on beta.152 (Properties > Compatibility checkbox, reboot, relaunch) and the flashing was unaffected — the Fullscreen Optimizations diagnosis was wrong, so the automated version of it was dead weight rather than a fix. Nothing else from beta.152 changes;fps_limitstill defaults to 0. -
1.0.0-beta.152 —
[render] fps_limitdefaults to0(follow display vsync) again, not30. The 30 default (beta.60, 2026-08-08) was tuned against one Linux box under 4K mirror + webcam load and requestedSDL_GL_SetSwapInterval(2)on every session on every platform; on Windows the driver serviced that as a multi-second deferred wait whenever the window had focus, which is what the graphics-flashing/TV-signal-loss reports traced back to (beta.124 already clamped a non-zero cap to interval 1 on Windows specifically — this removes the need for that clamp to ever fire by not capping anywhere by default). The setting itself is unchanged and still selectable (0/24/30/60) on the config editor's Performance tab. -
1.0.0-beta.151 — Second half of the fft_bands fix, caught live:
App.__init__built_audio_scratch_current/_audio_scratch_nextbefore a RESTART-badged fft_bands choice persisted from a previous session gets laid overself.cfg(that override only applies inrun()), so the scratch pair could still be sized at the stale on-disk default and crashcopy_audio_data()on the first real frame against the now-correctly-sized live buffers.App._configure_audio_data_fft_bins()is now called again right after the override is applied, beforeAudioManageris built. -
1.0.0-beta.150 — Fixed a crash on any
[audio] fft_bandschoice other than the default 512 (256/1024/2048, all offered in the config editor's Performance tab):AudioData.fftwas a hardcoded 512-length buffer, independent of the analyzer's own fft_bands-sized arrays, so the audio-analysis thread died on the first frame (could not broadcast input array from shape (N,) into shape (512,)) and every audio-reactive effect went silently dead for the rest of the session.AudioData.fft's length is now a single configured value (AudioData.configure_fft_bins) set fromfft_bandsbefore startup; Audio Centroid's FFT texture, Audio Chromogram's chroma-weight matrices and Audio Spectrogram's log-frequency LUT — each of which independently assumed 512 — now derive their own sizing from it too. -
1.0.0-beta.149 — OpenBLAS pinned to one thread at the entry point (
OPENBLAS_NUM_THREADS=1, before numpy loads): its per-core workers busy-waited after every call and took CPU from the audio, render and mixer-analysis threads; an exported value still wins, and the Demucs subprocess keeps its own thread counts. -
1.0.0-beta.148 — Config editor: a UI scale (1.0 at 1080p, 1.8 at 4K) so the panel keeps its 1080p proportions on a 4K primary; the Hotkeys tab shows the MIDI notes bound to each action (read-only, from the active note map). Help overlay: the "Extra effects (no direct key)" list wraps to its pane (at most three lines, ellipsized) instead of running off the panel. Runtime-capability discovery gains a loaded-only mode (reads the drop-in module cache, imports nothing) exposed as
vj_api.dropin_capabilities()andsubsystem_names(); control-room-01 0.19.0 uses it so the LAYOUT page names loaded drop-ins that have no panel yet. -
1.0.0-beta.147 — Tier B of the drop-in performance audit lands: banner-01 0.14.0 (drip, beat color), cta-01 0.10.0 (editor FX, drip, sparkles, density), midi-controllers-01 0.11.0 (LED refresh rate), effects-particles 0.10.0 (
particles), effects-tech 0.11.0 (march_steps), effects-rollercoast 0.5.0 (march_stepson every ride), images-01 0.9.0 ([images] max_image_edge), unicorn-tears-01 rc.3 ([dancing_unicorn] max_sparkles). The unicorn-tears controller joins the config editor's contributor attribute list. A handoff doc for the VJ seat covers auto-vj-01's rows (docs/planning). sims-01 and video-clips-01 were reclassified: no running cost knob to expose. -
1.0.0-beta.146 — Config editor contributor convention 2.0. Drop-ins are discovered through the subsystem registry (no more fixed list in core), a row can pick its own tab and carry the 2.0 presentation keys (kind, choices, label, hint, badge, section), live Performance rows are remembered in runtime state and replayed at startup, and
restartrows lay their config values over the loaded config now and at the next launch. Enables the drop-in Performance rows landing alongside (see docs/audits/2026-09-16-dropin-performance-options.md). -
1.0.0-beta.145 — Now Playing banner gets a switch. The track-change announcer had no key and no editor row: it was config-only, one
now_playing_bannerflag per source (mixer, Spotify, media player).Shift+Wnow mutes it across every source at once (Wstays the platter), and both the banner and the Now Spinning platter appear as toggles under Announcements on the config editor's Visuals tab. Both choices are remembered in runtime state across runs. -
1.0.0-beta.144 — Configuration editor 2.0. The
ceditor is rebuilt as a declarative control surface: rows are sliders, ON/OFF pills or segmented choice chips (with< label >arrows when the labels are long), grouped under section headers, with a hint line on the selected row and a tooltip on hover; click or drag a slider track, click a chip or pill, Enter flips a toggle, and the tab bar wraps onto a second row instead of overflowing. The read-only System and Auto VJ tabs are gone (the HUD and the Control Room already show that), and the remaining tabs run Effects first then alphabetical. New Performance tab collects every core cost knob: render scale, frame limit, present guard, preview capture / fps ceiling / width, capture latency, FFT bands, block size, system-monitor sampling, tooltips, the video-deck layer and its cache edge, per-frame perf logging. Live rows apply at once; RESTART rows are remembered and laid overconfig.tomlat the next launch. Every Performance choice persists in runtime state and is excluded from configuration profiles (it describes the machine, not the show). Visuals gains transition length, HUD auto-hide/timeout and flash messages; Recording rows use the new kinds. Drip: bass-lit neon frame with a halo, border sparkles, palette-gradient slider fills, a live FPS / frame-ms readout on the Performance tab. -
1.0.0-beta.143 — Startup no longer aborts when the audio open is slow (OBS holding PipeWire's JACK shim busy): a timed-out capture open is waited for instead of torn down underneath (PortAudio's JACK host API asserted and killed the process on
Pa_Terminate), and the retry adopts the late-finishing open rather than opening a second stream. -
1.0.0-beta.142 — Windows packs render every text surface with the bundled font: dj-mixer-01 0.194.3 and midi-controllers-01 0.10.1 now go through core's font resolver instead of Linux-only font paths (their operator window, help modal and profiles panel drew with PIL's tiny bitmap font on Windows).
-
1.0.0-beta.141 — Video decks audit fixes: a deck refused a source at the two-source cap is retried each frame and picks one up as soon as another deck frees it (it used to stay black for the rest of its track); the layer's shader compiles at construction, not on the first fade-in; an audio-only night costs a null check per frame; the open log line no longer prints a 0x0 size (the open is non-blocking — the first uploaded frame logs the real size);
DeckVideoSource.open_error(videos-01 0.8.2) is honored with one WARNING and the deck treated as no-video. -
1.0.0-beta.140 — Video decks: the layer targets the audible instant when the mixer publishes its output latency (
latency_sin the deck-state payload) — the mixer's position is its write cursor, and on this rig the output buffer is 256 ms, so without this the picture led the sound by a quarter second. Separate texture-upload timing, an opt-in per-frame trace (UNICORNVIZ_VIDEO_DECKS_TRACE=<file>), andtools/profiling/video_decks_report.pyfor the part-B measurement table. -
1.0.0-beta.139 — Music-video decks (core half). A DJ deck loaded with a video file draws its picture as a letterboxed layer at the deck's audibility — it fades with the fader, never with a timer — through the new
VideoDeckLayer, thepublish_deck_state/get_deck_statebus andget_video_layer_opacity()onvj_api, and a new[video_decks]config section (postfx_over_video,cache_window_s,cache_long_edge,swap_hold_opacity). The layer is a no-op without videos-01 or the mixer; its per-frame cost shows as thevideo_decksprofiler stage and the HUD showsVIDEO A 82%while a deck is visible. Design:docs/planning/music-video-decks-plan-2026-09-13.md. -
1.0.0-beta.138 — The pysdl2-dll "Using SDL2 binaries from pysdl2-dll" notice no longer prints at launch or in test runs; bundled binaries are the supported case, so the warning carried no information.
-
1.0.0-beta.137 — Switching display mode on the X11 driver under XWayland no longer kills the app with
X Error ... BadMatch (X_SetInputFocus): an Xlib error handler now swallows that one benign error (an SDL race inside its border toggle, where the window manager reparents the window between SDL's viewable check and its focus call) and forwards every other X error unchanged. -
1.0.0-beta.136 — The native quit confirmation dialog is back (owner decision): the in-app "press again" prompt from beta.121 was part of chasing the Windows stalls, which turned out to be the swap interval and the fullscreen flag; a real yes/no box is what the owner wants.
-
1.0.0-beta.135 — Category pinning for the effects browser. Shift+P (alongside plain P for a single-effect pin) pins rotation (auto_advance, auto-vj-01, manual next/prev) to the selected effect's browser category instead of leaving it -- unlike a single-effect pin (
lock_effect()/ProjectM-only mode), rotation keeps picking freely among every effect in the category rather than freezing on one.App.pin_category()/unpin_category()(mutually exclusive with the effect lock) narrow the playlist's own rotation viaPlaylist.set_disabled();_switch_effect()enforces the same boundary for any caller that bypasses the playlist (e.g. auto-vj-01's own picks), redirecting into an in-category replacement rather than silently dropping the switch.effects.registry.category_of()factors out the category formula (first tag, else pack) so this andbrowser_entries()share one source. Exposed onVJApiaspin_category()/unpin_category()/category_lock. -
1.0.0-beta.132 — Clean re-harvest: beta.131's fingerprint refresh unknowingly pooled buckets captured during the dual-window low-band analyzer bug (live 2026-09-04, fixed same day as this pass in beta.126) -- re-derived
expected_bands/expected_bands_sigma/vocal_hnr_mu/vocal_fmr_mufor all 13 real profiles +electronic's house-mirror inunicornviz/audio/profiles.py, excluding the corrupted window. Reverteddeep_house's ownbpm_hint_maxwidening (116->128, from an earlier phase of the same tuning session) after it broke house-family tempo ordering and was confirmed live as a top wrong-winner onhouse's own material. Two tests (test_default_enabled_true_for_profiles_that_dont_set_it,test_synthwave_is_registered) updated to assert the actual current disabled-roster set (a later, more specific "disable zero-coverage genres" owner directive superseded the "all enabled" rewrite an earlier same-day pass pinned) instead of "everything enabled." Seedocs/adr/vj-system.md§ "Clean Re-Harvest: the Dual-Window Contamination Window"; auto-vj-01 rc.138 (own commit already pushed). -
1.0.0-beta.131 — Re-derived every pilot-tested genre's fingerprint in
unicornviz/audio/profiles.py(14 profiles) from ALL pooled real corpus per genre, not just the pilot's own 5-track sample:downtempo/rnb/trapupgraded from a guessed tilt to real data;rap_rnb/rnbdecoupled into independent pools;trap/hyphyintentionally now share a source. Caught and avoided a near-miss onelectronic/"Dance," whose vocal-presence control-pair design would have been silently defeated by the crate's own (not actually vocal-free) real reading. Seedocs/adr/vj-system.md§ "All Fingerprints Updated From Pooled Real Corpus". -
1.0.0-beta.130 — Fixed a real bug found during the owner's Phase 5 pilot run:
vaporwave/chillwave/hardsynthnever receivedexpected_bands_sigmawhen their donor (synthwave) got a real one the same session, leaving them on the legacy cosine-similarity fallback path. Confirmed live (hardsynth's share of the recommender's live pick dropped from 25-31% to 2-12% across the same 4 re-run sessions once fixed). Seedocs/adr/vj-system.md§ "Phase 5 Pilot Run" for the full pilot-run findings, including a separate, still-openelectro/peak/dubstepover-broad-matching pattern. -
1.0.0-beta.129 — Phase 5 first pass on
unicornviz/audio/ profiles.py's fingerprint placeholders: real corpus derived forprogressive(15 already-packaged buckets, never used before) andsynthwave(filtered to 7 genuinely synth-titled tracks out of two lists found mixing in unrelated genres); the other 11 placeholder splits with no data of their own get a directional brighten/darken tilt of their donor's real ribbon instead of a verbatim copy;psytrancegains its first realexpected_bands_sigma. Seedocs/adr/vj-system.md§ "Phase 5 First Pass". -
1.0.0-beta.128 — Four new
AudioProfileentries inunicornviz/audio/profiles.py:rnb,trap,peak,progressive-- dark/bright split siblings ofrap_rnb/hyphy/peak_time/deep_houserespectively, each sharing its donor's exact BPM window as a placeholder fingerprint pending real corpus. Three display-name renames (rap_rnb"Rap / R&B" → "Rap",hyphy"Hyphy / Trap" → "Hyphy",peak_time"Hard House" → "Hard") resolve the donors into clean dark/bright pole pairs; dict keys unchanged. Seedocs/adr/vj-system.md§ "Fingerprint-Family Map Formalized". -
1.0.0-beta.127 — Removed
AudioProfile.spectral_centroid_mu/spectral_centroid_sigma(unicornviz/audio/profiles.py) along with auto-vj-01'scentroid_fitrecommender term that read them -- retired at weight 0.0 since 2026-08-20, deleted outright now per owner direction. No user-facing config change (the fields were never exposed inconfig.toml); seedocs/adr/vj-system.md§ "centroid_fitRemoved Entirely" and auto-vj-01's own changelog for the full account. -
1.0.0-beta.126 — Fixed the shared 64-band perceptual spectrum (
audio.bands) feeding two different units into one normalizer: the 2026-09-04 low-band fix substituted long-window FFT magnitudes into the bottom bands without applying the same peak-normalize-and-energy-scale the short path uses, crushing every band above the replacement boundary toward ~0 regardless of real signal (affected Audio Spectrum, Audio Bass Machine, and auto-vj-01'sspectral_shape_fit). Also corrected the replacement count from a hardcoded, wrong 25 to the real, rate-aware collapse count (19 at 48kHz, 18 at 44.1kHz). See docs/adr/vj-system.md "Correction: the Dual-Window Fix Was Feeding Two Different Units Into One Normalizer." -
1.0.0-beta.125 — Windows fullscreen is a borderless window covering the display instead of SDL's fullscreen-desktop flag, so Windows never treats the app as a fullscreen game: Alt+Tab and the Win key no longer flip the display path (TV signal drops, multi-second stalls).
[window] fullscreen_mode = "desktop"opts back in. -
1.0.0-beta.124 — Windows never asks the GL driver for a swap interval above 1: the 30 fps cap's "every second vblank" was serviced by the Intel driver as a ~1 s deferred wait whenever the window was foreground (2 s frames, TV signal drops). Linux keeps the exact vblank-divided cap.
-
1.0.0-beta.123 — Windows hardware-encoder probe: NVENC and Intel QSV only, 6 s ceiling per candidate (20 s elsewhere), so a hung probe on the first record press costs seconds, not most of a minute.
-
1.0.0-beta.122 — The hardware-encoder probe no longer runs at boot unless
[recording] auto_recordis on: its three ffmpeg test encodes share the GPU with the visualizer and, on an Intel iGPU under Windows, held the display at ~1 fps for the first 40 s of every session. VA-API is never probed on Windows or macOS. -
1.0.0-beta.121 — Quit confirmation is an in-app "press again" prompt (3 s window) instead of a native modal dialog, which on Windows froze the render loop under a fullscreen window and could not be reached with a hidden cursor. First launch creates
config.tomlfromconfig.dist.tomlwhen none exists, and rescues aconfig.toml.tomlsaved by Notepad;--self-testreports the config state. -
1.0.0-beta.120 — The SDL window carries the app icon (title bar, taskbar, alt-tab) instead of the bare python icon. Shipped defaults from the first Windows beta run:
config.dist.tomlstarts fullscreen, and color-grade-01 0.10.0 no longer grades at boot. New static testtests/test_core_dropin_independence.pypins the core/drop-in boundary. -
1.0.0-beta.119 — Fix a crash without the multi-head drop-in: the fallback controller's
rebuild_multihead_outputs()demanded two arguments the app never passes, so any display-topology event (monitor hot-plug, DPI change, remote-desktop resize) took the whole app down on a bundle that ships withoutmulti-head-01. -
1.0.0-beta.117 — Two help-icon-rail entries downgraded from fake-live to honest placeholders:
share(unicornviz/overlays.pyHELP_ICON_ENTRIES) no longer openshttps://unicorntears.com— it had no real share flow behind it, just a bare link masquerading as one;dropinsno longer opens the ProjectM preset manager, which is not a drop-ins browser and only confused operators clicking an icon labeled "Drop-ins" expecting drop-in acquisition/config. Both now flash "... coming soon" like the other five rail icons, honestly, until each has a real destination. -
1.0.0-beta.115 — P3 of
docs/planning/control-room-drop-in-integration-plan-2026-09-05.md.OperatorPagegainson_action(mirrorsOperatorPanel.on_action): a tier-2 page's own hotspots now route generically to its own action handler (page_action:<name>:<action>, control-room-01 side) instead of needing new elif branches in the host.Appnow callsmultihead.set_vj_api(vj_api)right after constructing it, so the drop-in can register its own Control Room page the same way everyset_vj_api()-idiom drop-in already does. -
1.0.0-beta.113 — P0 of
docs/planning/control-room-drop-in-integration-plan-2026-09-05.md.operator_panels.STANDARD_PAGES(fx/output/overlays/sources) so drop-ins can target shared Control Room pages without each re-registering an identical page; the "unregister inshutdown()" lifecycle rule documented in the module docstring. The runtime store is now built before multi-head so the monitor editor's persisted display excludes (multihead.monitor_editor.exclude_display_ indices) are handed toMultiHeadControlleron construction -- until now nothing read that key, so "Monitor excludes saved (restart required)" never changed anything (multi-head-01 1.1.0-rc.1 accepts the override). The live multi-head controller is registered as themultiheadsubsystem so drop-ins can reach display state throughvj_apiinstead of app internals. -
1.0.0-beta.112 — Operator-panel registry, P1 of
docs/planning/control-room-panel-registry-plan-2026-09-04.md. Newunicornviz/operator_panels.py(OperatorPanel/OperatorPagedescriptors with declarativePanelContentrows/buttons/meters and an opt-inPanelCanvasdraw tier) and the matchingvj_apiregistry (register_operator_panel()/operator_panels()/operator_panel(),register_operator_page()/operator_pages()/operator_page(), plus unregister). Drop-ins can now claim Control Room space through core instead of being hardcoded there; control-room-01 0.11.0 is the host. -
1.0.0-beta.111 — Four overlay/API fixes.
vj_api.auto_vj_snapshot()now reads the HUD state fromOverlays(it lived there all along; the App-side read always came back empty, so the Control Room's mood/scene/ BPM fields were blank). The HUD and recording indicator show again with[overlays] flash_messages = false(flash_namestill records the effect name, only the flash is skipped). Modals the Control Room cannot mirror (presets, effects browser, config editor, context menu) stay on the audience window while modal routing is on instead of vanishing from both surfaces with keys still captured.Overlays.destroy()now releases the font atlas, text/panel programs+buffers, and the CTA overlay, and a help-icon bucket change on resize releases the previous textures. -
1.0.0-beta.110 — Control Room deck-sim, P2+P3 ("full send" — see
drop-ins/control-room-01/docs/deck-sim-plan.md, now shipped end to end). Newvj_api.register_midi_active_actions()/midi_active_actions(), the companion to beta.108'smidi_action_colors(): a live "which actions are currently active" set so a consumer picks the right color of the idle/active pair without re-deriving pause/fullscreen/postfx/display-mode state itself. Backs midi-controllers-01 0.9.2'sAPCLedFeedback. active_actions()(extracted from the same logic the real LEDs already used) and control-room-01 0.10.0's deck-sim view, which is now the full two-way controller mirror: a header toggle, pad click-to-fire and fader drag through the existingmidi_inject_event()/tweakable_setpaths, and real-input mirroring via a raw MIDI listener. -
1.0.0-beta.108 — Control Room deck-sim, P1 (core surface only — no visible behavior change yet; see
drop-ins/control-room-01/docs/deck-sim-plan.md). Newunicornviz/deck_sim.py(DeckSimLayout/FaderSpec, pure physical- geometry descriptors for a controller-mirror view), a matchingvj_apiregistry (register_deck_sim_layout()/deck_sim_layouts()/active_deck_sim_layout()), a live color-palette pass-through (register_midi_action_colors()/midi_action_colors()), andvj_api.midi_preset_device()(reads the active preset'smeta.deviceback out of core'sBUILTIN_PRESETS, confirmed to survive there —MidiManager.register_preset()'s own type hint only documentsnote_map/cc_map, but the fuller payload rides along unenforced). Pointer: midi-controllers-01 0.9.1 registers the APC mini mk2's actual layout + color provider into this registry at startup. -
1.0.0-beta.107 — Effects-browser favorites (
Fkey): mark up to 16 effects, auto-assigned to the lowest free slot and persisted — data-layer groundwork for a future 1:1 mapping onto a 16-pad MIDI grid (an Akai APC mini's clip-launch pads), exposed throughVJApifor a future midi-controllers-01 pad handler. ProjectM preset HUD labels are nowpM: <name>, trimmed at both ends instead of the front only, so two presets differing only by a trailing "edit1"/"edit2" no longer read identically and the label actually fits the effect banner pane. New[auto_vj] hud_production_mode: one config flag for a live show that hides every "detector internals" readout — the Auto VJ status bar's BPM- confidence, the active profile's recommender score, the
REC PROFline, and (until beta.144 removed that tab) the same fields in the Configuration Editor's "Auto VJ" info tab — regardless of the individual[overlays] hud_show_*settings, without changing them.
- confidence, the active profile's recommender score, the
-
1.0.0-beta.106 — Two new
AudioDatafields, promoted from ad hoc computations that were invisible to effects and the training corpus:zcr(zero-crossing rate of the last 512-sample waveform window -- previously computed inline, from scratch, inside auto-vj-01's own recommender-sample path; now computed once inAnalyzer.process()likevocal_hnr/vocal_fmr) andcomplex_onset_flux(Bello, Duxbury, Davies & Sandler 2004 complex-domain onset detection function, raw per-tick value, feeding auto-vj-01's'dense_complex'env_source-- see that submodule's own1.0.0-rc.122changelog entry for the detector-side landing). Both fields flow throughcopy_audio_data()to every consumer, including corpus rows.zcrin particular closed a real gap: auto-vj-01'szcr_fit/onset_fitrecommender terms had no reconstructable-from-corpus input at all before this landed. -
1.0.0-beta.105 — Low-band resolution fix for the shared 64-band perceptual spectrum (
AudioData.bands, used by every effect and by the recommender's spectral-shape fingerprint matching): the short FFT (1024 samples, 46.875 Hz/bin at 48kHz) cannot resolve the bottom of the 64 log-spaced bands at all — 19 of 64 were collapsing onto a shared FFT bin, reading the exact same value every frame regardless of genre. Fixed with a second, independent 8192-sample FFT (170.7ms window) that replaces only the bottom 25 bands the short path can't resolve — every effect's transient response and every other signal (bass/mid/treble, flux, vocal features) is completely untouched, measured ~677µs totalprocess()cost (long FFT included) against a ~10.7ms real-time budget at 512-sample/48kHz blocks. Also fixes an adjacent bug found while building this:set_sample_rate()updated_bin_hzbut never recomputed the band-edge tables that formula feeds, silently leaving the whole session's band-to-Hz mapping wrong for any device not running at the 48000 Hz construction-time fallback. Also (earlier the same night, folded into this bump):AudioData.zcrandcomplex_onset_fluxfields, corpus/detector-facing additions — seedrop-ins/auto-vj-01/README.md's own changelog for the detector/recommender-side landings these feed. Seedocs/adr/vj-system.md"Low-Band Resolution: Dual-Window Fix" for the full diagnosis, the three options considered, and why this one won. -
1.0.0-beta.103 — Spectral contrast in the analyzer: EMA-smoothed mean log peak/valley gap over 6 octave bands ("peakiness" — harmonic-rich vs dense/noisy, a dimension centroid/zcr/bands never measured; owner-directed, MFCC and danceability deliberately skipped to keep the 64-band fingerprint distinctly ours). Exposed on AudioData; the recommender term ships DORMANT until the library bake-off fits real per-genre values.
-
1.0.0-beta.102 — Mid/side vocal-presence feature: AudioCapture now preserves the stereo side channel alongside the mono downmix, and the analyzer computes vocal_mid_ratio (mid-fraction of vocal-band energy — lead vocals are center-panned mono) plus vocal_syl (its syllable-rate 2-8 Hz modulation), cached on the fmr recompute pattern, invalid-flagged on mono input. Replaces the measurement role of the failed hnr/fmr heuristics (2026-08-31 audit: instrumentals read HIGHER hnr than acapellas). Validated offline at held-out AUC ~0.75 on vocal-mix-vs-instrumental with correct population ordering; recommender weight wiring deliberately NOT included — gated on the field check per the experiment ledger.
-
1.0.0-beta.101 — Detector-internals HUD toggles: the Auto VJ status bar's "BPM: nnn (conf)" readout, the active profile's recommender score, and the REC PROF line now default to hidden for end users ([overlays] hud_show_detector_bpm / hud_show_profile_score / hud_show_reco_profile opt back in; the owner's config enables all three). Owner decision 2026-09-01: the show reads smoother without the internals narrated.
-
1.0.0-beta.100 — dubstep
zcr_mu0.095 → 0.093(unicornviz/audio/profiles.py): owner-approved LLM tuning recommendation, applied by the training session as part of the auto-vj rc.101 batch (see that drop-in's changelog). -
1.0.0-beta.99 — New detector-facing
AudioData.bass_level_rawchannel: log1p of the profile-weighted RAW bass-band mean, read before the per-frame max-normalization (the same lesson flux learned — normalization erases magnitude). Feeds auto-vj-01's new drop trigger/sustain split (audit F1); effects keep reading bass/bass_n unchanged. -
1.0.0-beta.98 — Fixed a runaway onset-strength bug in
unicornviz/audio/analyzer.py:Analyzer._onset_threshold()'s MAD floor was+ 1e-6(a literal-division-by-zero guard, not a reasoned floor) -- during a near-silent/degenerate flux stretch, real MAD collapses toward zero, and the next real transient's strength computation ((flux - threshold) / mad) divides by almost nothing. Caught live via new training-corpus logging: a real session'sonset_strength_max_rawhit 1,171,176,147. Simulated candidate fixes against synthetic pathological/quiet/normal-material scenarios (tools/onset_strength_mad_floor_harness.py) before landing:mad = max(raw_mad, _BEAT_ABS_FLOOR)(reuses the existing threshold floor rather than a fresh arbitrary constant;max()— the same floor idiombeat_grid.pyuses throughout — makes it a true no-op once real MAD already exceeds the floor, unlike the old+-shaped floor, which kept dulling strength even on well-populated material). Landed alongside a defense-in-depth_ONSET_STRENGTH_CAP = 50.0hard clamp, independent of the floor fix, protecting every consumer of raw strength (not justbeat_grid.py's own log-compression). Owner: "look into that mad floor/clamp issue.. what is the most proper? can u sim it up and give us the best running start?" -
1.0.0-beta.97 — Audio Bass Machine tuning pass, from screenshots.
- Subwoofer: the woofer was oversized and its basket rim ran under the tweeters. Driver shrunk and the tweeters pulled clear, so nothing overlaps. Tweeters gained domes and highlights (they read as drivers now, not holes) and fire sparkle bursts on the highs. The cone's individual rings now colour-pulse outward from the dust cap on each bass hit instead of sitting as a flat gradient.
- Boombox: EQ ladder height trimmed, and drips no longer leak past the woofers — the drivers composite on top, but their coverage has thin gaps (between surround and basket rim, and outside the rim) that an unmasked drip bled through, so the drip layer is now masked by the speaker discs.
- Record player: platter, record, label, rim blocks and tonearm all scaled down, and the arm twitches on every beat — a quick angular kick that settles, the way a stylus jumps in a groove, with a flash at the headshell.
-
1.0.0-beta.96 — swept the collapsed sparkle hash out of every remaining effect. Audio Chromogram used the same
hash21(floor(frag * 0.5) + floor(t * 20.0))idiom fixed in beta.95, and three more (tech-01's Threat Matrix and Reactor Breach,vector-01's Laser Tunnel) used the sin-basedhash11(floor(t * ~30))variant, whose argument reaches ~300000 and degradessin()into GPU-dependent mush. Both hashes now wrap their input before hashing. Measured over a 40x40 patch, the sparkle field goes from 1 distinct value at t=10000 to 1234 — it was not merely banded at a typical randomised start time, it was completely dead. Eleven files in total across four repos (games-01 0.13.2, tech-01, vector-01, core); one line each, no visual design changed. -
1.0.0-beta.95 — Audio Bass Machine 2.0, from screenshot review.
- The spectrum bars were upside down. They hung down from the top edge, which reads as an inverted analyser and made both the subwoofer and the record player look flipped. They are now real bars pegged flush to each cabinet's bottom edge and growing upward, with peak caps. Drips are a separate layer again — wet paint with a heavy bead, running down from the top edge, where gravity actually points.
- Colours pulled from the mixer palette (
drop-ins/dj-mixer-01/ui.py): cyan / pink / green / amber / magenta. The old wood-and-olive tints read as mud; cabinets are now dark and neutral so the neon carries the frame. - Boombox refitted — shrunk so the carry handle clears the frame instead of being cropped, woofers pulled outward, and the EQ moved to the full width of the bottom edge, so nothing overlaps anything.
- Subwoofer: the grey pulsing badge is now a neon level trough with a bead dancing along it; ports moved up to flank the driver; feet added so the box can never read upside down.
- Record player: brushed-metal plinth with a lit seam and corner glow instead of a flat slab; a travelling group of rim blocks lights up in colour and chases around with the platter glow; a pitch fader fills the dead space.
- Backdrops react now — the speaker wall's cells and the skyline's towers pump off the spectrum instead of sitting there as static camouflage.
- Fixed the "ASCII dots". The sparkle idiom
hash21(floor(frag * 0.5) + floor(t * 20.0))feeds a term reaching ~12000 into a hash whose first multiply is x123.34, landing past float32's usable mantissa. Measured over a 40x40 patch it degrades from 1197 distinct values at t=0 to 15 at t=600s — a fixed lattice, i.e. the regular dotted grid. Becauseself.timestarts at a random value up to 10000, nearly every instance began in the collapsed regime.hash21now wraps its input, and the field is replaced with shooting laser streaks and rainbow edge drips. The same idiom appears in 8 other effects and is deliberately not touched here.
-
1.0.0-beta.94 — BPM tapper Enter-confirm: pressing
Enterwhile the KP-0 tap readout is live now sends the tapped BPM to the Auto VJ detector as a 30-second elevated-trust window (degrades to a flash message when auto-vj-01 is absent). NewOverlays.bpm_tap_valueproperty; help overlay line added. Also:dubstepaudio profile's BPM hint band widened138-142→70-160from real session data (the genre's two genuine tempo bands; detector-side prior unchanged), and detector-facing analyzer feedback unchanged — see auto-vj-011.0.0-rc.90for the paired drop-in changes. -
1.0.0-beta.93 — New shared track-path hint bus on
App/VJApi(publish_track_path/get_track_path, mirrors the existingpublish_bpm/get_bpmbus exactly): lets a source that knows a real local file for what's playing (dj-mixer-01) publish it for an offline consumer (training-kit-01's packaging step) to run independent analysis (Essentia) against, without either side depending on the other. Also corrects a version-header gap:unicornviz/__init__.pywas never bumped to1.0.0-beta.92when that entry below landed. See docs/adr/vj-system.md. -
1.0.0-beta.92 —
unicornviz/audio/analyzer.py: new publicAnalyzer.refractory_sproperty exposing the BPM-fed onset refractory (set_expected_bpm()'s internal_refractory_s) without a drop-in needing to reach across the module boundary. Added to test a candidate root-cause mechanism for BPM-lock entrenchment flagged bydocs/audits/2026-08-13-bpm-tempo-detection-audit.md(finding T4): a confident-but-wrong BPM estimate can suppress every other true beat at the source, making the onset evidence itself agree with the wrong lock. See docs/adr/vj-system.md. -
1.0.0-beta.91 —
unicornviz/audio/analyzer.py:_ASSUMED_SAMPLE_RATEwas a hardcoded48000constant used directly for the FFT bin→Hz mapping and onset/vocal-envelopedtmath, never reconciled withAudioCapture's actual detected device rate. NewAnalyzer.set_sample_rate(), synced byAudioManagerevery analysis frame (cheap no-op when unchanged) so a mid-session device/fallback switch to a differently-rated device can't go stale. Found auditing a live session for a suspected sample-rate-mismatch bug — that specific incident turned out not to be this, but the mismatch risk itself was real and latent. See docs/adr/vj-system.md. -
1.0.0-beta.90 —
unicornviz/audio/profiles.py: sigma-matches- hint-band pass across all 16 audio profiles —bpm_prior_sigmanow derives from each profile's own hand-dialedbpm_hint_min/maxinstead of being independently authored.electronicre-confirmed enabled (deliberate vocal-presence control pair againsthouse);hardstylebpm_hint_min145→155 (mu 150→160);drum_and_bassbpm_hint168-178→165-180. See auto-vj-01's own changelog (rc.59) and docs/adr/vj-system.md. -
1.0.0-beta.89 —
unicornviz/audio/capture.py's_candidate_monitor_devices()now logs aWARNING(naming the unmatched hint and every visible input device) when a configured--audio-devicehint matches nothing and silently falls back to the system default input. Previously silent — a headless/unattended session (e.g. training-kit-01's daemon) could run an entire session against the wrong device with zero errors in the log. Found live- probing the exact media-01 + training-daemon code path (VLC +PULSE_SINK+ this device-matching function) after a session came back with zero audio energy across every tick. -
1.0.0-beta.88 —
unicornviz/audio/analyzer.py'sOnsetEventgainsband_weight(bass fraction of the onset's flux, 0..1, computed from the already-existingdata.bass_flux) — feeds auto-vj-01's new strength/band-weighted phase coherence (the real fix forphase_confidence's structural cap). Default1.0keeps existing onset consumers unaffected. See auto-vj-01's own changelog and docs/adr/vj-system.md. -
1.0.0-beta.87 — New
--media-favoritesCLI flag: pairs with--media-sourceto boot media-01 into its namedfavoritesplaylist, unshuffled, no repeat, instead of the shuffled full library -- training-daemon--source medianow always uses it. See media-01 0.21.0 and training-kit-01 0.15.9's own changelogs. -
1.0.0-beta.85 — New detector-facing band channel:
unicornviz/audio/analyzer.pygainsAudioData.bass_det/mid_det/treble_det(unicornviz/effects/base.py), computed from the same pre-curve input asbass/mid/treblebut through a separately-tuned_shape()gain (bass2.0vs. the effects-facing6.6; mid/treble unchanged, already well-tuned) -- fixes realbassreadings pegging near ceiling almost always (median0.97-0.98across every director mode, verified against real session data) by giving the detector a channel with actual dynamic range instead of sharing the effects-tuned saturating curve. Effects and existingbass/mid/trebleconsumers are unaffected. Companion change in auto-vj-01 (band_blend's z-score now reads the new channel) -- see that drop-in's own changelog and docs/planning/auto-vj-drop-score-redesign-plan-2026-08-11.md. -
1.0.0-beta.84 — Fixed the
centroid_fitformula-mismatch bug:unicornviz/audio/analyzer.pygains a publicPERC_BAND_CENTERS_HZconstant (geometric-mean center Hz of the 64 log-spaced perceptual bands, matchingtools/gen_spectral_fingerprints.py's own_centers), which the Auto VJ recommender now uses to compute the live spectral centroid overaudio.bandsinstead of the raw linear-FFT bin array -- the same basis every profile'sspectral_centroid_muwas already derived in, closing the structural mismatch. Purely additive to core; no existing analyzer output changed. Companion change in auto-vj-01 -- see that drop-in's own changelog and docs/adr/vj-system.md § "Recommender centroid_fit Weight Cut + tech_house Disabled" (addendum). -
1.0.0-beta.83 —
tech_housedisabled inunicornviz/audio/profiles.py(enabled=False, same disable-not-delete pattern ashyphy), pending a library with enough tech_house-specific material to recalibratespectral_centroid_muagainst a real measured average -- it sat closest of any profile topeak_timeonbpm_prior_muand leaned on a known-buggy centroid signal to break that tie. Still directly resolvable viaget_profile('tech_house'). Companion change in auto-vj-01 (recommendercentroid_fitweight0.8 → 0.5, same open formula-mismatch bug) -- see that drop-in's own changelog and docs/adr/vj-system.md § "Recommender centroid_fit Weight Cut + tech_house Disabled". -
1.0.0-beta.82 —
hardgrooveeliminated entirely fromunicornviz/audio/profiles.py(not disabled -- no dict entry survives): zero validated library examples across every recent session, plus heavy overlap withtech_house/peak_time/hard_technoon BPM, spectral centroid, and an onset-density value identical topeak_time's. Live profile count: 16 (was 20).docs/audio-profile-reference.mdfully regenerated from live code in the same pass -- it had drifted stale, including an outdated claim thatbpm_hinthard-caps the ACF search range (removed 2026-08-04). See docs/adr/vj-system.md. Companion change in auto-vj-01 (_RECOMMENDER_VERSIONbump) -- see that drop-in's own changelog. -
1.0.0-beta.81 —
hyphytightened (bpm_prior_sigma0.20→0.15,spectral_centroid_sigma600→400 Hz) and disabled inunicornviz/audio/profiles.py, pending real trap/hyphy library material -- a real session found the recommender picking it for hip-hop tracks (987x) that should land onrap_rnb, with zero known hyphy/trap tracks in the library to validate any pick against. Still directly resolvable viaget_profile('hyphy'), same disable-not-delete pattern aselectronic/genericbefore. Companion changes in auto-vj-01 (drop_scorebass-gated reweight, mood-profile drop/climax ladder reboot,onset_fitweight bump) -- see that drop-in's own changelog and docs/adr/vj-system.md § "Drop Score Bass-Gated Reweight". -
1.0.0-beta.78 —
uk_garage,breaks, andgenericprofiles eliminated entirely fromunicornviz/audio/profiles.py(not disabled -- no dict entry survives), a same-day follow-on to the house-family consolidation below.get_profile()'s unknown-key fallback moved fromgeneric(now gone) tohouse. Live profile count: 17 (was 20). See docs/adr/vj-system.md § "House-Family Consolidation" addendum. Companion changes in training-kit-01 (dead genre-map entries removed) and auto-vj-01 (_RECOMMENDER_VERSIONbump) -- see those drop-ins' own changelogs. -
1.0.0-beta.77 — House-family consolidation in
unicornviz/audio/profiles.py(multi-session investigation withdj-mixer-01, see docs/adr/vj-system.md § "House-Family Consolidation" for the full account).deep_house/house/tech_houseBPM bands moved from soft/overlapping to adjacent (112-118/118-126/127-134) with tightenedbpm_prior_sigma;electronicrevived and renameddance(re-enabled, same band ashouse, split via vocal-presence terms alone -- the first real use of thevocal_hnr_fit/vocal_fmr_fitcopy-bug fix from the day before);rap_rnbmu set to an explicit owner judgment call rather than fit from a session sample later found unrepresentative;hyphyrelabeled "Hyphy / Trap" with a widened band. New drift-canary test asserts every profile'sbpm_prior_mufalls inside its ownbpm_hint_min/max. Companion change in auto-vj-01 (centroid_fitweight cut) -- see that drop-in's own changelog. -
1.0.0-beta.76 — Two core bugs reported by the owner in another session ("both are the same family as the vocal_hnr/vocal_fmr bug your other session just fixed... uber critical, please address now"): (1)
AudioData.bpmwas dead --Analyzer.process()never assigned it, so every effect read the constructor default (120.0) forever regardless of the real track tempo (Auto VJ never noticed, since it runs its own independent beat tracker and never readsdata.bpmback out). Fixed by wiring the existingset_expected_bpm()feedback hook (Auto VJ already calls it every frame) through todata.bpm, sticky across low-confidence and silent frames. (2)App._fill_audio_scratch()turned out to be a second, independently hand-writtenAudioDatacopy list with the exact same gap_copy_audio_into()had (missingvocal_hnr/vocal_fmr) -- found the same day the first one was fixed. Structural fix this time, not just a second patch: newunicornviz.effects.base.copy_audio_data()is the single source of truth forAudioData's field list; both copy sites now delegate to it. See docs/adr/vj-system.md § "Same-Day Second Occurrence." -
1.0.0-beta.74 — Selecting the DJ controller as an audio source no longer kills the app. The mixer opens that interface directly at real-time priority, bypassing PipeWire; opening the same hardware again for capture does not raise — it takes the process down inside the native audio stack, leaving no exception, no traceback and an empty faulthandler dump, with the log ending mid-line after
Audio: opening stream. Since it cannot be caught, it is now refused: any controller exposingowned_audio_device_names()contributes to a claimed-device set (convention only — core imports no drop-in), and claimed devices are rejected for manual selection, for auto-fallback, and in the selector's viability flags. Matching is by distinctive token rather than substring, because the layers name the same box differently and share no substring at all:DDJ-REV1: USB AudiovsDDJ-REV1 Analog Surround 4.0. Also explains the same session's other symptom — the monitor really was silent (rms=0.00000), so auto-selection was right to refuse it. -
1.0.0-beta.73 —
tech_house.spectral_centroid_mu(unicornviz/audio/profiles.py) adjusted2550 → 2900, an LLM tuning recommendation from a real training session (observed 2910.5). Checked for overlap before applying: increases tech_house's distance fromhouse's own mu (100 → 250), the exact profile pair behind that session's #1 confusion (Tech House → house, 1060x). Part of a larger director/detector/recommender refinement batch landed the same day in auto-vj-01 and training-kit-01 -- see docs/adr/vj-system.md § "Director/Detector/Recommender Refinement Batch" and docs/planning/auto-vj-director-detector-refinement-plan-2026-08-09.md. -
1.0.0-beta.72 —
spectral_centroid_murecalibrated for all 20 audio profiles (unicornviz/audio/profiles.py) -- was independently hand/LLM-authored and disagreed with each profile's ownexpected_bandsfingerprint by up to 1.9x, the real premise behind a spectral-centroid recommender term dominating composite scores in a live auto-vj-01 session. Now derived directly fromexpected_bands(same weighted-mean-frequency formula the live recommender uses), so the two "brightness" representations can't disagree by construction. See docs/adr/vj-system.md § "spectral_centroid_muRecalibrated" (auto-vj-01). -
1.0.0-beta.71 — Config Editor honors an optional per-row
stepin drop-in settings rows, making 0/1 toggles (webcam-01 1.1.2 selfie-seg) actually flippable — the default (max-min)/40 notch was floored back to 0 by quantizing setters on every nudge. The BPM tapper readout now repeats on every mirror tile, not just the resolved primary — the field no-show was the window moving displays one second before the taps, relocating the primary overlay tile away from the watched head. -
1.0.0-beta.70 — New public
AudioManager.sample_rateproperty (delegates toAudioCapture.sample_rate) so consumers computing a frequency axis from FFT bin indices can derive Nyquist from the real capture rate instead of assuming a fixed one -- auto-vj-01's spectral centroid calc was assuming 22050 Hz (44.1kHz) regardless of what the device/PipeWire actually negotiates, understating readings ~8.8% against this project's own documented 48kHz default. See docs/adr/vj-system.md § "Live-Session Follow-Up" (auto-vj-01). -
1.0.0-beta.69 — BPM tapper hardening after a field no-show: the readout now renders on the audience window even while the control room mirrors the HUD/modals (recording-indicator parity — the modal gate previously hid it entirely), uses real glyph metrics so it sits flush right instead of drifting, is a third bigger, and logs 'BPM tapper: tap sequence started' so a missing readout is diagnosable from the log alone.
-
1.0.0-beta.68 — Fixed:
vocal_hnr/vocal_fmraudio fields were always zero.AudioManager._copy_audio_into()(the hand-written field-by-field copy that publishes the analysis thread's snapshot to the main-thread audio buffers) never copied these two fields -- added after the copy function was written, never added to it. Any effect or drop-in readingaudio.vocal_hnr/audio.vocal_fmr(not just Auto VJ's recommender, where this was found) has been silently getting0.0regardless of what's actually playing. New regression test enumerates everyAudioDataslot dynamically so a future field added without a matching copy line fails loudly instead of the same way these two did. -
1.0.0-beta.67 — KP 0 is a BPM tapper: tap along on keypad-zero and a cyan readout appears in the top-right corner with the tapped tempo (averaged over up to 8 consecutive intervals; a 2.5 s pause starts a fresh measurement). The readout shows only while tapping (lingers 3 s) and drops below the recording indicator when that occupies the corner. Also removed the COMPOSE debug line from the HUD tweakables block.
-
1.0.0-beta.66 — Headless runs record training data by default.
--dj-mixer-sourceand--media-sourcenow imply--training;--no-trainingopts out. A headless source is a training run — that is what the flag group exists for — and requiring a separate opt-in failed silently: the session looked healthy, recorded video and audio correctly, and produced no training data at all, which is only discoverable once the set is over and no longer repeatable. The group description, both source flags and the configuration reference all now state it, anddocs/configuration.mdgains a Headless / training runs section (which also folds away a duplicate[render]heading). -
1.0.0-beta.65 —
--training: enable Auto VJ training capture for a run. There was no way to do this from the command line, so an unattended run recorded zero training data unless a human pressed the in-app toggle — which is precisely what headless operation is supposed to remove. The flag turns on all three streams together (decision log, live corpus, sequence corpus), because a decision log without the corpora cannot be scored and a corpus without decisions cannot be explained; partial capture is what makes a run unusable after the fact. -
1.0.0-beta.64 —
--dj-mixer-setand--dj-mixer-start-delay, pairing with dj-mixer-01 0.166.0: headless autoplay can now be told which set to play, ejects both decks first, and waits (default 3s) before the first track so audio, recording and deck loads settle. Previously it armed a mode but selected nothing, so an unattended run played a random walk of the whole library — watchable, but not a repeatable basis for training. Also pairs with immersive-01 0.10.1, which removes the deadiMiduniform that made Tunnel crash on its first frame and get quarantined mid-session. -
1.0.0-beta.63 — Stop killing ffmpeg while it is still writing the file.
-movflags +faststartmoves the index to the front of the MP4, which means rewriting the entire file once recording stops. A long set is many gigabytes, so that pass legitimately takes tens of seconds — but stop() allowed a fixed 10 s, then SIGINT, then SIGKILL after 10 s more. On a ~13 GB capture (2 hours at the measured rate) the rewrite needs roughly 15-30 s, so the safeguard would have destroyed the very recording it was closing, and the longer the set the likelier it got. Finalizing is now judged by progress, not elapsed time: while the output file keeps changing ffmpeg is working and is left alone; escalation happens only when it goes quiet for 20 s, with a 10-minute absolute ceiling. Found by asking what happens at the end of a live 2-hour headless run, before it got there. -
1.0.0-beta.62 — CLI flags for the two settings added this week:
--record-codec(auto / libx264 / h264_vaapi / …) and--fps-limit(0 follows the display vsync). Recording already had--record/--no-record,--record-audio,--record-dir,--record-fps,--record-crf,--ffmpeg-pathand--record-audio-device, so a headless capture no longer needsconfig.tomltouched at all — as does the mixer, via the existing--dj-mixer-autoplay-mode {autoload,cut,crossfade,smart}. -
1.0.0-beta.61 — Hardware video encoding for recordings.
[recording] codecnow defaults to"auto", which probes for a working H.264 encoder (NVENC, then VA-API, then QSV) and falls back to software x264 when none is usable. The probe encodes a real frame rather than trustingffmpeg -encoders: being built in says nothing about whether it runs here — Fedora's stock VA-API driver reportsNo usable encoding profile foundonly when you actually try. Each backend gets the command shape it needs: VA-API opens its device before any input and has the frame uploaded after the flip (vflip,format=nv12,hwupload), and-crf/-presetare dropped for hardware encoders that reject them. If a hardware encoder passes the probe but still fails to spawn at full resolution, the recorder retries once in software — losing the acceleration is better than losing the recording. Verified end-to-end on Intel Iris Xe: both paths produce valid, correct-length files. -
1.0.0-beta.60 — Render frame cap, defaulting to a locked 30. New
[render] fps_limit, also on the config editor's Visuals tab (display/24/30/60). Measured on the owner's rig, frames were landing at ~35 ms in both single and mirror modes — past the 60 Hz vblank, so the loop missed it and took the next one anyway, but unevenly: the effective rate was already ~30 with jitter on top, and jitter is what reads as judder. Requesting every second vblank up front makes that cadence exact and tear-free, and leaves GPU headroom for a capture tool on the same adapter. Implemented as a swap interval rather than a sleep loop, since a timer drifts against the vblank and reintroduces the very judder this removes; caps that do not divide the refresh rate evenly round to the nearest interval that does and say so in the log. -
1.0.0-beta.59 — Recording fps is now a named choice in the config editor (24/25/30/48/50/60) instead of a free 15-120 slider — the rate has to land on something an editor will accept, and the row shows the full list with the active one bracketed. A value set by hand in
config.tomlis left alone; the row just snaps to the nearest choice for display. Documents hardware video encoding in the configuration reference: Fedora'slibva-intel-media-driverships without H.264 encode, which is why VA-API probes reportNo usable encoding profile found— RPM Fusion'sintel-media-driverreplaces it and needs no reboot, only an application restart. Paired with projectm-01 0.10.3, which moves the preset-catalog rebuild off the render thread entirely. -
1.0.0-beta.58 — Third-party attribution now ships with every bundle. MIT, BSD and Apache dependencies all require their copyright notices to travel with any binary distribution, and none of the three packaging targets carried them.
tools/gen_third_party_licenses.pygeneratesTHIRD_PARTY_LICENSES.mdfrom the validated virtualenv — the full license text of all 16 runtime packages, including the twelve bundled-library notices insidepysdl2-dll— and the Flatpak, Snap and Windows installer each install it alongsideLICENSE. The Windows installer also stops sweeping the entire working tree into the package: restricted third-party assets (NVIDIA USD packs, MilkDrop presets), operator secrets and runtime state (.env,runtime/,logs/,recordings/) and build junk are now excluded, and setup shows the MIT license. ANSi art from ACiD Productions is credited to its individual artists inassets/ansi/README.md, generated from the SAUCE records. Full review indocs/audits/2026-08-08-licensing-audit.md. -
1.0.0-beta.58 — Quitting no longer core-dumps.
AudioManager.stop()calledsounddevice.terminate(), which does not exist — the real name is_terminate— so the deliberate PortAudio teardown raised into its ownexcepton every single shutdown and never ran. Python's atexit then ran it during interpreter exit instead, at which point a stream whose close had already timed out made PortAudio's JACK host API assert and abort (pa_jack.c:869: Terminate: Assertion 'err == 0' failed). Teardown now uses the correct name, and runs only when the capture closed cleanly; otherwise it unregisters the atexit hook and lets the OS reclaim the device, because terminating with a stream still open is worse than not terminating at all. Also: audio selector labels no longer overflow the panel — long device names are trimmed from the middle, always preserving the last few characters, since 'DDJ-REV1 Analog Surround 4.0' and 'DDJ-REV1 Analog Stereo' differ only at the tail. Paired with projectm-01 0.10.2, which removes an 11-second freeze when trashing a preset. -
1.0.0-beta.57 — Auto-selection ranks outputs ahead of microphones. The Linux tiers identify a monitor by looking for
monitorin the device name and by hostapi. PipeWire endpoints reached through the JACK hostapi satisfy neither — they arrive as plain descriptions likeDDJ-REV1 Analog Surround 4.0— so on such a machine every candidate fell into one tier and the order collapsed to USB enumeration order. Measured on the owner's rig, that put the DJ controller's output 7th, below five microphones, and booted the app onto a headphone-output monitor that is silent unless something plays to it; the silence fallback then walked straight into a webcam mic. Ranking now applies the same pactl-backed output/input classification added in beta.56, so every output precedes every input while the order inside each group is untouched. On the owner's rig the controller's output moves from 7th to 2nd. Where nothing can be classified (no pactl, no name hints) the previous order is preserved exactly. -
1.0.0-beta.56 — Selecting an audio source can no longer kill the app. Picking a webcam microphone from the Alt+A selector ended the process with
Fatal Python error: Aborted— a heap-corruption abort surfacing from the reader thread's numpy math, with no Python exception to catch and no clue at the abort site. Cause: the switch replaced the stream while the reader could still be insidestream.read()on it._stop_reader_thread's own docstring already said callers must not do this; the switch path logged a warning and did it anyway. Three fixes: the switch is now refused when the reader has not exited, a reader thread binds its stream once instead of re-readingself._streameach iteration (it could pick up a stream that was never its own), and each block is copied before any arithmetic so numpy never runs over PortAudio-owned memory. The selector is also no longer a minefield: real inputs (microphones, line-ins) are non-viable by default — only outputs being monitored are enabled — non-viable rows can no longer be activated by Return or reached by cycling, and the menu now lists outputs first, then a non-selectable divider, then inputs, each group sorted alphabetically. Classification uses pactl's sink table, since PipeWire endpoints reach sounddevice by description with no hint of their role. Auto-selection ranking is untouched. -
1.0.0-beta.55 — Keep shader compiles out of scene crossfades. New
BaseEffect.transition_active, set by the app on both the outgoing and incoming effect for the duration of a blend. During a crossfade two effects render every frame, so it is the worst moment for an effect to do expensive deferrable work — and ProjectM's preset advance callsprojectm_load_preset_file, which compiles the preset's shaders synchronously on the render thread. Paired with projectm-01 0.10.1, which holds its advance until the blend finishes (its timer keeps running, so the switch lands a second or two later at most). Most effects ignore the flag and are unaffected. -
1.0.0-beta.54 — Recording actually produces a usable file. Three faults, each independently enough to ruin a capture. (1) Silent audio: the recorder always captured the default sink's monitor, so a set playing through a DJ controller recorded a valid, entirely empty audio stream (measured at -91 dB — digital silence — in the owner's own captures). It now follows the source the visualizer is analyzing, falling back to an output that is actually playing, and only then to the default; verified end-to-end at -21 dB from a non-default sink. (2) Wrong playback speed: video was muxed as constant-rate while frames arrived at the variable render rate, so a loop running at 26 fps produced a file playing 2.3x too fast and drifting away from its real-time audio. A wallclock-paced writer now holds the declared rate, duplicating the newest frame when the render loop falls behind — a 5.01 s capture fed at 12 fps yields a 5.03 s file where the old path gave 2.00 s. (3) Invisible failures: ffmpeg's stderr went to
DEVNULLand liveness never checkedpoll(), so a dead encoder kept REC lit while every frame was dropped; both are now surfaced with ffmpeg's own explanation. Recording readback is also capped at the muxed frame rate (it previously forced a full-resolution ~25 MB GPU→CPU transfer on every rendered frame), and a new Recording tab in the config editor exposes fps, quality, preset, audio capture, and an audio-source picker. -
1.0.0-beta.53 — Performance remediation, measured (see
docs/planning/performance-remediation-plan-2026-08-08.md). The secondary-window present guard no longer starves the mixer: it degrades to every other frame instead of one in four, and its threshold derives from the real display refresh rather than a hardcoded 60 Hz — on a 30 Hz output the old figure sat below the display's own frame interval, so the guard throttled the mixer permanently by construction. The per-frame perf instrumentation is gated on a real[logging] perf_frameskey instead ofisEnabledFor(DEBUG), which was always True at INFO because the log bands filter at the handlers — it ran in every session forever.[audio] latencynow defaults to low (beta.38 only changed a fallback that never fired) and is selectable from the config editor. Paired with webcam-01 1.0.0-rc.4, which stops re-uploading unchanged camera frames — the measured top of the profile. -
1.0.0-beta.52 — video-out-01 0.4.1 fixes the
buffer overflowwarning seen on a live run: the v4l2 format struct was 4 bytes short of what the ioctl encodes (Python 3.14 caught it, preventing a kernel write past the buffer), and the resultingSystemErrorslipped past anexcept OSErrorso the device fd leaked and stayed held open — which is why retries then reported the device busy. -
1.0.0-beta.51 — Webcam drop-in restored (webcam-01 1.0.0-rc.3): the rc.2 privacy change made
__init__reach the capture worker before it existed, and the resultingAttributeErrorwas swallowed by the guarded loader — silently disabling the entire webcam subsystem. Core now logs that failure with a full traceback and says plainly that the subsystem is DISABLED, rather than emitting a bare one-line message. video-out-01 0.4.0 makes its own failures visible too: the toggle reports whether publishing actually started instead of always claiming success. -
1.0.0-beta.50 — Mixer-only boot got ~0.7s faster: the help-entry and tour-slide discovery scans (which import every drop-in module they visit — ~1.5s cold across the whole repo) are now scoped by the boot profile, so the mixer profile only scans the drop-ins it can actually load. Normal boot's scan is unchanged.
-
1.0.0-beta.49 — video-out-01 0.3.0: libfunnel now builds locally, without sudo (
drop-ins/video-out-01/install.sh) into a gitignoredvendor/at a pinned commit, and the drop-in prefers it over any system copy — v4l2loopback keeps its own sudo-requiring script since a kernel module has no app-local equivalent. Building it for real found five binding bugs the header alone hid, including EGL symbols living in a second shared object, a dequeue return code that would have discarded every frame, a buffering mode that blocks the render thread, and — the dangerous one — libfunnelassert()ing on misordered calls, which aborts the process uncatchably. Stream setup is now verified against a live PipeWire daemon; the EGL import and GPU blit still need a consumer attached. -
1.0.0-beta.48 — Zero-copy video out. video-out-01 0.2.0 adds the PipeWire/DMA-BUF backend — the real Linux equivalent of Spout/Syphon — and core now hands it the GL framebuffer directly each frame. Nothing is read back to the CPU, so unlike the v4l2 path it costs no frame-tap subscription at all; OBS consumes it via the obs-pwvideo plugin, GStreamer via
pipewiresrc. Needs a Wayland session and a locally-built libfunnel, and fails closed on either. Unverified against real hardware — v4l2loopback remains the dependable path until someone has watched this one work. -
1.0.0-beta.47 — The show can leave the window. New
video-out-01drop-in publishes the output as a virtual V4L2 camera (v4l2loopback), so OBS takes it as an ordinary Video Capture Device with no plugin — closing the worst row on the competitive scorecard, where every rival except projectM could already hand its output to another app and we could not. It rides the frame tap, so with recording or streaming already live it costs no extra GPU readback at all. Opt-in on both switches; a missing kernel module logs once and disables. First of four planned backends (PipeWire/DMA-BUF, NDI, Spout/Syphon to follow). Screenshots no longer freeze the show: the PNG encode and disk write moved to a daemon thread (that was the half-second stall, not the readback), pixels come from the frame tap when one is already in hand, and the capture switched offctx.screen.read()— the default-framebuffer path moderngl gets wrong and which the rest of the codebase already routes around. -
1.0.0-beta.46 — One GPU readback per frame instead of one per consumer. Recording and RTMP streaming each pulled the identical full-resolution frame off the GPU independently — with both live that was two transfers of the same picture every frame (~12 MB/frame at 1080p), and recording's was the synchronous path the 2026-08-03 audit flagged as a frame-budget problem. A new
unicornviz.frame_tap.FrameTapnow decides who is due (per-consumer rate caps included), the loop reads back once, and everyone due shares that buffer; the subsystem preview keeps reusing it when present. Recording moves onto the double-buffered PBO path as a side effect, which also fixes a latent size mismatch — it was fed drawable-sized bytes while ffmpeg was told the tracked canvas size, which differ under mixed-DPI scaling and mirror/span. Groundwork for the video-interop outputs (v4l2loopback, PipeWire/DMA-BUF, NDI), which would otherwise have made it four or five readbacks per frame. -
1.0.0-beta.45 —
--dj-mixer-source/--media-sourcenow automatically set[auto_vj] auto_exit_after_finale = true(_build_overrides(),__main__.py) -- a headless source is a headless run, so no separate flag orconfig.tomledit is needed to get the auto-exit behavior shipped in beta.44. The config key itself is unchanged and still available directly for non-headless-source use (e.g. a Spotify session with a configuredshow_duration_min). -
1.0.0-beta.44 — Closes
docs/planning/headless-auto-exit-plan-2026-08-07.md: newVjApi.grand_finale_activeproperty exposes the grand-finale sequence's active/idle completion edge, letting a consumer (auto-vj-01's newauto_exit_after_finale) request an unattended app exit once a timed finale actually finishes, instead of reaching intoapp._grand_finaledirectly. See docs/adr/vj-system.md § "Auto-Exit on Set End: the Grand-Finale Completion Watcher". -
1.0.0-beta.42 — Mixer-only mode (P1 of the mixer-only plan):
[dj_mixer] mixer_only = true, the--mixerflag, or the newunicorn-mixentrypoint boots straight into the DJ mixer console — no splash (new[splash] enabledgate, honored in normal mode too), no audio capture, no effect discovery, no visual drop-ins; only[dj_mixer] mixer_allow-listed drop-ins load. The mixer window auto-opens, its title reads "Unicorn Mix", help shows only the sections that still apply, and closing the console quits the app. Normal boot is unchanged (default off). Hosted single-window mode (P2) remains with the mixer team. -
1.0.0-beta.41 —
unicornviz/__main__.pygains--dj-mixer-source/--media-sourceCLI flags (plus--dj-mixer-autoplay-mode/--dj-mixer-music-dir/--dj-mixer-output-device/--media-dir) that force-enable and configure dj-mixer-01/media-01 for a single run via the existingConfig(overrides=...)mechanism, mirroring--record/--no-record's existing shape. NewApp._maybe_auto_play_media()boot trigger for[media] auto_play. Built sotools/training_daemon.py(training-kit-01) can drive dj-mixer-01/media-01 as fully unattended headless training sources instead of only Spotify -- seedrop-ins/training-kit-01/docs/headless-training.mdand docs/adr/vj-system.md § "Headless Training: dj-mixer-01/media-01 as Audio Sources". -
1.0.0-beta.40 — New
App/VjApicapability:pin_effect_pair()/cut_to_pinned_effect()/unpin_effect_pair()-- instantiate two effects once and hard-cut between them (a pointer swap) instead of a full instantiate+destroy transition on every switch. Built for auto-vj-01's effect ping-pong, which alternates between the same two effects for its whole run and was paying that cost on every single swap._switch_effect()defensively releases a pinned pair left behind by an interrupting normal transition (e.g. a manual "next effect" hotkey), so the off-screen instance can't leak. See docs/adr/vj-system.md § "Effect Ping-Pong Hard-Cuts Between Two Pinned Instances". -
1.0.0-beta.39 —
unicornviz/now_playing.py's now-playing snapshot contract gains an optionalgenrekey (dj-mixer-01 0.158.0 is the first source to populate it, from the loaded track's ID3 tag) -- feeds auto-vj-01's new Tier 2 recommender accuracy tracking (training-kit-01 0.10.0). See docs/adr/vj-system.md § "Tier 2: Genre-Tag Ground-Truth Accuracy Tracking". -
1.0.0-beta.38 — Audio stream latency now defaults to
"low"(was"high") everywhere in code: the[audio] latencyconfig default, the capture constructor, and the unsupported-value fallback. An explicitlatencyin config.toml still wins. -
1.0.0-beta.37 — ProjectM now respects being disabled in the effects browser. Auto VJ's projectM-affinity path calls
goto_effect('ProjectM Presets')directly, and a direct goto bypasses the playlist's disabled set (which only gates auto-rotation), so switching ProjectM off in the browser did not stop the director from pulling it straight back up.VJApi.projectm_available()now carries the enablement check as well as the registration check, and a newVJApi.effect_enabled()exposes the operator disable state to drop-ins through the public surface. The boot-time preset prescan (a ~10k-file disk walk) and the GL bridge warm-up are also gated on it, so a disabled ProjectM no longer pays either cost at startup. -
1.0.0-beta.36 — New
publish_session()/get_session()bus onApp/VjApi(plan §6.3), mirroringpublish_section()/get_section()exactly. Lets a source that knows when the set ends (dj-mixer-01's set clock, 0.152.0) publish set-level phase/timing and grand-finale cue data for a finale-aware consumer to read. Mixer side was already written and guarded, so it lit up with no further coordination. Consumer side (auto-vj-01 arming the finale off it) is not part of this change -- the channel exists, nothing reads it yet. See docs/adr/vj-system.md § "Set-Clock Hint Bus". -
1.0.0-beta.35 — The Now Spinning platter no longer flickers between tracks during DJ crossfades: it switches only after the new track has been reported continuously for
[now_spinning] switch_hold_s(default 5 s), so dominance flapping mid-fade never commits and a real change shows once. -
1.0.0-beta.34 —
rapandr&baudio profiles merged intorap_rnb(owner call: genuine siblings, 0.9856 cosine similarity, 3 BPM apart -- not a false catch-all pairing).hyphyandchillstepgot freshly-regeneratedexpected_bands(viatools/gen_spectral_fingerprints.py, scoped LLM rerun) targeting their real acoustic differences -- similarity improved 0.9788 → 0.9703 (real but modest gain; see the ADR for the honest caveat on a structural ceiling in cosine-similarity discrimination across this genre cluster). Drop-in pointer: auto-vj-01 1.0.0-rc.17 (_VJ_WEIGHTS_DOC_VERSIONbump only). See docs/adr/vj-system.md § "rap/r&bMerged;hyphy/chillstepFingerprints Regenerated". -
1.0.0-beta.33 —
electronicaudio profile disabled (owner call, confirmed by a cosine-similarity audit against the whole catalog): its spectral fingerprint was more similar to far-tempo profiles (trance,hyphy,psytrance) than to its own close-tempo neighbors, and its BPM range was already fully covered byhouse/tech_house/deep_house/peak_time-- a non-discriminating catch-all, not a genre with a real identity. Same disable-not-delete pattern asgeneric: excluded fromenabled_profiles()/Alt+Acycling, still resolvable by direct key lookup.hyphyandchillstepflagged with the same audit finding but not yet acted on;rap/r&bchecked and found borderline (real sibling relationship, not the same case). See docs/adr/vj-system.md § "Wide-Tier Catch-All Profiles:electronicDisabled". -
1.0.0-beta.32 —
fire_djaudio profile removed entirely fromunicornviz/audio/profiles.py(owner call). Its recommender-driven Fire DJ celebration trigger is replaced by a direct, profile-independent easter egg: fires when the DJ spans a wide BPM range within a rolling window. See docs/adr/vj-system.md § "Fire DJ Profile Removed, Replaced by a Wide-BPM-Range Easter Egg". Drop-in pointer: auto-vj-01 1.0.0-rc.16. -
1.0.0-beta.31 —
[audio] profiledefault reverted"generic"→"house"(owner call, live during a training session):"generic"isenabled=FalseinPROFILESon purpose -- a disabled, loose/uninformative catch-all never meant to be a real starting analyzer profile, and it made for a noticeably weaker start to training sessions specifically. Reverts the1.0.0-beta.26default across all three sites (config.py'sDEFAULT_CONFIG,AudioManager.__init__'s kwarg default,Analyzer.__init__'s fallback) plus the HUD's stale-fallback string and both config file comments. Noconfig.tomlsetting added -- this is a code-default-only fix. -
1.0.0-beta.30 — Per-profile
spectral_centroid_sigmaadded tounicornviz/audio/profiles.py(tight/medium/wide tiers by genre feel) so the recommender'scentroid_fitdiscriminates per genre instead of using one fixed 400 Hz sigma for all 22 profiles. Ships Tier 1 of the recommender accuracy-tracking spec (docs/planning/auto-vj-recommender-accuracy-tracking-2026-08-06.md) -- per-term signal-activity logging rolled up insession_scorecard.py. Drop-in pointer: auto-vj-01 1.0.0-rc.14. -
1.0.0-beta.29 — Recommender weight review:
breaks/rap/synthwavebpm_prior_sigmatightened (0.28/0.30/0.40→0.22/0.24/0.34;fire_djintentionally left at0.32-- its wide tempo span is by design, see drop-ins/auto-vj-01/docs/weights-and-thresholds.md). New versioned reference doc covers every detector/director/recommender weight and threshold with a glossary up front, kept in sync with_VJ_WEIGHTS_DOC_VERSIONinauto_vj.pyper a new CLAUDE.md agent rule ("VJ Weights & Thresholds Documentation"). Also adds a spec for recommender accuracy tracking (docs/planning/auto-vj-recommender-accuracy-tracking-2026-08-06.md) covering a term-discrimination proxy and a future tag-genre-as-ground-truth tier. Drop-in pointer: auto-vj-01 1.0.0-rc.13 (centroid_fitweight raise). -
1.0.0-beta.28 — New shared song-structure hint bus (
App/VjApipublish_section()/get_section(), mirrors the existing BPM bus exactly -- 5s TTL, validated against the canonicalHOLD/RISE/PEAK/FALL/CLOSErole set) so a source that has pre-analyzed a whole track (dj-mixer-01's new section detector) can tell auto-vj-01's director where the playhead is in the song's phrase structure. See docs/planning/auto-vj-phrase-structure-plan-2026-08-05.md §6 for the full contract and docs/adr/vj-system.md for the implementation record. Drop-in pointer: auto-vj-01 1.0.0-rc.11 (the consumer side). -
1.0.0-beta.27 — HUD/Monitor tab surfaces the active audio profile's own recommender score (
audio_profile_score) alongside the existing recommendation, in the overlay'sBPM PROFline, theAuto VJMonitor tab's new "Profile score" row, andvj_api.auto_vj_snapshot()for control-room/operator surfaces -- previously only the alternate ("what would the recommender switch to") was visible, not what the current pick's own score is. Also fixed the overlay's stale'house'fallback default (now'generic', matching1.0.0-beta.26). -
1.0.0-beta.26 —
[audio] profilenow defaults to"generic"(a wide/uninformative prior) instead of a hardcoded"house"-- seeding a real genre's tight prior at startup meant a mismatched profile had to be actively argued out of by auto-vj-01's recommender, and the documented workaround (manually set/restore[audio] profileper session) was easy to forget. Mirrors how the BPM detector itself starts with no lock rather than a seeded guess; the recommender converges onto the real genre from evidence within its first eval cycle regardless.config.full.example.toml's misplacedprofileexample (previously under[midi], a documentation bug) moved to[audio]and updated. -
1.0.0-beta.25 — event-driven display-state refresh (July item 5, finally):
SDL_WINDOWEVENT_MOVED/DISPLAY_CHANGED/FOCUS_GAINEDnow re-derive the display index and window origin from live SDL state, validity-gated against known display layouts and throttled — fixes stale overlays/tiling after workspace switches and monitor drags (needs owner multi-monitor verification). Shipstools/install/windows_deps.ps1(the field-notes bridge installer) and CLAUDE.md platform/font-asset updates. Drop-in pointer bumps: videos-01 0.7.1 (EOF sentinel leak, no-audio clock, observable reached_bottom), images-01 0.8.1 (cache caps), tech-01 0.10.0 (three precision-dead layers restored + glyph dirty-flag). -
1.0.0-beta.24 —
genericaudio profile'sbpm_hint_min/bpm_hint_maxremoved -- it's a disabled catch-all fallback (not a genre) and those fields are no longer read by any BPM tracker engine as of auto-vj-01 1.0.0-rc.7's BPM detector audit fixes. -
1.0.0-beta.23 — RC1 P1 stability/hygiene batch: a dead ffmpeg no longer leaves REC lit silently dropping frames (writer-thread failure latch, flashed once + recorder stopped); FBO rebuilds release their color/depth attachments (
_release_fbo) instead of leaking ~16 MB per render-scale change; all six implemented transitions +radial/cutaliases now pass config/CLI validation;requirements.txtpinned to the validated versions (floors stay in pyproject); push/PR CI now runs ruff + the core suite (.github/workflows/tests.yml); drop-in registry gains its 4 missing entries. Drop-in pointer bumps: chat-01 0.5.1 (inbound-text-inert tests), spotify-01 1.0.0-rc.3 (429/Retry-After + 0600 token store), webcam-01 1.0.0-rc.2 (no camera while hidden), tech-01 0.9.1 (cyber_war seed fix). -
1.0.0-beta.22 — New
VjApi.active_now_playing()wraps the existing now-playing hub (unicornviz.now_playing.NowPlayingHub.active()) with a read accessor -- the hub already had a write side (register_now_playing/unregister_now_playing) but nothing to read the currently-active source back, so drop-ins had to reach intoapp._privatestate to know what's playing. Degrades toNoneon older cores/when nothing is registered. -
1.0.0-beta.21 — Windows platform P0s: new shared
unicornviz.fontsresolver (bundled ui-font.ttf first, then per-platform system dirs incl.C:\Windows\Fontsandseguiemj.ttffor emoji) adopted across core and the control-room/media/banner drop-ins; per-monitor-v2 DPI awareness on Windows (crisp output on scaled displays, correct 76/152px icon bucket); recording remaps the Linuxpulsedefault to DirectShow with loopback-device discovery on Windows (video-only degrade when none) and stops ffmpeg via CTRL_BREAK_EVENT + kill escalation instead of the Windows-unsupported SIGINT; pyproject version now single-sourced fromunicornviz.__version__with the four missing runtime deps added. -
1.0.0-beta.20 — New
deep_houseaudio profile (118-124 BPM, warmer/lower-centroid thanhouse, chord-stab-driven fingerprint) -- the house family previously only hadhouse/tech_house, leaving the slower/warmer end uncovered.AudioProfilegains a capability-awareenabledflag (disable, don't delete -- still resolvable by direct lookup);genericis now disabled from discovery (Alt+Acycling and the Auto VJ recommender's candidate pool) since it was competing with, and getting confused with, genuinely calibrated genre profiles. -
1.0.0-beta.19 — Crash containment: a broken effect (constructor,
update(), orrender()) is quarantined and the show advances instead of dying; hotkey handler crashes drop the keypress instead of the app; teardown now runs even when the main loop raises (ensure_shutdown()), so audio/MIDI/recorder threads and SDL are never left dangling. -
1.0.0-beta.18 — New
synthwaveaudio profile (unicornviz/audio/profiles.py): warm analog bass, gated-reverb drums, and a bright 1.4-2.4 kHz lead-synth peak at 85-118 BPM, selectable viaAlt+Alike any other genre profile. First-pass, added after a livestream synthwave training session ran undergenericall night and sagged into psytrance/trance whenever detected BPM ran hot -- seedocs/adr/training-model.md. -
1.0.0-beta.17 — Tour polish: the dim scrim now hugs the dialog like every other modal (was a floating 60px skirt), and drop-in slides slot in alphabetically before the core good-bye slide so "Have a great show!" always closes the tour.
-
1.0.0-beta.16 — Fix: a silent audio source collapsed the frame rate. The capture auto-fallback probes a candidate device by spawning a Python subprocess (numpy + sounddevice + opening the device, ~200–800 ms), and
maybe_fallback()runs from the audio manager's per-frame update. Its cooldown was stamped only after a successful switch, so when every candidate was silent — the exact case the fallback exists for — the gate never closed and a probe ran on every frame, inline on the render thread. The cooldown is now stamped when a probe is attempted, and the probe runs on a worker thread whose result is collected on a later frame, so the render thread is never blocked by it at all. -
1.0.0-beta.15 — Shift+Delete re-activates the last Delete-key deactivation (running session only, LIFO): a disabled effect is re-enabled and jumped straight back to; a disabled ProjectM preset is re-enabled while ProjectM is still active. For when you fat-finger Delete mid-set.
-
1.0.0-beta.14 — Two
VJApipassthroughs for controller profiles:midi_preset_name()(read the active preset) andmidi_apply_preset(name)(switch it). Drop-ins cannot reach the MIDI manager directly, so without these a drop-in could neither tell which profile is live at startup nor offer a profile switcher. See midi-controllers-01 0.9.0. -
1.0.0-beta.13 —
MidiManager.apply_preset(): switch the live CC/note maps to a different registered preset at runtime without reopening ports. Rebuilds through the same layering__init__uses, so a switch never silently drops the operator's[midi.cc_map]/[midi.note_map]overrides. This is the core half of controller profiles (see midi-controllers-01 0.8.0). -
1.0.0-beta.12 — MIDI context-slot fix: the presets browser and effects browser now have their own context-slot tables, and a slot a context deliberately leaves unbound is swallowed instead of falling back to the performance bindings. Previously both modals fell through to performance, which re-dispatched the performance chord into the open modal — mostly inert, but in the effects browser slot 2 landed on
p(pin/unpin the selected effect) and slot 4 on Space (toggle it enabled), so reaching for "move down the list" silently re-pinned or disabled an effect. CC→parameter scaling now reads one sharedCC_PARAM_RANGEconstant instead of two divergent literals (0.1–4.0 and 0.0–4.0). Core no longer carries its own dead, hardcoded copy of the APC controller-help modal. -
1.0.0-beta.11 — Tour P2 (partial): drop-ins can now contribute slides to the first-run tour via module-level
TOUR_SLIDES(discovered the same way asHELP_ENTRIES, appended after the core deck when the tour opens). control-room-01 0.9.0 ships the first two drop-in slides. Discovery is fully fault-isolated: a broken drop-in never breaks the tour. -
1.0.0-beta.10 — First-run tour (v1): a centered slide dialog that walks new operators through the core surface — effects, audio sources, the help overlay, multi-display, and the right-click menu. Offered automatically on the first-ever launch (show-on-startup checkbox controls every run after), reopenable any time with F1 or the context menu's "Open Tour" entry, and it resumes at the slide you left off. Slide copy resolves hotkeys from the help registry at display time, so bindings are never hardcoded in the tour.
-
1.0.0-beta.9 — New core analyzer: Audio Chromogram — real pitch-class (chroma) analysis of the live FFT, folding every bin into one of the twelve equal-tempered pitch classes (C, C#, D, ... B) regardless of octave. Shown as a scrolling chromagram strip (pitch class vs. time) and a circular chroma wheel with a dominant-note highlight ring. Uses a soft Gaussian-in-semitone weighting with confidence-scaled low-frequency de-weighting rather than hard bin rounding, since the shared low-latency 1024-point FFT (46.875 Hz/ bin) is wider than a semitone below ~800 Hz — verified against synthetic tones to be exact from ~D5 up and within one semitone from ~C3 up, with sub-bass contributing an honest diffuse glow rather than a falsely precise note.
-
1.0.0-beta.8 — Now Spinning platter raised clear of the bottom banner ticker (bottom corners get 72px clearance)
-
1.0.0-beta.7 — Fix the promoted Now Spinning platter never rendering: the lazy overlay create referenced a nonexistent
App.ctx(AttributeError disabled it on first use since beta.4); it now uses the app GL context directly and waits quietly until GL is up -
1.0.0-beta.6 — Unknown-source quiet mode: when a now-playing source is playing but cannot name its track, the Now Spinning platter auto-hides and the track banner does not fire
-
1.0.0-beta.5 —
Whotkey toggles the Now Spinning platter overlay (listed in theHhelp) -
1.0.0-beta.4 — Now Spinning overlay promoted to core (
unicornviz/now_spinning.py): the corner platter card is now fed by the now-playing hub, so the DJ mixer, media player and Spotify all get it with zero wiring. Visual glow-up: pulsing cyan↔magenta neon frame with a comet sprite racing the border, two-line title/artist, more air around the disc.[now_spinning] enabled/cornerconfig;vj_api.set/toggle_now_spinning. -
1.0.0-beta.3 — Now-playing announcement hub (
unicornviz/now_playing.py): the track banner + HUD pane feed is now a registry (vj_api.register_now_playing) instead of hard-coded media/spotify references inapp.py. Sources register a shared-shape snapshot callable with a priority (dj mixer 30 > media 20 > spotify 10-ambient); the loudest claim wins, ambient sources show while idle, and older drop-ins are auto-registered for back-compat. -
1.0.0-beta.2 — Fix: ALSA sequencer client leak in the MIDI core. python-rtmidi objects need an explicit
delete()on Python 3.14 (GC does not release the client); every port scan leaked one portless client until the kernel's ~64-user-client table filled — MIDI died mid-session (controllers unresponsive, selector overlay empty) while open connections kept working. Port enumeration now reuses one cached probe per direction and every teardown path (stop,remove_input_device, aux reconnect,MidiOut.close) explicitly frees its clients. -
1.0.0-beta.1 — initial versioned release.