fix(streaming): slew-rate-limit continuous knobs so fast sweeps don't clip#275
Closed
qianghan wants to merge 2 commits into
Closed
fix(streaming): slew-rate-limit continuous knobs so fast sweeps don't clip#275qianghan wants to merge 2 commits into
qianghan wants to merge 2 commits into
Conversation
…ing an outage doesn't keep playing stale When the WS drops and the user switches to a different audio source before the auto-reconnect completes, the recovered session kept playing the previously-loaded track even though the UI showed the new selection. Root cause: the reconnect path (useStartSession) rebinds the fixture snapshotted at session start. The mid-outage track change is dropped because useFixtureSwap.run() bails while status !== "ready", and nothing re-applies it once the session recovers — so the recovered backend + AudioPlayer stay bound to the stale track while the perf store's fixture shows the new pick. Fix: track the live session's bound fixture in useSessionStore (boundFixture), set on initial connect and reconnect. useFixtureSwap now reconciles on the "reconnecting" -> "ready" edge: if the selected fixture diverged from the bound one, it re-runs the swap exactly once. No effect on a fresh Play or a clean reconnect where the selection never changed. Adds needsFixtureReconcile() + a focused unit test pinning the decision matrix, and asserts boundFixture clears on session reset. Co-authored-by: Cursor <cursoragent@cursor.com>
… clip A fast knob sweep landed as a raw discontinuity on consecutive generations (the runner reads the knob target once per tick and the backend turns it straight into per-inference-step values), driving the audio into distortion/clipping. There was no hysteresis or rate-limiting on control changes. Add an engine-side per-knob slew limiter at the once-per-tick knob read (ACEStepBackend.read_knobs), so a large jump ramps over a few ticks instead of snapping. Applied behind the GeneratorBackend seam, it protects every transport (web, VST, MCP, headless) with one implementation rather than per-UI tweens. Contract-first: the rate ceiling is registry metadata (KnobSpec.slew_max_per_s, projected into the /api/knobs catalog and bumped to KNOB_SCHEMA_VERSION 2). Default is range-relative (DEFAULT_SLEW_FRACTION_PER_S = 3.0, a full-range sweep ramps over ~0.33s) and applies only to continuous float knobs; int/enum/bool knobs (seed, step count, modes, toggles) and non-registry keys (curve specs, the playback clock) pass through verbatim, so the rebuild signature and discrete controls are unaffected. A slew limiter caps only the rate of change, so normal/slow adjustments incur zero added latency. hint_strength and x0_target now read from the slewed knob dict instead of the backing KnobState directly, so the whole translation path honors the limit. Regenerated packages/demon-client/types/wireContract.gen.ts; added tests/unit/test_knob_slew.py (bounded per-tick deltas on a jump, discrete/unknown passthrough, no-lag slow changes, dt cap, catalog projection). Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the tracker bug: "Quickly adjusting settings (especially when exploring unfamiliar parameters) can send output into heavy distortion or clipping. No hysteresis or rate-limiting on control changes means a single fast sweep can produce chaotic, unusable audio."
A fast knob sweep was applied to the running stream as a raw discontinuity. This adds an engine-side, contract-first slew/rate-limiter on continuous knobs so a fast sweep ramps instead of jumping.
Root cause analysis
The path a control change takes from client → engine:
paramsWS channel (~125 Hz).ws_adapter→StreamingSession.set_knobs(acestep/streaming/session.py) coerces it (coerce_knob_values) andvirtual_knobs.update(clean)writes the raw target into the sharedKnobState.backend.read_knobs()(acestep/streaming/pipeline_runner.py~L489). For ACE this returnedself.midi_knobs.get_all_values()verbatim — i.e. whatever the operator last set, with no smoothing (acestep/streaming/ace_backend.pyread_knobs).produce()→_prepare_tick()turns those values straight into the per-inference-step quantities the diffusion stream consumes (k1/denoise, feedback, shift, channel-group gains, guidance, DCW, hint_strength, x0_target, …) and pushes them onto the in-flight slots viaset_shared_curve.So between two consecutive ticks a knob could jump its entire range in one step; consecutive generations then differed discontinuously → audible distortion/clipping. There was no existing
smooth|slew|throttle|hysteresison this path (verified by search). Two values (hint_strength,x0_target) were additionally read straight offKnobState, bypassing even a future read-time smoothing point.Fix
KnobSlewLimiter, applied inACEStepBackend.read_knobs()— the single once-per-tick boundary. Behind theGeneratorBackendseam, so it protects every transport (web, VST, MCP, headless) with one implementation, not per-UI tweens.KnobSpec.slew_max_per_s— projected into the/api/knobscatalog and bumped toKNOB_SCHEMA_VERSION = 2(TS types regenerated).DEFAULT_SLEW_FRACTION_PER_S = 3.0→ a full-range sweep ramps over ~0.33 s. A slew limiter caps only the rate, so normal/slow adjustments pass through with zero added latency.floatknobs are slewed.int/enum/bool(seed,steps_override,rcfg_mode, DCW modes, toggles) and non-registry keys (curve specs, the playback clock) pass through verbatim — the rebuild signature and discrete controls are untouched.dtis wall-clock-paced and capped (max_dt_s) so resuming after an idle pause can't release one un-ramped jump.slew_max_per_s = 0opts a continuous knob out; per-knob overrides honored.hint_strength/x0_targetnow read from the slewed knob dict instead ofKnobStatedirectly, so the whole translation path honors the limit.Test plan / evidence
New
tests/unit/test_knob_slew.py(a large jump is broken into bounded per-tick deltas; discrete + unknown keys pass through; slow changes incur no lag; dt cap; first-sighting snap; runtime-knob limit rebuild; catalog projection).(Local box has no GPU/torch, so torch-dependent unit tests and the GPU golden harness were not run here; the changed translation path stays a verbatim pass-through whenever a knob is steady or moving below its ceiling.)
Made with Cursor