Skip to content

Releases: daydreamlive/demonTD

v0.2.17 — Stable streaming: keepalive + audio-bleed fixes, VST-matched defaults

11 Jun 18:16

Choose a tag to compare

Fixed — stable streaming

  • No more 1011 keepalive ping timeout ~40s into a session. demonTD was streaming params into the pod during its 30–40s initial encode, wedging the pod's keepalive. It now stays silent until ready, exactly like the web app and the VST.
  • Much less un-processed-audio bleed-through. Two zero-latency fixes: the playhead reported to the pod is de-jittered (so generated slices land where the playhead actually is), and LoRA-strength changes are debounced — dragging / automating a LoRA fader no longer triggers a pod weight-refit storm that stalls decoding.
  • Surfaces the server's real WS close code + reason; fragments the large source upload; coalesces the params keepalive so a slow pod can't choke the connection.

Changed — defaults now match the rtmg-vst

  • Prompt A acoustic deep house hybrid, Prompt B daft punk style, beautiful, four to the floor, angelic; Strength 0.9, Structure 0.8, Timbre 0.3, Seed 0; Ambient + Deep House LoRAs auto-enabled. Your saved toggles still win.
  • Quieter textport — handshake detail now behind Debug Logging.

Known issue

Brief flashes of the un-processed source can still occur during heavy GPU work on the pod (prompt re-encode, etc.) — this is fundamental to just-in-time decoding and affects all clients; we mitigate it client-side as far as is possible without adding latency.

Install

Download demonTD-v0.2.17.zip, unzip, and keep demonTD.tox next to the vendor/ folder — the .tox loads its Python deps (and the baked-in protocol contract) from vendor/. The bare demonTD.tox is only for upgrading an install that already has a matching vendor/.

v0.2.16 — Audio smoothness: choppy-audio root causes fixed

09 Jun 23:11

Choose a tag to compare

"Occasionally choppy" audio — root causes found and fixed

Everything except the PortAudio callback ran on TD's main thread, so any main-thread hitch degraded audio twice over: the continuous params stream (the WS keepalive AND the server's pacing signal, against a 0.25 s lead floor) went silent, and incoming slices stopped being patched — the playhead played stale loop content. The biggest recurring hitch was ours: the hosted-session heartbeat ran a synchronous HTTPS poll (fresh TLS handshake, 10 s timeout) on the main thread every 5 seconds.

  • Heartbeat HTTP off the main thread (queue_worker.py) — status poll + auto-extend on a worker thread; queue leave() calls are fire-and-forget too.
  • Dedicated params pacer thread (params_pacer.py, ~16 ms) — the keepalive survives TD frame hitches; also fixes a starved params snapshot and curve-override stomping.
  • Slices decode + patch on the WS recv thread (binary_router.py) — the loop stays fresh through any main-thread stall; per-connection zstd; stale recv threads are detached on reconnect.
  • Audio-callback hygiene — no more logging from the audio thread on underrun (one underrun cascaded into several); the oversized-block fallback played whole blocks as silence — now a zero-allocation chunked fill.
  • Smoothness telemetry (telemetry.py) — a [health] line names the guilty subsystem if chop ever returns.
  • PortAudio dylib resolution fixed — freshly built .tox files computed a garbage dylib path (file pars are cleared at build time) and lost Python audio out entirely; the binary now resolves from the discovered vendor/ root.

Bug sweep

Connection-generation tagging (stale recv-thread events could tear down a freshly reconnected session), NaN/Inf params no longer emit invalid JSON into the keepalive, decode_slice validates payload length, failover uses the current API key, LoopBuffer channel-race guards, WSClient connect/close lifecycle lock, error-handling polish.

Tests: 181 (up from 105). Full details in CHANGELOG.md.


Install: download demonTD-v0.2.16.zip and keep demonTD.tox next to the vendor/ folder — the bare .tox without vendor/ will not load.

v0.2.15 — LoRA triggers + Tags B + Structure label

09 Jun 21:38

Choose a tag to compare

Big one: LoRA trigger word prepend (fixes "LoRAs feel weaker in TD")

Every LoRA carries a metadata.primary_trigger_word — the activation token it was trained against. TD was passing the user's prompt through verbatim, so enabling a LoRA loaded it server-side but the encoder never saw the activation token; the style barely affected output. The canonical web client injects the trigger prefix at WS send time; TD now does the same.

  • New src/lora_triggers.py — pure-Python port of the canonical loraTriggers.ts.
  • _apply_lora_catalog captures metadata.primary_trigger_word into the catalog Table DAT + in-memory dict.
  • SendPrompt prepends the prefix to both tags and tags_b, stripping any stale leading triggers first.
  • New Auto-Prepend LoRA Triggers toggle on the Prompt+LoRA page (default On).
  • enable_lora / disable_lora now also re-push the prompt so the encoder picks up the new trigger immediately. Before this, toggling a LoRA loaded it server-side but you had to also touch a strength slider or pulse Sendprompt to make it audibly fire.

Tags B / Prompt blending now actually blends

Promptblend was streaming a value but blending nothing because the second prompt was empty mid-session. New Promptb Str param on the Prompt+LoRA page next to Prompt, editable any time. SendPrompt passes it as tags_b. _build_session_config reads prompt_b from the live Promptb — one source of truth. Deleted the obsolete Initpromptb.

Synthesis page: "Structure" (the canonical's label)

hint_strength is the model's structure control — it governs how closely the model follows the source's section / rhythm / dynamics. The canonical labels it "Structure"; TD was labeling it "Hint Strength". Wire key unchanged; only the user-facing label and tooltip. Also corrected: Denoise"Strength", DCW Scaler"DCW low", DCW High Scaler"DCW high" — all surfaced by the new drift checker's label-parity check.

Drift checker hardening — would have caught all of the above

Four new checks in scripts/check_protocol_drift.py:

  • ui_coverage — every pulse in DISCRETE_PULSE_TO_KIND must have a non-hidden Param.
  • label_parity — TD Param labels must contain the canonical's DISPLAY_NAMES word for shared wire keys.
  • lora_trigger_injection — verifies the three integration points stay wired.
  • tags_b_plumbing — if SessionConfig sends prompt_b, an encode_prompt call site must pass tags_b=.

Each verified by regression-testing.

Other fixes

  • UI lag fix: _apply_lora_catalog's signature no longer includes trigger_word, so server catalog echoes with intermittent metadata don't force a full Table DAT + dynamic-par rebuild on every event.
  • "DAT did not load from … overwrite anyway?" dialog on import: code DATs no longer bake the developer's absolute src/ path into the exported .tox. Embedded dat.text is the only source.

Known cosmetic mismatch

The shipped .tox carries BUILD_MARKER = v0.2.15-...-lora-resend (boot textport) but DEMON_TD_VERSION = 0.2.14 (User-Agent on cloud REST calls). TD's bidirectional DAT sync wrote version.py back to disk between the version bump and the rebuild. Functionally identical to v0.2.15 in every other respect; root cause (syncfile=True baked into the previous build's DATs) is fixed in this release for everyone running v0.2.16+.

Files

  • demonTD-v0.2.15.zip.tox + vendor/ bundle. Drop this. Keep vendor/ next to the .tox.
  • demonTD.tox — the operator only. Useful when you already have a matching vendor/.

Stats

105 unit tests pass (24 new). Drift checker clean against origin/main.

v0.2.14 — Seed is an arbitrary integer (+ Randomize Seed)

05 Jun 16:45

Choose a tag to compare

Download demonTD-v0.2.14.zip (the bundle), not the bare demonTD.tox. Extract it and keep demonTD.tox next to the vendor/ folder.

Fix: Seed is an arbitrary integer, not a 0–1 slider

The Seed param was a Float clamped to 0.0–1.0 — which can't express a real generation seed (every value collapsed to ~the same seed server-side). The reference web client uses an arbitrary uint32 with a dice button.

  • Seed → integer field: default 42, range 0 … 2147483647 (capped at int32 max to stay safely inside TouchDesigner's numeric-par range — still ~2.1 billion seeds). Streamed continuously, now as a proper integer.
  • New "Randomize Seed" pulse on the Synthesis page — sets Seed to a random integer (the web client's dice), since typing a 10-digit seed by hand is no fun.

Tested on macOS and Windows. 81 unit tests pass; protocol drift clean. BUILD_MARKER = v0.2.14-seed-int; UA DaydreamDEMON-TD/0.2.14.

v0.2.13 — Audio output device picker (fix connected-but-no-audio)

04 Jun 16:22

Choose a tag to compare

Download demonTD-v0.2.13.zip (the bundle), not the bare demonTD.tox. Extract it and keep demonTD.tox next to the vendor/ folder.

Fixed: connected but no audio (macOS + Windows)

The operator always opened the system default output device — whatever device PortAudio/TouchDesigner happened to be on — with no way to choose another. When that default wasn't the device you were actually listening on, the session connected but played silence.

New: Audio Output Device picker

On the Session page:

  • Refresh Audio Devices — enumerates your output devices (with host API: Core Audio / MME / WASAPI / …).
  • Audio Output Device menu — pick the one you want. Default (system) keeps the old behavior.
  • Live switching — change it while connected and playback moves to the new device immediately (no reconnect).

The textport also prints exactly which device opened on Connect:

[speaker_out] output device: dev=3 name='Speakers (Realtek)' hostApi='Windows WASAPI' maxOut=2 defaultSampleRate=48000

Also in this release

  • Removed a fabricated "Edit → Preferences → Audio → Audio Device → None" instruction that the operator printed to the textport and Status field — that setting does not exist in TouchDesigner on any platform. Guidance now leads with the real fixes (pick the device, or save + restart TD).
  • Internals: explicit-device open reuses the proven rate/buffer/format fallback matrix; device enumeration brackets a balanced Pa_Initialize/Pa_Terminate so it can't poison a later default open on macOS.

Tested: confirmed working on macOS and Windows 11 + TD 2025.32820. 81 unit tests pass; protocol drift clean. BUILD_MARKER = v0.2.13-audio-device-picker; UA DaydreamDEMON-TD/0.2.13.

If you still hit silence: pulse Refresh Audio Devices, pick your output, and check the [speaker_out] output device: line in the textport. See Audio output troubleshooting.

v0.2.12 — Fix client-side disconnects + backend protocol parity

03 Jun 17:46

Choose a tag to compare

Download demonTD-v0.2.12.zip (the bundle), not the bare demonTD.tox. Extract it and keep demonTD.tox next to the vendor/ folder — see the Windows install notes.

1. Fixed: client-side disconnects ("no errors on the pod")

Persistent disconnects where the client tore the connection down and the pod logged nothing.

Root cause: concurrent read+write on the same ssl.SSLSocket from two threads. Sends (main thread, SSL_write) raced the recv loop (bg thread, SSL_read) with no lock between them. Python's SSLSocket isn't safe for that — OpenSSL's record layer corrupts (SSL: BAD_LENGTH) → abrupt client-side close. v0.2.11's params-every-tick keepalive (~30–60 sends/s) made the collision far more likely, which is why disconnects got worse after that release.

Fix: ws_client now touches the socket from exactly one thread.

  • send_text/send_binary only enqueue onto a bounded outbound queue; the recv thread drains and sends it between recvs, so reads and writes never overlap.
  • close() signals the recv thread instead of touching the socket.
  • Removed the 25 s app-level WS ping (another cross-thread write) — keepalive is purely the param stream, like the browser web client.
  • Rich close diagnostics: closed (...) — uptime=Xs sent=N recv=N dropped=N since_last_recv=Xs.

Confirmed stable in a live hosted session.

2. Backend protocol parity (drift now clean)

Caught up to backend sync DEMON.git@1c07327:

  • lead_floor_s / lead_ceiling_s / lead_release_tau_s — server-side decode-buffer lookahead tuning (latency vs robustness to GPU contention). New Lead params on the Init page (web defaults 0.25 / 1.35 / 1.5), sent at Connect.
  • set_interp_method — per-path blend interpolation (slerp/linear) for prompt / timbre / structure / feedback. Four Blend Interpolation menus on the Prompt+LoRA page; applied on change and re-pushed on every ready.

Docs

  • New Windows install section (Win 11 + TD 2025.32820 confirmed): download the bundle zip, keep vendor/ next to the .tox, ffmpeg note for MP3/M4A, Windows audio-device guidance.
  • Fixed the stale "Source Audio File" quick-start step — source is now a wired Audio File In CHOP (the picker was removed in v0.2.8).

Tests: 79 passed (new tests/test_ws_client.py). Drift: clean vs demon-public-demo origin/main. BUILD_MARKER = v0.2.12-ws-socket-and-protocol-parity; UA DaydreamDEMON-TD/0.2.12.

v0.2.11 — Params keepalive: fix sessions dying right after ready

03 Jun 00:53

Choose a tag to compare

The big one. In-the-wild report: "it's audio reactive but I can't access the Daydream side… looks like it tries three times and then stops."

Symptom

The session connected, uploaded the source, received ready + the initial buffer + stem_assets — then the pod closed the WebSocket before streaming a single generation slice (binary_frames_recv never exceeded 1, across every pod and every failover). Looked server-side; it wasn't.

Root cause (found by comparing against demon-public-demo)

The web client's useParamSync sends a {type:"params"} message every 8 ms after ready, and that continuous stream is the only thing keeping the pod's WebSocket alive — there is no separate keepalive. If the pod receives nothing after ready, it idle-times-out and closes.

demonTD's param flush lives in OnTick, driven by the tick8ms Timer CHOP — which has been silent in practice (the same TD callback gremlin that kept OnHeartbeat silent; the v0.2.6 onTimerPulse rename didn't fix it). So after ready, demonTD went completely silent → the pod idle-timed-out → dropped the connection → zero generation slices. The heartbeat survived only because v0.2.6 added a frame_exec fallback for it; OnTick had no such fallback.

Fixes

  1. MaybeTickFromFrame — drives OnTick from frame_exec (the reliable, verified-firing hook) on a ~33 ms floor when the Timer CHOP is silent. Restores the post-ready param stream that keeps the session alive.
  2. OnTick sends params every tick, not just on change — accumulates into a running snapshot and re-sends it with an advancing playback_pos, matching the web client. (Send-on-change-only went silent the moment the user stopped touching params, re-triggering the timeout.)
  3. Send-failure teardown — after 3 consecutive WS send failures the connection is declared dead and torn down once, instead of retrying forever (kills the ~1,500-line SSL: BAD_LENGTH flood that pegged CPU and blocked failover when a stream got corrupted).
  4. Heartbeat tolerates status=unknown — a transient/unparseable status poll no longer disconnects a live session; the WS closing is the authoritative "ended" signal.

Built on top of #3 (the onCook AttributeError guard). Confirmed live: generation slices stream and the session stays up for the full lease.

Verification

  • After ready, binary_frames_recv climbs past 1 and the session holds.
  • Debug ON: [tick] Timer CHOP appears silent — driving OnTick from frame_exec fallback appears once; params flow continuously after.
  • A dropped connection tears down cleanly instead of flooding SSL: BAD_LENGTH.

Tests: 72 passed. Drift: clean against demon-public-demo @ f2be73b. BUILD_MARKER = v0.2.11-params-keepalive; UA → DaydreamDEMON-TD/0.2.11.

v0.2.9 — DaydreamDEMON-TD User-Agent on cloud REST calls

02 Jun 18:58

Choose a tag to compare

Mirrors rtmg-vst#7.

Every cloud REST call now sends User-Agent: DaydreamDEMON-TD/<ver> (e.g. DaydreamDEMON-TD/0.2.9). This lets the cloud orchestrator (demon-public-demo) tag each session by client — VST vs web vs TouchDesigner — from the User-Agent, replacing the brittle "no Origin header" heuristic. Shared convention across clients: DaydreamDEMON-<CLIENT>/<ver> (the VST sends DaydreamDEMON-VST/b<build>).

Changes

  • New src/version.py — single source of truth for DEMON_TD_VERSION and the derived USER_AGENT. Pure module (no TouchDesigner / third-party deps), imported by the HTTP modules via the same mod()/import dual-shim the rest of the codebase uses, with a literal fallback so a missing module can never break a request (the header is advisory).
  • queue_client.py — User-Agent added to _headers(); rides every /api/queue/{join,status,claim,extend,leave} call.
  • oauth.py — User-Agent added to the /users/profile API-key validation call.
  • build_tox.pyversion.py added to the synced source DATs + the module-reload list.
  • Tests assert the header is present on both the queue and auth clients.

Scope matches the VST: REST calls only. The WebSocket connects to a pre-signed URL and the session is tagged at queue/join time, so the REST-layer User-Agent is sufficient.

Tests: 72 passed. Drift: clean against demon-public-demo @ f2be73b. BUILD_MARKER = v0.2.9-user-agent

Note: the demonTD.tox asset is attached after a fresh rebuild that includes the new version.py DAT.

v0.2.8 — Hide Source Audio File picker + Audio Analyze fix

02 Jun 18:24

Choose a tag to compare

Two changes, each verified live before shipping.

1. Source Audio File picker hidden from the op UI

The Source Audio File picker confused users into thinking it was the audio input — but the real source is the CHOP you wire into the COMP. TouchDesigner can't programmatically hide a custom parameter (Par.hidden is read-only), so the build now simply doesn't create it.

New ui_hidden flag on the parameter schema: the entry stays in params.py (so internal lookups and _has_source_audio() still resolve to its "" default), it just isn't rendered. Runtime behavior is byte-identical — the build already reset this par to empty on every rebuild, so the pre-flight was already passing via wired-CHOP detection, not via this picker. Connection is unaffected.

2. Audio Analyze now receives an audio-rate stream

User report: "audio analyze chop isn't getting anything." Diagnosed in two stages from live logs:

Why it was silent: Python Audio Out plays the generated audio by reading the internal loop buffer directly via PortAudio, so nothing in TouchDesigner pulls audio_out. And critically — even with an Audio Analyze CHOP wired to the COMP's out, the pull does not propagate across the Base COMP boundary (telemetry showed audio_out_cooks=0). So audio_out wasn't cooking at all.

The fix (two pieces):

  1. Force-cook audio_out every frame from frame_exec's onFrameStart — guarantees it cooks regardless of the broken cross-COMP pull.
  2. audio_clock Wave CHOP carrier (Time Slice, 48 kHz) wired as audio_out's input. The earlier (long-reverted) force-cook produced frame-rate cooks (numSamples=1) because audio_out had no audio-rate input. With a Wave CHOP feeding it, each forced cook now spans one frame's worth of audio samples (~800 at 48k/60fps). OnCookRecv reads the carrier's sample count as the authoritative block size, ignores its values, and fills that many samples from the loop buffer.

A Debug-gated diagnostic in OnCookRecv reports the cook rate (out.numSamples / carrier.numSamples → AUDIO-rate / FRAME-rate) so this is verifiable from the textport.

Wire the COMP's out → an Audio Analyze CHOP and your audio-reactive material now responds to the generated audio.

Verification

  • Source Audio File picker is gone from the Session page; Connect works exactly as v0.2.7.
  • Build log: audio_out inputs=1 (waveCHOP audio-rate carrier).
  • Telemetry audio_out_cooks= > 0; Debug shows OnCookRecv ... → AUDIO-rate.
  • Audio Analyze populates — confirmed live.

Tests: 71 passed. Drift: clean against demon-public-demo @ f2be73b. BUILD_MARKER = v0.2.8-hide-sourcefile-forcecook-wave-clock

v0.2.7 — TD timeline pause pauses audio

02 Jun 17:47

Choose a tag to compare

Scoped, single-fix release. Reapplied incrementally on top of the confirmed-working v0.2.6 after an earlier bundled v0.2.7 attempt regressed pod connection. This release touches only the pause path — nothing in source resolution or the WebSocket/queue protocol changed, so connection behaves exactly as v0.2.6.

TD timeline pause now actually pauses audio

User report: "It doesn't seem to pause if you pause your touch designer."

Root cause: the frame_exec Execute DAT has a separate toggle parameter for each callback, and they're all OFF by default. Defining onPlayStateChange(state) in the DAT text without enabling the playstatechange toggle is a silent no-op — so the handler never fired.

Fix:

  • build/build_tox.py: enable the playstatechange toggle on frame_exec (alongside the existing framestart + active), and dispatch onPlayStateChange(state)DemonExt.OnPlayStateChange.
  • src/demon_ext.py: new OnPlayStateChange(state) sets a _paused flag on SpeakerOut.
  • src/audio.py: set_paused() + _paused + a pause fast-path in _pa_callback — when paused it emits silence and does not advance the LoopBuffer playhead, so un-pause resumes from the exact same sample. One bool read per PortAudio callback, GIL-atomic, no lock.

The WebSocket + queue heartbeats keep running through the pause — pausing the timeline is a "stop hearing audio" gesture, not a session teardown (that's Disconnect).

Verification

  • Connect behaves exactly as v0.2.6 (no source/WS code changed).
  • Hit Space / timeline pause during a session: audio cuts within one PortAudio callback (~85 ms); un-pause resumes from the same sample. With Debug ON: OnPlayStateChange: state=False → PAUSE.

Deferred to later (individually-verified) increments

  • Removing the Source Audio File par — needs a detection fix first (on some patches _has_source_audio() only passes because the par is set).
  • Feeding Audio Analyze at audio rate — still open; a Debug numSamples diagnostic will drive the fix.

Tests: 71 passed. Drift: clean against demon-public-demo @ f2be73b. BUILD_MARKER = v0.2.7-pause-only