vizcore 1.0.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/README.md +66 -648
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/index.html +2 -1
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/frontend/index.html +50 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +703 -45
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +401 -11
- data/frontend/src/renderer/layer-manager.js +490 -75
- 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/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- 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 +488 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/svg-arc.js +104 -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 +65 -9
- 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 +573 -33
- 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 +1072 -21
- 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 +549 -13
- 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 +5 -2
- 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 +190 -12
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +641 -23
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +513 -18
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +697 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +742 -0
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +34 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +154 -4
- metadata +29 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../deep_copy"
|
|
4
|
+
require_relative "../shape"
|
|
5
|
+
|
|
3
6
|
module Vizcore
|
|
4
7
|
module DSL
|
|
5
8
|
# Resolves `map` definitions into concrete per-layer parameter values.
|
|
@@ -8,20 +11,30 @@ module Vizcore
|
|
|
8
11
|
@mapping_state = {}
|
|
9
12
|
end
|
|
10
13
|
|
|
14
|
+
# Clear stateful transform memory such as smoothing, hold, decay, and hysteresis.
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
def reset!
|
|
18
|
+
@mapping_state.clear
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
# @param scene_layers [Array<Hash>]
|
|
12
22
|
# @param audio [Hash]
|
|
13
23
|
# @return [Array<Hash>] normalized layer payloads with resolved params
|
|
14
|
-
def resolve_layers(scene_layers:, audio:)
|
|
24
|
+
def resolve_layers(scene_layers:, audio:, time: 0.0, frame: 0, resolution: [1280, 720], globals: {}, custom_shape_overrides: {}, layer_param_overrides: {})
|
|
15
25
|
normalize_scene_layers(scene_layers).map do |layer|
|
|
16
|
-
resolve_layer(layer, audio)
|
|
26
|
+
resolve_layer(layer, audio, time: time, frame: frame, resolution: resolution, globals: globals, custom_shape_overrides: custom_shape_overrides, layer_param_overrides: layer_param_overrides)
|
|
17
27
|
end
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
private
|
|
21
31
|
|
|
22
|
-
def resolve_layer(layer, audio)
|
|
23
|
-
params = (layer[:params] || {})
|
|
24
|
-
|
|
32
|
+
def resolve_layer(layer, audio, time:, frame:, resolution:, globals:, custom_shape_overrides:, layer_param_overrides:)
|
|
33
|
+
params = deep_dup(layer[:params] || {})
|
|
34
|
+
apply_custom_shape_overrides!(params, layer_name: layer[:name], custom_shape_overrides: custom_shape_overrides)
|
|
35
|
+
merge_resolved_mappings!(params, resolve_mappings(layer[:mappings], audio, globals: globals, layer_name: layer[:name], time: time, frame: frame))
|
|
36
|
+
apply_layer_param_overrides!(params, layer_name: layer[:name], layer_param_overrides: layer_param_overrides)
|
|
37
|
+
expand_dynamic_custom_shapes!(params, layer: layer, audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
|
|
25
38
|
|
|
26
39
|
output = {
|
|
27
40
|
name: layer.fetch(:name).to_s,
|
|
@@ -35,14 +48,22 @@ module Vizcore
|
|
|
35
48
|
output
|
|
36
49
|
end
|
|
37
50
|
|
|
38
|
-
def resolve_mappings(mappings, audio, layer_name:)
|
|
51
|
+
def resolve_mappings(mappings, audio, globals:, layer_name:, time:, frame:)
|
|
39
52
|
Array(mappings).each_with_object({}) do |mapping, resolved|
|
|
40
53
|
source = mapping[:source]
|
|
41
54
|
target = mapping[:target]
|
|
42
55
|
next unless source && target
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
value =
|
|
57
|
+
state_key = [layer_name, target, source]
|
|
58
|
+
value = resolve_source_value(
|
|
59
|
+
source,
|
|
60
|
+
audio,
|
|
61
|
+
globals: globals,
|
|
62
|
+
time: time,
|
|
63
|
+
state_key: state_key,
|
|
64
|
+
frame: frame
|
|
65
|
+
)
|
|
66
|
+
value = apply_transform(value, mapping[:transform], state_key: state_key, frame: frame)
|
|
46
67
|
resolved[target.to_s] = value unless value.nil?
|
|
47
68
|
end
|
|
48
69
|
end
|
|
@@ -57,6 +78,107 @@ module Vizcore
|
|
|
57
78
|
end
|
|
58
79
|
end
|
|
59
80
|
|
|
81
|
+
def expand_dynamic_custom_shapes!(params, layer:, audio:, time:, frame:, resolution:, globals:)
|
|
82
|
+
descriptors = Array(params.delete(:custom_shapes) || params.delete("custom_shapes"))
|
|
83
|
+
return if descriptors.empty?
|
|
84
|
+
|
|
85
|
+
params[:shapes] = Array(params[:shapes])
|
|
86
|
+
controls = []
|
|
87
|
+
descriptors.each_with_index do |descriptor, index|
|
|
88
|
+
start_index = params[:shapes].length
|
|
89
|
+
expanded = expand_dynamic_custom_shape(descriptor, layer: layer, palette: params[:palette], audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
|
|
90
|
+
params[:shapes].concat(expanded)
|
|
91
|
+
controls << custom_shape_control_descriptor(descriptor, index: index, start_index: start_index, count: expanded.length)
|
|
92
|
+
end
|
|
93
|
+
params[:custom_shape_controls] = controls unless controls.empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def expand_dynamic_custom_shape(descriptor, layer:, palette:, audio:, time:, frame:, resolution:, globals:)
|
|
97
|
+
values = Hash(descriptor)
|
|
98
|
+
renderer = values.fetch(:renderer)
|
|
99
|
+
shape_name = values[:name] || renderer
|
|
100
|
+
primitives = Vizcore::Shape.expand_custom_shape(
|
|
101
|
+
renderer,
|
|
102
|
+
params: Hash(values[:params] || {}),
|
|
103
|
+
shape_id: values[:shape_id],
|
|
104
|
+
layer_name: layer[:name],
|
|
105
|
+
palette: Array(palette),
|
|
106
|
+
audio: audio,
|
|
107
|
+
time: time,
|
|
108
|
+
frame: frame,
|
|
109
|
+
resolution: resolution,
|
|
110
|
+
globals: globals,
|
|
111
|
+
shape_name: shape_name
|
|
112
|
+
)
|
|
113
|
+
primitives.each { |primitive| apply_custom_shape_attributes!(primitive, values) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def custom_shape_control_descriptor(descriptor, index:, start_index:, count:)
|
|
117
|
+
values = Hash(descriptor)
|
|
118
|
+
{
|
|
119
|
+
index: index,
|
|
120
|
+
name: (values[:name] || values["name"] || "custom_shape").to_s,
|
|
121
|
+
params: deep_dup(Hash(values[:params] || values["params"] || {})),
|
|
122
|
+
param_schema: Array(values[:param_schema] || values["param_schema"]).map { |entry| deep_dup(entry) },
|
|
123
|
+
shape_indices: (start_index...(start_index + count)).to_a
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def apply_custom_shape_overrides!(params, layer_name:, custom_shape_overrides:)
|
|
128
|
+
layer_overrides = custom_shape_layer_overrides(custom_shape_overrides, layer_name)
|
|
129
|
+
return if layer_overrides.empty?
|
|
130
|
+
|
|
131
|
+
descriptors = Array(params[:custom_shapes] || params["custom_shapes"])
|
|
132
|
+
layer_overrides.each do |index, values|
|
|
133
|
+
descriptor = descriptors[Integer(index)]
|
|
134
|
+
next unless descriptor && values.is_a?(Hash)
|
|
135
|
+
|
|
136
|
+
descriptor[:params] ||= {}
|
|
137
|
+
values.each do |param_name, value|
|
|
138
|
+
key = param_name.to_sym
|
|
139
|
+
descriptor[:params][key] = value
|
|
140
|
+
end
|
|
141
|
+
rescue ArgumentError, TypeError
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def custom_shape_layer_overrides(overrides, layer_name)
|
|
147
|
+
values = Hash(overrides)
|
|
148
|
+
name = layer_name.to_s
|
|
149
|
+
Hash(values[name] || values[layer_name.to_sym] || {})
|
|
150
|
+
rescue TypeError
|
|
151
|
+
{}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def apply_layer_param_overrides!(params, layer_name:, layer_param_overrides:)
|
|
155
|
+
layer_overrides = custom_shape_layer_overrides(layer_param_overrides, layer_name)
|
|
156
|
+
layer_overrides.each do |target, value|
|
|
157
|
+
target_name = target.to_s
|
|
158
|
+
if target_name.include?(".")
|
|
159
|
+
assign_nested_param(params, target_name.split("."), value)
|
|
160
|
+
else
|
|
161
|
+
params[target_name.to_sym] = value
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def apply_custom_shape_attributes!(primitive, descriptor)
|
|
167
|
+
style = Hash(descriptor[:style] || {})
|
|
168
|
+
style.each do |key, value|
|
|
169
|
+
symbol_key = key.to_sym
|
|
170
|
+
if symbol_key == :opacity && primitive.key?(:opacity)
|
|
171
|
+
primitive[:opacity] = numeric(style[:opacity] || style["opacity"], :opacity) * numeric(primitive[:opacity], :opacity)
|
|
172
|
+
else
|
|
173
|
+
primitive[symbol_key] = deep_dup(value) unless primitive.key?(symbol_key)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
transform = Hash(descriptor[:transform] || {})
|
|
178
|
+
primitive[:transform] = compose_shape_transform(transform, primitive[:transform]) unless transform.empty?
|
|
179
|
+
primitive
|
|
180
|
+
end
|
|
181
|
+
|
|
60
182
|
def assign_nested_param(container, path, value)
|
|
61
183
|
key = path.shift
|
|
62
184
|
if path.empty?
|
|
@@ -65,6 +187,7 @@ module Vizcore
|
|
|
65
187
|
end
|
|
66
188
|
|
|
67
189
|
next_container = nested_value(container, key)
|
|
190
|
+
next_container = create_nested_container(container, key, path.first) if next_container.nil?
|
|
68
191
|
return unless next_container
|
|
69
192
|
|
|
70
193
|
assign_nested_param(next_container, path, value)
|
|
@@ -77,6 +200,13 @@ module Vizcore
|
|
|
77
200
|
nil
|
|
78
201
|
end
|
|
79
202
|
|
|
203
|
+
def create_nested_container(container, key, next_key)
|
|
204
|
+
return unless container.is_a?(Hash)
|
|
205
|
+
|
|
206
|
+
value = integer_key?(next_key) ? [] : {}
|
|
207
|
+
container[key.to_sym] = value
|
|
208
|
+
end
|
|
209
|
+
|
|
80
210
|
def assign_nested_value(container, key, value)
|
|
81
211
|
if container.is_a?(Array) && integer_key?(key)
|
|
82
212
|
container[key.to_i] = value
|
|
@@ -89,12 +219,58 @@ module Vizcore
|
|
|
89
219
|
value.match?(/\A\d+\z/)
|
|
90
220
|
end
|
|
91
221
|
|
|
92
|
-
def
|
|
222
|
+
def compose_shape_transform(parent, child)
|
|
223
|
+
return deep_dup(child || {}) unless parent
|
|
224
|
+
|
|
225
|
+
child ||= {}
|
|
226
|
+
output = deep_dup(parent)
|
|
227
|
+
output[:translate] = add_shape_xy(parent[:translate], child[:translate]) if child.key?(:translate)
|
|
228
|
+
output[:origin] = child[:origin] if child.key?(:origin)
|
|
229
|
+
output[:rotate] = numeric(parent[:rotate] || 0, :rotate) + numeric(child[:rotate] || 0, :rotate) if child.key?(:rotate)
|
|
230
|
+
output[:scale] = multiply_shape_scale(parent[:scale], child[:scale]) if child.key?(:scale)
|
|
231
|
+
output
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def add_shape_xy(parent, child)
|
|
235
|
+
parent ||= {}
|
|
236
|
+
child ||= {}
|
|
237
|
+
{
|
|
238
|
+
x: numeric(parent[:x] || parent["x"] || 0, :"translate.x") + numeric(child[:x] || child["x"] || 0, :"translate.x"),
|
|
239
|
+
y: numeric(parent[:y] || parent["y"] || 0, :"translate.y") + numeric(child[:y] || child["y"] || 0, :"translate.y")
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def multiply_shape_scale(parent, child)
|
|
244
|
+
parent = shape_scale_pair(parent)
|
|
245
|
+
child = shape_scale_pair(child)
|
|
246
|
+
{ x: parent[:x] * child[:x], y: parent[:y] * child[:y] }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def shape_scale_pair(value)
|
|
250
|
+
return { x: numeric(value[:x] || value["x"] || 1, :"scale.x"), y: numeric(value[:y] || value["y"] || 1, :"scale.y") } if value.is_a?(Hash)
|
|
251
|
+
|
|
252
|
+
scale = numeric(value || 1, :scale)
|
|
253
|
+
{ x: scale, y: scale }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def numeric(value, name)
|
|
257
|
+
Float(value)
|
|
258
|
+
rescue ArgumentError, TypeError
|
|
259
|
+
raise ArgumentError, "param #{name} must be numeric"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def resolve_source_value(source, audio, globals: {}, time: 0.0, state_key: nil, frame: 0)
|
|
263
|
+
return 0.0 unless source
|
|
264
|
+
|
|
93
265
|
case source[:kind]&.to_sym
|
|
94
266
|
when :amplitude
|
|
95
267
|
audio[:amplitude]
|
|
268
|
+
when :peak
|
|
269
|
+
audio[:peak]
|
|
96
270
|
when :frequency_band
|
|
97
271
|
audio.dig(:bands, source[:band]&.to_sym)
|
|
272
|
+
when :frequency_band_peak
|
|
273
|
+
audio.dig(:band_peaks, source[:band]&.to_sym)
|
|
98
274
|
when :fft_spectrum
|
|
99
275
|
audio[:fft]
|
|
100
276
|
when :onset
|
|
@@ -109,13 +285,252 @@ module Vizcore
|
|
|
109
285
|
audio[:beat_pulse]
|
|
110
286
|
when :beat_count
|
|
111
287
|
audio[:beat_count]
|
|
288
|
+
when :beat_phase
|
|
289
|
+
audio[:beat_phase]
|
|
290
|
+
when :beat_2
|
|
291
|
+
audio[:beat_2]
|
|
292
|
+
when :beat_4
|
|
293
|
+
audio[:beat_4]
|
|
294
|
+
when :beat_8
|
|
295
|
+
audio[:beat_8]
|
|
296
|
+
when :beat_triplet, :triplet
|
|
297
|
+
audio[:beat_triplet]
|
|
298
|
+
when :bar_phase
|
|
299
|
+
audio[:bar_phase]
|
|
300
|
+
when :bar_count
|
|
301
|
+
audio[:bar_count]
|
|
302
|
+
when :phrase_count
|
|
303
|
+
audio[:phrase_count]
|
|
112
304
|
when :bpm
|
|
113
305
|
audio[:bpm]
|
|
306
|
+
when :bpm_confidence
|
|
307
|
+
audio[:bpm_confidence]
|
|
308
|
+
when :spectral_centroid
|
|
309
|
+
audio[:spectral_centroid]
|
|
310
|
+
when :spectral_rolloff
|
|
311
|
+
audio[:spectral_rolloff]
|
|
312
|
+
when :spectral_flatness
|
|
313
|
+
audio[:spectral_flatness]
|
|
314
|
+
when :spectral_flux
|
|
315
|
+
audio[:spectral_flux]
|
|
316
|
+
when :zero_crossing_rate
|
|
317
|
+
audio[:zero_crossing_rate]
|
|
318
|
+
when :global
|
|
319
|
+
resolve_global(source, globals)
|
|
320
|
+
when :lfo
|
|
321
|
+
resolve_lfo(source, time)
|
|
322
|
+
when :adsr, :envelope
|
|
323
|
+
resolve_envelope(source, audio, globals: globals, time: time, state_key: state_key, frame: frame)
|
|
114
324
|
else
|
|
115
325
|
nil
|
|
116
326
|
end
|
|
117
327
|
end
|
|
118
328
|
|
|
329
|
+
def resolve_envelope(source, audio, globals: {}, time: 0.0, state_key:, frame:)
|
|
330
|
+
state = envelope_state(state_key)
|
|
331
|
+
params = envelope_params(source)
|
|
332
|
+
nested = source[:source] || :kick
|
|
333
|
+
normalized_nested = normalize_source_descriptor(nested)
|
|
334
|
+
trigger_value = resolve_nested_source_value(normalized_nested, audio, globals: globals, time: time)
|
|
335
|
+
trigger = trigger_numeric(trigger_value)
|
|
336
|
+
|
|
337
|
+
now = normalized_time(time)
|
|
338
|
+
state[:time] = now
|
|
339
|
+
state[:last_frame] = frame
|
|
340
|
+
state[:gate] = trigger > params[:threshold]
|
|
341
|
+
state[:note_on] = state[:gate]
|
|
342
|
+
|
|
343
|
+
peak = normalize_envelope_peak(params.fetch(:peak)) * trigger
|
|
344
|
+
if state[:gate]
|
|
345
|
+
if state[:phase] == :idle || state[:phase] == :release
|
|
346
|
+
state[:phase] = :attack
|
|
347
|
+
state[:phase_started_at] = now
|
|
348
|
+
state[:phase_start_value] = state[:value]
|
|
349
|
+
state[:peak] = peak
|
|
350
|
+
else
|
|
351
|
+
state[:peak] = [state[:peak], peak].max
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
state[:value], state[:phase] = next_envelope_step(
|
|
356
|
+
state,
|
|
357
|
+
params: params,
|
|
358
|
+
now: now
|
|
359
|
+
)
|
|
360
|
+
state[:value]
|
|
361
|
+
rescue StandardError
|
|
362
|
+
0.0
|
|
363
|
+
ensure
|
|
364
|
+
@mapping_state[state_key] = state if state_key
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def resolve_nested_source_value(source, audio, globals:, time:)
|
|
368
|
+
return resolve_source_value({ kind: :amplitude }, audio, globals: globals, time: time) if source.nil?
|
|
369
|
+
|
|
370
|
+
nested_kind = source[:kind]&.to_sym
|
|
371
|
+
return 0.0 if nested_kind == :adsr || nested_kind == :envelope
|
|
372
|
+
|
|
373
|
+
resolve_source_value(source, audio, globals: globals, time: time)
|
|
374
|
+
rescue StandardError
|
|
375
|
+
0.0
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def normalize_source_descriptor(source)
|
|
379
|
+
return source if source.is_a?(Hash) && source[:kind]
|
|
380
|
+
|
|
381
|
+
{ kind: source.to_sym }
|
|
382
|
+
rescue StandardError
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def resolve_lfo(source, time)
|
|
387
|
+
rate = Float(source[:rate] || 1.0)
|
|
388
|
+
phase = Float(source[:phase] || 0.0)
|
|
389
|
+
position = (Float(time) * rate + phase) % 1.0
|
|
390
|
+
case source[:wave]&.to_sym
|
|
391
|
+
when :triangle
|
|
392
|
+
1.0 - ((position * 2.0) - 1.0).abs
|
|
393
|
+
when :saw
|
|
394
|
+
position
|
|
395
|
+
when :square
|
|
396
|
+
position < 0.5 ? 1.0 : 0.0
|
|
397
|
+
else
|
|
398
|
+
(Math.sin(position * Math::PI * 2.0) + 1.0) * 0.5
|
|
399
|
+
end
|
|
400
|
+
rescue ArgumentError, TypeError
|
|
401
|
+
0.0
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def resolve_global(source, globals)
|
|
405
|
+
name = source[:name]&.to_sym
|
|
406
|
+
return nil unless name
|
|
407
|
+
|
|
408
|
+
values = Hash(globals || {})
|
|
409
|
+
values[name] || values[name.to_s]
|
|
410
|
+
rescue StandardError
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def envelope_state(state_key)
|
|
415
|
+
return {} unless state_key
|
|
416
|
+
|
|
417
|
+
@mapping_state[state_key] ||= {
|
|
418
|
+
phase: :idle,
|
|
419
|
+
value: 0.0,
|
|
420
|
+
peak: 0.0,
|
|
421
|
+
phase_started_at: 0.0,
|
|
422
|
+
phase_start_value: 0.0,
|
|
423
|
+
time: 0.0,
|
|
424
|
+
note_on: false,
|
|
425
|
+
gate: false
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def envelope_params(source)
|
|
430
|
+
{
|
|
431
|
+
attack: Float(source[:attack] || 0.02),
|
|
432
|
+
decay: Float(source[:decay] || 0.08),
|
|
433
|
+
sustain: Float(source[:sustain] || 0.7).clamp(0.0, 1.0),
|
|
434
|
+
release: Float(source[:release] || 0.16),
|
|
435
|
+
threshold: Float(source[:threshold] || 0.0),
|
|
436
|
+
peak: Float(source[:peak] || 1.0)
|
|
437
|
+
}
|
|
438
|
+
rescue StandardError
|
|
439
|
+
{ attack: 0.02, decay: 0.08, sustain: 0.7, release: 0.16, threshold: 0.0, peak: 1.0 }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def normalized_time(value)
|
|
443
|
+
numeric = Float(value)
|
|
444
|
+
numeric.nan? ? 0.0 : numeric
|
|
445
|
+
rescue StandardError
|
|
446
|
+
0.0
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def normalize_envelope_peak(value)
|
|
450
|
+
value = Float(value)
|
|
451
|
+
value.nan? ? 1.0 : value
|
|
452
|
+
rescue StandardError
|
|
453
|
+
1.0
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def next_envelope_step(state, params:, now:)
|
|
457
|
+
phase = state[:phase] || :idle
|
|
458
|
+
if phase == :attack
|
|
459
|
+
return [state[:peak], :sustain] if params[:attack] <= 0.0
|
|
460
|
+
|
|
461
|
+
elapsed = now - state[:phase_started_at]
|
|
462
|
+
if elapsed >= params[:attack]
|
|
463
|
+
state[:phase_started_at] = now
|
|
464
|
+
state[:phase_start_value] = state[:peak]
|
|
465
|
+
return [state[:peak], :decay]
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
ratio = [elapsed / params[:attack], 1.0].min
|
|
469
|
+
value = state[:phase_start_value] + (state[:peak] - state[:phase_start_value]) * ratio
|
|
470
|
+
return [value, :attack]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
if phase == :decay
|
|
474
|
+
return [state[:peak] * params[:sustain], :sustain] if params[:decay] <= 0.0
|
|
475
|
+
|
|
476
|
+
elapsed = now - state[:phase_started_at]
|
|
477
|
+
target = state[:peak] * params[:sustain]
|
|
478
|
+
if elapsed >= params[:decay]
|
|
479
|
+
state[:phase_started_at] = now
|
|
480
|
+
state[:phase_start_value] = target
|
|
481
|
+
return [target, :sustain]
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
ratio = [elapsed / params[:decay], 1.0].min
|
|
485
|
+
value = state[:phase_start_value] + (target - state[:phase_start_value]) * ratio
|
|
486
|
+
return [value, :decay]
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
if phase == :sustain
|
|
490
|
+
return state[:phase_start_value], :sustain if state[:gate]
|
|
491
|
+
|
|
492
|
+
state[:phase] = :release
|
|
493
|
+
state[:phase_started_at] = now
|
|
494
|
+
state[:phase_start_value] = state[:value]
|
|
495
|
+
return [state[:value], :release]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
if phase == :release
|
|
499
|
+
return [0.0, :idle] if params[:release] <= 0.0
|
|
500
|
+
|
|
501
|
+
elapsed = now - state[:phase_started_at]
|
|
502
|
+
if elapsed >= params[:release]
|
|
503
|
+
state[:phase_started_at] = now
|
|
504
|
+
state[:phase_start_value] = 0.0
|
|
505
|
+
return [0.0, :idle]
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
ratio = [elapsed / params[:release], 1.0].min
|
|
509
|
+
value = state[:phase_start_value] * (1.0 - ratio)
|
|
510
|
+
return [value, :release]
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
if phase == :idle
|
|
514
|
+
return [0.0, :idle] unless state[:gate] && params[:attack] > 0.0
|
|
515
|
+
|
|
516
|
+
state[:phase_started_at] = now
|
|
517
|
+
state[:phase_start_value] = 0.0
|
|
518
|
+
return [0.0, :attack]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
[0.0, :idle]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def trigger_numeric(value)
|
|
525
|
+
return 0.0 if value == false || value == 0
|
|
526
|
+
return 1.0 if value == true
|
|
527
|
+
return 0.0 if value.nil?
|
|
528
|
+
|
|
529
|
+
Float(value)
|
|
530
|
+
rescue StandardError
|
|
531
|
+
0.0
|
|
532
|
+
end
|
|
533
|
+
|
|
119
534
|
def resolve_onset(source, audio)
|
|
120
535
|
band = source[:band]&.to_sym
|
|
121
536
|
return audio[:onset] unless band
|
|
@@ -123,15 +538,18 @@ module Vizcore
|
|
|
123
538
|
audio.dig(:onsets, band)
|
|
124
539
|
end
|
|
125
540
|
|
|
126
|
-
def apply_transform(value, transform, state_key:)
|
|
541
|
+
def apply_transform(value, transform, state_key:, frame:)
|
|
127
542
|
return value if transform.nil? || transform.empty?
|
|
128
543
|
return transform_array(value, transform) if value.is_a?(Array)
|
|
129
544
|
return nil if value.is_a?(Hash) || value.nil?
|
|
130
545
|
|
|
131
|
-
transformed = transform_scalar(value, transform)
|
|
546
|
+
transformed = transform_scalar(value, transform, state_key: state_key)
|
|
132
547
|
return nil if transformed.nil?
|
|
133
548
|
|
|
134
|
-
|
|
549
|
+
transformed = apply_trigger_mode(transformed, transform, state_key: state_key) if transform[:as] == :trigger
|
|
550
|
+
transformed = apply_event_shaping(transformed, transform, state_key: state_key, frame: frame)
|
|
551
|
+
return apply_smoothing(transformed, transform, state_key) unless transform[:as] == :trigger
|
|
552
|
+
transformed
|
|
135
553
|
end
|
|
136
554
|
|
|
137
555
|
def transform_array(value, transform)
|
|
@@ -140,11 +558,12 @@ module Vizcore
|
|
|
140
558
|
end
|
|
141
559
|
end
|
|
142
560
|
|
|
143
|
-
def transform_scalar(value, transform, fallback: nil)
|
|
561
|
+
def transform_scalar(value, transform, fallback: nil, state_key: nil)
|
|
144
562
|
numeric = numeric_value(value, fallback: fallback)
|
|
145
563
|
return nil if numeric.nil?
|
|
146
564
|
|
|
147
565
|
numeric = 0.0 if transform.key?(:deadzone) && numeric.abs < Float(transform[:deadzone])
|
|
566
|
+
numeric = apply_threshold(numeric, transform, state_key: state_key)
|
|
148
567
|
numeric *= Float(transform[:gain]) if transform.key?(:gain)
|
|
149
568
|
numeric = apply_curve(numeric, transform[:curve]) if transform[:curve]
|
|
150
569
|
numeric = [numeric, Float(transform[:min])].max if transform.key?(:min)
|
|
@@ -152,6 +571,20 @@ module Vizcore
|
|
|
152
571
|
numeric
|
|
153
572
|
end
|
|
154
573
|
|
|
574
|
+
def apply_threshold(value, transform, state_key:)
|
|
575
|
+
return value unless transform.key?(:threshold) || transform.key?(:hysteresis)
|
|
576
|
+
|
|
577
|
+
threshold = Float(transform.fetch(:threshold, 0.5))
|
|
578
|
+
hysteresis = Float(transform.fetch(:hysteresis, 0.0))
|
|
579
|
+
return value >= threshold ? value : 0.0 if hysteresis <= 0.0 || state_key.nil?
|
|
580
|
+
|
|
581
|
+
key = [:hysteresis, state_key]
|
|
582
|
+
active = !!@mapping_state[key]
|
|
583
|
+
active = value >= (active ? threshold - hysteresis : threshold)
|
|
584
|
+
@mapping_state[key] = active
|
|
585
|
+
active ? value : 0.0
|
|
586
|
+
end
|
|
587
|
+
|
|
155
588
|
def numeric_value(value, fallback:)
|
|
156
589
|
return value ? 1.0 : 0.0 if value == true || value == false
|
|
157
590
|
|
|
@@ -171,12 +604,111 @@ module Vizcore
|
|
|
171
604
|
when :ease_out
|
|
172
605
|
clamped = [[value, 0.0].max, 1.0].min
|
|
173
606
|
1.0 - ((1.0 - clamped) * (1.0 - clamped))
|
|
607
|
+
when :ease_in
|
|
608
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
609
|
+
clamped * clamped
|
|
610
|
+
when :ease_in_out
|
|
611
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
612
|
+
clamped < 0.5 ? 2.0 * clamped * clamped : 1.0 - ((-2.0 * clamped + 2.0)**2 / 2.0)
|
|
613
|
+
when :smoothstep
|
|
614
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
615
|
+
clamped * clamped * (3.0 - 2.0 * clamped)
|
|
616
|
+
when :exp
|
|
617
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
618
|
+
((Math.exp(clamped) - 1.0) / (Math::E - 1.0)).clamp(0.0, 1.0)
|
|
619
|
+
when :log
|
|
620
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
621
|
+
Math.log1p(clamped * (Math::E - 1.0))
|
|
622
|
+
when :step
|
|
623
|
+
value >= 0.5 ? 1.0 : 0.0
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def apply_trigger_mode(value, _transform, state_key:)
|
|
628
|
+
key = [:trigger, state_key]
|
|
629
|
+
active = value.to_f > 0.0
|
|
630
|
+
previous = !!@mapping_state[key]
|
|
631
|
+
@mapping_state[key] = active
|
|
632
|
+
(active && !previous) ? 1.0 : 0.0
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def apply_event_shaping(value, transform, state_key:, frame:)
|
|
636
|
+
shaped = value
|
|
637
|
+
shaped = apply_cooldown(shaped, transform, state_key: state_key, frame: frame) if transform.key?(:cooldown)
|
|
638
|
+
shaped = apply_one_shot(shaped, transform, state_key: state_key) if transform[:one_shot]
|
|
639
|
+
shaped = apply_hold(shaped, transform, state_key: state_key, frame: frame) if transform.key?(:hold)
|
|
640
|
+
shaped = apply_decay(shaped, transform, state_key: state_key) if transform.key?(:decay)
|
|
641
|
+
shaped
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def apply_cooldown(value, transform, state_key:, frame:)
|
|
645
|
+
cooldown_frames = (Float(transform[:cooldown]) * 60.0).ceil
|
|
646
|
+
return value unless cooldown_frames.positive?
|
|
647
|
+
|
|
648
|
+
key = [:cooldown, state_key]
|
|
649
|
+
state = @mapping_state[key] || { until_frame: 0 }
|
|
650
|
+
current_frame = Integer(frame)
|
|
651
|
+
return value unless value.to_f > 0.0
|
|
652
|
+
|
|
653
|
+
if current_frame >= state[:until_frame]
|
|
654
|
+
state[:until_frame] = current_frame + cooldown_frames
|
|
655
|
+
@mapping_state[key] = state
|
|
656
|
+
value
|
|
657
|
+
else
|
|
658
|
+
0.0
|
|
659
|
+
end
|
|
660
|
+
rescue StandardError
|
|
661
|
+
value
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def apply_one_shot(value, _transform, state_key:)
|
|
665
|
+
key = [:one_shot, state_key]
|
|
666
|
+
|
|
667
|
+
active = value.to_f > 0.0
|
|
668
|
+
return 0.0 unless active
|
|
669
|
+
|
|
670
|
+
fired = !!@mapping_state[key]
|
|
671
|
+
return 0.0 if fired
|
|
672
|
+
|
|
673
|
+
@mapping_state[key] = true
|
|
674
|
+
value
|
|
675
|
+
rescue StandardError
|
|
676
|
+
value
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def apply_hold(value, transform, state_key:, frame:)
|
|
680
|
+
hold_frames = (Float(transform[:hold]) * 60.0).ceil
|
|
681
|
+
return value unless hold_frames.positive?
|
|
682
|
+
|
|
683
|
+
key = [:hold, state_key]
|
|
684
|
+
state = @mapping_state[key] || { until_frame: -1, value: 0.0 }
|
|
685
|
+
current_frame = Integer(frame)
|
|
686
|
+
if value.to_f.positive?
|
|
687
|
+
state = { until_frame: current_frame + hold_frames, value: value }
|
|
688
|
+
elsif current_frame <= state[:until_frame]
|
|
689
|
+
value = state[:value]
|
|
174
690
|
end
|
|
691
|
+
@mapping_state[key] = state
|
|
692
|
+
value
|
|
693
|
+
rescue StandardError
|
|
694
|
+
value
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def apply_decay(value, transform, state_key:)
|
|
698
|
+
decay = Float(transform[:decay]).clamp(0.0, 1.0)
|
|
699
|
+
key = [:decay, state_key]
|
|
700
|
+
previous = @mapping_state[key].to_f
|
|
701
|
+
output = [value.to_f, previous * decay].max
|
|
702
|
+
@mapping_state[key] = output
|
|
703
|
+
output
|
|
704
|
+
rescue StandardError
|
|
705
|
+
value
|
|
175
706
|
end
|
|
176
707
|
|
|
177
708
|
def apply_smoothing(value, transform, state_key)
|
|
178
709
|
return value unless transform.key?(:attack) || transform.key?(:release)
|
|
179
710
|
|
|
711
|
+
state_key = [:smooth, state_key]
|
|
180
712
|
previous = @mapping_state[state_key]
|
|
181
713
|
if previous.nil?
|
|
182
714
|
@mapping_state[state_key] = value
|
|
@@ -193,6 +725,10 @@ module Vizcore
|
|
|
193
725
|
Array(scene_layers).map { |layer| deep_symbolize(layer) }
|
|
194
726
|
end
|
|
195
727
|
|
|
728
|
+
def deep_dup(value)
|
|
729
|
+
Vizcore::DeepCopy.copy(value)
|
|
730
|
+
end
|
|
731
|
+
|
|
196
732
|
def deep_symbolize(value)
|
|
197
733
|
case value
|
|
198
734
|
when Hash
|