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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/index.html +24 -2
  3. data/frontend/src/audio-inspector.js +9 -0
  4. data/frontend/src/live-controls.js +219 -7
  5. data/frontend/src/main.js +447 -57
  6. data/frontend/src/midi-learn.js +22 -2
  7. data/frontend/src/performance-monitor.js +137 -1
  8. data/frontend/src/renderer/engine.js +391 -10
  9. data/frontend/src/renderer/layer-manager.js +472 -71
  10. data/frontend/src/runtime-control-preset.js +44 -0
  11. data/frontend/src/scene-patches.js +159 -0
  12. data/frontend/src/shader-error-overlay.js +1 -0
  13. data/frontend/src/visuals/image-renderer.js +19 -0
  14. data/frontend/src/visuals/particle-system.js +10 -0
  15. data/frontend/src/visuals/shape-renderer.js +13 -0
  16. data/frontend/src/visuals/spectrogram-renderer.js +14 -0
  17. data/frontend/src/visuals/text-renderer.js +13 -0
  18. data/frontend/src/websocket-client.js +6 -0
  19. data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
  20. data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
  21. data/lib/vizcore/analysis/feature_recorder.rb +117 -7
  22. data/lib/vizcore/analysis/feature_replay.rb +48 -9
  23. data/lib/vizcore/analysis/pipeline.rb +258 -9
  24. data/lib/vizcore/analysis/tap_tempo.rb +17 -2
  25. data/lib/vizcore/audio/calibration.rb +156 -0
  26. data/lib/vizcore/audio/file_input.rb +28 -0
  27. data/lib/vizcore/audio/input_manager.rb +36 -1
  28. data/lib/vizcore/audio/midi_input.rb +5 -0
  29. data/lib/vizcore/audio/ring_buffer.rb +22 -0
  30. data/lib/vizcore/audio.rb +1 -0
  31. data/lib/vizcore/cli/dsl_reference.rb +64 -8
  32. data/lib/vizcore/cli/plugin_checker.rb +93 -0
  33. data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
  34. data/lib/vizcore/cli/scene_inspector.rb +35 -1
  35. data/lib/vizcore/cli/scene_validator.rb +487 -39
  36. data/lib/vizcore/cli/shader_template.rb +7 -2
  37. data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
  38. data/lib/vizcore/cli.rb +268 -15
  39. data/lib/vizcore/config.rb +40 -3
  40. data/lib/vizcore/control_preset.rb +29 -0
  41. data/lib/vizcore/deep_copy.rb +21 -0
  42. data/lib/vizcore/dsl/color_helpers.rb +155 -0
  43. data/lib/vizcore/dsl/engine.rb +219 -23
  44. data/lib/vizcore/dsl/layer_builder.rb +278 -15
  45. data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
  46. data/lib/vizcore/dsl/layout_helpers.rb +290 -0
  47. data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
  49. data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
  50. data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
  51. data/lib/vizcore/dsl/reaction_builder.rb +1 -0
  52. data/lib/vizcore/dsl/scene_builder.rb +83 -13
  53. data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
  54. data/lib/vizcore/dsl/style_builder.rb +3 -0
  55. data/lib/vizcore/dsl/timeline_builder.rb +91 -8
  56. data/lib/vizcore/dsl/transition_controller.rb +157 -18
  57. data/lib/vizcore/dsl.rb +2 -0
  58. data/lib/vizcore/layer_catalog.rb +1 -0
  59. data/lib/vizcore/plugin_asset_policy.rb +55 -0
  60. data/lib/vizcore/project_manifest.rb +12 -2
  61. data/lib/vizcore/renderer/render_sequence.rb +104 -13
  62. data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
  63. data/lib/vizcore/renderer/scene_serializer.rb +38 -0
  64. data/lib/vizcore/renderer/snapshot.rb +4 -3
  65. data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
  66. data/lib/vizcore/scene_trust.rb +31 -0
  67. data/lib/vizcore/server/frame_broadcaster.rb +469 -23
  68. data/lib/vizcore/server/rack_app.rb +151 -4
  69. data/lib/vizcore/server/runner.rb +676 -82
  70. data/lib/vizcore/server/websocket_handler.rb +236 -14
  71. data/lib/vizcore/server.rb +21 -0
  72. data/lib/vizcore/shape.rb +39 -16
  73. data/lib/vizcore/sync/osc_message.rb +66 -9
  74. data/lib/vizcore/version.rb +1 -1
  75. data/lib/vizcore.rb +33 -0
  76. data/scripts/browser_capture.mjs +31 -2
  77. data/sig/vizcore.rbs +55 -4
  78. metadata +18 -3
@@ -122,7 +122,9 @@ export const midiLearnActionLabel = (action) => {
122
122
  }
123
123
 
124
124
  if (safeAction.type === "switch_scene") {
125
- return `Switch scene: ${safeAction.scene}`;
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
- return scene ? { type, scene } : null;
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
+ };