vizcore 0.1.0 → 1.0.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +544 -9
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/site.css +744 -0
  5. data/docs/assets/vizcore-demo.gif +0 -0
  6. data/docs/assets/vizcore-poster.png +0 -0
  7. data/docs/assets/vj-tunnel.js +159 -0
  8. data/docs/index.html +224 -0
  9. data/examples/README.md +59 -0
  10. data/examples/assets/README.md +19 -0
  11. data/examples/audio_inspector.rb +34 -0
  12. data/examples/club_intro_drop.rb +78 -0
  13. data/examples/kansai_rubykaigi_visual.rb +70 -0
  14. data/examples/live_coding_minimal.rb +22 -0
  15. data/examples/midi_controller_show.rb +78 -0
  16. data/examples/midi_scene_switch.rb +3 -1
  17. data/examples/parser_visualizer.rb +48 -0
  18. data/examples/readme_demo.rb +17 -0
  19. data/examples/rhythm_geometry.rb +34 -0
  20. data/examples/ruby_crystal_show.rb +35 -0
  21. data/examples/shader_playground.rb +18 -0
  22. data/examples/unyo_liquid.rb +59 -0
  23. data/examples/vj_ambient_chill_room.rb +124 -0
  24. data/examples/vj_dnb_jungle.rb +170 -0
  25. data/examples/vj_festival_mainstage.rb +245 -0
  26. data/examples/vj_festival_mainstage.yml +17 -0
  27. data/examples/vj_glitch_industrial.rb +164 -0
  28. data/examples/vj_hiphop_cipher.rb +167 -0
  29. data/examples/vj_jpop_idol_live.rb +210 -0
  30. data/examples/vj_synthwave_retro.rb +173 -0
  31. data/examples/vj_techno_warehouse.rb +195 -0
  32. data/frontend/index.html +468 -2
  33. data/frontend/src/audio-inspector.js +40 -0
  34. data/frontend/src/live-controls.js +131 -0
  35. data/frontend/src/main.js +792 -16
  36. data/frontend/src/midi-learn.js +194 -0
  37. data/frontend/src/performance-monitor.js +183 -0
  38. data/frontend/src/plugin-runtime.js +130 -0
  39. data/frontend/src/projector-mode.js +56 -0
  40. data/frontend/src/renderer/engine.js +148 -3
  41. data/frontend/src/renderer/layer-manager.js +428 -30
  42. data/frontend/src/renderer/shader-manager.js +26 -0
  43. data/frontend/src/runtime-control-preset.js +11 -0
  44. data/frontend/src/shader-error-overlay.js +29 -0
  45. data/frontend/src/shader-param-controls.js +93 -0
  46. data/frontend/src/shaders/builtins.js +380 -2
  47. data/frontend/src/shaders/post-effects.js +52 -0
  48. data/frontend/src/visual-regression.js +67 -0
  49. data/frontend/src/visual-settings-preset.js +103 -0
  50. data/frontend/src/visuals/geometry.js +268 -0
  51. data/frontend/src/visuals/image-renderer.js +291 -0
  52. data/frontend/src/visuals/particle-system.js +56 -10
  53. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  54. data/frontend/src/visuals/text-renderer.js +112 -11
  55. data/frontend/src/websocket-client.js +12 -1
  56. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  57. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  58. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  59. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  60. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  61. data/lib/vizcore/analysis/pipeline.rb +235 -11
  62. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  63. data/lib/vizcore/analysis.rb +4 -0
  64. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  65. data/lib/vizcore/audio/fixture_input.rb +65 -0
  66. data/lib/vizcore/audio/input_manager.rb +4 -2
  67. data/lib/vizcore/audio/mic_input.rb +24 -8
  68. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  69. data/lib/vizcore/audio.rb +1 -0
  70. data/lib/vizcore/cli/doctor.rb +159 -0
  71. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  72. data/lib/vizcore/cli/layer_docs.rb +46 -0
  73. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  74. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  75. data/lib/vizcore/cli/scene_validator.rb +245 -0
  76. data/lib/vizcore/cli/shader_template.rb +68 -0
  77. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  78. data/lib/vizcore/cli.rb +689 -18
  79. data/lib/vizcore/config.rb +103 -2
  80. data/lib/vizcore/control_preset.rb +68 -0
  81. data/lib/vizcore/dsl/engine.rb +277 -5
  82. data/lib/vizcore/dsl/layer_builder.rb +491 -22
  83. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  84. data/lib/vizcore/dsl/mapping_resolver.rb +132 -3
  85. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  86. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  87. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  88. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  89. data/lib/vizcore/dsl/style_builder.rb +68 -0
  90. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  91. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  92. data/lib/vizcore/dsl.rb +5 -1
  93. data/lib/vizcore/layer_catalog.rb +273 -0
  94. data/lib/vizcore/project_manifest.rb +152 -0
  95. data/lib/vizcore/renderer/png_writer.rb +57 -0
  96. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  97. data/lib/vizcore/renderer/scene_frame_source.rb +119 -0
  98. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  99. data/lib/vizcore/renderer/snapshot.rb +38 -0
  100. data/lib/vizcore/renderer/snapshot_renderer.rb +446 -0
  101. data/lib/vizcore/renderer.rb +5 -0
  102. data/lib/vizcore/server/frame_broadcaster.rb +91 -5
  103. data/lib/vizcore/server/gallery_app.rb +155 -0
  104. data/lib/vizcore/server/gallery_page.rb +100 -0
  105. data/lib/vizcore/server/gallery_runner.rb +48 -0
  106. data/lib/vizcore/server/rack_app.rb +203 -4
  107. data/lib/vizcore/server/runner.rb +370 -22
  108. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  109. data/lib/vizcore/server/websocket_handler.rb +60 -10
  110. data/lib/vizcore/server.rb +4 -0
  111. data/lib/vizcore/sync/osc_message.rb +103 -0
  112. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  113. data/lib/vizcore/sync.rb +4 -0
  114. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  115. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  116. data/lib/vizcore/templates/plugin_readme.md +23 -0
  117. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  118. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  119. data/lib/vizcore/templates/project_readme.md +7 -23
  120. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  121. data/lib/vizcore/version.rb +1 -1
  122. data/lib/vizcore.rb +27 -0
  123. data/scripts/browser_capture.mjs +75 -0
  124. data/sig/vizcore.rbs +362 -0
  125. metadata +83 -3
  126. data/docs/GETTING_STARTED.md +0 -105
data/frontend/index.html CHANGED
@@ -51,6 +51,8 @@
51
51
  background: var(--panel-bg);
52
52
  backdrop-filter: blur(6px);
53
53
  min-width: 18rem;
54
+ max-height: calc(100vh - 2rem);
55
+ overflow-y: auto;
54
56
  }
55
57
 
56
58
  .hud h1 {
@@ -122,9 +124,311 @@
122
124
  opacity: 0.6;
123
125
  cursor: not-allowed;
124
126
  }
127
+
128
+ .live-controls {
129
+ margin-top: 0.5rem;
130
+ display: grid;
131
+ grid-template-columns: repeat(2, minmax(0, 1fr));
132
+ gap: 0.35rem;
133
+ }
134
+
135
+ .live-controls button {
136
+ margin-top: 0;
137
+ padding: 0.42rem 0.5rem;
138
+ font-weight: 700;
139
+ text-transform: uppercase;
140
+ }
141
+
142
+ .live-controls button.is-active {
143
+ border-color: rgba(248, 250, 252, 0.52);
144
+ color: #fff7ed;
145
+ }
146
+
147
+ #blackout-toggle.is-active {
148
+ background: rgba(127, 29, 29, 0.78);
149
+ }
150
+
151
+ #freeze-toggle.is-active {
152
+ background: rgba(133, 77, 14, 0.78);
153
+ }
154
+
155
+ .hud .live-controls__status {
156
+ grid-column: 1 / -1;
157
+ margin-top: 0;
158
+ color: #d9e8ff;
159
+ font-variant-numeric: tabular-nums;
160
+ }
161
+
162
+ .hud .performance-monitor {
163
+ margin-top: 0.45rem;
164
+ padding-top: 0.45rem;
165
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
166
+ color: #a8d8ff;
167
+ font-variant-numeric: tabular-nums;
168
+ line-height: 1.35;
169
+ }
170
+
171
+ .reactivity-controls {
172
+ margin-top: 0.55rem;
173
+ padding-top: 0.55rem;
174
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
175
+ }
176
+
177
+ .reactivity-actions {
178
+ display: flex;
179
+ flex-wrap: wrap;
180
+ gap: 0.45rem;
181
+ margin-top: 0.45rem;
182
+ }
183
+
184
+ .reactivity-actions button,
185
+ .midi-learn button {
186
+ border: 1px solid rgba(148, 163, 184, 0.28);
187
+ border-radius: 6px;
188
+ background: rgba(15, 23, 42, 0.68);
189
+ color: #d9e8ff;
190
+ cursor: pointer;
191
+ font: inherit;
192
+ padding: 0.28rem 0.62rem;
193
+ }
194
+
195
+ .reactivity-actions button:hover,
196
+ .midi-learn button:hover {
197
+ border-color: rgba(130, 255, 196, 0.55);
198
+ color: #d9ffe8;
199
+ }
200
+
201
+ .midi-learn {
202
+ margin-top: 0.55rem;
203
+ padding-top: 0.55rem;
204
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
205
+ }
206
+
207
+ .midi-learn__actions {
208
+ display: grid;
209
+ grid-template-columns: repeat(2, minmax(0, 1fr));
210
+ gap: 0.35rem;
211
+ margin-top: 0.4rem;
212
+ }
213
+
214
+ .midi-learn button {
215
+ margin-top: 0;
216
+ width: 100%;
217
+ font-size: 0.76rem;
218
+ }
219
+
220
+ .audio-inspector {
221
+ margin-top: 0.55rem;
222
+ padding-top: 0.55rem;
223
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
224
+ }
225
+
226
+ .audio-inspector__header,
227
+ .audio-meter {
228
+ display: grid;
229
+ grid-template-columns: 3.25rem minmax(7rem, 1fr) 3.25rem;
230
+ align-items: center;
231
+ gap: 0.45rem;
232
+ }
233
+
234
+ .audio-inspector__header {
235
+ margin-bottom: 0.35rem;
236
+ color: #d9e8ff;
237
+ font-size: 0.72rem;
238
+ font-weight: 700;
239
+ letter-spacing: 0.08em;
240
+ text-transform: uppercase;
241
+ }
242
+
243
+ .audio-inspector__header span:last-child {
244
+ grid-column: 2 / 4;
245
+ justify-self: end;
246
+ color: var(--muted);
247
+ font-weight: 500;
248
+ letter-spacing: 0;
249
+ text-transform: none;
250
+ }
251
+
252
+ .audio-meter {
253
+ margin-top: 0.24rem;
254
+ color: #aebfda;
255
+ font-size: 0.72rem;
256
+ font-variant-numeric: tabular-nums;
257
+ }
258
+
259
+ .meter-track {
260
+ position: relative;
261
+ height: 0.34rem;
262
+ overflow: hidden;
263
+ border-radius: 999px;
264
+ background: rgba(148, 163, 184, 0.16);
265
+ }
266
+
267
+ .meter-fill {
268
+ position: absolute;
269
+ inset: 0;
270
+ border-radius: inherit;
271
+ transform: scaleX(var(--meter-value, 0));
272
+ transform-origin: left center;
273
+ transition: transform 90ms linear;
274
+ background: linear-gradient(90deg, #65ffb0, #38bdf8 58%, #f472b6);
275
+ }
276
+
277
+ .fft-preview {
278
+ display: grid;
279
+ grid-template-columns: repeat(16, minmax(0, 1fr));
280
+ align-items: end;
281
+ gap: 0.12rem;
282
+ height: 2.45rem;
283
+ margin-top: 0.48rem;
284
+ padding: 0.18rem 0;
285
+ }
286
+
287
+ .fft-bar {
288
+ height: 100%;
289
+ min-width: 2px;
290
+ border-radius: 2px 2px 0 0;
291
+ transform: scaleY(var(--bin-value, 0));
292
+ transform-origin: bottom center;
293
+ transition: transform 90ms linear;
294
+ background: linear-gradient(180deg, #f8fafc, #38bdf8 45%, #65ffb0);
295
+ opacity: 0.9;
296
+ }
297
+
298
+ .shader-error {
299
+ position: absolute;
300
+ right: 1rem;
301
+ top: 1rem;
302
+ width: min(26rem, calc(100vw - 2rem));
303
+ max-height: calc(100vh - 2rem);
304
+ overflow: auto;
305
+ padding: 0.75rem;
306
+ border: 1px solid rgba(248, 113, 113, 0.45);
307
+ border-radius: 0.5rem;
308
+ background: rgba(20, 6, 10, 0.86);
309
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.34);
310
+ color: #fee2e2;
311
+ backdrop-filter: blur(8px);
312
+ }
313
+
314
+ .shader-error[hidden] {
315
+ display: none;
316
+ }
317
+
318
+ .shader-error__header {
319
+ display: grid;
320
+ grid-template-columns: minmax(0, 1fr) auto;
321
+ align-items: center;
322
+ gap: 0.6rem;
323
+ margin-bottom: 0.45rem;
324
+ }
325
+
326
+ .shader-error h2 {
327
+ margin: 0;
328
+ overflow-wrap: anywhere;
329
+ color: #fecaca;
330
+ font-size: 0.88rem;
331
+ letter-spacing: 0;
332
+ }
333
+
334
+ .shader-error button {
335
+ min-width: 4rem;
336
+ border: 1px solid rgba(254, 202, 202, 0.4);
337
+ border-radius: 0.45rem;
338
+ padding: 0.32rem 0.52rem;
339
+ background: rgba(127, 29, 29, 0.5);
340
+ color: #fff1f2;
341
+ cursor: pointer;
342
+ }
343
+
344
+ .shader-error pre {
345
+ margin: 0;
346
+ white-space: pre-wrap;
347
+ overflow-wrap: anywhere;
348
+ color: #fed7aa;
349
+ font: 0.76rem/1.45 "SFMono-Regular", Consolas, monospace;
350
+ }
351
+
352
+ body.is-projector {
353
+ background: #000;
354
+ cursor: none;
355
+ }
356
+
357
+ body.is-projector .hud,
358
+ body.is-projector .shader-error {
359
+ display: none;
360
+ }
361
+
362
+ .reactivity-controls label,
363
+ .shader-param-controls label {
364
+ display: grid;
365
+ grid-template-columns: minmax(5.5rem, 0.8fr) minmax(8rem, 1.2fr) minmax(2.8rem, 0.35fr);
366
+ align-items: center;
367
+ gap: 0.5rem;
368
+ margin-top: 0.3rem;
369
+ font-size: 0.78rem;
370
+ color: #c9d9ee;
371
+ }
372
+
373
+ .reactivity-controls label {
374
+ grid-template-columns: minmax(5.5rem, 0.8fr) minmax(8rem, 1.2fr);
375
+ }
376
+
377
+ .reactivity-controls input[type="range"],
378
+ .shader-param-controls input[type="range"] {
379
+ width: 100%;
380
+ }
381
+
382
+ .reactivity-controls p,
383
+ .shader-param-controls p {
384
+ margin-top: 0.45rem;
385
+ line-height: 1.35;
386
+ }
387
+
388
+ .shader-param-controls {
389
+ margin-top: 0.55rem;
390
+ padding-top: 0.55rem;
391
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
392
+ }
393
+
394
+ .shader-param-controls[hidden] {
395
+ display: none;
396
+ }
397
+
398
+ .shader-param-controls__title {
399
+ color: #d9e8ff;
400
+ font-size: 0.72rem;
401
+ font-weight: 700;
402
+ letter-spacing: 0.08em;
403
+ text-transform: uppercase;
404
+ }
405
+
406
+ .shader-param-controls output {
407
+ color: #a8d8ff;
408
+ font-variant-numeric: tabular-nums;
409
+ text-align: right;
410
+ }
411
+
412
+ @media (max-width: 520px) {
413
+ .hud {
414
+ right: 1rem;
415
+ min-width: 0;
416
+ }
417
+
418
+ .audio-inspector__header,
419
+ .audio-meter {
420
+ grid-template-columns: 2.75rem minmax(5rem, 1fr) 2.9rem;
421
+ }
422
+
423
+ .shader-error {
424
+ left: 1rem;
425
+ top: auto;
426
+ bottom: 1rem;
427
+ }
428
+ }
125
429
  </style>
126
430
  </head>
127
- <body>
431
+ <body data-projector-mode="false" data-display-mode="auto">
128
432
  <div id="app">
129
433
  <canvas id="vizcore-canvas"></canvas>
130
434
  <section class="hud" aria-live="polite">
@@ -135,14 +439,176 @@
135
439
  <p id="frame-status">Amplitude: 0.0000</p>
136
440
  <p id="bpm-status" class="is-accent">BPM: --</p>
137
441
  <p id="beat-status">Beat: off | Count: 0</p>
442
+ <div class="live-controls" aria-label="Live emergency controls">
443
+ <button id="blackout-toggle" type="button" aria-pressed="false">Blackout</button>
444
+ <button id="freeze-toggle" type="button" aria-pressed="false">Freeze</button>
445
+ <p id="live-control-status" class="live-controls__status">Live: output</p>
446
+ </div>
447
+ <p id="performance-monitor" class="performance-monitor">Perf: -- FPS | Frame -- | WS -- | RTT -- | Clock -- | Drop 0 | Audio -- | Shader -- | Reconnect 0</p>
448
+ <div class="audio-inspector" aria-label="Audio inspector">
449
+ <div class="audio-inspector__header">
450
+ <span>Audio</span>
451
+ <span id="inspector-peak">Peak: --</span>
452
+ </div>
453
+ <div class="audio-meter">
454
+ <span>amp</span>
455
+ <span class="meter-track"><span id="inspector-amplitude-fill" class="meter-fill"></span></span>
456
+ <span id="inspector-amplitude-value">0.000</span>
457
+ </div>
458
+ <div class="audio-meter">
459
+ <span>sub</span>
460
+ <span class="meter-track"><span id="inspector-band-sub-fill" class="meter-fill"></span></span>
461
+ <span id="inspector-band-sub-value">0.00</span>
462
+ </div>
463
+ <div class="audio-meter">
464
+ <span>low</span>
465
+ <span class="meter-track"><span id="inspector-band-low-fill" class="meter-fill"></span></span>
466
+ <span id="inspector-band-low-value">0.00</span>
467
+ </div>
468
+ <div class="audio-meter">
469
+ <span>mid</span>
470
+ <span class="meter-track"><span id="inspector-band-mid-fill" class="meter-fill"></span></span>
471
+ <span id="inspector-band-mid-value">0.00</span>
472
+ </div>
473
+ <div class="audio-meter">
474
+ <span>high</span>
475
+ <span class="meter-track"><span id="inspector-band-high-fill" class="meter-fill"></span></span>
476
+ <span id="inspector-band-high-value">0.00</span>
477
+ </div>
478
+ <div id="fft-preview" class="fft-preview" aria-label="FFT preview"></div>
479
+ </div>
138
480
  <p id="audio-source-status">Audio Source: unknown</p>
139
481
  <p id="audio-track-status">Track: none</p>
140
482
  <p id="audio-playback-status">Playback: unavailable</p>
483
+ <div class="reactivity-controls" aria-label="Visual reactivity controls">
484
+ <label>Visual Gain <input id="visual-gain-control" type="range" min="1" max="8" step="0.1" value="2.5" /></label>
485
+ <label>Bass Boost <input id="bass-boost-control" type="range" min="0" max="4" step="0.1" value="1.4" /></label>
486
+ <label>Smoothing <input id="smoothing-control" type="range" min="0" max="0.9" step="0.05" value="0.25" /></label>
487
+ <label>Beat Hold <input id="beat-hold-control" type="range" min="50" max="400" step="10" value="180" /></label>
488
+ <label>Wobble <input id="wobble-control" type="range" min="0.25" max="3" step="0.05" value="1" /></label>
489
+ <div class="reactivity-actions" aria-label="Reactivity preset controls">
490
+ <button id="reactivity-save" type="button">Save</button>
491
+ <button id="reactivity-load" type="button">Load</button>
492
+ <button id="reactivity-project-save" type="button" hidden>Save Project</button>
493
+ <button id="reactivity-export" type="button">Export</button>
494
+ <button id="reactivity-import" type="button">Import</button>
495
+ </div>
496
+ <p id="reactivity-status">Visual Gain: 2.5x | Bass: 1.4x | Smooth: 0.25 | Beat Hold: 180ms | Wobble: 1.00x</p>
497
+ </div>
498
+ <div class="midi-learn" aria-label="MIDI learn controls">
499
+ <p id="midi-learn-status">MIDI Learn: idle | Bindings: 0</p>
500
+ <div class="midi-learn__actions">
501
+ <button type="button" data-midi-learn-action="current-scene">Learn Scene</button>
502
+ <button type="button" data-midi-learn-action="blackout">Learn Blackout</button>
503
+ <button type="button" data-midi-learn-action="freeze">Learn Freeze</button>
504
+ <button type="button" data-midi-learn-action="visual:visualGain">Learn Gain</button>
505
+ <button type="button" data-midi-learn-action="visual:bassBoost">Learn Bass</button>
506
+ <button type="button" data-midi-learn-action="visual:wobbleAmount">Learn Wobble</button>
507
+ </div>
508
+ </div>
509
+ <div id="shader-param-controls" class="shader-param-controls" hidden aria-label="Shader parameter controls"></div>
141
510
  <div id="scene-switcher" class="scene-switcher" hidden aria-label="Scene switcher"></div>
142
511
  <button id="audio-toggle" type="button" hidden>Play Audio</button>
143
512
  </section>
513
+ <section id="shader-error-overlay" class="shader-error" aria-live="assertive" hidden>
514
+ <div class="shader-error__header">
515
+ <h2 id="shader-error-title">Shader Error</h2>
516
+ <button id="shader-error-close" type="button">Close</button>
517
+ </div>
518
+ <pre id="shader-error-message"></pre>
519
+ </section>
144
520
  </div>
145
521
 
146
- <script type="module" src="/src/main.js"></script>
522
+ <script>
523
+ window.__vizcoreMainStarted = false;
524
+ (function () {
525
+ var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
526
+ var websocketUrl = protocol + "//" + window.location.host + "/ws";
527
+ var fallbackTimer = window.setTimeout(function () {
528
+ if (window.__vizcoreMainStarted) return;
529
+
530
+ var wsStatus = document.getElementById("ws-status");
531
+ var sceneStatus = document.getElementById("scene-status");
532
+ var frameStatus = document.getElementById("frame-status");
533
+ var bpmStatus = document.getElementById("bpm-status");
534
+ var beatStatus = document.getElementById("beat-status");
535
+ var audioSourceStatus = document.getElementById("audio-source-status");
536
+ var peakStatus = document.getElementById("inspector-peak");
537
+ var ampValue = document.getElementById("inspector-amplitude-value");
538
+ var ampFill = document.getElementById("inspector-amplitude-fill");
539
+ var bands = ["sub", "low", "mid", "high"];
540
+
541
+ function setText(element, text) {
542
+ if (element) element.textContent = text;
543
+ }
544
+
545
+ function setMeter(id, value) {
546
+ var fill = document.getElementById("inspector-band-" + id + "-fill");
547
+ var label = document.getElementById("inspector-band-" + id + "-value");
548
+ if (fill) fill.style.setProperty("--meter-value", value.toFixed(4));
549
+ if (label) label.textContent = value.toFixed(2);
550
+ }
551
+
552
+ setText(wsStatus, "WebSocket: fallback connecting (" + websocketUrl + ")");
553
+ window.fetch("/runtime", { cache: "no-store" })
554
+ .then(function (response) { return response.ok ? response.json() : null; })
555
+ .then(function (runtime) {
556
+ if (runtime && audioSourceStatus) {
557
+ audioSourceStatus.textContent = "Audio Source: " + runtime.audio_source;
558
+ }
559
+ })
560
+ .catch(function () {});
561
+
562
+ var socket = new WebSocket(websocketUrl);
563
+ socket.addEventListener("open", function () {
564
+ setText(wsStatus, "WebSocket: fallback connected (" + websocketUrl + ")");
565
+ });
566
+ socket.addEventListener("error", function () {
567
+ setText(wsStatus, "WebSocket: fallback error (" + websocketUrl + ")");
568
+ });
569
+ socket.addEventListener("close", function () {
570
+ setText(wsStatus, "WebSocket: fallback closed (" + websocketUrl + ")");
571
+ });
572
+ socket.addEventListener("message", function (event) {
573
+ var message;
574
+ try {
575
+ message = JSON.parse(event.data);
576
+ } catch (_error) {
577
+ return;
578
+ }
579
+ if (!message || message.type !== "audio_frame" || !message.payload) return;
580
+
581
+ var payload = message.payload;
582
+ var audio = payload.audio || {};
583
+ var scene = payload.scene || {};
584
+ var amplitude = Math.max(0, Math.min(1, Number(audio.amplitude || 0)));
585
+ var bpm = Number(audio.bpm || 0);
586
+ var beat = !!audio.beat;
587
+ var beatCount = Math.max(0, Number(audio.beat_count || 0) || 0);
588
+
589
+ setText(sceneStatus, "Scene: " + (scene.name || "unknown"));
590
+ setText(frameStatus, "Amplitude: " + amplitude.toFixed(4));
591
+ setText(bpmStatus, "BPM: " + (bpm > 0 ? bpm.toFixed(1) : "--"));
592
+ setText(beatStatus, "Beat: " + (beat ? "ON" : "off") + " | Count: " + beatCount);
593
+ if (ampValue) ampValue.textContent = amplitude.toFixed(3);
594
+ if (ampFill) ampFill.style.setProperty("--meter-value", amplitude.toFixed(4));
595
+ if (peakStatus) {
596
+ var peak = Number(audio.peak_frequency || 0);
597
+ peakStatus.textContent = peak > 0 ? "Peak: " + Math.round(peak) + " Hz" : "Peak: --";
598
+ }
599
+
600
+ var audioBands = audio.bands || {};
601
+ bands.forEach(function (key) {
602
+ setMeter(key, Math.max(0, Math.min(1, Number(audioBands[key] || 0))));
603
+ });
604
+ });
605
+ }, 1200);
606
+
607
+ window.addEventListener("beforeunload", function () {
608
+ window.clearTimeout(fallbackTimer);
609
+ });
610
+ })();
611
+ </script>
612
+ <script type="module" src="/src/main.js?v=20260516d"></script>
147
613
  </body>
148
614
  </html>
@@ -0,0 +1,40 @@
1
+ export const BAND_KEYS = ["sub", "low", "mid", "high"];
2
+ export const DEFAULT_FFT_BINS = 16;
3
+
4
+ export const buildAudioInspectorState = (audio, fftBins = DEFAULT_FFT_BINS) => {
5
+ const bands = BAND_KEYS.reduce((result, key) => {
6
+ result[key] = clamp01(audio?.bands?.[key]);
7
+ return result;
8
+ }, {});
9
+
10
+ return {
11
+ amplitude: clamp01(audio?.amplitude),
12
+ bands,
13
+ fft: normalizeFft(audio?.fft, fftBins),
14
+ bpm: Number(audio?.bpm || 0),
15
+ beat: !!audio?.beat,
16
+ beatPulse: clamp01(audio?.beat_pulse),
17
+ peakFrequency: Math.max(0, Number(audio?.peak_frequency || 0) || 0),
18
+ };
19
+ };
20
+
21
+ export const formatMeterValue = (value, digits = 2) => {
22
+ const numeric = Number(value);
23
+ if (!Number.isFinite(numeric)) {
24
+ return Number(0).toFixed(digits);
25
+ }
26
+ return numeric.toFixed(digits);
27
+ };
28
+
29
+ const normalizeFft = (value, size) => {
30
+ const input = Array.isArray(value) || ArrayBuffer.isView(value) ? Array.from(value) : [];
31
+ return Array.from({ length: size }, (_entry, index) => clamp01(input[index]));
32
+ };
33
+
34
+ const clamp01 = (value) => {
35
+ const numeric = Number(value);
36
+ if (!Number.isFinite(numeric)) {
37
+ return 0;
38
+ }
39
+ return Math.min(Math.max(numeric, 0), 1);
40
+ };
@@ -0,0 +1,131 @@
1
+ export const createLiveControlState = () => ({
2
+ blackout: false,
3
+ freeze: false,
4
+ });
5
+
6
+ export const toggleLiveControl = (state, key) => {
7
+ const control = String(key || "");
8
+ if (control !== "blackout" && control !== "freeze") {
9
+ return { ...state };
10
+ }
11
+
12
+ return {
13
+ ...state,
14
+ [control]: !state?.[control],
15
+ };
16
+ };
17
+
18
+ export const liveControlStatusText = (state) => {
19
+ const values = [];
20
+ if (state?.blackout) values.push("Blackout");
21
+ if (state?.freeze) values.push("Freeze");
22
+ return values.length ? `Live: ${values.join(" + ")}` : "Live: output";
23
+ };
24
+
25
+ export const shortcutActionForKey = (event) => {
26
+ if (isEditableShortcutTarget(event?.target)) {
27
+ return null;
28
+ }
29
+
30
+ const key = String(event?.key || "").toLowerCase();
31
+ if (key === "b") return "blackout";
32
+ if (key === "f") return "freeze";
33
+ return null;
34
+ };
35
+
36
+ export const shortcutSceneIndexForKey = (event, sceneCount) => {
37
+ if (isEditableShortcutTarget(event?.target)) {
38
+ return null;
39
+ }
40
+
41
+ const index = Number.parseInt(String(event?.key || ""), 10) - 1;
42
+ if (!Number.isInteger(index) || index < 0 || index >= 9 || index >= sceneCount) {
43
+ return null;
44
+ }
45
+
46
+ return index;
47
+ };
48
+
49
+ export const keyboardActionForKey = (event, mappings) => {
50
+ if (isEditableShortcutTarget(event?.target)) {
51
+ return null;
52
+ }
53
+
54
+ const key = normalizeShortcutKey(event?.key);
55
+ if (!key) {
56
+ return null;
57
+ }
58
+
59
+ const mapping = normalizeKeyboardMappings(mappings).find((entry) => entry.key === key);
60
+ return mapping?.action || null;
61
+ };
62
+
63
+ export const normalizeKeyboardMappings = (mappings) => {
64
+ if (!Array.isArray(mappings)) {
65
+ return [];
66
+ }
67
+
68
+ return mappings.flatMap((entry) => {
69
+ const key = normalizeShortcutKey(entry?.key);
70
+ const action = normalizeKeyboardAction(entry?.action);
71
+ if (!key || !action) {
72
+ return [];
73
+ }
74
+
75
+ return [{ key, action }];
76
+ });
77
+ };
78
+
79
+ export const isTapTempoShortcut = (event, configuredKey) => {
80
+ if (isEditableShortcutTarget(event?.target)) {
81
+ return false;
82
+ }
83
+
84
+ const key = normalizeShortcutKey(configuredKey);
85
+ if (!key) {
86
+ return false;
87
+ }
88
+
89
+ return normalizeShortcutKey(event?.key) === key;
90
+ };
91
+
92
+ export const normalizeShortcutKey = (value) => {
93
+ const raw = String(value || "");
94
+ if (raw === " ") {
95
+ return "space";
96
+ }
97
+
98
+ const key = raw.trim().toLowerCase();
99
+ if (key === "spacebar") {
100
+ return "space";
101
+ }
102
+
103
+ return key;
104
+ };
105
+
106
+ const normalizeKeyboardAction = (action) => {
107
+ const type = String(action?.type || "").trim();
108
+ if (type === "switch_scene") {
109
+ const scene = String(action?.scene || "").trim();
110
+ return scene ? { type, scene } : null;
111
+ }
112
+
113
+ if (type === "live_control") {
114
+ const control = String(action?.control || "").trim();
115
+ return control === "blackout" || control === "freeze" ? { type, control } : null;
116
+ }
117
+
118
+ return null;
119
+ };
120
+
121
+ export const isEditableShortcutTarget = (target) => {
122
+ if (!target) {
123
+ return false;
124
+ }
125
+
126
+ const tagName = String(target.tagName || "").toLowerCase();
127
+ return tagName === "input"
128
+ || tagName === "textarea"
129
+ || tagName === "select"
130
+ || target.isContentEditable === true;
131
+ };