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
|
@@ -20,12 +20,39 @@ module Vizcore
|
|
|
20
20
|
fps: DEFAULT_FRAME_RATE,
|
|
21
21
|
width: SnapshotRenderer::DEFAULT_WIDTH,
|
|
22
22
|
height: SnapshotRenderer::DEFAULT_HEIGHT,
|
|
23
|
+
duration: nil,
|
|
24
|
+
from_frame: 1,
|
|
25
|
+
to_frame: nil,
|
|
26
|
+
resume: false,
|
|
27
|
+
seed: nil,
|
|
28
|
+
transparent: false,
|
|
29
|
+
video_codec: nil,
|
|
30
|
+
video_bitrate: nil,
|
|
31
|
+
video_crf: nil,
|
|
32
|
+
pixel_format: "yuv420p",
|
|
33
|
+
progress_reporter: nil,
|
|
23
34
|
command_runner: Open3,
|
|
24
35
|
ffmpeg_checker: nil
|
|
25
36
|
)
|
|
26
37
|
@config = config
|
|
27
|
-
@frames = normalize_frame_count(frames)
|
|
28
38
|
@fps = normalize_frame_rate(fps)
|
|
39
|
+
@frames = normalize_frame_count(duration ? (Float(duration) * @fps).ceil : frames)
|
|
40
|
+
@from_frame = normalize_frame_index(from_frame, "from-frame")
|
|
41
|
+
@to_frame = normalize_optional_frame_index(to_frame, "to-frame")
|
|
42
|
+
@to_frame = @frames if @to_frame.nil?
|
|
43
|
+
raise ArgumentError, "to-frame must be greater than or equal to from-frame" if @to_frame < @from_frame
|
|
44
|
+
raise ArgumentError, "from-frame must be within rendered frame count" if @from_frame > @frames
|
|
45
|
+
|
|
46
|
+
@to_frame = [@to_frame, @frames].min
|
|
47
|
+
@output_frames = @to_frame - @from_frame + 1
|
|
48
|
+
@resume = !!resume
|
|
49
|
+
@seed = normalize_seed(seed)
|
|
50
|
+
@transparent = !!transparent
|
|
51
|
+
@video_codec = optional_string(video_codec, "video codec")
|
|
52
|
+
@video_bitrate = optional_string(video_bitrate, "video bitrate")
|
|
53
|
+
@video_crf = optional_string(video_crf, "video crf")
|
|
54
|
+
@pixel_format = optional_string(pixel_format, "pixel format") || "yuv420p"
|
|
55
|
+
@progress_reporter = progress_reporter
|
|
29
56
|
@width = width
|
|
30
57
|
@height = height
|
|
31
58
|
@command_runner = command_runner
|
|
@@ -56,32 +83,44 @@ module Vizcore
|
|
|
56
83
|
metadata = nil
|
|
57
84
|
Dir.mktmpdir("vizcore-render-frames") do |dir|
|
|
58
85
|
frame_dir = Pathname.new(dir)
|
|
59
|
-
metadata = render_frames(frame_dir)
|
|
86
|
+
metadata = render_frames(frame_dir, preserve_frame_numbers: false)
|
|
60
87
|
encode_mp4(frame_dir: frame_dir, output_file: output_file)
|
|
61
88
|
end
|
|
62
89
|
metadata.merge(path: output_file, format: :mp4)
|
|
63
90
|
end
|
|
64
91
|
|
|
65
|
-
def render_frames(output_dir)
|
|
66
|
-
source = SceneFrameSource.new(config: @config, frame_rate: @fps)
|
|
92
|
+
def render_frames(output_dir, preserve_frame_numbers: true)
|
|
93
|
+
source = SceneFrameSource.new(config: @config, frame_rate: @fps, seed: @seed)
|
|
67
94
|
source.start
|
|
68
|
-
renderer = SnapshotRenderer.new(width: @width, height: @height)
|
|
95
|
+
renderer = SnapshotRenderer.new(width: @width, height: @height, transparent: @transparent)
|
|
69
96
|
scene_name = nil
|
|
70
97
|
|
|
71
98
|
@frames.times do |index|
|
|
99
|
+
frame_number = index + 1
|
|
100
|
+
break if frame_number > @to_frame
|
|
101
|
+
|
|
72
102
|
frame = source.capture
|
|
73
103
|
scene_name ||= frame.fetch(:scene_name)
|
|
104
|
+
next if frame_number < @from_frame || frame_number > @to_frame
|
|
105
|
+
output_frame_number = preserve_frame_numbers ? frame_number : frame_number - @from_frame + 1
|
|
106
|
+
next if @resume && frame_path(output_dir, output_frame_number).file?
|
|
107
|
+
|
|
74
108
|
File.binwrite(
|
|
75
|
-
frame_path(output_dir,
|
|
109
|
+
frame_path(output_dir, output_frame_number),
|
|
76
110
|
renderer.render(scene: frame.fetch(:scene), audio: frame.fetch(:audio))
|
|
77
111
|
)
|
|
112
|
+
emit_progress(frame_number: frame_number, output_frame_number: output_frame_number)
|
|
78
113
|
end
|
|
79
114
|
|
|
80
115
|
{
|
|
81
|
-
frames: @
|
|
116
|
+
frames: @output_frames,
|
|
117
|
+
total_frames: @frames,
|
|
118
|
+
from_frame: @from_frame,
|
|
119
|
+
to_frame: @to_frame,
|
|
82
120
|
fps: @fps,
|
|
83
121
|
width: renderer.width,
|
|
84
122
|
height: renderer.height,
|
|
123
|
+
transparent: @transparent,
|
|
85
124
|
scene: scene_name
|
|
86
125
|
}
|
|
87
126
|
ensure
|
|
@@ -92,8 +131,8 @@ module Vizcore
|
|
|
92
131
|
%w[.mp4 .mov .webm].include?(path.extname.downcase)
|
|
93
132
|
end
|
|
94
133
|
|
|
95
|
-
def frame_path(output_dir,
|
|
96
|
-
output_dir.join(format("frame_%05d.png",
|
|
134
|
+
def frame_path(output_dir, frame_number)
|
|
135
|
+
output_dir.join(format("frame_%05d.png", frame_number))
|
|
97
136
|
end
|
|
98
137
|
|
|
99
138
|
def encode_mp4(frame_dir:, output_file:)
|
|
@@ -106,7 +145,7 @@ module Vizcore
|
|
|
106
145
|
end
|
|
107
146
|
|
|
108
147
|
def ffmpeg_command(frame_dir:, output_file:)
|
|
109
|
-
[
|
|
148
|
+
command = [
|
|
110
149
|
"ffmpeg",
|
|
111
150
|
"-y",
|
|
112
151
|
"-framerate",
|
|
@@ -114,17 +153,37 @@ module Vizcore
|
|
|
114
153
|
"-i",
|
|
115
154
|
frame_dir.join("frame_%05d.png").to_s,
|
|
116
155
|
"-vf",
|
|
117
|
-
"format
|
|
156
|
+
"format=#{@pixel_format}",
|
|
118
157
|
"-pix_fmt",
|
|
119
|
-
|
|
120
|
-
output_file.to_s
|
|
158
|
+
@pixel_format
|
|
121
159
|
]
|
|
160
|
+
command.concat(["-c:v", @video_codec]) if @video_codec
|
|
161
|
+
command.concat(["-b:v", @video_bitrate]) if @video_bitrate
|
|
162
|
+
command.concat(["-crf", @video_crf]) if @video_crf
|
|
163
|
+
command << output_file.to_s
|
|
164
|
+
command
|
|
122
165
|
end
|
|
123
166
|
|
|
124
167
|
def ffmpeg_available?
|
|
125
168
|
system("ffmpeg", "-version", out: File::NULL, err: File::NULL)
|
|
126
169
|
end
|
|
127
170
|
|
|
171
|
+
def emit_progress(frame_number:, output_frame_number:)
|
|
172
|
+
return unless @progress_reporter.respond_to?(:call)
|
|
173
|
+
|
|
174
|
+
@progress_reporter.call(
|
|
175
|
+
frame: frame_number,
|
|
176
|
+
output_frame: output_frame_number,
|
|
177
|
+
from_frame: @from_frame,
|
|
178
|
+
to_frame: @to_frame,
|
|
179
|
+
total_frames: @frames,
|
|
180
|
+
output_frames: @output_frames,
|
|
181
|
+
percent: ((frame_number - @from_frame + 1).to_f / @output_frames * 100).clamp(0.0, 100.0)
|
|
182
|
+
)
|
|
183
|
+
rescue StandardError
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
128
187
|
def format_frame_rate
|
|
129
188
|
return @fps.to_i.to_s if @fps == @fps.to_i
|
|
130
189
|
|
|
@@ -140,6 +199,21 @@ module Vizcore
|
|
|
140
199
|
raise ArgumentError, "frames must be a positive integer"
|
|
141
200
|
end
|
|
142
201
|
|
|
202
|
+
def normalize_frame_index(value, name)
|
|
203
|
+
index = Integer(value)
|
|
204
|
+
raise ArgumentError, "#{name} must be positive" unless index.positive?
|
|
205
|
+
|
|
206
|
+
index
|
|
207
|
+
rescue ArgumentError, TypeError
|
|
208
|
+
raise ArgumentError, "#{name} must be a positive integer"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def normalize_optional_frame_index(value, name)
|
|
212
|
+
return nil if value.nil?
|
|
213
|
+
|
|
214
|
+
normalize_frame_index(value, name)
|
|
215
|
+
end
|
|
216
|
+
|
|
143
217
|
def normalize_frame_rate(value)
|
|
144
218
|
rate = Float(value)
|
|
145
219
|
raise ArgumentError, "fps must be positive" unless rate.positive?
|
|
@@ -148,6 +222,23 @@ module Vizcore
|
|
|
148
222
|
rescue ArgumentError, TypeError
|
|
149
223
|
raise ArgumentError, "fps must be a positive number"
|
|
150
224
|
end
|
|
225
|
+
|
|
226
|
+
def normalize_seed(value)
|
|
227
|
+
return nil if value.nil?
|
|
228
|
+
|
|
229
|
+
Integer(value)
|
|
230
|
+
rescue ArgumentError, TypeError
|
|
231
|
+
raise ArgumentError, "seed must be an integer"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def optional_string(value, name)
|
|
235
|
+
return nil if value.nil?
|
|
236
|
+
|
|
237
|
+
normalized = value.to_s.strip
|
|
238
|
+
raise ArgumentError, "#{name} must not be empty" if normalized.empty?
|
|
239
|
+
|
|
240
|
+
normalized
|
|
241
|
+
end
|
|
151
242
|
end
|
|
152
243
|
end
|
|
153
244
|
end
|
|
@@ -8,20 +8,40 @@ module Vizcore
|
|
|
8
8
|
module Renderer
|
|
9
9
|
# Produces analyzed scene frames for offline renderers.
|
|
10
10
|
class SceneFrameSource
|
|
11
|
-
def initialize(config:, frame_rate: nil)
|
|
11
|
+
def initialize(config:, frame_rate: nil, seed: nil)
|
|
12
12
|
@config = config
|
|
13
13
|
@frame_rate = frame_rate
|
|
14
|
+
@seed = seed
|
|
14
15
|
@shader_source_resolver = Vizcore::DSL::ShaderSourceResolver.new
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# @return [Vizcore::Renderer::SceneFrameSource]
|
|
18
19
|
def start
|
|
19
20
|
@definition = resolve_shader_sources(Vizcore::DSL::Engine.load_file(@config.scene_file.to_s))
|
|
20
|
-
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
@
|
|
21
|
+
apply_seed!
|
|
22
|
+
@source_started_at = monotonic_seconds
|
|
23
|
+
scenes = Array(@definition[:scenes])
|
|
24
|
+
initial_timeline_entry = initial_timeline_entry(@definition)
|
|
25
|
+
@scene = resolve_initial_scene(scenes, initial_timeline_entry)
|
|
26
|
+
@transition_controller = Vizcore::DSL::TransitionController.new(
|
|
27
|
+
scenes: scenes,
|
|
28
|
+
transitions: Array(@definition[:transitions]),
|
|
29
|
+
error_reporter: lambda do |message|
|
|
30
|
+
report_transition_error(message)
|
|
31
|
+
end
|
|
32
|
+
)
|
|
33
|
+
@mapping_resolver = Vizcore::DSL::MappingResolver.new
|
|
34
|
+
unless feature_replay?
|
|
35
|
+
@input_manager = build_input_manager
|
|
36
|
+
@input_manager.start
|
|
37
|
+
@capture_size = capture_size
|
|
38
|
+
end
|
|
39
|
+
@pipeline = replay_pipeline || build_pipeline
|
|
40
|
+
@frame_count = 0
|
|
41
|
+
@scene_frame_base = 0
|
|
42
|
+
@scene_elapsed_base = 0.0
|
|
43
|
+
@scene_beat_base = 0
|
|
44
|
+
align_timeline_start(entry: initial_timeline_entry)
|
|
25
45
|
self
|
|
26
46
|
end
|
|
27
47
|
|
|
@@ -29,13 +49,29 @@ module Vizcore
|
|
|
29
49
|
def capture
|
|
30
50
|
ensure_started!
|
|
31
51
|
|
|
32
|
-
audio =
|
|
33
|
-
|
|
52
|
+
audio = if feature_replay?
|
|
53
|
+
@pipeline.call
|
|
54
|
+
else
|
|
55
|
+
@pipeline.call(@input_manager.capture_frame(@capture_size))
|
|
56
|
+
end
|
|
57
|
+
@frame_count += 1
|
|
58
|
+
scene = @scene
|
|
59
|
+
layers = @mapping_resolver.resolve_layers(
|
|
60
|
+
scene_layers: scene[:layers],
|
|
61
|
+
audio: audio,
|
|
62
|
+
time: frame_time,
|
|
63
|
+
frame: @frame_count
|
|
64
|
+
)
|
|
65
|
+
evaluate_transition(audio)
|
|
34
66
|
|
|
35
67
|
{
|
|
36
|
-
scene: {
|
|
68
|
+
scene: {
|
|
69
|
+
schema_version: Vizcore::Renderer::SceneSerializer::SCENE_SCHEMA_VERSION,
|
|
70
|
+
name: scene[:name],
|
|
71
|
+
layers: layers
|
|
72
|
+
},
|
|
37
73
|
audio: audio,
|
|
38
|
-
scene_name:
|
|
74
|
+
scene_name: scene[:name].to_s
|
|
39
75
|
}
|
|
40
76
|
end
|
|
41
77
|
|
|
@@ -50,6 +86,15 @@ module Vizcore
|
|
|
50
86
|
@shader_source_resolver.resolve(definition: definition, scene_file: @config.scene_file.to_s)
|
|
51
87
|
end
|
|
52
88
|
|
|
89
|
+
def apply_seed!
|
|
90
|
+
seed = @seed || @definition[:seed]
|
|
91
|
+
return if seed.nil?
|
|
92
|
+
|
|
93
|
+
Kernel.srand(Integer(seed))
|
|
94
|
+
rescue ArgumentError, TypeError
|
|
95
|
+
raise ArgumentError, "render seed must be an integer"
|
|
96
|
+
end
|
|
97
|
+
|
|
53
98
|
def first_scene(definition)
|
|
54
99
|
scene = Array(definition[:scenes]).first
|
|
55
100
|
return scene if scene
|
|
@@ -57,6 +102,95 @@ module Vizcore
|
|
|
57
102
|
{ name: @config.scene_file.basename(".rb").to_sym, layers: [] }
|
|
58
103
|
end
|
|
59
104
|
|
|
105
|
+
def initial_timeline_entry(definition)
|
|
106
|
+
timelines = Array(definition[:timelines])
|
|
107
|
+
timelines.each do |timeline|
|
|
108
|
+
first_entry = Array(timeline).first
|
|
109
|
+
return first_entry if first_entry
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def resolve_initial_scene(scenes, initial_timeline_entry)
|
|
116
|
+
scene_name = initial_timeline_entry&.dig(:scene)
|
|
117
|
+
scene = scenes.find { |entry| entry[:name].to_s == scene_name.to_s } if scene_name
|
|
118
|
+
scene || first_scene({ scenes: scenes })
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def align_timeline_start(entry:)
|
|
122
|
+
return unless entry
|
|
123
|
+
|
|
124
|
+
unit = entry[:unit].to_s
|
|
125
|
+
start_position = Float(entry[:at] || 0.0)
|
|
126
|
+
return unless start_position.positive?
|
|
127
|
+
|
|
128
|
+
@scene_elapsed_base = start_position if unit == "seconds"
|
|
129
|
+
@scene_beat_base = Integer(start_position) if unit == "beats"
|
|
130
|
+
rescue StandardError
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def evaluate_transition(audio)
|
|
135
|
+
transition = @transition_controller.next_transition(
|
|
136
|
+
scene_name: @scene[:name],
|
|
137
|
+
audio: transition_audio(audio),
|
|
138
|
+
frame_count: scene_frame_count,
|
|
139
|
+
elapsed_seconds: scene_elapsed_seconds
|
|
140
|
+
)
|
|
141
|
+
return unless transition
|
|
142
|
+
|
|
143
|
+
@scene = transition.fetch(:scene)
|
|
144
|
+
reset_scene_counters(audio)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def transition_audio(audio)
|
|
148
|
+
audio_hash = Hash(audio)
|
|
149
|
+
scene_count = scene_beat_count(audio_hash)
|
|
150
|
+
audio_hash.merge(scene_musical_counts(audio_hash, beat_count: scene_count))
|
|
151
|
+
rescue StandardError
|
|
152
|
+
{ beat_count: 0 }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def scene_frame_count
|
|
156
|
+
[@frame_count - @scene_frame_base, 0].max
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def scene_elapsed_seconds
|
|
160
|
+
[frame_time - @scene_elapsed_base, 0.0].max
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def scene_beat_count(audio)
|
|
164
|
+
global_beat_count = Integer(Hash(audio)[:beat_count] || 0)
|
|
165
|
+
[global_beat_count - @scene_beat_base, 0].max
|
|
166
|
+
rescue StandardError
|
|
167
|
+
0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def reset_scene_counters(audio)
|
|
171
|
+
audio_hash = Hash(audio)
|
|
172
|
+
@scene_frame_base = @frame_count
|
|
173
|
+
@scene_elapsed_base = frame_time
|
|
174
|
+
@scene_beat_base = Integer(audio_hash[:beat_count] || 0) - (audio_hash[:beat] ? 1 : 0)
|
|
175
|
+
rescue StandardError
|
|
176
|
+
@scene_frame_base = @frame_count
|
|
177
|
+
@scene_elapsed_base = frame_time
|
|
178
|
+
@scene_beat_base = 0
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def scene_musical_counts(audio, beat_count:)
|
|
182
|
+
beat_index = beat_count.positive? ? beat_count - 1 : 0
|
|
183
|
+
beat_phase = Float(audio[:beat_phase] || audio["beat_phase"] || 0.0).clamp(0.0, 1.0)
|
|
184
|
+
{
|
|
185
|
+
beat_count: beat_count,
|
|
186
|
+
bar_phase: (((beat_index % 4) + beat_phase) / 4.0).clamp(0.0, 1.0),
|
|
187
|
+
bar_count: beat_index / 4,
|
|
188
|
+
phrase_count: beat_index / 32
|
|
189
|
+
}
|
|
190
|
+
rescue StandardError
|
|
191
|
+
{ beat_count: beat_count, bar_phase: 0.0, bar_count: 0, phrase_count: 0 }
|
|
192
|
+
end
|
|
193
|
+
|
|
60
194
|
def build_input_manager
|
|
61
195
|
Vizcore::Audio::InputManager.new(
|
|
62
196
|
source: @config.audio_source,
|
|
@@ -65,12 +199,26 @@ module Vizcore
|
|
|
65
199
|
)
|
|
66
200
|
end
|
|
67
201
|
|
|
202
|
+
def replay_pipeline
|
|
203
|
+
return unless feature_replay?
|
|
204
|
+
|
|
205
|
+
Vizcore::Analysis::FeatureReplay.new(path: @config.feature_file)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def feature_replay?
|
|
209
|
+
!!@config.feature_file
|
|
210
|
+
end
|
|
211
|
+
|
|
68
212
|
def capture_size
|
|
69
213
|
return @input_manager.frame_size unless @frame_rate
|
|
70
214
|
|
|
71
215
|
@input_manager.realtime_capture_size(@frame_rate)
|
|
72
216
|
end
|
|
73
217
|
|
|
218
|
+
def report_transition_error(message)
|
|
219
|
+
warn(message)
|
|
220
|
+
end
|
|
221
|
+
|
|
74
222
|
def build_pipeline
|
|
75
223
|
Vizcore::Analysis::Pipeline.new(
|
|
76
224
|
sample_rate: @input_manager.sample_rate,
|
|
@@ -78,16 +226,46 @@ module Vizcore
|
|
|
78
226
|
noise_gate: @config.noise_gate,
|
|
79
227
|
audio_normalize: audio_normalize_settings,
|
|
80
228
|
bpm: bpm_setting,
|
|
81
|
-
bpm_lock: bpm_lock_setting
|
|
229
|
+
bpm_lock: bpm_lock_setting,
|
|
230
|
+
onset_sensitivity: analysis_setting(:onset_sensitivity, 1.0),
|
|
231
|
+
fft_preview_bins: analysis_setting(:fft_bins, Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS),
|
|
232
|
+
peak_hold_frames: analysis_setting(:peak_hold_frames, 0),
|
|
233
|
+
silence_reset_frames: analysis_setting(:silence_reset_frames, Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES)
|
|
82
234
|
)
|
|
83
235
|
end
|
|
84
236
|
|
|
237
|
+
def frame_time
|
|
238
|
+
return monotonic_elapsed unless @frame_rate
|
|
239
|
+
|
|
240
|
+
(@frame_count - 1).fdiv(@frame_rate)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def monotonic_elapsed
|
|
244
|
+
now = monotonic_seconds
|
|
245
|
+
return 0.0 unless @source_started_at
|
|
246
|
+
|
|
247
|
+
elapsed = now - @source_started_at
|
|
248
|
+
elapsed.positive? ? elapsed : 0.0
|
|
249
|
+
rescue StandardError
|
|
250
|
+
0.0
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def monotonic_seconds
|
|
254
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
255
|
+
end
|
|
256
|
+
|
|
85
257
|
def audio_normalize_settings
|
|
86
258
|
Hash(@definition[:analysis] || {})[:audio_normalize]
|
|
87
259
|
rescue StandardError
|
|
88
260
|
nil
|
|
89
261
|
end
|
|
90
262
|
|
|
263
|
+
def analysis_setting(key, fallback)
|
|
264
|
+
Hash(@definition[:analysis] || {}).fetch(key, fallback)
|
|
265
|
+
rescue StandardError
|
|
266
|
+
fallback
|
|
267
|
+
end
|
|
268
|
+
|
|
91
269
|
def bpm_setting
|
|
92
270
|
@config.bpm || Hash(@definition[:analysis] || {})[:bpm]
|
|
93
271
|
rescue StandardError
|
|
@@ -110,7 +288,7 @@ module Vizcore
|
|
|
110
288
|
end
|
|
111
289
|
|
|
112
290
|
def ensure_started!
|
|
113
|
-
return if @
|
|
291
|
+
return if @pipeline && @scene && (!feature_replay? || @input_manager.nil? || @input_manager.running?)
|
|
114
292
|
|
|
115
293
|
raise RuntimeError, "scene frame source has not been started"
|
|
116
294
|
end
|
|
@@ -4,6 +4,11 @@ module Vizcore
|
|
|
4
4
|
module Renderer
|
|
5
5
|
# Serializes analysis and scene state into transport payloads.
|
|
6
6
|
class SceneSerializer
|
|
7
|
+
SCENE_SCHEMA_VERSION = "vizcore.scene.v1"
|
|
8
|
+
FRAME_SCHEMA_VERSION = "vizcore.frame.v1"
|
|
9
|
+
LAYER_SCHEMA_VERSION = "vizcore.layer.v1"
|
|
10
|
+
MAPPING_SCHEMA_VERSION = "vizcore.mapping.v1"
|
|
11
|
+
|
|
7
12
|
# @param timestamp [Numeric]
|
|
8
13
|
# @param audio [Hash]
|
|
9
14
|
# @param scene_name [String, Symbol]
|
|
@@ -13,6 +18,7 @@ module Vizcore
|
|
|
13
18
|
# @return [Hash]
|
|
14
19
|
def audio_frame(timestamp:, audio:, scene_name:, scene_layers:, transition: nil, metrics: nil)
|
|
15
20
|
frame = {
|
|
21
|
+
schema_version: FRAME_SCHEMA_VERSION,
|
|
16
22
|
timestamp: Float(timestamp),
|
|
17
23
|
audio: serialize_audio(audio),
|
|
18
24
|
scene: serialize_scene(scene_name, scene_layers),
|
|
@@ -26,12 +32,15 @@ module Vizcore
|
|
|
26
32
|
|
|
27
33
|
def serialize_audio(audio)
|
|
28
34
|
bands = symbolize_hash(audio[:bands])
|
|
35
|
+
band_peaks = { sub: 0.0, low: 0.0, mid: 0.0, high: 0.0 }.merge(symbolize_hash(audio[:band_peaks]))
|
|
29
36
|
onsets = { sub: 0.0, low: 0.0, mid: 0.0, high: 0.0 }.merge(symbolize_hash(audio[:onsets]))
|
|
30
37
|
drums = { kick: 0.0, snare: 0.0, hihat: 0.0 }.merge(symbolize_hash(audio[:drums]))
|
|
31
38
|
|
|
32
39
|
{
|
|
33
40
|
amplitude: round_float(audio[:amplitude]),
|
|
41
|
+
peak: round_float(audio[:peak]),
|
|
34
42
|
bands: bands.transform_values { |value| round_float(value) },
|
|
43
|
+
band_peaks: band_peaks.transform_values { |value| round_float(value) },
|
|
35
44
|
fft: Array(audio[:fft]).map { |value| round_float(value) },
|
|
36
45
|
onset: round_float(audio[:onset]),
|
|
37
46
|
onsets: onsets.transform_values { |value| round_float(value) },
|
|
@@ -40,13 +49,28 @@ module Vizcore
|
|
|
40
49
|
beat_confidence: round_float(audio[:beat_confidence]),
|
|
41
50
|
beat_pulse: round_float(audio[:beat_pulse]),
|
|
42
51
|
beat_count: Integer(audio[:beat_count] || 0),
|
|
52
|
+
beat_phase: round_float(audio[:beat_phase]),
|
|
53
|
+
beat_2: !!audio[:beat_2],
|
|
54
|
+
beat_4: !!audio[:beat_4],
|
|
55
|
+
beat_8: !!audio[:beat_8],
|
|
56
|
+
beat_triplet: !!audio[:beat_triplet],
|
|
57
|
+
bar_phase: round_float(audio[:bar_phase]),
|
|
58
|
+
bar_count: Integer(audio[:bar_count] || 0),
|
|
59
|
+
phrase_count: Integer(audio[:phrase_count] || 0),
|
|
43
60
|
bpm: audio[:bpm],
|
|
61
|
+
bpm_confidence: round_float(audio[:bpm_confidence]),
|
|
62
|
+
spectral_centroid: round_float(audio[:spectral_centroid]),
|
|
63
|
+
spectral_rolloff: round_float(audio[:spectral_rolloff]),
|
|
64
|
+
spectral_flatness: round_float(audio[:spectral_flatness]),
|
|
65
|
+
spectral_flux: round_float(audio[:spectral_flux]),
|
|
66
|
+
zero_crossing_rate: round_float(audio[:zero_crossing_rate]),
|
|
44
67
|
peak_frequency: round_float(audio[:peak_frequency])
|
|
45
68
|
}
|
|
46
69
|
end
|
|
47
70
|
|
|
48
71
|
def serialize_scene(scene_name, scene_layers)
|
|
49
72
|
{
|
|
73
|
+
schema_version: SCENE_SCHEMA_VERSION,
|
|
50
74
|
name: scene_name.to_s,
|
|
51
75
|
layers: Array(scene_layers).map { |layer| serialize_layer(layer) }
|
|
52
76
|
}
|
|
@@ -58,12 +82,14 @@ module Vizcore
|
|
|
58
82
|
output = {
|
|
59
83
|
name: values.fetch(:name).to_s,
|
|
60
84
|
type: (values[:type] || :geometry).to_s,
|
|
85
|
+
schema_version: LAYER_SCHEMA_VERSION,
|
|
61
86
|
params: symbolize_hash(values[:params])
|
|
62
87
|
}
|
|
63
88
|
output[:shader] = values[:shader].to_s if values[:shader]
|
|
64
89
|
output[:glsl] = values[:glsl].to_s if values[:glsl]
|
|
65
90
|
output[:glsl_source] = values[:glsl_source].to_s if values[:glsl_source]
|
|
66
91
|
output[:param_schema] = serialize_param_schema(values[:param_schema]) if values[:param_schema]
|
|
92
|
+
output[:mappings] = serialize_mappings(values[:mappings]) unless Array(values[:mappings]).empty?
|
|
67
93
|
output
|
|
68
94
|
end
|
|
69
95
|
|
|
@@ -80,6 +106,18 @@ module Vizcore
|
|
|
80
106
|
end
|
|
81
107
|
end
|
|
82
108
|
|
|
109
|
+
def serialize_mappings(mappings)
|
|
110
|
+
Array(mappings).map do |mapping|
|
|
111
|
+
values = symbolize_hash(mapping)
|
|
112
|
+
{
|
|
113
|
+
schema_version: MAPPING_SCHEMA_VERSION,
|
|
114
|
+
source: values[:source],
|
|
115
|
+
target: values[:target].to_s,
|
|
116
|
+
transform: symbolize_hash(values[:transform])
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
83
121
|
def serialize_metrics(metrics)
|
|
84
122
|
symbolize_hash(metrics).each_with_object({}) do |(key, value), output|
|
|
85
123
|
output[key] = key == :frame_id ? Integer(value) : round_float(value)
|
|
@@ -9,10 +9,11 @@ module Vizcore
|
|
|
9
9
|
module Renderer
|
|
10
10
|
# Builds one analyzed scene frame and writes a PNG preview.
|
|
11
11
|
class Snapshot
|
|
12
|
-
def initialize(config:, width: SnapshotRenderer::DEFAULT_WIDTH, height: SnapshotRenderer::DEFAULT_HEIGHT)
|
|
12
|
+
def initialize(config:, width: SnapshotRenderer::DEFAULT_WIDTH, height: SnapshotRenderer::DEFAULT_HEIGHT, transparent: false)
|
|
13
13
|
@config = config
|
|
14
14
|
@width = width
|
|
15
15
|
@height = height
|
|
16
|
+
@transparent = !!transparent
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# @param out [String, Pathname]
|
|
@@ -22,14 +23,14 @@ module Vizcore
|
|
|
22
23
|
frame_source = SceneFrameSource.new(config: @config)
|
|
23
24
|
frame_source.start
|
|
24
25
|
frame = frame_source.capture
|
|
25
|
-
png = SnapshotRenderer.new(width: @width, height: @height).render(
|
|
26
|
+
png = SnapshotRenderer.new(width: @width, height: @height, transparent: @transparent).render(
|
|
26
27
|
scene: frame.fetch(:scene),
|
|
27
28
|
audio: frame.fetch(:audio)
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
FileUtils.mkdir_p(output_path.dirname)
|
|
31
32
|
File.binwrite(output_path, png)
|
|
32
|
-
{ path: output_path, scene: frame.fetch(:scene_name), width: @width, height: @height }
|
|
33
|
+
{ path: output_path, scene: frame.fetch(:scene_name), width: @width, height: @height, transparent: @transparent }
|
|
33
34
|
ensure
|
|
34
35
|
frame_source&.stop
|
|
35
36
|
end
|