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
|
@@ -4,7 +4,7 @@ module Vizcore
|
|
|
4
4
|
module Analysis
|
|
5
5
|
# Estimates tempo (BPM) from beat onsets using lag autocorrelation.
|
|
6
6
|
class BPMEstimator
|
|
7
|
-
attr_reader :frame_rate
|
|
7
|
+
attr_reader :confidence, :frame_rate
|
|
8
8
|
|
|
9
9
|
# @param frame_rate [Float] analysis frames per second
|
|
10
10
|
# @param min_bpm [Float] minimum candidate BPM
|
|
@@ -21,6 +21,7 @@ module Vizcore
|
|
|
21
21
|
@min_onsets = Integer(min_onsets)
|
|
22
22
|
@history = []
|
|
23
23
|
@current_bpm = 0.0
|
|
24
|
+
@confidence = 0.0
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# @param beat [Boolean] whether the current frame contains a beat onset
|
|
@@ -29,10 +30,17 @@ module Vizcore
|
|
|
29
30
|
@history << (beat ? 1.0 : 0.0)
|
|
30
31
|
@history.shift while @history.length > @history_size
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
if onset_count < @min_onsets
|
|
34
|
+
@confidence = 0.0
|
|
35
|
+
return @current_bpm
|
|
36
|
+
end
|
|
33
37
|
|
|
34
|
-
candidate = estimate_candidate_bpm
|
|
35
|
-
|
|
38
|
+
candidate, confidence = estimate_candidate_bpm
|
|
39
|
+
if candidate <= 0.0
|
|
40
|
+
@confidence = 0.0
|
|
41
|
+
return @current_bpm
|
|
42
|
+
end
|
|
43
|
+
@confidence = confidence
|
|
36
44
|
|
|
37
45
|
@current_bpm =
|
|
38
46
|
if @current_bpm <= 0.0
|
|
@@ -50,6 +58,7 @@ module Vizcore
|
|
|
50
58
|
def reset
|
|
51
59
|
@history.clear
|
|
52
60
|
@current_bpm = 0.0
|
|
61
|
+
@confidence = 0.0
|
|
53
62
|
end
|
|
54
63
|
|
|
55
64
|
private
|
|
@@ -60,11 +69,11 @@ module Vizcore
|
|
|
60
69
|
|
|
61
70
|
def estimate_candidate_bpm
|
|
62
71
|
n = @history.length
|
|
63
|
-
return 0.0 if n < 2
|
|
72
|
+
return [0.0, 0.0] if n < 2
|
|
64
73
|
|
|
65
74
|
min_lag = [(60.0 * @frame_rate / @max_bpm).round, 1].max
|
|
66
75
|
max_lag = [(60.0 * @frame_rate / @min_bpm).round, n - 1].min
|
|
67
|
-
return 0.0 if min_lag > max_lag
|
|
76
|
+
return [0.0, 0.0] if min_lag > max_lag
|
|
68
77
|
|
|
69
78
|
best_lag = nil
|
|
70
79
|
best_score = -Float::INFINITY
|
|
@@ -77,9 +86,10 @@ module Vizcore
|
|
|
77
86
|
best_lag = lag
|
|
78
87
|
end
|
|
79
88
|
|
|
80
|
-
return 0.0 unless best_lag && best_score.positive?
|
|
89
|
+
return [0.0, 0.0] unless best_lag && best_score.positive?
|
|
81
90
|
|
|
82
|
-
(60.0 * @frame_rate / best_lag).clamp(@min_bpm, @max_bpm)
|
|
91
|
+
bpm = (60.0 * @frame_rate / best_lag).clamp(@min_bpm, @max_bpm)
|
|
92
|
+
[bpm, (best_score / onset_count.to_f).clamp(0.0, 1.0)]
|
|
83
93
|
end
|
|
84
94
|
|
|
85
95
|
def autocorrelation_at_lag(lag)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "digest"
|
|
4
5
|
require "json"
|
|
5
6
|
require "pathname"
|
|
6
7
|
require_relative "../audio/file_input"
|
|
@@ -22,7 +23,8 @@ module Vizcore
|
|
|
22
23
|
noise_gate: Pipeline::DEFAULT_NOISE_GATE,
|
|
23
24
|
audio_normalize: nil,
|
|
24
25
|
bpm: nil,
|
|
25
|
-
bpm_lock: false
|
|
26
|
+
bpm_lock: false,
|
|
27
|
+
cache_root: nil
|
|
26
28
|
)
|
|
27
29
|
@audio_file = Pathname.new(audio_file.to_s).expand_path
|
|
28
30
|
@frames = normalize_frame_count(frames)
|
|
@@ -31,6 +33,16 @@ module Vizcore
|
|
|
31
33
|
@audio_normalize = audio_normalize
|
|
32
34
|
@bpm = bpm
|
|
33
35
|
@bpm_lock = bpm_lock
|
|
36
|
+
@cache_root = normalize_cache_root(cache_root)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Absolute cache file path for current recorder settings.
|
|
40
|
+
#
|
|
41
|
+
# @return [Pathname, nil]
|
|
42
|
+
def cache_path
|
|
43
|
+
return nil unless @cache_root
|
|
44
|
+
|
|
45
|
+
@cache_root.join("#{cache_key}.json")
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
# @param out [String, Pathname] JSON output path
|
|
@@ -38,18 +50,116 @@ module Vizcore
|
|
|
38
50
|
def write(out:)
|
|
39
51
|
output_path = Pathname.new(out.to_s).expand_path
|
|
40
52
|
FileUtils.mkdir_p(output_path.dirname)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
|
|
54
|
+
if cache_path
|
|
55
|
+
cached_payload = load_cached_payload(cache_path)
|
|
56
|
+
output_payload = cached_payload if cached_payload
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
output_payload ||= record
|
|
60
|
+
output_path.write("#{JSON.pretty_generate(output_payload)}\n")
|
|
61
|
+
write_cached_output(output_payload) if cache_path && cache_path != output_path
|
|
62
|
+
|
|
63
|
+
metadata_from_payload(output_payload, path: output_path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Stable hash key for this recorder configuration.
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def cache_key
|
|
70
|
+
self.class.cache_key(
|
|
71
|
+
version: VERSION,
|
|
72
|
+
audio_file: @audio_file,
|
|
45
73
|
frames: @frames,
|
|
46
74
|
fps: @fps,
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
noise_gate: @noise_gate,
|
|
76
|
+
audio_normalize: @audio_normalize,
|
|
77
|
+
bpm: @bpm,
|
|
78
|
+
bpm_lock: @bpm_lock
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Hash] recorder metadata
|
|
83
|
+
def self.cache_key(version:, audio_file:, frames:, fps:, noise_gate:, audio_normalize:, bpm:, bpm_lock:)
|
|
84
|
+
audio_path = Pathname.new(audio_file.to_s).expand_path
|
|
85
|
+
Digest::SHA256.hexdigest(
|
|
86
|
+
JSON.generate(
|
|
87
|
+
{
|
|
88
|
+
version: version,
|
|
89
|
+
audio_file: audio_path.to_s,
|
|
90
|
+
audio_file_size: audio_file_size(audio_path),
|
|
91
|
+
audio_file_mtime: audio_file_mtime(audio_path),
|
|
92
|
+
frames: frames,
|
|
93
|
+
fps: fps,
|
|
94
|
+
noise_gate: noise_gate,
|
|
95
|
+
audio_normalize: audio_normalize,
|
|
96
|
+
bpm: bpm,
|
|
97
|
+
bpm_lock: bpm_lock
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.audio_file_size(path)
|
|
104
|
+
file_stat_value(path, :size)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.audio_file_mtime(path)
|
|
108
|
+
file_stat_value(path, :mtime).to_i
|
|
109
|
+
rescue StandardError
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.file_stat_value(path, name)
|
|
114
|
+
stat = Pathname.new(path).stat
|
|
115
|
+
stat.send(name)
|
|
116
|
+
rescue StandardError
|
|
117
|
+
0
|
|
49
118
|
end
|
|
50
119
|
|
|
51
120
|
private
|
|
52
121
|
|
|
122
|
+
def load_cached_payload(path)
|
|
123
|
+
payload = JSON.parse(Pathname.new(path).read)
|
|
124
|
+
return unless payload.is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
payload if valid_cached_payload?(payload)
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def valid_cached_payload?(payload)
|
|
132
|
+
payload.fetch("version", nil) == VERSION &&
|
|
133
|
+
payload.fetch("metadata", {}).fetch("frames", nil) == @frames &&
|
|
134
|
+
(Float(payload.fetch("metadata", {}).fetch("fps", nil)) - @fps).abs < Float::EPSILON &&
|
|
135
|
+
payload.fetch("features", nil).is_a?(Array) &&
|
|
136
|
+
!payload.fetch("features", []).empty?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def write_cached_output(payload)
|
|
140
|
+
cache = cache_path
|
|
141
|
+
return unless cache
|
|
142
|
+
|
|
143
|
+
FileUtils.mkdir_p(cache.dirname)
|
|
144
|
+
cache.write("#{JSON.pretty_generate(payload)}\n")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def metadata_from_payload(payload, path:)
|
|
148
|
+
metadata = payload.fetch("metadata", {})
|
|
149
|
+
{
|
|
150
|
+
path: path,
|
|
151
|
+
frames: metadata.fetch("frames"),
|
|
152
|
+
fps: metadata.fetch("fps"),
|
|
153
|
+
sample_rate: metadata.fetch("sample_rate")
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def normalize_cache_root(value)
|
|
158
|
+
return nil if value.nil?
|
|
159
|
+
|
|
160
|
+
Pathname.new(value.to_s).expand_path
|
|
161
|
+
end
|
|
162
|
+
|
|
53
163
|
def record
|
|
54
164
|
validate_audio_file!
|
|
55
165
|
input = Vizcore::Audio::FileInput.new(path: @audio_file.to_s)
|
|
@@ -8,7 +8,7 @@ module Vizcore
|
|
|
8
8
|
module Analysis
|
|
9
9
|
# Replays recorded analysis features as a pipeline-compatible source.
|
|
10
10
|
class FeatureReplay
|
|
11
|
-
attr_reader :metadata
|
|
11
|
+
attr_reader :metadata, :cursor
|
|
12
12
|
|
|
13
13
|
def initialize(path:)
|
|
14
14
|
@path = Pathname.new(path.to_s).expand_path
|
|
@@ -30,8 +30,48 @@ module Vizcore
|
|
|
30
30
|
@features.length
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Move the replay cursor to a frame index.
|
|
34
|
+
#
|
|
35
|
+
# @param index [Integer]
|
|
36
|
+
# @return [Vizcore::Analysis::FeatureReplay]
|
|
37
|
+
def seek(index)
|
|
38
|
+
@cursor = normalize_index(index)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Move the replay cursor to the frame nearest to the given timestamp.
|
|
43
|
+
#
|
|
44
|
+
# @param seconds [Numeric]
|
|
45
|
+
# @return [Vizcore::Analysis::FeatureReplay]
|
|
46
|
+
def seek_seconds(seconds)
|
|
47
|
+
fps = metadata_fps
|
|
48
|
+
raise ArgumentError, "feature metadata fps must be positive to seek by seconds" unless fps.positive?
|
|
49
|
+
|
|
50
|
+
seek((numeric_seconds(seconds) * fps).floor)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Read a specific feature frame without changing the replay cursor.
|
|
54
|
+
#
|
|
55
|
+
# @param index [Integer]
|
|
56
|
+
# @return [Hash<Symbol, Object>]
|
|
57
|
+
def frame(index)
|
|
58
|
+
deep_dup(@features.fetch(normalize_index(index)))
|
|
59
|
+
end
|
|
60
|
+
|
|
33
61
|
private
|
|
34
62
|
|
|
63
|
+
def metadata_fps
|
|
64
|
+
Float(metadata[:fps] || metadata["fps"] || 0.0)
|
|
65
|
+
rescue ArgumentError, TypeError
|
|
66
|
+
0.0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def numeric_seconds(value)
|
|
70
|
+
Float(value)
|
|
71
|
+
rescue ArgumentError, TypeError
|
|
72
|
+
raise ArgumentError, "seconds must be numeric"
|
|
73
|
+
end
|
|
74
|
+
|
|
35
75
|
def load_payload
|
|
36
76
|
raise ArgumentError, "Feature file not found: #{@path}" unless @path.file?
|
|
37
77
|
|
|
@@ -56,6 +96,12 @@ module Vizcore
|
|
|
56
96
|
features
|
|
57
97
|
end
|
|
58
98
|
|
|
99
|
+
def normalize_index(value)
|
|
100
|
+
Integer(value) % @features.length
|
|
101
|
+
rescue ArgumentError, TypeError
|
|
102
|
+
raise ArgumentError, "feature frame index must be an integer"
|
|
103
|
+
end
|
|
104
|
+
|
|
59
105
|
def deep_symbolize(value)
|
|
60
106
|
case value
|
|
61
107
|
when Hash
|
|
@@ -70,14 +116,7 @@ module Vizcore
|
|
|
70
116
|
end
|
|
71
117
|
|
|
72
118
|
def deep_dup(value)
|
|
73
|
-
|
|
74
|
-
when Hash
|
|
75
|
-
value.each_with_object({}) { |(key, entry), output| output[key] = deep_dup(entry) }
|
|
76
|
-
when Array
|
|
77
|
-
value.map { |entry| deep_dup(entry) }
|
|
78
|
-
else
|
|
79
|
-
value
|
|
80
|
-
end
|
|
119
|
+
Vizcore::DeepCopy.copy(value)
|
|
81
120
|
end
|
|
82
121
|
end
|
|
83
122
|
end
|