vizcore 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/frontend/index.html +24 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +447 -57
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +391 -10
- data/frontend/src/renderer/layer-manager.js +472 -71
- data/frontend/src/runtime-control-preset.js +44 -0
- data/frontend/src/scene-patches.js +159 -0
- data/frontend/src/shader-error-overlay.js +1 -0
- data/frontend/src/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +13 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/text-renderer.js +13 -0
- data/frontend/src/websocket-client.js +6 -0
- data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
- data/lib/vizcore/analysis/feature_recorder.rb +117 -7
- data/lib/vizcore/analysis/feature_replay.rb +48 -9
- data/lib/vizcore/analysis/pipeline.rb +258 -9
- data/lib/vizcore/analysis/tap_tempo.rb +17 -2
- data/lib/vizcore/audio/calibration.rb +156 -0
- data/lib/vizcore/audio/file_input.rb +28 -0
- data/lib/vizcore/audio/input_manager.rb +36 -1
- data/lib/vizcore/audio/midi_input.rb +5 -0
- data/lib/vizcore/audio/ring_buffer.rb +22 -0
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/dsl_reference.rb +64 -8
- data/lib/vizcore/cli/plugin_checker.rb +93 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
- data/lib/vizcore/cli/scene_inspector.rb +35 -1
- data/lib/vizcore/cli/scene_validator.rb +487 -39
- data/lib/vizcore/cli/shader_template.rb +7 -2
- data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
- data/lib/vizcore/cli.rb +268 -15
- data/lib/vizcore/config.rb +40 -3
- data/lib/vizcore/control_preset.rb +29 -0
- data/lib/vizcore/deep_copy.rb +21 -0
- data/lib/vizcore/dsl/color_helpers.rb +155 -0
- data/lib/vizcore/dsl/engine.rb +219 -23
- data/lib/vizcore/dsl/layer_builder.rb +278 -15
- data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
- data/lib/vizcore/dsl/layout_helpers.rb +290 -0
- data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
- data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
- data/lib/vizcore/dsl/reaction_builder.rb +1 -0
- data/lib/vizcore/dsl/scene_builder.rb +83 -13
- data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
- data/lib/vizcore/dsl/style_builder.rb +3 -0
- data/lib/vizcore/dsl/timeline_builder.rb +91 -8
- data/lib/vizcore/dsl/transition_controller.rb +157 -18
- data/lib/vizcore/dsl.rb +2 -0
- data/lib/vizcore/layer_catalog.rb +1 -0
- data/lib/vizcore/plugin_asset_policy.rb +55 -0
- data/lib/vizcore/project_manifest.rb +12 -2
- data/lib/vizcore/renderer/render_sequence.rb +104 -13
- data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +469 -23
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +676 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +39 -16
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +33 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +55 -4
- metadata +18 -3
data/frontend/src/midi-learn.js
CHANGED
|
@@ -122,7 +122,9 @@ export const midiLearnActionLabel = (action) => {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
if (safeAction.type === "switch_scene") {
|
|
125
|
-
return
|
|
125
|
+
return safeAction.effect
|
|
126
|
+
? `Switch scene: ${safeAction.scene} (${safeAction.effect?.name || "with effect"})`
|
|
127
|
+
: `Switch scene: ${safeAction.scene}`;
|
|
126
128
|
}
|
|
127
129
|
if (safeAction.type === "live_control") {
|
|
128
130
|
return `Toggle ${safeAction.control}`;
|
|
@@ -150,7 +152,11 @@ const normalizeMidiAction = (action) => {
|
|
|
150
152
|
|
|
151
153
|
if (type === "switch_scene") {
|
|
152
154
|
const scene = String(action.scene || "").trim();
|
|
153
|
-
|
|
155
|
+
if (!scene) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const effect = normalizeTransitionEffect(action.effect);
|
|
159
|
+
return effect ? { type, scene, effect } : { type, scene };
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
if (type === "live_control") {
|
|
@@ -167,6 +173,20 @@ const normalizeMidiSignature = (signature) => {
|
|
|
167
173
|
return value.includes(":") ? value : null;
|
|
168
174
|
};
|
|
169
175
|
|
|
176
|
+
const normalizeTransitionEffect = (effect) => {
|
|
177
|
+
if (!effect) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (typeof effect === "string" || typeof effect === "number" || typeof effect === "symbol") {
|
|
181
|
+
return { name: String(effect) };
|
|
182
|
+
}
|
|
183
|
+
if (typeof effect !== "object" || Array.isArray(effect)) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return effect;
|
|
188
|
+
};
|
|
189
|
+
|
|
170
190
|
const normalizeMidiBytes = (data) => {
|
|
171
191
|
if (Array.isArray(data) || ArrayBuffer.isView(data)) {
|
|
172
192
|
return Array.from(data).map((value) => Number(value) || 0);
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
const DEFAULT_EXPECTED_FRAME_MS = 1000 / 60;
|
|
2
2
|
const FPS_WINDOW_MS = 500;
|
|
3
|
+
const LATENCY_HISTORY_MAX = 120;
|
|
3
4
|
|
|
4
5
|
export const createPerformanceMonitorState = () => ({
|
|
5
6
|
audioLatencyMs: null,
|
|
7
|
+
audioCaptureMs: null,
|
|
8
|
+
audioAnalysisMs: null,
|
|
9
|
+
sceneBuildMs: null,
|
|
6
10
|
clockOffsetMs: null,
|
|
7
11
|
droppedFrames: 0,
|
|
12
|
+
wsDroppedFrames: 0,
|
|
13
|
+
wsActiveClients: 0,
|
|
14
|
+
wsEstimatedLagFrames: 0,
|
|
15
|
+
wsAvgPayloadBytes: 0,
|
|
8
16
|
fps: 0,
|
|
9
17
|
frameMs: 0,
|
|
10
18
|
lastRenderAtMs: null,
|
|
11
19
|
lastSocketTimestampMs: null,
|
|
20
|
+
latencyProbeSamples: [],
|
|
21
|
+
latencyProbeMaxMs: null,
|
|
22
|
+
latencyProbeP95Ms: null,
|
|
23
|
+
rendererFloatColorBuffer: false,
|
|
24
|
+
rendererTextureFloat: false,
|
|
25
|
+
rendererMaxDrawBuffers: null,
|
|
12
26
|
reconnects: 0,
|
|
27
|
+
rendererDpr: null,
|
|
28
|
+
rendererMaxTextureSize: null,
|
|
29
|
+
rendererSafeMode: false,
|
|
13
30
|
renderWindowFrames: 0,
|
|
14
31
|
renderWindowStartedAtMs: null,
|
|
15
32
|
rttMs: null,
|
|
@@ -69,12 +86,18 @@ export const recordSocketFrame = (
|
|
|
69
86
|
const frameGapMs = previousTimestamp === null ? 0 : Math.max(0, timestampMs - previousTimestamp);
|
|
70
87
|
const droppedFrames = Number(state?.droppedFrames || 0) + estimateDroppedFrames(frameGapMs, expectedFrameMs);
|
|
71
88
|
const audioLatencyMs = audioLatencyFromMetrics(frame?.metrics, state?.audioLatencyMs);
|
|
89
|
+
const audioCaptureMs = audioCaptureFromMetrics(frame?.metrics, state?.audioCaptureMs);
|
|
90
|
+
const audioAnalysisMs = audioAnalysisFromMetrics(frame?.metrics, state?.audioAnalysisMs);
|
|
91
|
+
const sceneBuildMs = sceneBuildFromMetrics(frame?.metrics, state?.sceneBuildMs);
|
|
72
92
|
const clockOffsetMs = Number.isFinite(state?.clockOffsetMs) ? Number(state.clockOffsetMs) : 0;
|
|
73
93
|
const browserTimestampMs = timestampMs - clockOffsetMs;
|
|
74
94
|
|
|
75
95
|
return {
|
|
76
96
|
...state,
|
|
77
97
|
audioLatencyMs,
|
|
98
|
+
audioCaptureMs,
|
|
99
|
+
audioAnalysisMs,
|
|
100
|
+
sceneBuildMs,
|
|
78
101
|
droppedFrames,
|
|
79
102
|
lastSocketTimestampMs: timestampMs,
|
|
80
103
|
wsLatencyMs: Math.max(0, Math.round(receivedAt - browserTimestampMs)),
|
|
@@ -101,9 +124,18 @@ export const recordLatencyProbe = (state, payload, receivedAtMs) => {
|
|
|
101
124
|
const serverProcessingMs = serverSentAtMs - serverReceivedAtMs;
|
|
102
125
|
const rttMs = Math.max(0, browserReceivedAtMs - clientSentAtMs - serverProcessingMs);
|
|
103
126
|
const clockOffsetMs = ((serverReceivedAtMs - clientSentAtMs) + (serverSentAtMs - browserReceivedAtMs)) / 2;
|
|
127
|
+
const nextLatencySamples = appendNumericHistory(
|
|
128
|
+
Array.isArray(state?.latencyProbeSamples) ? state.latencyProbeSamples : [],
|
|
129
|
+
rttMs,
|
|
130
|
+
LATENCY_HISTORY_MAX,
|
|
131
|
+
);
|
|
132
|
+
const { max, p95 } = latencyProbeStats(nextLatencySamples);
|
|
104
133
|
|
|
105
134
|
return {
|
|
106
135
|
...state,
|
|
136
|
+
latencyProbeSamples: nextLatencySamples,
|
|
137
|
+
latencyProbeMaxMs: max,
|
|
138
|
+
latencyProbeP95Ms: p95,
|
|
107
139
|
clockOffsetMs: Math.round(clockOffsetMs),
|
|
108
140
|
rttMs: Math.round(rttMs),
|
|
109
141
|
};
|
|
@@ -132,6 +164,46 @@ export const recordShaderCompile = (state, detail) => {
|
|
|
132
164
|
};
|
|
133
165
|
};
|
|
134
166
|
|
|
167
|
+
export const recordRendererCapabilities = (state, detail) => {
|
|
168
|
+
const effectiveDpr = coerceMetric(detail?.effectiveDevicePixelRatio);
|
|
169
|
+
const maxTextureSize = coerceMetric(detail?.maxTextureSize);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...state,
|
|
173
|
+
rendererDpr: effectiveDpr ?? state?.rendererDpr ?? null,
|
|
174
|
+
rendererMaxTextureSize: maxTextureSize ?? state?.rendererMaxTextureSize ?? null,
|
|
175
|
+
rendererMaxDrawBuffers: Number.isFinite(coerceMetric(detail?.maxDrawBuffers))
|
|
176
|
+
? coerceMetric(detail.maxDrawBuffers)
|
|
177
|
+
: state?.rendererMaxDrawBuffers ?? null,
|
|
178
|
+
rendererFloatColorBuffer: !!detail?.floatColorBuffer,
|
|
179
|
+
rendererTextureFloat: !!detail?.textureFloat,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const recordRendererSafeMode = (state, detail) => {
|
|
184
|
+
return {
|
|
185
|
+
...state,
|
|
186
|
+
rendererSafeMode: !!detail?.active,
|
|
187
|
+
rendererDpr: coerceMetric(detail?.effectiveDevicePixelRatio) ?? state?.rendererDpr ?? null,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const recordWebSocketBackpressure = (state, detail) => {
|
|
192
|
+
const total = detail?.total || {};
|
|
193
|
+
const clients = Array.isArray(detail?.clients) ? detail.clients : [];
|
|
194
|
+
const averageLag = clients.length > 0
|
|
195
|
+
? Math.max(0, clients.reduce((acc, entry) => acc + coerceFiniteNumber(entry?.estimated_lag_frames || 0), 0) / clients.length)
|
|
196
|
+
: 0;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...state,
|
|
200
|
+
wsDroppedFrames: Number(total?.dropped_frames || 0),
|
|
201
|
+
wsActiveClients: Number(detail?.active_clients || 0),
|
|
202
|
+
wsAvgPayloadBytes: Number(coerceMetric(total?.avg_payload_bytes) || 0),
|
|
203
|
+
wsEstimatedLagFrames: averageLag,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
135
207
|
export const formatPerformanceMonitorText = (state) => {
|
|
136
208
|
const fps = Number(state?.fps || 0) > 0 ? Number(state.fps).toFixed(1) : "--";
|
|
137
209
|
const frameMs = Number(state?.frameMs || 0) > 0 ? `${Number(state.frameMs).toFixed(1)}ms` : "--";
|
|
@@ -139,11 +211,25 @@ export const formatPerformanceMonitorText = (state) => {
|
|
|
139
211
|
const rtt = Number.isFinite(state?.rttMs) ? `${Math.round(state.rttMs)}ms` : "--";
|
|
140
212
|
const clockOffset = Number.isFinite(state?.clockOffsetMs) ? `${formatSignedInteger(state.clockOffsetMs)}ms` : "--";
|
|
141
213
|
const audioLatency = Number.isFinite(state?.audioLatencyMs) ? `${Number(state.audioLatencyMs).toFixed(1)}ms` : "--";
|
|
214
|
+
const audioCaptureMs = Number.isFinite(state?.audioCaptureMs) ? `${Number(state.audioCaptureMs).toFixed(1)}ms` : "--";
|
|
215
|
+
const audioAnalysisMs = Number.isFinite(state?.audioAnalysisMs) ? `${Number(state.audioAnalysisMs).toFixed(1)}ms` : "--";
|
|
216
|
+
const sceneBuildMs = Number.isFinite(state?.sceneBuildMs) ? `${Number(state.sceneBuildMs).toFixed(1)}ms` : "--";
|
|
217
|
+
const probeMax = Number.isFinite(state?.latencyProbeMaxMs) ? `${Math.round(state.latencyProbeMaxMs)}ms` : "--";
|
|
218
|
+
const probeP95 = Number.isFinite(state?.latencyProbeP95Ms) ? `${Math.round(state.latencyProbeP95Ms)}ms` : "--";
|
|
142
219
|
const shaderCompile = Number.isFinite(state?.shaderCompileMs) ? `${Number(state.shaderCompileMs).toFixed(1)}ms` : "--";
|
|
220
|
+
const rendererDpr = Number.isFinite(state?.rendererDpr) ? `${Number(state.rendererDpr).toFixed(2)}x` : "--";
|
|
221
|
+
const maxTexture = Number.isFinite(state?.rendererMaxTextureSize) ? Math.round(state.rendererMaxTextureSize) : "--";
|
|
222
|
+
const maxDrawBuffers = Number.isFinite(state?.rendererMaxDrawBuffers) ? Math.round(state.rendererMaxDrawBuffers) : "--";
|
|
223
|
+
const floatColorBuffer = state?.rendererFloatColorBuffer ? "floatColorBuffer yes" : "floatColorBuffer no";
|
|
224
|
+
const textureFloat = state?.rendererTextureFloat ? "textureFloat yes" : "textureFloat no";
|
|
225
|
+
const safeMode = state?.rendererSafeMode ? "on" : "off";
|
|
143
226
|
const droppedFrames = Math.max(0, Number(state?.droppedFrames || 0));
|
|
227
|
+
const wsDroppedFrames = Math.max(0, Number(state?.wsDroppedFrames || 0));
|
|
228
|
+
const wsEstimatedLagFrames = Math.max(0, Number(state?.wsEstimatedLagFrames || 0));
|
|
144
229
|
const reconnects = Math.max(0, Number(state?.reconnects || 0));
|
|
230
|
+
const wsAvgPayload = Number.isFinite(state?.wsAvgPayloadBytes) ? `${Math.round(state.wsAvgPayloadBytes)}B` : "--";
|
|
145
231
|
|
|
146
|
-
return `Perf: ${fps} FPS | Frame ${frameMs} | WS ${wsLatency} | RTT ${rtt} | Clock ${clockOffset} | Drop ${droppedFrames} | Audio ${audioLatency} | Shader ${shaderCompile} | Reconnect ${reconnects}`;
|
|
232
|
+
return `Perf: ${fps} FPS | Frame ${frameMs} | WS ${wsLatency} | RTT ${rtt} | Probe max ${probeMax} | Probe p95 ${probeP95} | Clock ${clockOffset} | Drop ${droppedFrames} | BDrop ${wsDroppedFrames} | WSLag ${wsEstimatedLagFrames.toFixed(1)}f | Audio ${audioLatency} | Capture ${audioCaptureMs} | Analyze ${audioAnalysisMs} | Build ${sceneBuildMs} | Shader ${shaderCompile} | DPR ${rendererDpr} | MaxTex ${maxTexture} | DrawBuf ${maxDrawBuffers} | ${floatColorBuffer} | ${textureFloat} | Safe ${safeMode} | Backpressure ${wsAvgPayload} | Reconnect ${reconnects}`;
|
|
147
233
|
};
|
|
148
234
|
|
|
149
235
|
export const estimateDroppedFrames = (frameGapMs, expectedFrameMs = DEFAULT_EXPECTED_FRAME_MS) => {
|
|
@@ -166,6 +252,21 @@ const audioLatencyFromMetrics = (metrics, fallback) => {
|
|
|
166
252
|
return (captureMs || 0) + (analysisMs || 0);
|
|
167
253
|
};
|
|
168
254
|
|
|
255
|
+
const audioCaptureFromMetrics = (metrics, fallback) => {
|
|
256
|
+
const captureMs = coerceMetric(metrics?.audio_capture_ms);
|
|
257
|
+
return captureMs ?? fallback ?? null;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const audioAnalysisFromMetrics = (metrics, fallback) => {
|
|
261
|
+
const analysisMs = coerceMetric(metrics?.audio_analysis_ms);
|
|
262
|
+
return analysisMs ?? fallback ?? null;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const sceneBuildFromMetrics = (metrics, fallback) => {
|
|
266
|
+
const buildMs = coerceMetric(metrics?.scene_build_ms);
|
|
267
|
+
return buildMs ?? fallback ?? null;
|
|
268
|
+
};
|
|
269
|
+
|
|
169
270
|
const coerceMetric = (value) => {
|
|
170
271
|
const numeric = Number(value);
|
|
171
272
|
if (!Number.isFinite(numeric)) {
|
|
@@ -177,7 +278,42 @@ const coerceMetric = (value) => {
|
|
|
177
278
|
|
|
178
279
|
const roundOneDecimal = (value) => Math.round(value * 10) / 10;
|
|
179
280
|
|
|
281
|
+
const coerceFiniteNumber = (value) => {
|
|
282
|
+
const numeric = Number(value);
|
|
283
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
284
|
+
};
|
|
285
|
+
|
|
180
286
|
const formatSignedInteger = (value) => {
|
|
181
287
|
const rounded = Math.round(Number(value) || 0);
|
|
182
288
|
return rounded > 0 ? `+${rounded}` : `${rounded}`;
|
|
183
289
|
};
|
|
290
|
+
|
|
291
|
+
const appendNumericHistory = (history = [], sample, maxLength = LATENCY_HISTORY_MAX) => {
|
|
292
|
+
if (!Number.isFinite(sample)) {
|
|
293
|
+
return Array.isArray(history) ? history.slice(-maxLength) : [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const keep = Math.max(1, Number(maxLength) || 1);
|
|
297
|
+
const sanitizedHistory = Array.isArray(history)
|
|
298
|
+
? history.filter((entry) => Number.isFinite(entry)).map((entry) => Number(entry))
|
|
299
|
+
: [];
|
|
300
|
+
const next = [...sanitizedHistory, Number(sample)];
|
|
301
|
+
if (next.length <= keep) {
|
|
302
|
+
return next;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return next.slice(next.length - keep);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const latencyProbeStats = (samples) => {
|
|
309
|
+
const numericSamples = (Array.isArray(samples) ? samples : []).filter((entry) => Number.isFinite(entry));
|
|
310
|
+
if (numericSamples.length === 0) {
|
|
311
|
+
return { max: null, p95: null };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sorted = [...numericSamples].sort((left, right) => left - right);
|
|
315
|
+
const max = sorted[sorted.length - 1];
|
|
316
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(0.95 * sorted.length) - 1));
|
|
317
|
+
|
|
318
|
+
return { max, p95: sorted[index] };
|
|
319
|
+
};
|