vizcore 0.1.0 → 1.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -117
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/playground-worker.js +373 -0
  5. data/docs/assets/playground.css +440 -0
  6. data/docs/assets/playground.js +652 -0
  7. data/docs/assets/site.css +744 -0
  8. data/docs/assets/vizcore-demo.gif +0 -0
  9. data/docs/assets/vizcore-poster.png +0 -0
  10. data/docs/assets/vj-tunnel.js +159 -0
  11. data/docs/index.html +225 -0
  12. data/docs/playground.html +81 -0
  13. data/docs/shape_dsl.md +269 -0
  14. data/examples/README.md +59 -0
  15. data/examples/assets/README.md +19 -0
  16. data/examples/audio_inspector.rb +34 -0
  17. data/examples/club_intro_drop.rb +78 -0
  18. data/examples/kansai_rubykaigi_visual.rb +70 -0
  19. data/examples/live_coding_minimal.rb +22 -0
  20. data/examples/midi_controller_show.rb +78 -0
  21. data/examples/midi_scene_switch.rb +3 -1
  22. data/examples/parser_visualizer.rb +48 -0
  23. data/examples/readme_demo.rb +17 -0
  24. data/examples/rhythm_geometry.rb +34 -0
  25. data/examples/ruby_crystal_show.rb +35 -0
  26. data/examples/shader_playground.rb +18 -0
  27. data/examples/unyo_liquid.rb +59 -0
  28. data/examples/vj_ambient_chill_room.rb +124 -0
  29. data/examples/vj_dnb_jungle.rb +170 -0
  30. data/examples/vj_festival_mainstage.rb +245 -0
  31. data/examples/vj_festival_mainstage.yml +17 -0
  32. data/examples/vj_glitch_industrial.rb +164 -0
  33. data/examples/vj_hiphop_cipher.rb +167 -0
  34. data/examples/vj_jpop_idol_live.rb +210 -0
  35. data/examples/vj_synthwave_retro.rb +173 -0
  36. data/examples/vj_techno_warehouse.rb +195 -0
  37. data/frontend/index.html +494 -2
  38. data/frontend/src/audio-inspector.js +40 -0
  39. data/frontend/src/custom-shape-param-controls.js +106 -0
  40. data/frontend/src/live-controls.js +131 -0
  41. data/frontend/src/main.js +1060 -16
  42. data/frontend/src/mapping-target-selector.js +109 -0
  43. data/frontend/src/midi-learn.js +194 -0
  44. data/frontend/src/performance-monitor.js +183 -0
  45. data/frontend/src/plugin-runtime.js +130 -0
  46. data/frontend/src/projector-mode.js +56 -0
  47. data/frontend/src/renderer/engine.js +157 -3
  48. data/frontend/src/renderer/layer-manager.js +442 -30
  49. data/frontend/src/renderer/shader-manager.js +26 -0
  50. data/frontend/src/runtime-control-preset.js +11 -0
  51. data/frontend/src/shader-error-overlay.js +29 -0
  52. data/frontend/src/shader-param-controls.js +93 -0
  53. data/frontend/src/shaders/builtins.js +380 -2
  54. data/frontend/src/shaders/post-effects.js +52 -0
  55. data/frontend/src/shape-editor-controls.js +157 -0
  56. data/frontend/src/visual-regression.js +67 -0
  57. data/frontend/src/visual-settings-preset.js +103 -0
  58. data/frontend/src/visuals/geometry.js +666 -0
  59. data/frontend/src/visuals/image-renderer.js +291 -0
  60. data/frontend/src/visuals/particle-system.js +56 -10
  61. data/frontend/src/visuals/shape-renderer.js +475 -0
  62. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  63. data/frontend/src/visuals/svg-arc.js +104 -0
  64. data/frontend/src/visuals/text-renderer.js +112 -11
  65. data/frontend/src/websocket-client.js +12 -1
  66. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  67. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  68. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  69. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  70. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  71. data/lib/vizcore/analysis/pipeline.rb +235 -11
  72. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  73. data/lib/vizcore/analysis.rb +4 -0
  74. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  75. data/lib/vizcore/audio/fixture_input.rb +65 -0
  76. data/lib/vizcore/audio/input_manager.rb +4 -2
  77. data/lib/vizcore/audio/mic_input.rb +24 -8
  78. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  79. data/lib/vizcore/audio.rb +1 -0
  80. data/lib/vizcore/cli/doctor.rb +159 -0
  81. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  82. data/lib/vizcore/cli/layer_docs.rb +46 -0
  83. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  84. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  85. data/lib/vizcore/cli/scene_validator.rb +337 -0
  86. data/lib/vizcore/cli/shader_template.rb +68 -0
  87. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  88. data/lib/vizcore/cli.rb +689 -18
  89. data/lib/vizcore/config.rb +103 -2
  90. data/lib/vizcore/control_preset.rb +68 -0
  91. data/lib/vizcore/dsl/engine.rb +277 -5
  92. data/lib/vizcore/dsl/layer_builder.rb +1280 -23
  93. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  94. data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
  95. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  96. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  97. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  98. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  99. data/lib/vizcore/dsl/style_builder.rb +68 -0
  100. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  101. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  102. data/lib/vizcore/dsl.rb +5 -1
  103. data/lib/vizcore/layer_catalog.rb +275 -0
  104. data/lib/vizcore/project_manifest.rb +152 -0
  105. data/lib/vizcore/renderer/png_writer.rb +57 -0
  106. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  107. data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
  108. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  109. data/lib/vizcore/renderer/snapshot.rb +38 -0
  110. data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
  111. data/lib/vizcore/renderer.rb +5 -0
  112. data/lib/vizcore/server/frame_broadcaster.rb +143 -8
  113. data/lib/vizcore/server/gallery_app.rb +155 -0
  114. data/lib/vizcore/server/gallery_page.rb +100 -0
  115. data/lib/vizcore/server/gallery_runner.rb +48 -0
  116. data/lib/vizcore/server/rack_app.rb +203 -4
  117. data/lib/vizcore/server/runner.rb +391 -22
  118. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  119. data/lib/vizcore/server/websocket_handler.rb +60 -10
  120. data/lib/vizcore/server.rb +4 -0
  121. data/lib/vizcore/shape.rb +719 -0
  122. data/lib/vizcore/sync/osc_message.rb +103 -0
  123. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  124. data/lib/vizcore/sync.rb +4 -0
  125. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  126. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  127. data/lib/vizcore/templates/plugin_readme.md +23 -0
  128. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  129. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  130. data/lib/vizcore/templates/project_readme.md +7 -23
  131. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  132. data/lib/vizcore/version.rb +1 -1
  133. data/lib/vizcore.rb +28 -0
  134. data/scripts/browser_capture.mjs +75 -0
  135. data/sig/vizcore.rbs +461 -0
  136. metadata +94 -3
  137. data/docs/GETTING_STARTED.md +0 -105
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../analysis"
4
+ require_relative "../audio"
5
+ require_relative "../dsl"
6
+
7
+ module Vizcore
8
+ module Renderer
9
+ # Produces analyzed scene frames for offline renderers.
10
+ class SceneFrameSource
11
+ def initialize(config:, frame_rate: nil)
12
+ @config = config
13
+ @frame_rate = frame_rate
14
+ @shader_source_resolver = Vizcore::DSL::ShaderSourceResolver.new
15
+ end
16
+
17
+ # @return [Vizcore::Renderer::SceneFrameSource]
18
+ def start
19
+ @definition = resolve_shader_sources(Vizcore::DSL::Engine.load_file(@config.scene_file.to_s))
20
+ @scene = first_scene(@definition)
21
+ @input_manager = build_input_manager
22
+ @input_manager.start
23
+ @capture_size = capture_size
24
+ @pipeline = build_pipeline
25
+ @frame_count = 0
26
+ self
27
+ end
28
+
29
+ # @return [Hash] frame data consumed by offline renderers
30
+ def capture
31
+ ensure_started!
32
+
33
+ audio = @pipeline.call(@input_manager.capture_frame(@capture_size))
34
+ @frame_count += 1
35
+ layers = Vizcore::DSL::MappingResolver.new.resolve_layers(
36
+ scene_layers: @scene[:layers],
37
+ audio: audio,
38
+ time: frame_time,
39
+ frame: @frame_count
40
+ )
41
+
42
+ {
43
+ scene: { name: @scene[:name], layers: layers },
44
+ audio: audio,
45
+ scene_name: @scene[:name].to_s
46
+ }
47
+ end
48
+
49
+ # @return [void]
50
+ def stop
51
+ @input_manager&.stop
52
+ end
53
+
54
+ private
55
+
56
+ def resolve_shader_sources(definition)
57
+ @shader_source_resolver.resolve(definition: definition, scene_file: @config.scene_file.to_s)
58
+ end
59
+
60
+ def first_scene(definition)
61
+ scene = Array(definition[:scenes]).first
62
+ return scene if scene
63
+
64
+ { name: @config.scene_file.basename(".rb").to_sym, layers: [] }
65
+ end
66
+
67
+ def build_input_manager
68
+ Vizcore::Audio::InputManager.new(
69
+ source: @config.audio_source,
70
+ file_path: @config.audio_file&.to_s,
71
+ audio_device: @config.audio_device
72
+ )
73
+ end
74
+
75
+ def capture_size
76
+ return @input_manager.frame_size unless @frame_rate
77
+
78
+ @input_manager.realtime_capture_size(@frame_rate)
79
+ end
80
+
81
+ def build_pipeline
82
+ Vizcore::Analysis::Pipeline.new(
83
+ sample_rate: @input_manager.sample_rate,
84
+ fft_size: supported_fft_size(@input_manager.frame_size),
85
+ noise_gate: @config.noise_gate,
86
+ audio_normalize: audio_normalize_settings,
87
+ bpm: bpm_setting,
88
+ bpm_lock: bpm_lock_setting
89
+ )
90
+ end
91
+
92
+ def frame_time
93
+ return 0.0 unless @frame_rate
94
+
95
+ (@frame_count - 1).fdiv(@frame_rate)
96
+ end
97
+
98
+ def audio_normalize_settings
99
+ Hash(@definition[:analysis] || {})[:audio_normalize]
100
+ rescue StandardError
101
+ nil
102
+ end
103
+
104
+ def bpm_setting
105
+ @config.bpm || Hash(@definition[:analysis] || {})[:bpm]
106
+ rescue StandardError
107
+ @config.bpm
108
+ end
109
+
110
+ def bpm_lock_setting
111
+ @config.bpm_lock? || !!Hash(@definition[:analysis] || {})[:bpm_lock]
112
+ rescue StandardError
113
+ @config.bpm_lock?
114
+ end
115
+
116
+ def supported_fft_size(size)
117
+ value = Integer(size)
118
+ return value if value.positive? && (value & (value - 1)).zero?
119
+
120
+ 1024
121
+ rescue StandardError
122
+ 1024
123
+ end
124
+
125
+ def ensure_started!
126
+ return if @input_manager && @pipeline && @scene
127
+
128
+ raise RuntimeError, "scene frame source has not been started"
129
+ end
130
+ end
131
+ end
132
+ end
@@ -9,28 +9,39 @@ module Vizcore
9
9
  # @param scene_name [String, Symbol]
10
10
  # @param scene_layers [Array<Hash>]
11
11
  # @param transition [Hash, nil]
12
+ # @param metrics [Hash, nil]
12
13
  # @return [Hash]
13
- def audio_frame(timestamp:, audio:, scene_name:, scene_layers:, transition: nil)
14
- {
14
+ def audio_frame(timestamp:, audio:, scene_name:, scene_layers:, transition: nil, metrics: nil)
15
+ frame = {
15
16
  timestamp: Float(timestamp),
16
17
  audio: serialize_audio(audio),
17
18
  scene: serialize_scene(scene_name, scene_layers),
18
19
  transition: transition
19
20
  }
21
+ frame[:metrics] = serialize_metrics(metrics) if metrics
22
+ frame
20
23
  end
21
24
 
22
25
  private
23
26
 
24
27
  def serialize_audio(audio)
25
28
  bands = symbolize_hash(audio[:bands])
29
+ onsets = { sub: 0.0, low: 0.0, mid: 0.0, high: 0.0 }.merge(symbolize_hash(audio[:onsets]))
30
+ drums = { kick: 0.0, snare: 0.0, hihat: 0.0 }.merge(symbolize_hash(audio[:drums]))
26
31
 
27
32
  {
28
33
  amplitude: round_float(audio[:amplitude]),
29
34
  bands: bands.transform_values { |value| round_float(value) },
30
35
  fft: Array(audio[:fft]).map { |value| round_float(value) },
36
+ onset: round_float(audio[:onset]),
37
+ onsets: onsets.transform_values { |value| round_float(value) },
38
+ drums: drums.transform_values { |value| round_float(value) },
31
39
  beat: !!audio[:beat],
40
+ beat_confidence: round_float(audio[:beat_confidence]),
41
+ beat_pulse: round_float(audio[:beat_pulse]),
32
42
  beat_count: Integer(audio[:beat_count] || 0),
33
- bpm: audio[:bpm]
43
+ bpm: audio[:bpm],
44
+ peak_frequency: round_float(audio[:peak_frequency])
34
45
  }
35
46
  end
36
47
 
@@ -52,9 +63,31 @@ module Vizcore
52
63
  output[:shader] = values[:shader].to_s if values[:shader]
53
64
  output[:glsl] = values[:glsl].to_s if values[:glsl]
54
65
  output[:glsl_source] = values[:glsl_source].to_s if values[:glsl_source]
66
+ output[:param_schema] = serialize_param_schema(values[:param_schema]) if values[:param_schema]
55
67
  output
56
68
  end
57
69
 
70
+ def serialize_param_schema(schema)
71
+ Array(schema).map do |entry|
72
+ values = symbolize_hash(entry)
73
+ {
74
+ name: values.fetch(:name).to_s,
75
+ default: round_float(values[:default]),
76
+ min: round_float(values[:min]),
77
+ max: round_float(values[:max]),
78
+ step: round_float(values[:step])
79
+ }.compact
80
+ end
81
+ end
82
+
83
+ def serialize_metrics(metrics)
84
+ symbolize_hash(metrics).each_with_object({}) do |(key, value), output|
85
+ output[key] = key == :frame_id ? Integer(value) : round_float(value)
86
+ rescue StandardError
87
+ output[key] = 0
88
+ end
89
+ end
90
+
58
91
  def symbolize_hash(value)
59
92
  Hash(value).each_with_object({}) do |(key, entry), output|
60
93
  output[key.to_sym] = entry
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+ require_relative "scene_frame_source"
6
+ require_relative "snapshot_renderer"
7
+
8
+ module Vizcore
9
+ module Renderer
10
+ # Builds one analyzed scene frame and writes a PNG preview.
11
+ class Snapshot
12
+ def initialize(config:, width: SnapshotRenderer::DEFAULT_WIDTH, height: SnapshotRenderer::DEFAULT_HEIGHT)
13
+ @config = config
14
+ @width = width
15
+ @height = height
16
+ end
17
+
18
+ # @param out [String, Pathname]
19
+ # @return [Hash] snapshot metadata
20
+ def write(out:)
21
+ output_path = Pathname.new(out.to_s).expand_path
22
+ frame_source = SceneFrameSource.new(config: @config)
23
+ frame_source.start
24
+ frame = frame_source.capture
25
+ png = SnapshotRenderer.new(width: @width, height: @height).render(
26
+ scene: frame.fetch(:scene),
27
+ audio: frame.fetch(:audio)
28
+ )
29
+
30
+ FileUtils.mkdir_p(output_path.dirname)
31
+ File.binwrite(output_path, png)
32
+ { path: output_path, scene: frame.fetch(:scene_name), width: @width, height: @height }
33
+ ensure
34
+ frame_source&.stop
35
+ end
36
+ end
37
+ end
38
+ end