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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/index.html +24 -2
  3. data/frontend/src/audio-inspector.js +9 -0
  4. data/frontend/src/live-controls.js +219 -7
  5. data/frontend/src/main.js +447 -57
  6. data/frontend/src/midi-learn.js +22 -2
  7. data/frontend/src/performance-monitor.js +137 -1
  8. data/frontend/src/renderer/engine.js +391 -10
  9. data/frontend/src/renderer/layer-manager.js +472 -71
  10. data/frontend/src/runtime-control-preset.js +44 -0
  11. data/frontend/src/scene-patches.js +159 -0
  12. data/frontend/src/shader-error-overlay.js +1 -0
  13. data/frontend/src/visuals/image-renderer.js +19 -0
  14. data/frontend/src/visuals/particle-system.js +10 -0
  15. data/frontend/src/visuals/shape-renderer.js +13 -0
  16. data/frontend/src/visuals/spectrogram-renderer.js +14 -0
  17. data/frontend/src/visuals/text-renderer.js +13 -0
  18. data/frontend/src/websocket-client.js +6 -0
  19. data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
  20. data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
  21. data/lib/vizcore/analysis/feature_recorder.rb +117 -7
  22. data/lib/vizcore/analysis/feature_replay.rb +48 -9
  23. data/lib/vizcore/analysis/pipeline.rb +258 -9
  24. data/lib/vizcore/analysis/tap_tempo.rb +17 -2
  25. data/lib/vizcore/audio/calibration.rb +156 -0
  26. data/lib/vizcore/audio/file_input.rb +28 -0
  27. data/lib/vizcore/audio/input_manager.rb +36 -1
  28. data/lib/vizcore/audio/midi_input.rb +5 -0
  29. data/lib/vizcore/audio/ring_buffer.rb +22 -0
  30. data/lib/vizcore/audio.rb +1 -0
  31. data/lib/vizcore/cli/dsl_reference.rb +64 -8
  32. data/lib/vizcore/cli/plugin_checker.rb +93 -0
  33. data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
  34. data/lib/vizcore/cli/scene_inspector.rb +35 -1
  35. data/lib/vizcore/cli/scene_validator.rb +487 -39
  36. data/lib/vizcore/cli/shader_template.rb +7 -2
  37. data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
  38. data/lib/vizcore/cli.rb +268 -15
  39. data/lib/vizcore/config.rb +40 -3
  40. data/lib/vizcore/control_preset.rb +29 -0
  41. data/lib/vizcore/deep_copy.rb +21 -0
  42. data/lib/vizcore/dsl/color_helpers.rb +155 -0
  43. data/lib/vizcore/dsl/engine.rb +219 -23
  44. data/lib/vizcore/dsl/layer_builder.rb +278 -15
  45. data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
  46. data/lib/vizcore/dsl/layout_helpers.rb +290 -0
  47. data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
  49. data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
  50. data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
  51. data/lib/vizcore/dsl/reaction_builder.rb +1 -0
  52. data/lib/vizcore/dsl/scene_builder.rb +83 -13
  53. data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
  54. data/lib/vizcore/dsl/style_builder.rb +3 -0
  55. data/lib/vizcore/dsl/timeline_builder.rb +91 -8
  56. data/lib/vizcore/dsl/transition_controller.rb +157 -18
  57. data/lib/vizcore/dsl.rb +2 -0
  58. data/lib/vizcore/layer_catalog.rb +1 -0
  59. data/lib/vizcore/plugin_asset_policy.rb +55 -0
  60. data/lib/vizcore/project_manifest.rb +12 -2
  61. data/lib/vizcore/renderer/render_sequence.rb +104 -13
  62. data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
  63. data/lib/vizcore/renderer/scene_serializer.rb +38 -0
  64. data/lib/vizcore/renderer/snapshot.rb +4 -3
  65. data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
  66. data/lib/vizcore/scene_trust.rb +31 -0
  67. data/lib/vizcore/server/frame_broadcaster.rb +469 -23
  68. data/lib/vizcore/server/rack_app.rb +151 -4
  69. data/lib/vizcore/server/runner.rb +676 -82
  70. data/lib/vizcore/server/websocket_handler.rb +236 -14
  71. data/lib/vizcore/server.rb +21 -0
  72. data/lib/vizcore/shape.rb +39 -16
  73. data/lib/vizcore/sync/osc_message.rb +66 -9
  74. data/lib/vizcore/version.rb +1 -1
  75. data/lib/vizcore.rb +33 -0
  76. data/scripts/browser_capture.mjs +31 -2
  77. data/sig/vizcore.rbs +55 -4
  78. metadata +18 -3
@@ -28,7 +28,8 @@ module Vizcore
28
28
  # @param audio_device [String, Integer, nil] input device index/name for `:mic`
29
29
  def initialize(source: :mic, sample_rate: DEFAULT_SAMPLE_RATE, frame_size: DEFAULT_FRAME_SIZE, ring_buffer_size: DEFAULT_RING_BUFFER_SIZE, file_path: nil, audio_device: nil)
30
30
  @source_name = source.to_sym
31
- @sample_rate = Integer(sample_rate)
31
+ @requested_sample_rate = Integer(sample_rate)
32
+ @sample_rate = @requested_sample_rate
32
33
  @frame_size = Integer(frame_size)
33
34
  @ring_buffer = RingBuffer.new(ring_buffer_size)
34
35
  @audio_device = audio_device
@@ -69,6 +70,32 @@ module Vizcore
69
70
  ring_buffer.latest(count)
70
71
  end
71
72
 
73
+ # @return [Hash] current input health values for runtime status endpoints.
74
+ def status
75
+ {
76
+ source: source_name.to_s,
77
+ sample_rate: sample_rate,
78
+ frame_size: frame_size,
79
+ ring_buffer: ring_buffer.respond_to?(:metrics) ? ring_buffer.metrics : {},
80
+ requested_sample_rate: @requested_sample_rate,
81
+ sample_rate_mismatch: sample_rate_mismatch?
82
+ }
83
+ end
84
+
85
+ # @return [Float, nil] current transport position seconds for file sources
86
+ def transport_position_seconds
87
+ return @input.transport_position_seconds if @input.respond_to?(:transport_position_seconds)
88
+
89
+ nil
90
+ end
91
+
92
+ # @return [Float, nil] input track duration seconds for file sources
93
+ def track_duration_seconds
94
+ return @input.track_duration_seconds if @input.respond_to?(:track_duration_seconds)
95
+
96
+ nil
97
+ end
98
+
72
99
  # @param frame_rate [Numeric]
73
100
  # @return [Integer] approximate real-time sample count to ingest per render tick
74
101
  def realtime_capture_size(frame_rate)
@@ -130,6 +157,14 @@ module Vizcore
130
157
  rescue StandardError
131
158
  fallback
132
159
  end
160
+
161
+ def sample_rate_mismatch?
162
+ return false unless @input.respond_to?(:stream_sample_rate)
163
+
164
+ Integer(@input.stream_sample_rate) != Integer(@requested_sample_rate)
165
+ rescue StandardError
166
+ false
167
+ end
133
168
  end
134
169
  end
135
170
  end
@@ -29,6 +29,11 @@ module Vizcore
29
29
  []
30
30
  end
31
31
 
32
+ # @return [Boolean] true when the optional UniMIDI backend can be loaded
33
+ def available?
34
+ !load_backend.nil?
35
+ end
36
+
32
37
  private
33
38
 
34
39
  def load_backend
@@ -16,6 +16,9 @@ module Vizcore
16
16
  @buffer = Array.new(@capacity, 0.0)
17
17
  @write_index = 0
18
18
  @size = 0
19
+ @write_count = 0
20
+ @overrun_count = 0
21
+ @underrun_count = 0
19
22
  @mutex = Mutex.new
20
23
  end
21
24
 
@@ -27,8 +30,10 @@ module Vizcore
27
30
 
28
31
  @mutex.synchronize do
29
32
  normalized.each do |sample|
33
+ @overrun_count += 1 if @size == @capacity
30
34
  @buffer[@write_index] = sample
31
35
  @write_index = (@write_index + 1) % @capacity
36
+ @write_count += 1
32
37
  @size += 1 if @size < @capacity
33
38
  end
34
39
  end
@@ -49,6 +54,7 @@ module Vizcore
49
54
  requested = count ? Integer(count) : @size
50
55
  return [] if requested <= 0
51
56
 
57
+ @underrun_count += requested - @size if requested > @size
52
58
  length = [requested, @size].min
53
59
  start = (@write_index - length) % @capacity
54
60
 
@@ -61,12 +67,28 @@ module Vizcore
61
67
  @mutex.synchronize { @size }
62
68
  end
63
69
 
70
+ # @return [Hash] buffer health counters for runtime diagnostics.
71
+ def metrics
72
+ @mutex.synchronize do
73
+ {
74
+ capacity: @capacity,
75
+ size: @size,
76
+ write_count: @write_count,
77
+ overrun_count: @overrun_count,
78
+ underrun_count: @underrun_count
79
+ }
80
+ end
81
+ end
82
+
64
83
  # @return [void]
65
84
  def clear
66
85
  @mutex.synchronize do
67
86
  @buffer.fill(0.0)
68
87
  @write_index = 0
69
88
  @size = 0
89
+ @write_count = 0
90
+ @overrun_count = 0
91
+ @underrun_count = 0
70
92
  end
71
93
  end
72
94
 
data/lib/vizcore/audio.rb CHANGED
@@ -7,6 +7,7 @@ module Vizcore
7
7
  end
8
8
 
9
9
  require_relative "audio/base_input"
10
+ require_relative "audio/calibration"
10
11
  require_relative "audio/dummy_sine_input"
11
12
  require_relative "audio/file_input"
12
13
  require_relative "audio/fixture_input"
@@ -12,20 +12,23 @@ module Vizcore
12
12
  Entry.new(syntax: "audio :mic, **options", description: "Register an audio input definition."),
13
13
  Entry.new(syntax: "midi :controller, **options", description: "Register a MIDI input definition."),
14
14
  Entry.new(syntax: "audio_normalize mode: :adaptive", description: "Configure analysis-level normalization."),
15
+ Entry.new(syntax: "audio_analysis onset_sensitivity: 1.4, fft_bins: 64", description: "Tune analysis feature extraction."),
15
16
  Entry.new(syntax: "bpm 128 / bpm_lock true", description: "Set and optionally lock the analysis BPM."),
16
17
  Entry.new(syntax: "tap_tempo key: :space", description: "Enable browser tap tempo events."),
17
18
  Entry.new(syntax: "set :global_intensity, 0.8", description: "Set a runtime global exposed to shaders."),
18
19
  Entry.new(syntax: "style :name { ... } / theme :name { ... }", description: "Define reusable layer params or scene defaults."),
20
+ Entry.new(syntax: "mapping :name { ... }", description: "Define reusable layer mapping groups."),
19
21
  Entry.new(syntax: "scene :name, extends: :base { ... }", description: "Define a scene and optional inherited layers."),
20
- Entry.new(syntax: "section :intro, bars: 8 { ... }", description: "Define beat-counted scenes and generated transitions."),
21
- Entry.new(syntax: "timeline { at beats(0), scene: :intro }", description: "Define ordered scene markers."),
22
+ Entry.new(syntax: "section :intro, bars: 8, loop: true, hold: 2, outro: false", description: "Define beat-counted scenes with optional auto-loop/outro/hold behavior."),
23
+ Entry.new(syntax: "timeline { at beats(0), scene: :intro, cue: :prelude }", description: "Define ordered scene markers with optional cue metadata."),
22
24
  Entry.new(syntax: "transition from: :intro, to: :drop { ... }", description: "Define explicit scene transitions."),
23
- Entry.new(syntax: "midi_map note: 36 { ... }", description: "Map MIDI events to runtime actions."),
25
+ Entry.new(syntax: "midi_map cc: 1, channel: 1, deadband: 2, smooth: 0.25, allow_multiple: true { ... }", description: "Map MIDI events to runtime actions."),
24
26
  Entry.new(syntax: "key \"d\" { switch_scene :drop }", description: "Map browser keyboard shortcuts to runtime actions.")
25
27
  ].freeze
26
28
 
27
29
  SCENE = [
28
30
  Entry.new(syntax: "use_theme :name", description: "Apply scene-wide layer defaults."),
31
+ Entry.new(syntax: "use_mapping :name", description: "Apply a reusable mapping group to a layer."),
29
32
  Entry.new(syntax: "group :foreground { layer :name { ... } }", description: "Apply shared params to a related layer group."),
30
33
  Entry.new(syntax: "layer :name { ... }", description: "Append a render layer to the scene.")
31
34
  ].freeze
@@ -43,19 +46,72 @@ module Vizcore
43
46
  Entry.new(syntax: "circle count: 8 { radius 100 } / rect width: 320, height: 160 / custom_shape :flower", description: "Render declarative and Ruby-generated 2D shape primitives."),
44
47
  Entry.new(syntax: "font \"Inter\" / letter_spacing 4", description: "Set text presentation params."),
45
48
  Entry.new(syntax: "palette \"#ff0055\", \"#00ffff\"", description: "Set ordered colors for supported layer renderers."),
49
+ Entry.new(syntax: "hsl(240, 100, 50) / hsv(0, 100, 100)", description: "Generate CSS hex color values from HSL / HSV."),
50
+ Entry.new(syntax: "gradient(type: :radial, colors: [\"#f00\", \"#0ff\"])", description: "Build reusable gradient descriptors for color fields."),
51
+ Entry.new(syntax: "grid(count: 12, columns: 4, spacing: 20)", description: "Generate point layouts for shape geometry."),
52
+ Entry.new(syntax: "radial(count: 16, radius: 120, start_angle: -90)", description: "Generate polar layout points."),
53
+ Entry.new(syntax: "spiral(count: 24, radius: 150, turns: 4)", description: "Generate expanding spiral points."),
54
+ Entry.new(syntax: "circle_pack(count: 20, radius: 140)", description: "Generate packed points in concentric circles."),
55
+ Entry.new(syntax: "scatter(count: 12, width: 320, height: 180, seed: 7)", description: "Generate deterministic scattered point layouts."),
46
56
  Entry.new(syntax: "blend :add / effect :bloom / vj_effect :mirror", description: "Set compositing and browser effects."),
57
+ Entry.new(syntax: "post :bloom / post :motion_blur / post :chromatic", description: "Build a post effect chain for a layer."),
47
58
  Entry.new(syntax: "param :wobble, default: 0.3, range: 0.0..2.0", description: "Declare numeric shader parameter metadata."),
48
59
  Entry.new(syntax: "map amplitude, to: :speed, range: 0.0..2.0", description: "Map audio features to layer params."),
49
60
  Entry.new(syntax: "react_to bass { change :size }", description: "Group mappings by source.")
50
61
  ].freeze
51
62
 
52
- SOURCES = %w[
53
- amplitude frequency_band(:low) sub low bass mid high treble fft_spectrum
54
- onset onset(:high) kick snare hihat beat? beat beat_confidence beat_pulse beat_count bpm
63
+ SOURCES = [
64
+ "amplitude",
65
+ "frequency_band(:low)",
66
+ "frequency_band_peak(:low)",
67
+ "sub",
68
+ "low",
69
+ "bass",
70
+ "bass_peak",
71
+ "mid",
72
+ "mid_peak",
73
+ "high",
74
+ "high_peak",
75
+ "treble",
76
+ "fft_spectrum",
77
+ "onset",
78
+ "onset(:high)",
79
+ "kick",
80
+ "snare",
81
+ "hihat",
82
+ "beat?",
83
+ "beat",
84
+ "beat_confidence",
85
+ "beat_pulse",
86
+ "beat_count",
87
+ "adsr(:kick, attack: 0.02, decay: 0.08, sustain: 0.7, release: 0.16, threshold: 0.0, peak: 1.0)",
88
+ "envelope(:kick, attack: 0.02)",
89
+ "beat_phase",
90
+ "beat_2",
91
+ "beat_4",
92
+ "beat_8",
93
+ "triplet",
94
+ "bar_phase",
95
+ "bar_count",
96
+ "phrase_count",
97
+ "bpm"
55
98
  ].freeze
56
99
 
57
- TRANSFORMS = %w[
58
- gain range min max curve deadzone smooth(attack:,release:)
100
+ TRANSFORMS = [
101
+ "as(:trigger)",
102
+ "gain",
103
+ "range",
104
+ "min",
105
+ "max",
106
+ "curve",
107
+ "deadzone",
108
+ "threshold",
109
+ "hysteresis",
110
+ "hold",
111
+ "decay",
112
+ "cooldown",
113
+ "one_shot",
114
+ "smooth(attack:,release:)"
59
115
  ].freeze
60
116
 
61
117
  # @return [Array<String>]
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "../plugin_asset_policy"
5
+
6
+ module Vizcore
7
+ module CLISupport
8
+ # Performs local smoke checks for a Vizcore plugin scaffold.
9
+ class PluginChecker
10
+ Check = Struct.new(:name, :status, :message, keyword_init: true) do
11
+ def failure?
12
+ status == :fail
13
+ end
14
+ end
15
+
16
+ Report = Struct.new(:checks, keyword_init: true) do
17
+ def failure?
18
+ checks.any?(&:failure?)
19
+ end
20
+ end
21
+
22
+ # @param root [String, Pathname]
23
+ def initialize(root)
24
+ @root = Pathname.new(root).expand_path
25
+ end
26
+
27
+ # @return [Report]
28
+ def call
29
+ return Report.new(checks: [fail_check("Plugin root", "directory not found: #{@root}")]) unless @root.directory?
30
+
31
+ Report.new(
32
+ checks: [
33
+ ruby_layer_check,
34
+ frontend_asset_check,
35
+ example_scene_check
36
+ ]
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def ruby_layer_check
43
+ files = @root.join("lib").glob("*.rb")
44
+ return fail_check("Ruby layer", "missing lib/*.rb") if files.empty?
45
+
46
+ syntax_check("Ruby layer", files.first)
47
+ end
48
+
49
+ def frontend_asset_check
50
+ files = @root.join("frontend").glob("*.{js,mjs}")
51
+ return fail_check("Frontend renderer", "missing frontend/*.js or frontend/*.mjs") if files.empty?
52
+
53
+ Vizcore::PluginAssetPolicy.validate!(files.first, root: @root)
54
+ body = files.first.read
55
+ return warn_check("Frontend renderer", "#{files.first} does not register globalThis.VizcorePlugins") unless body.include?("VizcorePlugins")
56
+
57
+ ok("Frontend renderer", "#{files.first.relative_path_from(@root)} is loadable")
58
+ rescue ArgumentError => e
59
+ fail_check("Frontend renderer", e.message)
60
+ end
61
+
62
+ def example_scene_check
63
+ files = @root.join("examples").glob("*.rb")
64
+ return fail_check("Example scene", "missing examples/*.rb") if files.empty?
65
+
66
+ syntax_check("Example scene", files.first)
67
+ end
68
+
69
+ def syntax_check(name, path)
70
+ if defined?(RubyVM::InstructionSequence)
71
+ RubyVM::InstructionSequence.compile_file(path.to_s)
72
+ else
73
+ return warn_check(name, "syntax check skipped on this Ruby engine")
74
+ end
75
+ ok(name, "#{path.relative_path_from(@root)} has valid Ruby syntax")
76
+ rescue SyntaxError => e
77
+ fail_check(name, "#{path.relative_path_from(@root)} has invalid Ruby syntax: #{e.message.lines.first&.strip}")
78
+ end
79
+
80
+ def ok(name, message)
81
+ Check.new(name: name, status: :ok, message: message)
82
+ end
83
+
84
+ def warn_check(name, message)
85
+ Check.new(name: name, status: :warn, message: message)
86
+ end
87
+
88
+ def fail_check(name, message)
89
+ Check.new(name: name, status: :fail, message: message)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -7,8 +7,8 @@ module Vizcore
7
7
  module CLISupport
8
8
  # Facade used by Thor commands for scene validation and inspection.
9
9
  class SceneDiagnostics
10
- def initialize(scene_file:)
11
- @validator = SceneValidator.new(scene_file: scene_file)
10
+ def initialize(scene_file:, strict: false)
11
+ @validator = SceneValidator.new(scene_file: scene_file, strict: strict)
12
12
  end
13
13
 
14
14
  def validate
@@ -19,8 +19,27 @@ module Vizcore
19
19
  output
20
20
  end
21
21
 
22
+ def to_h
23
+ sanitize(@definition)
24
+ end
25
+
22
26
  private
23
27
 
28
+ def sanitize(value)
29
+ case value
30
+ when Hash
31
+ value.each_with_object({}) do |(key, entry), output|
32
+ output[key.to_s] = sanitize(entry)
33
+ end
34
+ when Array
35
+ value.map { |entry| sanitize(entry) }
36
+ when Symbol
37
+ value.to_s
38
+ else
39
+ value.respond_to?(:call) ? true : value
40
+ end
41
+ end
42
+
24
43
  def append_inputs(output, label, values)
25
44
  return if values.empty?
26
45
 
@@ -95,10 +114,21 @@ module Vizcore
95
114
  return "unknown" unless values[:kind]
96
115
  return "frequency_band(#{values[:band]})" if values[:kind].to_sym == :frequency_band
97
116
  return "onset(#{values[:band]})" if values[:kind].to_sym == :onset && values[:band]
117
+ return "global(#{values[:name]})" if values[:kind].to_sym == :global && values[:name]
118
+ return format_lfo_source(values) if values[:kind].to_sym == :lfo
98
119
 
99
120
  values[:kind].to_s
100
121
  end
101
122
 
123
+ def format_lfo_source(values)
124
+ wave = values[:wave] || :sine
125
+ parts = []
126
+ parts << "rate=#{values[:rate]}" if values.key?(:rate)
127
+ parts << "phase=#{values[:phase]}" if values.key?(:phase)
128
+ suffix = parts.empty? ? "" : ", #{parts.join(', ')}"
129
+ "lfo(#{wave}#{suffix})"
130
+ end
131
+
102
132
  def format_transform(transform)
103
133
  values = Hash(transform || {})
104
134
  return "" if values.empty?
@@ -121,7 +151,11 @@ module Vizcore
121
151
  when :switch_scene
122
152
  "switch_scene #{values[:scene]}"
123
153
  when :live_control
124
- values[:control].to_s
154
+ options = values.slice(:value, :fade, :release)
155
+ control = values[:control].to_s
156
+ return control if options.empty?
157
+
158
+ "#{control}#{format_options(options)}"
125
159
  else
126
160
  "unknown"
127
161
  end