vizcore 0.1.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +170 -0
  4. data/docs/GETTING_STARTED.md +105 -0
  5. data/examples/assets/complex_demo_loop.wav +0 -0
  6. data/examples/basic.rb +9 -0
  7. data/examples/complex_audio_showcase.rb +261 -0
  8. data/examples/custom_shader.rb +21 -0
  9. data/examples/file_audio_demo.rb +74 -0
  10. data/examples/intro_drop.rb +38 -0
  11. data/examples/midi_scene_switch.rb +32 -0
  12. data/examples/shaders/custom_wave.frag +30 -0
  13. data/exe/vizcore +6 -0
  14. data/frontend/index.html +148 -0
  15. data/frontend/src/main.js +304 -0
  16. data/frontend/src/renderer/engine.js +135 -0
  17. data/frontend/src/renderer/layer-manager.js +456 -0
  18. data/frontend/src/renderer/shader-manager.js +69 -0
  19. data/frontend/src/shaders/builtins.js +244 -0
  20. data/frontend/src/shaders/post-effects.js +85 -0
  21. data/frontend/src/visuals/geometry.js +66 -0
  22. data/frontend/src/visuals/particle-system.js +148 -0
  23. data/frontend/src/visuals/text-renderer.js +143 -0
  24. data/frontend/src/visuals/vj-effects.js +56 -0
  25. data/frontend/src/websocket-client.js +131 -0
  26. data/lib/vizcore/analysis/band_splitter.rb +63 -0
  27. data/lib/vizcore/analysis/beat_detector.rb +70 -0
  28. data/lib/vizcore/analysis/bpm_estimator.rb +86 -0
  29. data/lib/vizcore/analysis/fft_processor.rb +224 -0
  30. data/lib/vizcore/analysis/fftw_ffi.rb +50 -0
  31. data/lib/vizcore/analysis/pipeline.rb +72 -0
  32. data/lib/vizcore/analysis/smoother.rb +74 -0
  33. data/lib/vizcore/analysis.rb +14 -0
  34. data/lib/vizcore/audio/base_input.rb +39 -0
  35. data/lib/vizcore/audio/dummy_sine_input.rb +40 -0
  36. data/lib/vizcore/audio/file_input.rb +163 -0
  37. data/lib/vizcore/audio/input_manager.rb +133 -0
  38. data/lib/vizcore/audio/mic_input.rb +121 -0
  39. data/lib/vizcore/audio/midi_input.rb +246 -0
  40. data/lib/vizcore/audio/portaudio_ffi.rb +243 -0
  41. data/lib/vizcore/audio/ring_buffer.rb +92 -0
  42. data/lib/vizcore/audio.rb +16 -0
  43. data/lib/vizcore/cli.rb +115 -0
  44. data/lib/vizcore/config.rb +46 -0
  45. data/lib/vizcore/dsl/engine.rb +229 -0
  46. data/lib/vizcore/dsl/file_watcher.rb +108 -0
  47. data/lib/vizcore/dsl/layer_builder.rb +182 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +81 -0
  49. data/lib/vizcore/dsl/midi_map_executor.rb +188 -0
  50. data/lib/vizcore/dsl/scene_builder.rb +44 -0
  51. data/lib/vizcore/dsl/shader_source_resolver.rb +71 -0
  52. data/lib/vizcore/dsl/transition_controller.rb +166 -0
  53. data/lib/vizcore/dsl.rb +16 -0
  54. data/lib/vizcore/errors.rb +27 -0
  55. data/lib/vizcore/renderer/frame_scheduler.rb +75 -0
  56. data/lib/vizcore/renderer/scene_serializer.rb +73 -0
  57. data/lib/vizcore/renderer.rb +10 -0
  58. data/lib/vizcore/server/frame_broadcaster.rb +351 -0
  59. data/lib/vizcore/server/rack_app.rb +183 -0
  60. data/lib/vizcore/server/runner.rb +357 -0
  61. data/lib/vizcore/server/websocket_handler.rb +163 -0
  62. data/lib/vizcore/server.rb +12 -0
  63. data/lib/vizcore/templates/basic_scene.rb +10 -0
  64. data/lib/vizcore/templates/custom_shader_scene.rb +22 -0
  65. data/lib/vizcore/templates/custom_wave.frag +31 -0
  66. data/lib/vizcore/templates/intro_drop_scene.rb +40 -0
  67. data/lib/vizcore/templates/midi_control_scene.rb +33 -0
  68. data/lib/vizcore/templates/project_readme.md +35 -0
  69. data/lib/vizcore/version.rb +6 -0
  70. data/lib/vizcore.rb +37 -0
  71. metadata +186 -0
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ module DSL
5
+ # Executes `midi_map` action blocks against incoming MIDI events.
6
+ class MidiMapExecutor
7
+ # @param midi_maps [Array<Hash>]
8
+ # @param scenes [Array<Hash>]
9
+ # @param globals [Hash]
10
+ def initialize(midi_maps:, scenes:, globals: {})
11
+ update(midi_maps: midi_maps, scenes: scenes, globals: globals)
12
+ end
13
+
14
+ # @param midi_maps [Array<Hash>]
15
+ # @param scenes [Array<Hash>]
16
+ # @param globals [Hash, nil]
17
+ # @return [void]
18
+ def update(midi_maps:, scenes:, globals: nil)
19
+ @midi_maps = normalize_midi_maps(midi_maps)
20
+ @scenes = normalize_scenes(scenes)
21
+ @globals = normalize_globals(globals) unless globals.nil?
22
+ end
23
+
24
+ # @return [Hash] mutable global parameter snapshot
25
+ def globals
26
+ @globals.dup
27
+ end
28
+
29
+ # @param event [Vizcore::Audio::MidiInput::Event]
30
+ # @return [Array<Hash>] runtime actions (`:switch_scene`, `:set_global`)
31
+ def handle_event(event)
32
+ @midi_maps.each_with_object([]) do |mapping, actions|
33
+ next unless mapping_match?(mapping[:trigger], event)
34
+
35
+ context = ActionContext.new(scenes: @scenes, globals: @globals)
36
+ invoke_action_block(context, mapping[:action], event, mapping[:trigger])
37
+ actions.concat(context.actions)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def normalize_midi_maps(midi_maps)
44
+ Array(midi_maps).filter_map do |mapping|
45
+ values = symbolize_hash(mapping)
46
+ trigger = symbolize_hash(values[:trigger])
47
+ action = values[:action]
48
+ next if trigger.empty?
49
+ next unless action.respond_to?(:call)
50
+
51
+ {
52
+ trigger: trigger,
53
+ action: action
54
+ }
55
+ end
56
+ end
57
+
58
+ def normalize_scenes(scenes)
59
+ Array(scenes).each_with_object({}) do |scene, output|
60
+ values = symbolize_hash(scene)
61
+ name = values[:name]
62
+ next unless name
63
+
64
+ output[name.to_sym] = {
65
+ name: name.to_sym,
66
+ layers: Array(values[:layers]).map { |layer| deep_dup(layer) }
67
+ }
68
+ end
69
+ end
70
+
71
+ def normalize_globals(globals)
72
+ symbolize_hash(globals)
73
+ end
74
+
75
+ def mapping_match?(trigger, event)
76
+ if trigger.key?(:note)
77
+ event.type == :note_on && event.data1 == trigger[:note].to_i
78
+ elsif trigger.key?(:cc)
79
+ event.type == :control_change && event.data1 == trigger[:cc].to_i
80
+ elsif trigger.key?(:pc)
81
+ event.type == :program_change && event.data1 == trigger[:pc].to_i
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ def invoke_action_block(context, action, event, trigger)
88
+ value = event_value(event, trigger)
89
+ if action.arity.zero?
90
+ context.instance_exec(&action)
91
+ else
92
+ context.instance_exec(value, &action)
93
+ end
94
+ end
95
+
96
+ def event_value(event, trigger)
97
+ if trigger.key?(:note) || trigger.key?(:cc)
98
+ event.data2.to_i.clamp(0, 127)
99
+ elsif trigger.key?(:pc)
100
+ event.data1.to_i.clamp(0, 127)
101
+ else
102
+ 0
103
+ end
104
+ end
105
+
106
+ def symbolize_hash(value)
107
+ Hash(value).each_with_object({}) do |(key, entry), output|
108
+ output[key.to_sym] = entry
109
+ end
110
+ rescue StandardError
111
+ {}
112
+ end
113
+
114
+ def deep_dup(value)
115
+ case value
116
+ when Hash
117
+ value.each_with_object({}) do |(key, entry), output|
118
+ output[key] = deep_dup(entry)
119
+ end
120
+ when Array
121
+ value.map { |entry| deep_dup(entry) }
122
+ else
123
+ value
124
+ end
125
+ end
126
+
127
+ # Runtime DSL context used while executing one `midi_map` action block.
128
+ # @api private
129
+ class ActionContext
130
+ # Collected runtime actions emitted by DSL calls.
131
+ attr_reader :actions
132
+
133
+ # @param scenes [Hash]
134
+ # @param globals [Hash]
135
+ def initialize(scenes:, globals:)
136
+ @scenes = scenes
137
+ @globals = globals
138
+ @actions = []
139
+ end
140
+
141
+ # @param name [Symbol, String]
142
+ # @param effect [Hash, nil]
143
+ # @return [void]
144
+ def switch_scene(name, effect: nil)
145
+ scene = @scenes[name.to_sym]
146
+ return unless scene
147
+
148
+ @actions << {
149
+ type: :switch_scene,
150
+ scene: {
151
+ name: scene[:name],
152
+ layers: scene[:layers].map { |layer| deep_dup(layer) }
153
+ },
154
+ effect: deep_dup(effect)
155
+ }
156
+ end
157
+
158
+ # @param key [Symbol, String]
159
+ # @param value [Object]
160
+ # @return [void]
161
+ def set(key, value)
162
+ symbol_key = key.to_sym
163
+ @globals[symbol_key] = value
164
+ @actions << {
165
+ type: :set_global,
166
+ key: symbol_key,
167
+ value: value
168
+ }
169
+ end
170
+
171
+ private
172
+
173
+ def deep_dup(value)
174
+ case value
175
+ when Hash
176
+ value.each_with_object({}) do |(key, entry), output|
177
+ output[key] = deep_dup(entry)
178
+ end
179
+ when Array
180
+ value.map { |entry| deep_dup(entry) }
181
+ else
182
+ value
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "layer_builder"
4
+
5
+ module Vizcore
6
+ module DSL
7
+ # Collects layer definitions inside a single scene block.
8
+ class SceneBuilder
9
+ # @param name [Symbol, String] scene identifier
10
+ def initialize(name:)
11
+ @name = name.to_sym
12
+ @layers = []
13
+ end
14
+
15
+ # Evaluate a scene block.
16
+ #
17
+ # @yield Layer definitions
18
+ # @return [Vizcore::DSL::SceneBuilder]
19
+ def evaluate(&block)
20
+ instance_eval(&block) if block
21
+ self
22
+ end
23
+
24
+ # Define one layer in this scene.
25
+ #
26
+ # @param name [Symbol, String] layer identifier
27
+ # @yield Layer definition block
28
+ # @return [void]
29
+ def layer(name, &block)
30
+ builder = LayerBuilder.new(name: name)
31
+ builder.evaluate(&block)
32
+ @layers << builder.to_h
33
+ end
34
+
35
+ # @return [Hash] serialized scene payload
36
+ def to_h
37
+ {
38
+ name: @name,
39
+ layers: @layers.map { |layer| layer.dup }
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Vizcore
6
+ module DSL
7
+ # Replaces layer `glsl` paths with inlined shader source text.
8
+ class ShaderSourceResolver
9
+ # @param definition [Hash] DSL definition payload
10
+ # @param scene_file [String, Pathname] source scene file
11
+ # @raise [ArgumentError] when a referenced GLSL file is missing
12
+ # @return [Hash] deep-copied definition with `:glsl_source` entries
13
+ def resolve(definition:, scene_file:)
14
+ scene_path = Pathname.new(scene_file.to_s).expand_path
15
+ base_dir = scene_path.dirname
16
+ output = deep_dup(definition)
17
+ output[:scenes] = Array(output[:scenes]).map do |scene|
18
+ scene_hash = symbolize_hash(scene)
19
+ scene_hash[:layers] = Array(scene_hash[:layers]).map do |layer|
20
+ resolve_layer(layer, base_dir: base_dir)
21
+ end
22
+ scene_hash
23
+ end
24
+ output
25
+ end
26
+
27
+ private
28
+
29
+ def resolve_layer(layer, base_dir:)
30
+ layer_hash = symbolize_hash(layer)
31
+ shader_path = layer_hash[:glsl]
32
+ return layer_hash unless shader_path
33
+
34
+ full_path = resolve_path(base_dir: base_dir, shader_path: shader_path)
35
+ raise ArgumentError, "GLSL file not found: #{shader_path}" unless full_path.file?
36
+
37
+ layer_hash[:glsl] = shader_path.to_s
38
+ layer_hash[:glsl_source] = full_path.read
39
+ layer_hash
40
+ end
41
+
42
+ def resolve_path(base_dir:, shader_path:)
43
+ path = Pathname.new(shader_path.to_s)
44
+ return path.expand_path if path.absolute?
45
+
46
+ base_dir.join(path).expand_path
47
+ end
48
+
49
+ def symbolize_hash(value)
50
+ Hash(value).each_with_object({}) do |(key, entry), output|
51
+ output[key.to_sym] = entry
52
+ end
53
+ rescue StandardError
54
+ {}
55
+ end
56
+
57
+ def deep_dup(value)
58
+ case value
59
+ when Hash
60
+ value.each_with_object({}) do |(key, entry), output|
61
+ output[key] = deep_dup(entry)
62
+ end
63
+ when Array
64
+ value.map { |entry| deep_dup(entry) }
65
+ else
66
+ value
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ module DSL
5
+ # Evaluates transition rules and returns scene-change payloads.
6
+ class TransitionController
7
+ # @param scenes [Array<Hash>]
8
+ # @param transitions [Array<Hash>]
9
+ def initialize(scenes:, transitions:)
10
+ update(scenes: scenes, transitions: transitions)
11
+ end
12
+
13
+ # @param scenes [Array<Hash>]
14
+ # @param transitions [Array<Hash>]
15
+ # @return [void]
16
+ def update(scenes:, transitions:)
17
+ @scenes_by_name = normalize_scenes(scenes)
18
+ @transitions = normalize_transitions(transitions)
19
+ end
20
+
21
+ # @param scene_name [String, Symbol]
22
+ # @param audio [Hash]
23
+ # @param frame_count [Integer]
24
+ # @return [Hash, nil] transition payload when condition matches
25
+ def next_transition(scene_name:, audio:, frame_count: 0)
26
+ current = scene_name.to_sym
27
+ transition = @transitions.find do |entry|
28
+ entry[:from] == current && trigger_match?(entry[:trigger], audio, frame_count)
29
+ end
30
+ return nil unless transition
31
+
32
+ target_scene = @scenes_by_name[transition[:to]]
33
+ return nil unless target_scene
34
+
35
+ {
36
+ from: transition[:from],
37
+ to: transition[:to],
38
+ effect: transition[:effect],
39
+ scene: deep_dup(target_scene)
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ def normalize_scenes(scenes)
46
+ Array(scenes).each_with_object({}) do |scene, output|
47
+ values = symbolize_hash(scene)
48
+ name = values[:name]
49
+ next unless name
50
+
51
+ output[name.to_sym] = {
52
+ name: name.to_sym,
53
+ layers: Array(values[:layers]).map { |layer| deep_dup(layer) }
54
+ }
55
+ end
56
+ end
57
+
58
+ def normalize_transitions(transitions)
59
+ Array(transitions).filter_map do |transition|
60
+ values = symbolize_hash(transition)
61
+ from = values[:from]
62
+ to = values[:to]
63
+ next unless from && to
64
+
65
+ {
66
+ from: from.to_sym,
67
+ to: to.to_sym,
68
+ trigger: values[:trigger],
69
+ effect: deep_dup(values[:effect])
70
+ }
71
+ end
72
+ end
73
+
74
+ def trigger_match?(trigger, audio, frame_count)
75
+ return false unless trigger.respond_to?(:call)
76
+
77
+ TriggerContext.new(audio, frame_count: frame_count).instance_exec(&trigger)
78
+ rescue StandardError
79
+ false
80
+ end
81
+
82
+ def symbolize_hash(value)
83
+ Hash(value).each_with_object({}) do |(key, entry), output|
84
+ output[key.to_sym] = entry
85
+ end
86
+ rescue StandardError
87
+ {}
88
+ end
89
+
90
+ def deep_dup(value)
91
+ case value
92
+ when Hash
93
+ value.each_with_object({}) do |(key, entry), output|
94
+ output[key] = deep_dup(entry)
95
+ end
96
+ when Array
97
+ value.map { |entry| deep_dup(entry) }
98
+ else
99
+ value
100
+ end
101
+ end
102
+
103
+ # Runtime DSL context exposed to transition trigger blocks.
104
+ # @api private
105
+ class TriggerContext
106
+ # @param audio [Hash]
107
+ # @param frame_count [Integer]
108
+ def initialize(audio, frame_count:)
109
+ @audio = symbolize_hash(audio)
110
+ @bands = symbolize_hash(@audio[:bands])
111
+ @frame_count = Integer(frame_count)
112
+ rescue StandardError
113
+ @frame_count = 0
114
+ end
115
+
116
+ # @return [Float]
117
+ def amplitude
118
+ @audio[:amplitude].to_f
119
+ end
120
+
121
+ # @param name [Symbol, String]
122
+ # @return [Float]
123
+ def frequency_band(name)
124
+ @bands[name.to_sym].to_f
125
+ end
126
+
127
+ # @return [Array<Float>]
128
+ def fft_spectrum
129
+ Array(@audio[:fft])
130
+ end
131
+
132
+ # @return [Boolean]
133
+ def beat?
134
+ !!@audio[:beat]
135
+ end
136
+
137
+ # @return [Integer]
138
+ def beat_count
139
+ Integer(@audio[:beat_count] || 0)
140
+ rescue StandardError
141
+ 0
142
+ end
143
+
144
+ # @return [Float]
145
+ def bpm
146
+ @audio[:bpm].to_f
147
+ end
148
+
149
+ # @return [Integer]
150
+ def frame_count
151
+ @frame_count
152
+ end
153
+
154
+ private
155
+
156
+ def symbolize_hash(value)
157
+ Hash(value).each_with_object({}) do |(key, entry), output|
158
+ output[key.to_sym] = entry
159
+ end
160
+ rescue StandardError
161
+ {}
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ # DSL builders and runtime helpers.
5
+ module DSL
6
+ end
7
+ end
8
+
9
+ require_relative "dsl/layer_builder"
10
+ require_relative "dsl/file_watcher"
11
+ require_relative "dsl/mapping_resolver"
12
+ require_relative "dsl/midi_map_executor"
13
+ require_relative "dsl/scene_builder"
14
+ require_relative "dsl/shader_source_resolver"
15
+ require_relative "dsl/transition_controller"
16
+ require_relative "dsl/engine"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ # Invalid or missing user-provided configuration.
5
+ class ConfigurationError < ArgumentError; end
6
+
7
+ # Scene DSL could not be loaded or resolved.
8
+ class SceneLoadError < ArgumentError; end
9
+
10
+ # Audio source initialization/processing failure.
11
+ class AudioSourceError < StandardError; end
12
+
13
+ # Frame generation failed in the render pipeline.
14
+ class FrameBuildError < StandardError; end
15
+
16
+ # Small helper for concise contextual error messages.
17
+ module ErrorFormatting
18
+ module_function
19
+
20
+ # @param error [Exception]
21
+ # @param context [String]
22
+ # @return [String]
23
+ def summarize(error, context:)
24
+ "#{context}: #{error.class}: #{error.message}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ module Renderer
5
+ # Fixed-interval scheduler used by the frame broadcast loop.
6
+ class FrameScheduler
7
+ # Default frame rate used by renderer loops.
8
+ DEFAULT_FRAME_RATE = 60.0
9
+
10
+ # @param frame_rate [Float]
11
+ # @param monotonic_clock [#call, nil]
12
+ # @param sleeper [#call, nil]
13
+ # @param error_handler [#call, nil]
14
+ # @yieldparam elapsed [Float]
15
+ def initialize(frame_rate: DEFAULT_FRAME_RATE, monotonic_clock: nil, sleeper: nil, error_handler: nil, &on_tick)
16
+ @frame_rate = Float(frame_rate)
17
+ raise ArgumentError, "frame_rate must be positive" unless @frame_rate.positive?
18
+
19
+ @frame_interval = 1.0 / @frame_rate
20
+ @monotonic_clock = monotonic_clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
21
+ @sleeper = sleeper || ->(seconds) { sleep(seconds) }
22
+ @error_handler = error_handler || ->(error) { raise error }
23
+ @on_tick = on_tick
24
+ @running = false
25
+ @thread = nil
26
+ end
27
+
28
+ # @return [void]
29
+ def start
30
+ return if running?
31
+
32
+ @running = true
33
+ started_at = @monotonic_clock.call
34
+ @thread = Thread.new { run_loop(started_at) }
35
+ end
36
+
37
+ # @param timeout [Float]
38
+ # @return [void]
39
+ def stop(timeout: 1.0)
40
+ return unless running?
41
+
42
+ @running = false
43
+ thread = @thread
44
+ @thread = nil
45
+ return unless thread
46
+ return if thread == Thread.current
47
+
48
+ thread.join(timeout)
49
+ end
50
+
51
+ # @return [Boolean]
52
+ def running?
53
+ @running
54
+ end
55
+
56
+ private
57
+
58
+ def run_loop(started_at)
59
+ while running?
60
+ begin
61
+ loop_started = @monotonic_clock.call
62
+ elapsed = loop_started - started_at
63
+ @on_tick&.call(elapsed)
64
+
65
+ duration = @monotonic_clock.call - loop_started
66
+ sleep_time = @frame_interval - duration
67
+ @sleeper.call(sleep_time) if sleep_time.positive?
68
+ rescue StandardError => e
69
+ @error_handler.call(e)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ module Renderer
5
+ # Serializes analysis and scene state into transport payloads.
6
+ class SceneSerializer
7
+ # @param timestamp [Numeric]
8
+ # @param audio [Hash]
9
+ # @param scene_name [String, Symbol]
10
+ # @param scene_layers [Array<Hash>]
11
+ # @param transition [Hash, nil]
12
+ # @return [Hash]
13
+ def audio_frame(timestamp:, audio:, scene_name:, scene_layers:, transition: nil)
14
+ {
15
+ timestamp: Float(timestamp),
16
+ audio: serialize_audio(audio),
17
+ scene: serialize_scene(scene_name, scene_layers),
18
+ transition: transition
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ def serialize_audio(audio)
25
+ bands = symbolize_hash(audio[:bands])
26
+
27
+ {
28
+ amplitude: round_float(audio[:amplitude]),
29
+ bands: bands.transform_values { |value| round_float(value) },
30
+ fft: Array(audio[:fft]).map { |value| round_float(value) },
31
+ beat: !!audio[:beat],
32
+ beat_count: Integer(audio[:beat_count] || 0),
33
+ bpm: audio[:bpm]
34
+ }
35
+ end
36
+
37
+ def serialize_scene(scene_name, scene_layers)
38
+ {
39
+ name: scene_name.to_s,
40
+ layers: Array(scene_layers).map { |layer| serialize_layer(layer) }
41
+ }
42
+ end
43
+
44
+ def serialize_layer(layer)
45
+ values = symbolize_hash(layer)
46
+
47
+ output = {
48
+ name: values.fetch(:name).to_s,
49
+ type: (values[:type] || :geometry).to_s,
50
+ params: symbolize_hash(values[:params])
51
+ }
52
+ output[:shader] = values[:shader].to_s if values[:shader]
53
+ output[:glsl] = values[:glsl].to_s if values[:glsl]
54
+ output[:glsl_source] = values[:glsl_source].to_s if values[:glsl_source]
55
+ output
56
+ end
57
+
58
+ def symbolize_hash(value)
59
+ Hash(value).each_with_object({}) do |(key, entry), output|
60
+ output[key.to_sym] = entry
61
+ end
62
+ rescue StandardError
63
+ {}
64
+ end
65
+
66
+ def round_float(value, digits: 4)
67
+ Float(value).round(digits)
68
+ rescue StandardError
69
+ 0.0
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ # Renderer-side frame scheduling and scene serialization.
5
+ module Renderer
6
+ end
7
+ end
8
+
9
+ require_relative "renderer/frame_scheduler"
10
+ require_relative "renderer/scene_serializer"