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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c750ff1ca41db0024074232a588262d5220e4063cdb057421c25465b1de102eb
4
- data.tar.gz: 3123d34ed03e497dde1530bc41ade0c5177c519072713817429d5e8187d97059
3
+ metadata.gz: 7944c441173090fb3e3ed70588cc7a6705697c140f516bc9502c5208c76577d3
4
+ data.tar.gz: 34896b6dedbb509e42d36a36a22d19f775e1f1a56031890bcf1d295744e331bb
5
5
  SHA512:
6
- metadata.gz: ce9f65e426497957526ce5e73cd6eb4cfb0f5981fb1f3151cb12a39201d4d0bb3b5dad0b2563e5fe7220c2357e94e5efcb7956e49efd8167f836cbf89d22b875
7
- data.tar.gz: b6ba9c3755a3ca4ba4e1fb4f163d55e0275c7ba513d8d39891893528238e39fe553cc020c14538bfac49e1a1f355e161e899904c1c0f57e77aa47e027444c774
6
+ metadata.gz: 3f86f14c01cfc947279eab536433e6f95fd2ee1abdaa0b1e55fc617af28c555f2241bc929113679fc247dd847099c08bc554a7126dacda16b8c689773f9f59d4
7
+ data.tar.gz: 2ec0d8c023f3657250e5752475df36c7c50dac117153df03618c9bdb21c19f7f8bcdbd5028c365be96e9fc633a8b62a718a32d04fc5edca2f08f7d00cf74e93a
data/frontend/index.html CHANGED
@@ -459,6 +459,7 @@
459
459
  <p id="ws-status">WebSocket: connecting...</p>
460
460
  <p id="scene-status">Scene: unknown</p>
461
461
  <p id="transition-status">Transition: none</p>
462
+ <p id="runtime-error-status">Runtime: ok</p>
462
463
  <p id="frame-status">Amplitude: 0.0000</p>
463
464
  <p id="bpm-status" class="is-accent">BPM: --</p>
464
465
  <p id="beat-status">Beat: off | Count: 0</p>
@@ -467,7 +468,7 @@
467
468
  <button id="freeze-toggle" type="button" aria-pressed="false">Freeze</button>
468
469
  <p id="live-control-status" class="live-controls__status">Live: output</p>
469
470
  </div>
470
- <p id="performance-monitor" class="performance-monitor">Perf: -- FPS | Frame -- | WS -- | RTT -- | Clock -- | Drop 0 | Audio -- | Shader -- | Reconnect 0</p>
471
+ <p id="performance-monitor" class="performance-monitor">Perf: -- FPS | Frame -- | WS -- | RTT -- | Probe max -- | Probe p95 -- | Clock -- | Drop 0 | BDrop 0 | WSLag --f | Audio -- | Capture -- | Analyze -- | Build -- | Shader -- | DPR -- | MaxTex -- | DrawBuf -- | floatColorBuffer no | textureFloat no | Safe off | Backpressure -- | Reconnect 0</p>
471
472
  <div class="audio-inspector" aria-label="Audio inspector">
472
473
  <div class="audio-inspector__header">
473
474
  <span>Audio</span>
@@ -501,6 +502,7 @@
501
502
  <div id="fft-preview" class="fft-preview" aria-label="FFT preview"></div>
502
503
  </div>
503
504
  <p id="audio-source-status">Audio Source: unknown</p>
505
+ <p id="audio-health-status">Audio Health: unknown</p>
504
506
  <p id="audio-track-status">Track: none</p>
505
507
  <p id="audio-playback-status">Playback: unavailable</p>
506
508
  <div class="reactivity-controls" aria-label="Visual reactivity controls">
@@ -549,7 +551,12 @@
549
551
  window.__vizcoreMainStarted = false;
550
552
  (function () {
551
553
  var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
552
- var websocketUrl = protocol + "//" + window.location.host + "/ws";
554
+ var pathname = String(window.location.pathname || "");
555
+ var search = String(window.location.search || "");
556
+ var params = new URLSearchParams(search);
557
+ var mode = String(params.get("mode") || "").toLowerCase();
558
+ var role = mode === "projector" || pathname.indexOf("/projector") === 0 ? "projector" : (mode === "monitor" ? "monitor" : "control");
559
+ var websocketUrl = protocol + "//" + window.location.host + "/ws?role=" + encodeURIComponent(role);
553
560
  var fallbackTimer = window.setTimeout(function () {
554
561
  if (window.__vizcoreMainStarted) return;
555
562
 
@@ -559,6 +566,7 @@
559
566
  var bpmStatus = document.getElementById("bpm-status");
560
567
  var beatStatus = document.getElementById("beat-status");
561
568
  var audioSourceStatus = document.getElementById("audio-source-status");
569
+ var audioHealthStatus = document.getElementById("audio-health-status");
562
570
  var peakStatus = document.getElementById("inspector-peak");
563
571
  var ampValue = document.getElementById("inspector-amplitude-value");
564
572
  var ampFill = document.getElementById("inspector-amplitude-fill");
@@ -582,6 +590,20 @@
582
590
  if (runtime && audioSourceStatus) {
583
591
  audioSourceStatus.textContent = "Audio Source: " + runtime.audio_source;
584
592
  }
593
+ if (runtime && audioHealthStatus) {
594
+ var input = runtime.input || {};
595
+ var ringBuffer = input.ring_buffer || {};
596
+ var sampleRate = Number(input.sample_rate || 0);
597
+ var requestedRate = Number(input.requested_sample_rate || 0);
598
+ var frameSize = Number(input.frame_size || 0);
599
+ var overrun = Number(ringBuffer.overrun_count || 0);
600
+ var underrun = Number(ringBuffer.underrun_count || 0);
601
+ var mismatch = input.sample_rate_mismatch ? " mismatched" : "";
602
+ var rateText = "Rate: " + (sampleRate > 0 ? sampleRate : "--") + "Hz" + (requestedRate > 0 && requestedRate !== sampleRate ? " (req " + requestedRate + "Hz)" : "");
603
+ var frameText = "Frame: " + (frameSize > 0 ? frameSize : "--");
604
+ var healthText = "Ring: " + overrun + "/" + underrun + " over/under";
605
+ audioHealthStatus.textContent = "Audio Health: " + rateText + ", " + frameText + mismatch + ", " + healthText;
606
+ }
585
607
  })
586
608
  .catch(function () {});
587
609
 
@@ -6,14 +6,23 @@ export const buildAudioInspectorState = (audio, fftBins = DEFAULT_FFT_BINS) => {
6
6
  result[key] = clamp01(audio?.bands?.[key]);
7
7
  return result;
8
8
  }, {});
9
+ const bandPeaks = BAND_KEYS.reduce((result, key) => {
10
+ result[key] = clamp01(audio?.band_peaks?.[key]);
11
+ return result;
12
+ }, {});
9
13
 
10
14
  return {
11
15
  amplitude: clamp01(audio?.amplitude),
12
16
  bands,
17
+ bandPeaks,
13
18
  fft: normalizeFft(audio?.fft, fftBins),
14
19
  bpm: Number(audio?.bpm || 0),
15
20
  beat: !!audio?.beat,
16
21
  beatPulse: clamp01(audio?.beat_pulse),
22
+ beatPhase: clamp01(audio?.beat_phase),
23
+ barPhase: clamp01(audio?.bar_phase),
24
+ barCount: Math.max(0, Number(audio?.bar_count || 0) || 0),
25
+ phraseCount: Math.max(0, Number(audio?.phrase_count || 0) || 0),
17
26
  peakFrequency: Math.max(0, Number(audio?.peak_frequency || 0) || 0),
18
27
  };
19
28
  };
@@ -1,24 +1,185 @@
1
+ const finiteFloat = (value) => {
2
+ const numeric = Number(value);
3
+ return Number.isFinite(numeric) ? numeric : null;
4
+ };
5
+
6
+ const clamp01 = (value) => {
7
+ if (!Number.isFinite(value)) {
8
+ return null;
9
+ }
10
+ return Math.min(1, Math.max(0, value));
11
+ };
12
+
13
+ const parseHexColor = (value) => {
14
+ const raw = String(value || "").trim();
15
+ if (!raw.startsWith("#")) {
16
+ return null;
17
+ }
18
+
19
+ const rawHex = raw.slice(1);
20
+ const isShort = rawHex.length === 3 || rawHex.length === 4;
21
+ const isLong = rawHex.length === 6 || rawHex.length === 8;
22
+ if (!/^[0-9a-fA-F]+$/.test(rawHex) || (!isShort && !isLong)) {
23
+ return null;
24
+ }
25
+
26
+ const expanded = isShort
27
+ ? rawHex.split("").map((entry) => `${entry}${entry}`).join("")
28
+ : rawHex;
29
+
30
+ const channels = [];
31
+ for (let index = 0; index < expanded.length; index += 2) {
32
+ const channel = Number.parseInt(expanded.slice(index, index + 2), 16);
33
+ channels.push(clamp01(channel / 255));
34
+ }
35
+
36
+ return channels;
37
+ };
38
+
39
+ const normalizeColorChannels = (value, hasAlpha = false) => {
40
+ const values = Array.from(value || []);
41
+ if (values.length < 3 || values.length > 4) {
42
+ return null;
43
+ }
44
+
45
+ const rgbValues = values.slice(0, 3);
46
+ const shouldScaleRgbBy255 = rgbValues.some((channel) => channel > 1);
47
+ const rgb = rgbValues.map((channel) => (shouldScaleRgbBy255 ? clamp01(channel / 255) : clamp01(channel)));
48
+ const alpha = values.length === 4 ? clamp01(values[3] > 1 ? values[3] / 255 : values[3]) : null;
49
+ if (rgb.includes(null) || (values.length === 4 && alpha === null)) {
50
+ return null;
51
+ }
52
+
53
+ return hasAlpha ? [...rgb, alpha] : rgb;
54
+ };
55
+
56
+ const normalizeLiveControlColor = (value) => {
57
+ if (value == null) {
58
+ return null;
59
+ }
60
+
61
+ if (Array.isArray(value)) {
62
+ const channels = Array.from(value)
63
+ .slice(0, 4)
64
+ .map((channel) => Number(channel));
65
+ if (channels.length < 3 || channels.some((channel) => !Number.isFinite(channel))) {
66
+ return null;
67
+ }
68
+
69
+ const rgbValues = channels.slice(0, 3);
70
+ const alpha = channels.length === 4 ? clamp01(channels[3] > 1 ? channels[3] / 255 : channels[3]) : null;
71
+ if (channels.length === 4 && alpha === null) {
72
+ return null;
73
+ }
74
+
75
+ const shouldScaleRgbBy255 = rgbValues.some((channel) => channel > 1);
76
+ const normalizedRgb = rgbValues.map((channel) => (shouldScaleRgbBy255 ? clamp01(channel / 255) : clamp01(channel)));
77
+ return channels.length === 4 ? [...normalizedRgb, alpha] : normalizedRgb;
78
+ }
79
+
80
+ const parsed = parseHexColor(value);
81
+ if (!parsed) {
82
+ return null;
83
+ }
84
+
85
+ return normalizeColorChannels(parsed, parsed.length === 4);
86
+ };
87
+
88
+ const createLiveControlEntry = (enabled = false, fade = undefined, release = undefined, color = undefined) => {
89
+ return compactLiveControlEntry({
90
+ enabled: !!enabled,
91
+ fade: finiteFloat(fade),
92
+ release: finiteFloat(release),
93
+ color: normalizeLiveControlColor(color),
94
+ });
95
+ };
96
+
97
+ const compactLiveControlEntry = (entry) => {
98
+ const output = { ...entry };
99
+ if (!Object.prototype.hasOwnProperty.call(output, "fade")) {
100
+ return output;
101
+ }
102
+ if (output.fade === null || output.fade === undefined) {
103
+ delete output.fade;
104
+ }
105
+ if (output.release === null || output.release === undefined) {
106
+ delete output.release;
107
+ }
108
+ if (output.color === null || output.color === undefined) {
109
+ delete output.color;
110
+ }
111
+ return output;
112
+ };
113
+
114
+ const normalizeLiveControlState = (value) => {
115
+ if (value && typeof value === "object" && !Array.isArray(value)) {
116
+ if (Object.prototype.hasOwnProperty.call(value, "value")) {
117
+ return compactLiveControlEntry(createLiveControlEntry(
118
+ !!value.value,
119
+ value.fade,
120
+ value.release,
121
+ value.color
122
+ ));
123
+ }
124
+
125
+ return compactLiveControlEntry(createLiveControlEntry(
126
+ Object.prototype.hasOwnProperty.call(value, "enabled") ? !!value.enabled : false,
127
+ value.fade,
128
+ value.release,
129
+ value.color
130
+ ));
131
+ }
132
+
133
+ return compactLiveControlEntry(createLiveControlEntry(!!value));
134
+ };
135
+
1
136
  export const createLiveControlState = () => ({
2
- blackout: false,
3
- freeze: false,
137
+ blackout: createLiveControlEntry(false),
138
+ freeze: createLiveControlEntry(false),
4
139
  });
5
140
 
141
+ export const isLiveControlEnabled = (state) => {
142
+ if (!state) {
143
+ return false;
144
+ }
145
+
146
+ if (typeof state === "object") {
147
+ return !!state.enabled;
148
+ }
149
+
150
+ return !!state;
151
+ };
152
+
153
+ export const isLiveControlActive = (state) => {
154
+ if (!state || typeof state !== "object" || Array.isArray(state)) {
155
+ return isLiveControlEnabled(state);
156
+ }
157
+
158
+ return !!state.enabled;
159
+ };
160
+
6
161
  export const toggleLiveControl = (state, key) => {
7
162
  const control = String(key || "");
8
163
  if (control !== "blackout" && control !== "freeze") {
9
164
  return { ...state };
10
165
  }
11
166
 
167
+ const current = normalizeLiveControlState(state?.[control]);
12
168
  return {
13
169
  ...state,
14
- [control]: !state?.[control],
170
+ [control]: {
171
+ ...current,
172
+ enabled: !current.enabled,
173
+ },
15
174
  };
16
175
  };
17
176
 
177
+ export const normalizeLiveControlPayload = (state) => normalizeLiveControlState(state);
178
+
18
179
  export const liveControlStatusText = (state) => {
19
180
  const values = [];
20
- if (state?.blackout) values.push("Blackout");
21
- if (state?.freeze) values.push("Freeze");
181
+ if (isLiveControlActive(state?.blackout)) values.push("Blackout");
182
+ if (isLiveControlActive(state?.freeze)) values.push("Freeze");
22
183
  return values.length ? `Live: ${values.join(" + ")}` : "Live: output";
23
184
  };
24
185
 
@@ -107,17 +268,59 @@ const normalizeKeyboardAction = (action) => {
107
268
  const type = String(action?.type || "").trim();
108
269
  if (type === "switch_scene") {
109
270
  const scene = String(action?.scene || "").trim();
110
- return scene ? { type, scene } : null;
271
+ if (!scene) {
272
+ return null;
273
+ }
274
+ const effect = normalizeTransitionEffect(action?.effect);
275
+ return effect ? { type, scene, effect } : { type, scene };
111
276
  }
112
277
 
113
278
  if (type === "live_control") {
114
279
  const control = String(action?.control || "").trim();
115
- return control === "blackout" || control === "freeze" ? { type, control } : null;
280
+ if (control !== "blackout" && control !== "freeze") {
281
+ return null;
282
+ }
283
+
284
+ const payload = { type, control };
285
+ if (Object.prototype.hasOwnProperty.call(action, "value")) {
286
+ payload.value = action.value;
287
+ }
288
+
289
+ const fade = finiteFloat(action?.fade);
290
+ if (fade !== null) {
291
+ payload.fade = fade;
292
+ }
293
+
294
+ const release = finiteFloat(action?.release);
295
+ if (release !== null) {
296
+ payload.release = release;
297
+ }
298
+
299
+ const color = normalizeLiveControlColor(action?.color);
300
+ if (color !== null) {
301
+ payload.color = color;
302
+ }
303
+
304
+ return payload;
116
305
  }
117
306
 
118
307
  return null;
119
308
  };
120
309
 
310
+ const normalizeTransitionEffect = (effect) => {
311
+ if (!effect) {
312
+ return null;
313
+ }
314
+ if (typeof effect === "string" || typeof effect === "number" || typeof effect === "symbol") {
315
+ return { name: String(effect) };
316
+ }
317
+ if (typeof effect !== "object" || Array.isArray(effect)) {
318
+ return null;
319
+ }
320
+
321
+ return effect;
322
+ };
323
+
121
324
  export const isEditableShortcutTarget = (target) => {
122
325
  if (!target) {
123
326
  return false;
@@ -129,3 +332,12 @@ export const isEditableShortcutTarget = (target) => {
129
332
  || tagName === "select"
130
333
  || target.isContentEditable === true;
131
334
  };
335
+
336
+ const clamp = (value, min, max) => {
337
+ const numeric = Number(value);
338
+ if (!Number.isFinite(numeric)) {
339
+ return 0;
340
+ }
341
+
342
+ return Math.min(max, Math.max(min, numeric));
343
+ };