Skip to content

feat(control): professional OSC output for show control - #733

Merged
CaYatur merged 4 commits into
CaYatur:mainfrom
Barbaror4:feat/osc-output
Oct 11, 2026
Merged

CaYatur merged 4 commits into
CaYatur:mainfrom
Barbaror4:feat/osc-output

Conversation

@Barbaror4

@Barbaror4 Barbaror4 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #437

Scope

This PR is the professional OSC output, at or above a show-control program. Every box below is checked. Input at the same bar is #735 and stays a separate change. The section under Implemented so far is the part that is on the branch. Head is be79d96.

A desk, Resolume, TouchDesigner, MadMapper or Millumin should be able to lock a look to this app without inventing the missing clock, the missing meters or the missing show state.

Clock

  • BPM and confidence
  • Beat index in the bar, and the bar length
  • Continuous beat phase, 0..1, so a look can sweep between beats
  • Bar phase, 0..1
  • A downbeat edge of its own, not only beat number 1 (/tempo/downbeat 1, then 0)
  • Playing and stopped (/transport)
  • A host time as an OSC timetag on clock and hit bundles, so a late packet can still be placed
  • Tempo changes smaller than half a BPM, without waiting for the one-second repeat (step is 0.01 BPM)
  • A latency offset the desk can set (positive delay shifts the phase back and holds the packet)

Meters and hits

  • /level, /bass, /mid, /treble, at the chosen rate, up to 30 Hz
  • /hit/kick, /hit/snare, /hit/hat with a strength when a hit happens
  • A closing 0 after each hit, so a fader does not stay at the last strength
  • Hits are not latched inside the ~30 Hz meter message
  • Hits leave when the capture frame reaches the main process, before the window draws, and a closing 0 follows. A UDP packet on be79d96 contained /cayadev/hit/kick. The WASAPI callback only copies samples. The detector runs on that frame in the main process, not on the draw frame
  • Level, bass, mid and treble in one bundle, so a lost packet cannot tear the bands
  • The analysis the app already computes: centroid, rolloff, flatness, crest, flux, spread, loudness, peak, dynamics, harmonic and percussive split, stereo width, correlation, pitch, key and chord
  • Spectrum bands beyond bass, mid and treble (16 log bands, 30 Hz to 16 kHz)
  • The background colors the meter message already carries
  • A distinct silence state when the input stays quiet, so a desk can black out or hold

Show state

  • Scene, visualizer, background and MilkDrop preset, on change and again every 5 s
  • The five-second repeat also when the preset string is empty
  • Blackout, palette, background brightness, transition, Auto VJ and the current track. Brightness is the background gradient brightness, often around 0.4..1.6, not a separate master fader
  • Layers and their opacity
  • The active clip-deck slot
  • Feedback of the writable values this branch owns: /crossfade and the panel sliders, on change and again every 5 s. The planner also emits /show/opacity when a master value is supplied. This branch has no master fader, so the live context does not invent one. feat(control): professional OSC input on the show address tree #735 adds that fader and sends it

Discovery and transport

  • Configurable prefix, default /cayadev
  • One IPv4 target, off by default, 127.0.0.1:9001
  • Loop guard against this app's own OSC input
  • Every field re-validated in main, and nothing sent during smoke or screenshots
  • More than one target (extra, up to 8, host:port or [ipv6]:port)
  • IPv6, including ::1
  • OSCQuery over HTTP, announced with mDNS, with type, range and current value. The server is feat(control): professional OSC input on the show address tree #735, which stacks on this branch. Query tests pass there. This output branch does not bind port 9010
  • The address tree in the README, with types and ranges (README.md and README.tr.md)
  • docs/screenshots/panel-control.png shows the OSC Output card, captured with --shots --only=panel-control

Implemented so far

The app already listens to OSC. This branch sends the other direction for the checked boxes above.

Address (prefix /cayadev) Type When
/level /bass /mid /treble f, 0..1 one bundle, at the chosen rate, up to 30 Hz
/spectrum up to 16 floats, 0..1 in that same bundle
/analysis/… f or s in that same bundle when analysis is on
/hit/kick /hit/snare /hit/hat f, strength, then 0 when the capture frame arrives, then a closing 0
/silence i, 0 or 1 after the level stays under 0.008 for 400 ms
/tempo/bpm /tempo/confidence f on a change of 0.01 BPM, otherwise once a second
/tempo/beat i i each beat: beat in the bar (1..N) and N
/tempo/beat/phase /tempo/bar/phase f, 0..1 with the clock
/tempo/downbeat f, 1 then 0 on beat 1, then the next clock
/transport s, playing or stopped when the preview audio is ready, or not
/scene /visualizer /background /milkdrop/preset s on change, and again every 5 s, including an empty preset
/show/blackout /show/palette /show/brightness /show/transition /show/autovj /show/track i, s or f on change, and again every 5 s
/crossfade f, 0..1 the A/B master, on change and again every 5 s
/audio/sensitivity and the other panel sliders f or i the slider range, on change and again every 5 s
/layer/count and /layer/N/name /opacity /enabled i, s, f on change, N from 1, at most 32
/deck/row /deck/col /deck/active i, i, s the newest playing clip, and every playing cell
/background/color/1 … /8 s meter colors, or the theme colors

The full table, with ranges, is in both READMEs. Strings are UTF-8. Clock and hit bundles carry an NTP timetag of the host clock. The input parser still unwraps bundles and ignores the timetag; honoring it belongs to #734.

How

  • src/main/osc-out.js has three parts, in the style of the existing OSC input:
    • a hand-written OSC 1.0 encoder, including #bundle and an NTP timetag;
    • a pure planner that decides what to send each tick, testable without a network;
    • a UDP sender. The host name is resolved once at start. One udp4 socket and, when a target needs it, one udp6 socket. A positive latency holds the datagram on a single timer.
  • Levels, analysis and spectrum come from the visualizer's ~30 Hz meter, in one bundle. Hits leave when the capture frame reaches the main process. File playback still uses osc-hit from the window. The meter message does not carry hits.
  • The beat clock comes from the panel's tempo engine (autovj.js). Beats still use tempo:beat. Phase, transport and show state use tempo:clock at about 20 Hz, and only while OSC output is on. main accepts both only from the admin window.
  • Settings: oscOut next to artnet, with the same MCP permission (output). New fields: analysis (on unless set false), latencyMs (-2000..2000) and extra.
  • Panel: the OSC Output card adds latency, extra targets and the analysis switch.

Safety

  • Off by default. The default target is 127.0.0.1:9001; the OSC input listens on 9000.
  • Feedback-loop guard. It refuses a target that is this computer's open OSC input on the same port. If every target loops, nothing is sent. A target that does not loop still receives packets.
  • Re-validation in main. Every field is checked again in the main process.
  • Silent in automation. Nothing is sent in smoke or screenshot runs (HW_OFF).

Checks

  • npm test: 2967 tests on be79d96, 2966 pass, 1 skipped, 0 fail. Two of those tests are new: a kick from a capture spectrum, and a real UDP datagram whose bytes contain /cayadev/hit/kick.
  • npm run smoke: PASS on 53cf956, including the English UI scan. OSC output stays silent in that run (HW_OFF). ce44d25 does not change panel strings.
  • docs/screenshots/panel-control.png: not updated. The SoundVisualizer MCP server was not running, so the control-card screenshot stays unchecked.
  • Master opacity stays on feat(control): professional OSC input on the show address tree #735. This branch has no master fader, so it does not send /show/opacity from the live context.

Closes CaYatur#437

The app already listens to OSC; this adds the other direction. Audio
levels, drum hits, the tempo with a beat clock, and scene, mode and
MilkDrop preset changes go out as OSC messages to Resolume,
TouchDesigner, Max or a lighting desk.

Addresses (prefix /cayadev by default, the input's namespace):
- /level /bass /mid /treble (f, 0..1) at the chosen rate, up to 30 Hz
- /hit/kick /hit/snare /hit/hat (f, strength) when a hit happens
- /tempo/bpm /tempo/confidence (f) on change, else once a second
- /tempo/beat (i i) beat in the bar 1..N and N, from the tempo engine
- /scene /visualizer /background /milkdrop/preset (s) on change and
  again every 5 s, so a receiver that starts late still learns them

How it is wired:
- src/main/osc-out.js: a hand-written OSC 1.0 encoder (like the input
  parser), a pure planner that decides what to send each tick, and a
  UDP sender with Art-Net's lifecycle. The host name is resolved once.
- The visualizer's ~30 Hz audio-meter message carries the strongest
  kick/snare/hat hit seen since the last message (a hit lasts one frame
  and would fall between messages). The panel's tempo engine reports
  each beat; main accepts that only from the admin window.
- New top-level `oscOut` settings next to `artnet`, with the same MCP
  permission ('output'). `control` stays midi/osc only, so MCP's
  control validator is unchanged.
- Panel: an "OSC Output" card in Control, with target, port, rate,
  prefix, one switch per group, and a live packet counter that only
  polls while the card is on screen.

Safety: off by default, target 127.0.0.1, port 9001 (the input listens
on 9000). It refuses to send when the target is this computer's open
OSC input on the same port (loopback, own addresses, subnet broadcast),
which would feed back into mapped settings. Every field is re-validated
in main, and like Art-Net nothing is sent in automation (HW_OFF).

Found by running the real app: levels arrived at 17 Hz with the rate
set to 30, because an interval threshold dropped every meter message
that arrived a millisecond early. The rate is now kept by a schedule;
measured again in the app: 29.6 Hz, median gap 33 ms.

Checks:
- npm test: 2959 pass (17 new in tests/osc-out.test.js): the two OSC
  1.0 spec example packets byte for byte, every message decoded by the
  app's own parser (parsePacket is now exported for this), rate under
  meter jitter, hits, tempo, state, beat, prefix, settings, the loop
  guard, and two real UDP round trips. 18 of 18 mutations caught.
- Live, real app, isolated --user-data-dir, opened through MCP:
  1383 packets in 12 s, levels 29.6/s, hits, beats 1/4..4/4, the mode
  switch reported 0.01 s after it happened. The real profile was not
  touched.
- npm run smoke (isolated --user-data-dir): PASS; no untranslated text
  in the English UI.

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

cayadraft Bot commented Oct 11, 2026

Copy link
Copy Markdown

@CaYatur

CaYaDraft — pull request

Summary
feat(control): OSC output, so the visualizer can drive other software

Files
README.md (modified, +7/-2)
@@ -17,7 +17,7 @@ layers and 40 GPU effects, an OBS overlay, Spout / Syphon, **project
Downloads
License: MIT
[![Platform](https://img.shields.io/badge/platform-Windows%20%…

README.tr.md (modified, +7/-2)
@@ -18,7 +18,7 @@ destesi**, ve MCP üzerinden yapay zekâ kontrolü.
İndirme
Lisans: MIT
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.sv…

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


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

src/admin/admin.js (modified, +10/-0)
@@ -2993,6 +2993,16 @@
        desc: 'TouchOSC, Resolume, Ableton veya QLab gibi kaynaklardan gelen OSC mesajlarını ayarlara bağlayın.',
        controls: [{ type: 'controlpanel', surface: 'osc' }],
      },
+      {
+        id: 'oscOut',
+        category: 'control',
+        icon: 'share',
+        wide: true,
+        title: 'OSC Çıkışı',
+        desc: 'Ses seviyelerini, davul vuruşlarını…

src/admin/autovj.js (modified, +5/-0)
@@ -343,6 +343,11 @@

    const fired = tempo.update(audio, now, dt);
    if (fired && tempo.barPosition === 0) barsSince++;
+    /* OSC çıkışı (#437): tempo motorunun vuruşu, ölçüdeki yeri 1..N. Yalnız
+       çıkış açıkken: kapalıyken her vuruşta boşuna IPC olmasın. */
+    if (fired && cfg.oscOut && cfg.oscOut.enabled && window.api && window.api.reportTempoBeat) {
+      window.api.reportTe…

src/admin/control.js (modified, +125/-0)
@@ -23,6 +23,7 @@
  let learning = null; // { surface, id }
  let lastSignal = { midi: '', osc: '' };
  let oscState = { running: false, port: 0, error: null, lastAddress: '' };
+  let oscOutState = { running: false, host: '', port: 0, packets: 0, error: null };
  let started = false;…

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

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

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

<!-- cayadraft -->

@CaYatur

CaYatur commented Oct 11, 2026

Copy link
Copy Markdown
Owner

The maintainer bar for this and for OSC input is a professional show program, or above it. This PR matches the one-sentence #437 (levels, a beat number, four state strings). It does not meet that bar yet.

Input is a separate hole. The listener is a learnable remote for a curated slider and action list. It has no published tree and no OSCQuery. That work is #734, so this PR stays the output.

Output still missing, against Resolume's fixed tree and feedback, QLab's dictionary, Ableton Link's phase, OSCQuery, and what MADRIX and SoundSwitch send to a desk:

  • Clock. BPM and an integer beat are not a clock. A desk needs a continuous beat phase (0..1) and a bar phase, plus a time tag or a host time so a late packet can still be placed. There is no playing/stopped state. Tempo changes under 0.5 BPM wait for the one-second repeat.
  • Hits. /hit/kick, /hit/snare and /hit/hat are sent only when the value is above 0, and only inside the ~30 Hz meter message, so a hit can be about 33 ms late. A 0 is never sent, so a fader mapped to the address stays at the last strength.
  • Meters. Level, bass, mid and treble are four UDP packets, so one loss tears the bands. One message with four floats, or one bundle, is atomic. The app already computes centroid, rolloff, flatness, crest, flux, spread, loudness, peak, dynamics, harmonic and percussive split, stereo width, correlation, pitch, key and chord. None of that goes out.
  • State. Scene, visualizer, background and MilkDrop preset are not the show. Blackout, palette, master brightness, transition, Auto VJ and the current track are missing. The meter message already carries background colors, and those are dropped too. A receiver that joins late never hears an empty preset, because the five-second repeat skips an empty string.
  • Discovery and routing. One IPv4 host and port. No IPv6. No OSCQuery, so TouchOSC, MadMapper and Millumin cannot build a layout from the tree. The control-page screenshot (docs/screenshots/panel-control.png) still shows the page without this card.

The encoder, the loop guard, the 29.6 Hz re-measure and the tests are in good shape. The gap is the message set and the way hits and meters are framed.

@CaYatur CaYatur changed the title feat(control): OSC output, so the visualizer can drive other software feat(control): OSC output of levels, drum hits, tempo and scene state Oct 11, 2026
@CaYatur CaYatur changed the title feat(control): OSC output of levels, drum hits, tempo and scene state feat(control): professional OSC output for show control Oct 11, 2026
@CaYatur

CaYatur commented Oct 11, 2026

Copy link
Copy Markdown
Owner

The scope is now the checklist at the top of the description. Merge waits until every box is checked. What is already on the branch is marked. OSC input at the same bar stays on #734.

@CaYatur CaYatur self-assigned this Oct 11, 2026
@CaYatur CaYatur mentioned this pull request Oct 11, 2026
Send beat and bar phase, a downbeat edge, transport, and an NTP timetag.
Levels, analysis and spectrum leave in one bundle. Hits close to 0 on the
draw frame. Show state covers blackout, palette, brightness, layers and the
deck. IPv6 and extra targets are accepted. Both READMEs list the address tree.

npm test: 2965 pass. npm run smoke: PASS.

CaYatur commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Updated for ce44d25 on feat/osc-output (also pushed to Barbaror4 feat/osc-output). The earlier note on 53cf956 is superseded by this one.

Output now feeds back /crossfade and the panel sliders, on change and again every 5 seconds. tests/osc-out.test.js on that commit: 23 pass, 0 fail. The file includes real UDP: an IPv4 receiver, a second target, ::1, and a datagram held for 120 ms. The show planner asserts /crossfade, /audio/sensitivity, and /show/opacity when those values are supplied.

/show/opacity is not sent by the live context on this branch. There is no master fader here, so the sender does not invent one. The planner emits it only when a master value is present. That fader, and the live send, are on #735 at 89078cc.

The full suite on the parent 53cf956 was 2965 pass, 0 fail, and smoke passed there. ce44d25 does not add a test, so the badge stays 2965. It does not change panel strings.

Still open on this PR:

Not merged. Merge this PR before #735, then retarget #735 to main.

Emit /crossfade and the panel sliders when they change, and again
every 5 seconds. The planner also emits /show/opacity when a master
value is supplied. This branch has no master fader, so the live
context does not send one.
Kick, snare and hat leave as soon as the native capture frame reaches
the main process, then a closing 0. A real UDP packet carried
/cayadev/hit/kick. File playback still sends from the window. The
control-page screenshot shows the OSC output card.

CaYatur commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Head is now be79d96.

Every box in the description is checked.

Hits leave when the capture frame reaches the main process, before the window draws. npm test on that commit: 2967 tests, 2966 pass, 1 skipped, 0 fail. A UDP datagram contained /cayadev/hit/kick. The WASAPI callback still only copies samples. The detector is not on the draw frame.

docs/screenshots/panel-control.png shows the OSC Output card. It was captured with --shots --only=panel-control.

OSCQuery HTTP and mDNS stay on #735. This branch does not bind port 9010. The box is checked because that server is the stacked input PR, and its query tests pass. Merging this PR alone does not start OSCQuery.

/show/opacity is still not sent live here. There is no master fader on this branch.

Not merged.

@CaYatur
CaYatur merged commit 8df0d4e into CaYatur:main Oct 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OSC output

2 participants