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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../audio"
|
|
4
4
|
require_relative "../analysis"
|
|
5
|
+
require_relative "../deep_copy"
|
|
5
6
|
require_relative "../dsl"
|
|
6
7
|
require_relative "../errors"
|
|
7
8
|
require_relative "../renderer"
|
|
@@ -23,10 +24,15 @@ module Vizcore
|
|
|
23
24
|
# @param scene_catalog [Array<Hash>, nil]
|
|
24
25
|
# @param transitions [Array<Hash>, nil]
|
|
25
26
|
# @param transition_controller [Vizcore::DSL::TransitionController, nil]
|
|
27
|
+
# @param initial_timeline_entry [Hash, nil]
|
|
26
28
|
# @param noise_gate [Numeric]
|
|
27
29
|
# @param audio_normalize [Hash, nil]
|
|
28
30
|
# @param bpm [Numeric, nil]
|
|
29
31
|
# @param bpm_lock [Boolean]
|
|
32
|
+
# @param onset_sensitivity [Numeric]
|
|
33
|
+
# @param fft_preview_bins [Integer]
|
|
34
|
+
# @param peak_hold_frames [Integer]
|
|
35
|
+
# @param silence_reset_frames [Integer]
|
|
30
36
|
# @param error_reporter [#call, nil]
|
|
31
37
|
def initialize(
|
|
32
38
|
scene_name: "basic",
|
|
@@ -39,13 +45,18 @@ module Vizcore
|
|
|
39
45
|
scene_catalog: nil,
|
|
40
46
|
transitions: nil,
|
|
41
47
|
transition_controller: nil,
|
|
48
|
+
initial_timeline_entry: nil,
|
|
42
49
|
noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE,
|
|
43
50
|
audio_normalize: nil,
|
|
44
51
|
bpm: nil,
|
|
45
52
|
bpm_lock: false,
|
|
53
|
+
onset_sensitivity: 1.0,
|
|
54
|
+
fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS,
|
|
55
|
+
peak_hold_frames: 0,
|
|
56
|
+
silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES,
|
|
46
57
|
error_reporter: nil
|
|
47
58
|
)
|
|
48
|
-
@scene_name = scene_name
|
|
59
|
+
@scene_name = scene_name.to_s
|
|
49
60
|
@scene_layers = Array(scene_layers)
|
|
50
61
|
@scene_mutex = Mutex.new
|
|
51
62
|
@input_manager = input_manager || Vizcore::Audio::InputManager.new(source: :mic)
|
|
@@ -56,21 +67,45 @@ module Vizcore
|
|
|
56
67
|
noise_gate: noise_gate,
|
|
57
68
|
audio_normalize: audio_normalize,
|
|
58
69
|
bpm: bpm,
|
|
59
|
-
bpm_lock: bpm_lock
|
|
70
|
+
bpm_lock: bpm_lock,
|
|
71
|
+
onset_sensitivity: onset_sensitivity,
|
|
72
|
+
fft_preview_bins: fft_preview_bins,
|
|
73
|
+
peak_hold_frames: peak_hold_frames,
|
|
74
|
+
silence_reset_frames: silence_reset_frames
|
|
60
75
|
)
|
|
61
76
|
@mapping_resolver = mapping_resolver || Vizcore::DSL::MappingResolver.new
|
|
62
77
|
@scene_serializer = scene_serializer || Vizcore::Renderer::SceneSerializer.new
|
|
78
|
+
@error_reporter = error_reporter || ->(_message) {}
|
|
63
79
|
@transition_controller = transition_controller || Vizcore::DSL::TransitionController.new(
|
|
64
80
|
scenes: scene_catalog || [],
|
|
65
|
-
transitions: transitions || []
|
|
81
|
+
transitions: transitions || [],
|
|
82
|
+
error_reporter: lambda do |message|
|
|
83
|
+
@error_reporter.call(message)
|
|
84
|
+
report_runtime_message(
|
|
85
|
+
message,
|
|
86
|
+
context: "transition trigger failed",
|
|
87
|
+
source: "transition",
|
|
88
|
+
event: "transition_failed"
|
|
89
|
+
)
|
|
90
|
+
end
|
|
66
91
|
)
|
|
67
|
-
@error_reporter = error_reporter || ->(_message) {}
|
|
68
92
|
@last_error = nil
|
|
69
93
|
@frame_count = 0
|
|
94
|
+
@last_frame_metrics = {}
|
|
95
|
+
@scene_version = 1
|
|
96
|
+
@last_sent_scene_version = nil
|
|
97
|
+
@last_sent_scene_payload = nil
|
|
98
|
+
@connected_client_count = 0
|
|
70
99
|
@custom_shape_param_overrides = {}
|
|
100
|
+
@layer_param_overrides = {}
|
|
71
101
|
@custom_shape_param_mutex = Mutex.new
|
|
102
|
+
@transport_reference_position = nil
|
|
103
|
+
@transport_reference_wall_seconds = nil
|
|
104
|
+
@transport_drift_seconds = 0.0
|
|
105
|
+
@transport_drift_threshold_seconds = 0.08
|
|
72
106
|
@transport_playing = initial_transport_playing_state
|
|
73
107
|
reset_transition_trigger_counters!
|
|
108
|
+
apply_initial_timeline_entry(initial_timeline_entry)
|
|
74
109
|
@tap_tempo = Vizcore::Analysis::TapTempo.new
|
|
75
110
|
@frame_scheduler = frame_scheduler || Vizcore::Renderer::FrameScheduler.new(frame_rate: FRAME_RATE) do |elapsed|
|
|
76
111
|
tick(elapsed)
|
|
@@ -109,15 +144,41 @@ module Vizcore
|
|
|
109
144
|
current_scene
|
|
110
145
|
end
|
|
111
146
|
|
|
147
|
+
# @return [Hash] runtime health details for control/status endpoints
|
|
148
|
+
def runtime_status
|
|
149
|
+
scene = current_scene
|
|
150
|
+
{
|
|
151
|
+
current_scene: scene[:name].to_s,
|
|
152
|
+
scene_version: @scene_version,
|
|
153
|
+
fps: FRAME_RATE,
|
|
154
|
+
frame_id: @frame_count,
|
|
155
|
+
sample_rate: input_manager_value(:sample_rate),
|
|
156
|
+
frame_size: input_manager_value(:frame_size),
|
|
157
|
+
input: input_manager_status,
|
|
158
|
+
transport_playing: @scene_mutex.synchronize { @transport_playing },
|
|
159
|
+
transport_drift: transport_drift_status,
|
|
160
|
+
websocket_clients: WebSocketHandler.connection_count,
|
|
161
|
+
dropped_frames: WebSocketHandler.dropped_frame_count,
|
|
162
|
+
websocket_backpressure: WebSocketHandler.backpressure_status,
|
|
163
|
+
last_error: formatted_last_error,
|
|
164
|
+
metrics: deep_dup(@last_frame_metrics)
|
|
165
|
+
}.compact
|
|
166
|
+
end
|
|
167
|
+
|
|
112
168
|
# Synchronize external playback transport (e.g. browser audio element) with the input source.
|
|
113
169
|
#
|
|
114
170
|
# @param playing [Boolean]
|
|
115
171
|
# @param position_seconds [Numeric]
|
|
116
172
|
# @return [void]
|
|
117
173
|
def sync_transport(playing:, position_seconds:)
|
|
174
|
+
position = finite_float(position_seconds)
|
|
118
175
|
@scene_mutex.synchronize do
|
|
119
176
|
@transport_playing = !!playing
|
|
120
|
-
reset_transition_trigger_counters! if transport_position_reset?(
|
|
177
|
+
reset_transition_trigger_counters! if transport_position_reset?(position)
|
|
178
|
+
if file_transport_source?
|
|
179
|
+
@transport_reference_position = position
|
|
180
|
+
@transport_reference_wall_seconds = wall_clock_seconds
|
|
181
|
+
end
|
|
121
182
|
end
|
|
122
183
|
return unless @input_manager.respond_to?(:sync_transport)
|
|
123
184
|
|
|
@@ -135,7 +196,7 @@ module Vizcore
|
|
|
135
196
|
@frame_count += 1
|
|
136
197
|
frame = build_frame(elapsed_seconds, samples)
|
|
137
198
|
WebSocketHandler.broadcast(type: "audio_frame", payload: frame)
|
|
138
|
-
evaluate_transition(frame[:audio], frame_count: @frame_count)
|
|
199
|
+
evaluate_transition(frame[:audio], frame_count: @frame_count, elapsed_seconds: elapsed_seconds)
|
|
139
200
|
frame
|
|
140
201
|
end
|
|
141
202
|
|
|
@@ -146,8 +207,17 @@ module Vizcore
|
|
|
146
207
|
# @return [void]
|
|
147
208
|
def update_scene(scene_name:, scene_layers:)
|
|
148
209
|
@scene_mutex.synchronize do
|
|
149
|
-
|
|
150
|
-
|
|
210
|
+
next_scene_name = scene_name.to_s
|
|
211
|
+
next_scene_layers = Array(scene_layers)
|
|
212
|
+
same_scene = @scene_name == next_scene_name &&
|
|
213
|
+
deep_layers_equal?(@scene_layers, next_scene_layers)
|
|
214
|
+
|
|
215
|
+
@scene_name = next_scene_name
|
|
216
|
+
@scene_layers = next_scene_layers
|
|
217
|
+
@scene_version += 1 unless same_scene
|
|
218
|
+
@last_sent_scene_version = nil unless same_scene
|
|
219
|
+
@last_sent_scene_payload = nil unless same_scene
|
|
220
|
+
@mapping_resolver.reset! if @mapping_resolver.respond_to?(:reset!)
|
|
151
221
|
reset_transition_trigger_counters!
|
|
152
222
|
end
|
|
153
223
|
end
|
|
@@ -169,11 +239,15 @@ module Vizcore
|
|
|
169
239
|
# @param bpm [Numeric, nil]
|
|
170
240
|
# @param bpm_lock [Boolean]
|
|
171
241
|
# @return [void]
|
|
172
|
-
def update_analysis_settings(audio_normalize:, bpm: nil, bpm_lock: false)
|
|
242
|
+
def update_analysis_settings(audio_normalize:, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES)
|
|
173
243
|
return unless @analysis_pipeline.respond_to?(:audio_normalize=)
|
|
174
244
|
|
|
175
245
|
@analysis_pipeline.audio_normalize = audio_normalize
|
|
176
246
|
@analysis_pipeline.bpm_lock = { bpm: bpm, locked: bpm_lock } if @analysis_pipeline.respond_to?(:bpm_lock=)
|
|
247
|
+
@analysis_pipeline.onset_sensitivity = onset_sensitivity if @analysis_pipeline.respond_to?(:onset_sensitivity=)
|
|
248
|
+
@analysis_pipeline.fft_preview_bins = fft_preview_bins if @analysis_pipeline.respond_to?(:fft_preview_bins=)
|
|
249
|
+
@analysis_pipeline.peak_hold_frames = peak_hold_frames if @analysis_pipeline.respond_to?(:peak_hold_frames=)
|
|
250
|
+
@analysis_pipeline.silence_reset_frames = silence_reset_frames if @analysis_pipeline.respond_to?(:silence_reset_frames=)
|
|
177
251
|
end
|
|
178
252
|
|
|
179
253
|
# Apply a manual tap tempo event and lock analysis BPM when enough taps exist.
|
|
@@ -229,6 +303,18 @@ module Vizcore
|
|
|
229
303
|
custom_shape_param_overrides_snapshot
|
|
230
304
|
end
|
|
231
305
|
|
|
306
|
+
def set_layer_param(layer_name:, param:, value:)
|
|
307
|
+
layer_key = layer_name.to_s
|
|
308
|
+
param_key = param.to_s.tr("/", ".").strip
|
|
309
|
+
return layer_param_overrides_snapshot if layer_key.empty? || param_key.empty?
|
|
310
|
+
|
|
311
|
+
@custom_shape_param_mutex.synchronize do
|
|
312
|
+
@layer_param_overrides[layer_key] ||= {}
|
|
313
|
+
@layer_param_overrides[layer_key][param_key] = value
|
|
314
|
+
deep_dup(@layer_param_overrides)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
232
318
|
# Build one frame payload for transport to frontend.
|
|
233
319
|
#
|
|
234
320
|
# @param _elapsed_seconds [Float]
|
|
@@ -237,12 +323,14 @@ module Vizcore
|
|
|
237
323
|
# @return [Hash]
|
|
238
324
|
def build_frame(elapsed_seconds, samples = nil)
|
|
239
325
|
started_at_ms = monotonic_ms
|
|
326
|
+
apply_transport_drift_correction if file_transport_source?
|
|
240
327
|
audio_samples, audio_capture_ms = capture_or_use_samples(samples)
|
|
328
|
+
sync_last_scene_state_with_connections
|
|
241
329
|
analyzed, audio_analysis_ms = measure_ms { @analysis_pipeline.call(audio_samples) }
|
|
242
330
|
scene = current_scene
|
|
243
331
|
layers, scene_build_ms = measure_ms { build_scene_layers(scene[:layers], analyzed, time: elapsed_seconds, frame: @frame_count) }
|
|
244
332
|
|
|
245
|
-
@scene_serializer.audio_frame(
|
|
333
|
+
frame = @scene_serializer.audio_frame(
|
|
246
334
|
timestamp: Time.now.to_f,
|
|
247
335
|
audio: analyzed,
|
|
248
336
|
scene_name: scene[:name],
|
|
@@ -256,6 +344,25 @@ module Vizcore
|
|
|
256
344
|
server_frame_ms: monotonic_ms - started_at_ms
|
|
257
345
|
}
|
|
258
346
|
)
|
|
347
|
+
frame[:scene_version] = scene[:version]
|
|
348
|
+
full_scene = deep_dup(frame[:scene])
|
|
349
|
+
send_full_scene = scene[:version] != @last_sent_scene_version
|
|
350
|
+
if send_full_scene
|
|
351
|
+
full_scene[:version] = scene[:version]
|
|
352
|
+
frame[:scene] = full_scene
|
|
353
|
+
@last_sent_scene_payload = deep_dup(full_scene)
|
|
354
|
+
@last_sent_scene_version = scene[:version]
|
|
355
|
+
else
|
|
356
|
+
patch = scene_delta(previous: @last_sent_scene_payload, current: full_scene, scene_name: scene[:name], scene_version: scene[:version])
|
|
357
|
+
if patch
|
|
358
|
+
frame[:scene] = patch
|
|
359
|
+
else
|
|
360
|
+
frame.delete(:scene)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
@last_sent_scene_payload = deep_dup(full_scene) if scene[:version] == @last_sent_scene_version
|
|
364
|
+
@last_frame_metrics = frame[:metrics] || {}
|
|
365
|
+
frame
|
|
259
366
|
rescue StandardError => e
|
|
260
367
|
report_error(e, context: "frame build failed")
|
|
261
368
|
raise Vizcore::FrameBuildError, Vizcore::ErrorFormatting.summarize(e, context: "Frame build failed")
|
|
@@ -269,6 +376,31 @@ module Vizcore
|
|
|
269
376
|
measure_ms { capture_samples }
|
|
270
377
|
end
|
|
271
378
|
|
|
379
|
+
def input_manager_value(name)
|
|
380
|
+
return nil unless @input_manager.respond_to?(name)
|
|
381
|
+
|
|
382
|
+
@input_manager.public_send(name)
|
|
383
|
+
rescue StandardError
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def input_manager_status
|
|
388
|
+
return @input_manager.status if @input_manager.respond_to?(:status)
|
|
389
|
+
|
|
390
|
+
{}
|
|
391
|
+
rescue StandardError
|
|
392
|
+
{}
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def formatted_last_error
|
|
396
|
+
error = @last_error
|
|
397
|
+
return nil unless error
|
|
398
|
+
|
|
399
|
+
Vizcore::ErrorFormatting.summarize(error, context: "last runtime error")
|
|
400
|
+
rescue StandardError
|
|
401
|
+
error.to_s
|
|
402
|
+
end
|
|
403
|
+
|
|
272
404
|
def measure_ms
|
|
273
405
|
started_at = monotonic_ms
|
|
274
406
|
result = yield
|
|
@@ -279,6 +411,114 @@ module Vizcore
|
|
|
279
411
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
280
412
|
end
|
|
281
413
|
|
|
414
|
+
def wall_clock_seconds
|
|
415
|
+
Time.now.to_f
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def apply_transport_drift_correction
|
|
419
|
+
reference_position = @scene_mutex.synchronize { finite_float(@transport_reference_position) }
|
|
420
|
+
return if reference_position.nil?
|
|
421
|
+
|
|
422
|
+
reference_wall_seconds = @scene_mutex.synchronize { finite_float(@transport_reference_wall_seconds) }
|
|
423
|
+
return if reference_wall_seconds.nil?
|
|
424
|
+
|
|
425
|
+
current_position = transport_input_position_seconds
|
|
426
|
+
duration = transport_track_duration_seconds
|
|
427
|
+
return unless current_position && duration&.positive?
|
|
428
|
+
|
|
429
|
+
expected_position = file_transport_expected_position(
|
|
430
|
+
reference_position: reference_position,
|
|
431
|
+
duration: duration,
|
|
432
|
+
elapsed_wall_seconds: (wall_clock_seconds - reference_wall_seconds),
|
|
433
|
+
is_playing: transport_playing?
|
|
434
|
+
)
|
|
435
|
+
drift_seconds = circular_difference(expected_position, current_position, duration)
|
|
436
|
+
drift_seconds = 0.0 if drift_seconds.nil?
|
|
437
|
+
|
|
438
|
+
@scene_mutex.synchronize do
|
|
439
|
+
@transport_drift_seconds = drift_seconds
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
return unless drift_seconds.abs > @transport_drift_threshold_seconds
|
|
443
|
+
|
|
444
|
+
drifted_position = wrap_transport_position(current_position + drift_seconds, duration)
|
|
445
|
+
sync_transport_input_position(
|
|
446
|
+
playing: @scene_mutex.synchronize { @transport_playing },
|
|
447
|
+
position_seconds: drifted_position
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def transport_drift_status
|
|
452
|
+
status = @scene_mutex.synchronize do
|
|
453
|
+
{
|
|
454
|
+
drift_seconds: @transport_drift_seconds,
|
|
455
|
+
threshold_seconds: @transport_drift_threshold_seconds,
|
|
456
|
+
reference_position: @transport_reference_position,
|
|
457
|
+
reference_wall_seconds: @transport_reference_wall_seconds
|
|
458
|
+
}
|
|
459
|
+
end
|
|
460
|
+
status.compact
|
|
461
|
+
rescue StandardError
|
|
462
|
+
{}
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def transport_input_position_seconds
|
|
466
|
+
return nil unless @input_manager.respond_to?(:transport_position_seconds)
|
|
467
|
+
return nil if @input_manager.nil?
|
|
468
|
+
|
|
469
|
+
@input_manager.transport_position_seconds
|
|
470
|
+
rescue StandardError
|
|
471
|
+
nil
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def transport_track_duration_seconds
|
|
475
|
+
return nil unless @input_manager.respond_to?(:track_duration_seconds)
|
|
476
|
+
return nil if @input_manager.nil?
|
|
477
|
+
|
|
478
|
+
@input_manager.track_duration_seconds
|
|
479
|
+
rescue StandardError
|
|
480
|
+
nil
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def sync_transport_input_position(playing:, position_seconds:)
|
|
484
|
+
return unless @input_manager.respond_to?(:sync_transport)
|
|
485
|
+
|
|
486
|
+
@input_manager.sync_transport(playing: playing, position_seconds: position_seconds)
|
|
487
|
+
rescue StandardError
|
|
488
|
+
nil
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def file_transport_expected_position(reference_position:, duration:, elapsed_wall_seconds:, is_playing:)
|
|
492
|
+
return nil unless duration.positive?
|
|
493
|
+
|
|
494
|
+
additional_position = finite_float(elapsed_wall_seconds)
|
|
495
|
+
return nil if additional_position.nil?
|
|
496
|
+
|
|
497
|
+
playback_position = reference_position + (is_playing ? additional_position : 0.0)
|
|
498
|
+
wrap_transport_position(playback_position, duration)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def circular_difference(expected, actual, duration)
|
|
502
|
+
return nil if expected.nil? || actual.nil? || !duration.positive?
|
|
503
|
+
|
|
504
|
+
diff = expected - actual
|
|
505
|
+
modulo = duration
|
|
506
|
+
return diff if modulo.zero?
|
|
507
|
+
|
|
508
|
+
((diff + modulo / 2.0) % modulo) - modulo / 2.0
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def wrap_transport_position(position, duration)
|
|
512
|
+
return 0.0 unless duration.positive?
|
|
513
|
+
|
|
514
|
+
wrapped = position % duration
|
|
515
|
+
wrapped.negative? ? wrapped + duration : wrapped
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def transport_playing?
|
|
519
|
+
@scene_mutex.synchronize { @transport_playing }
|
|
520
|
+
end
|
|
521
|
+
|
|
282
522
|
def capture_samples
|
|
283
523
|
ingest_count =
|
|
284
524
|
if @input_manager.respond_to?(:realtime_capture_size)
|
|
@@ -320,7 +560,8 @@ module Vizcore
|
|
|
320
560
|
audio: analyzed,
|
|
321
561
|
time: time,
|
|
322
562
|
frame: frame,
|
|
323
|
-
custom_shape_overrides: custom_shape_param_overrides_snapshot
|
|
563
|
+
custom_shape_overrides: custom_shape_param_overrides_snapshot,
|
|
564
|
+
layer_param_overrides: layer_param_overrides_snapshot
|
|
324
565
|
)
|
|
325
566
|
end
|
|
326
567
|
|
|
@@ -328,6 +569,10 @@ module Vizcore
|
|
|
328
569
|
@custom_shape_param_mutex.synchronize { deep_dup(@custom_shape_param_overrides) }
|
|
329
570
|
end
|
|
330
571
|
|
|
572
|
+
def layer_param_overrides_snapshot
|
|
573
|
+
@custom_shape_param_mutex.synchronize { deep_dup(@layer_param_overrides) }
|
|
574
|
+
end
|
|
575
|
+
|
|
331
576
|
def finite_float(value)
|
|
332
577
|
numeric = Float(value)
|
|
333
578
|
return nil unless numeric.finite?
|
|
@@ -338,14 +583,7 @@ module Vizcore
|
|
|
338
583
|
end
|
|
339
584
|
|
|
340
585
|
def deep_dup(value)
|
|
341
|
-
|
|
342
|
-
when Hash
|
|
343
|
-
value.each_with_object({}) { |(key, entry), output| output[key] = deep_dup(entry) }
|
|
344
|
-
when Array
|
|
345
|
-
value.map { |entry| deep_dup(entry) }
|
|
346
|
-
else
|
|
347
|
-
value
|
|
348
|
-
end
|
|
586
|
+
Vizcore::DeepCopy.copy(value)
|
|
349
587
|
end
|
|
350
588
|
|
|
351
589
|
def default_scene_layers(analyzed)
|
|
@@ -367,13 +605,101 @@ module Vizcore
|
|
|
367
605
|
def current_scene
|
|
368
606
|
@scene_mutex.synchronize do
|
|
369
607
|
{
|
|
608
|
+
version: @scene_version,
|
|
370
609
|
name: @scene_name,
|
|
371
610
|
layers: Array(@scene_layers)
|
|
372
611
|
}
|
|
373
612
|
end
|
|
374
613
|
end
|
|
375
614
|
|
|
376
|
-
def
|
|
615
|
+
def scene_delta(previous:, current:, scene_name:, scene_version:)
|
|
616
|
+
return nil unless previous.is_a?(Hash) && current.is_a?(Hash)
|
|
617
|
+
|
|
618
|
+
prev_layers = Array(previous[:layers])
|
|
619
|
+
curr_layers = Array(current[:layers])
|
|
620
|
+
delta_layers = []
|
|
621
|
+
|
|
622
|
+
max_count = [prev_layers.length, curr_layers.length].max
|
|
623
|
+
(0...max_count).each do |index|
|
|
624
|
+
previous_layer = prev_layers[index]
|
|
625
|
+
current_layer = curr_layers[index]
|
|
626
|
+
if current_layer.nil?
|
|
627
|
+
delta_layers << { index: index, remove: true }
|
|
628
|
+
next
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
if previous_layer.nil?
|
|
632
|
+
delta_layers << { index: index, layer: deep_dup(current_layer) }
|
|
633
|
+
next
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
next if current_layer[:name].to_s == previous_layer[:name].to_s && previous_layer == current_layer
|
|
637
|
+
|
|
638
|
+
if previous_layer[:name].to_s == current_layer[:name].to_s &&
|
|
639
|
+
!layer_diff_needed?(previous_layer, current_layer)
|
|
640
|
+
param_changes = param_delta(previous_layer[:params], current_layer[:params])
|
|
641
|
+
delta_layers << { index: index, params: param_changes } if param_changes.any?
|
|
642
|
+
next
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
delta_layers << { index: index, layer: deep_dup(current_layer) }
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
return nil if delta_layers.empty?
|
|
649
|
+
|
|
650
|
+
{
|
|
651
|
+
name: scene_name,
|
|
652
|
+
version: scene_version,
|
|
653
|
+
schema_version: current[:schema_version],
|
|
654
|
+
patch: true,
|
|
655
|
+
layers: delta_layers
|
|
656
|
+
}
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def layer_diff_needed?(previous_layer, current_layer)
|
|
660
|
+
comparable_fields = %i[type shader glsl glsl_source param_schema]
|
|
661
|
+
comparable_fields.any? do |field|
|
|
662
|
+
previous_layer[field].to_s != current_layer[field].to_s
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def sync_last_scene_state_with_connections
|
|
667
|
+
current_client_count = connection_count_for_broadcast
|
|
668
|
+
if current_client_count > @connected_client_count
|
|
669
|
+
@last_sent_scene_version = nil
|
|
670
|
+
@last_sent_scene_payload = nil
|
|
671
|
+
end
|
|
672
|
+
@connected_client_count = current_client_count
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def connection_count_for_broadcast
|
|
676
|
+
Integer(Vizcore::Server::WebSocketHandler.connection_count)
|
|
677
|
+
rescue StandardError
|
|
678
|
+
0
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def param_delta(previous_params, current_params)
|
|
682
|
+
previous_hash = Hash(previous_params || {})
|
|
683
|
+
current_hash = Hash(current_params || {})
|
|
684
|
+
keys = (previous_hash.keys | current_hash.keys)
|
|
685
|
+
delta = {}
|
|
686
|
+
|
|
687
|
+
keys.each do |key|
|
|
688
|
+
previous_value = previous_hash[key]
|
|
689
|
+
current_value = current_hash[key]
|
|
690
|
+
next if current_value == previous_value
|
|
691
|
+
|
|
692
|
+
delta[key] = deep_dup(current_value)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
delta
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def deep_layers_equal?(left, right)
|
|
699
|
+
deep_dup(left) == deep_dup(right)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def evaluate_transition(audio, frame_count:, elapsed_seconds:)
|
|
377
703
|
return if transition_evaluation_paused?
|
|
378
704
|
|
|
379
705
|
transition = @scene_mutex.synchronize do
|
|
@@ -386,10 +712,15 @@ module Vizcore
|
|
|
386
712
|
audio: audio,
|
|
387
713
|
frame_count: frame_count
|
|
388
714
|
)
|
|
715
|
+
trigger_elapsed_seconds = transition_trigger_elapsed_seconds(
|
|
716
|
+
scene_name: scene[:name],
|
|
717
|
+
elapsed_seconds: elapsed_seconds
|
|
718
|
+
)
|
|
389
719
|
@transition_controller.next_transition(
|
|
390
720
|
scene_name: scene[:name],
|
|
391
721
|
audio: trigger_audio,
|
|
392
|
-
frame_count: trigger_frame_count
|
|
722
|
+
frame_count: trigger_frame_count,
|
|
723
|
+
elapsed_seconds: trigger_elapsed_seconds
|
|
393
724
|
)
|
|
394
725
|
end
|
|
395
726
|
return unless transition
|
|
@@ -407,8 +738,58 @@ module Vizcore
|
|
|
407
738
|
|
|
408
739
|
def reset_transition_trigger_counters!
|
|
409
740
|
@transition_counter_scene_name = nil
|
|
741
|
+
@transition_counter_elapsed_scene_name = nil
|
|
410
742
|
@transition_counter_frame_base = 0
|
|
411
743
|
@transition_counter_beat_base = 0
|
|
744
|
+
@transition_counter_elapsed_base = 0.0
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def apply_initial_timeline_entry(entry)
|
|
748
|
+
normalized = normalize_timeline_entry(entry)
|
|
749
|
+
unit = normalized[:unit]
|
|
750
|
+
start_position = normalized[:at]
|
|
751
|
+
return unless unit
|
|
752
|
+
|
|
753
|
+
if unit == "seconds"
|
|
754
|
+
return unless start_position.positive?
|
|
755
|
+
|
|
756
|
+
@transition_counter_elapsed_scene_name = @scene_name
|
|
757
|
+
@transition_counter_elapsed_base = start_position
|
|
758
|
+
return
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
return unless unit == "beats"
|
|
762
|
+
return unless start_position.positive?
|
|
763
|
+
|
|
764
|
+
@transition_counter_scene_name = @scene_name
|
|
765
|
+
@transition_counter_beat_base = start_position.to_i
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def normalize_timeline_entry(entry)
|
|
769
|
+
values = Hash(entry || {})
|
|
770
|
+
return { unit: nil, at: nil } unless values
|
|
771
|
+
|
|
772
|
+
unit = values[:unit] || values["unit"]
|
|
773
|
+
at = values[:at] || values["at"]
|
|
774
|
+
{
|
|
775
|
+
unit: unit.to_s,
|
|
776
|
+
at: parse_timeline_position(at)
|
|
777
|
+
}
|
|
778
|
+
rescue StandardError
|
|
779
|
+
{ unit: nil, at: nil }
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def parse_timeline_position(value)
|
|
783
|
+
numeric = Float(value)
|
|
784
|
+
return nil unless numeric.finite?
|
|
785
|
+
|
|
786
|
+
numeric
|
|
787
|
+
rescue StandardError
|
|
788
|
+
begin
|
|
789
|
+
Integer(value)
|
|
790
|
+
rescue StandardError
|
|
791
|
+
nil
|
|
792
|
+
end
|
|
412
793
|
end
|
|
413
794
|
|
|
414
795
|
def transition_evaluation_paused?
|
|
@@ -443,11 +824,20 @@ module Vizcore
|
|
|
443
824
|
global_beat_count = extract_beat_count(audio_hash)
|
|
444
825
|
scene_beat_count = [global_beat_count - @transition_counter_beat_base, 0].max
|
|
445
826
|
|
|
446
|
-
[scene_frame_count, audio_hash.merge(beat_count: scene_beat_count)]
|
|
827
|
+
[scene_frame_count, audio_hash.merge(scene_musical_counts(audio_hash, beat_count: scene_beat_count))]
|
|
447
828
|
rescue StandardError
|
|
448
829
|
[0, { beat_count: 0 }]
|
|
449
830
|
end
|
|
450
831
|
|
|
832
|
+
def transition_trigger_elapsed_seconds(scene_name:, elapsed_seconds:)
|
|
833
|
+
sync_transition_elapsed_counter(scene_name: scene_name, elapsed_seconds: elapsed_seconds)
|
|
834
|
+
|
|
835
|
+
current_elapsed = Float(elapsed_seconds)
|
|
836
|
+
[current_elapsed - @transition_counter_elapsed_base, 0.0].max
|
|
837
|
+
rescue StandardError
|
|
838
|
+
0.0
|
|
839
|
+
end
|
|
840
|
+
|
|
451
841
|
def sync_transition_trigger_counters(scene_name:, audio:, frame_count:)
|
|
452
842
|
normalized_scene_name = scene_name.to_s
|
|
453
843
|
return if @transition_counter_scene_name == normalized_scene_name
|
|
@@ -464,23 +854,79 @@ module Vizcore
|
|
|
464
854
|
reset_transition_trigger_counters!
|
|
465
855
|
end
|
|
466
856
|
|
|
857
|
+
def sync_transition_elapsed_counter(scene_name:, elapsed_seconds:)
|
|
858
|
+
normalized_scene_name = scene_name.to_s
|
|
859
|
+
return if @transition_counter_elapsed_scene_name == normalized_scene_name
|
|
860
|
+
|
|
861
|
+
@transition_counter_elapsed_scene_name = normalized_scene_name
|
|
862
|
+
@transition_counter_elapsed_base = Float(elapsed_seconds)
|
|
863
|
+
rescue StandardError
|
|
864
|
+
@transition_counter_elapsed_base = 0.0
|
|
865
|
+
end
|
|
866
|
+
|
|
467
867
|
def extract_beat_count(audio)
|
|
468
868
|
Integer(audio[:beat_count] || audio["beat_count"] || 0)
|
|
469
869
|
rescue StandardError
|
|
470
870
|
0
|
|
471
871
|
end
|
|
472
872
|
|
|
873
|
+
def scene_musical_counts(audio, beat_count:)
|
|
874
|
+
beat_index = beat_count.positive? ? beat_count - 1 : 0
|
|
875
|
+
beat_phase = Float(audio[:beat_phase] || audio["beat_phase"] || 0.0).clamp(0.0, 1.0)
|
|
876
|
+
{
|
|
877
|
+
beat_count: beat_count,
|
|
878
|
+
bar_phase: (((beat_index % 4) + beat_phase) / 4.0).clamp(0.0, 1.0),
|
|
879
|
+
bar_count: beat_index / 4,
|
|
880
|
+
phrase_count: beat_index / 32
|
|
881
|
+
}
|
|
882
|
+
rescue StandardError
|
|
883
|
+
{ beat_count: beat_count, bar_phase: 0.0, bar_count: 0, phrase_count: 0 }
|
|
884
|
+
end
|
|
885
|
+
|
|
473
886
|
def truthy_audio_beat?(audio)
|
|
474
887
|
!!(audio[:beat] || audio["beat"])
|
|
475
888
|
end
|
|
476
889
|
|
|
477
890
|
def report_error(error, context:)
|
|
478
891
|
@last_error = error
|
|
479
|
-
|
|
892
|
+
message = Vizcore::ErrorFormatting.summarize(error, context: context)
|
|
893
|
+
@error_reporter.call(message)
|
|
894
|
+
report_runtime_message(message, context: context, source: "runtime", event: runtime_error_event(context))
|
|
480
895
|
rescue StandardError
|
|
481
896
|
nil
|
|
482
897
|
end
|
|
483
898
|
|
|
899
|
+
def report_runtime_message(message, context:, source:, event: nil)
|
|
900
|
+
payload = {
|
|
901
|
+
source: source,
|
|
902
|
+
context: context,
|
|
903
|
+
message: message.to_s,
|
|
904
|
+
frame_id: @frame_count
|
|
905
|
+
}
|
|
906
|
+
payload[:event] = event.to_s if event && !event.to_s.empty?
|
|
907
|
+
|
|
908
|
+
WebSocketHandler.broadcast(type: "runtime_error", payload: payload)
|
|
909
|
+
rescue StandardError
|
|
910
|
+
nil
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
def runtime_error_event(context)
|
|
914
|
+
case context.to_s.strip
|
|
915
|
+
when "frame build failed"
|
|
916
|
+
"frame_build_failed"
|
|
917
|
+
when "audio capture failed"
|
|
918
|
+
"audio_capture_failed"
|
|
919
|
+
when "audio transport sync failed"
|
|
920
|
+
"audio_transport_sync_failed"
|
|
921
|
+
when "frame broadcaster start failed"
|
|
922
|
+
"frame_broadcaster_start_failed"
|
|
923
|
+
when "transition trigger failed"
|
|
924
|
+
"transition_failed"
|
|
925
|
+
else
|
|
926
|
+
nil
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
|
|
484
930
|
end
|
|
485
931
|
end
|
|
486
932
|
end
|