vizcore 0.1.0 → 1.0.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +544 -9
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/site.css +744 -0
  5. data/docs/assets/vizcore-demo.gif +0 -0
  6. data/docs/assets/vizcore-poster.png +0 -0
  7. data/docs/assets/vj-tunnel.js +159 -0
  8. data/docs/index.html +224 -0
  9. data/examples/README.md +59 -0
  10. data/examples/assets/README.md +19 -0
  11. data/examples/audio_inspector.rb +34 -0
  12. data/examples/club_intro_drop.rb +78 -0
  13. data/examples/kansai_rubykaigi_visual.rb +70 -0
  14. data/examples/live_coding_minimal.rb +22 -0
  15. data/examples/midi_controller_show.rb +78 -0
  16. data/examples/midi_scene_switch.rb +3 -1
  17. data/examples/parser_visualizer.rb +48 -0
  18. data/examples/readme_demo.rb +17 -0
  19. data/examples/rhythm_geometry.rb +34 -0
  20. data/examples/ruby_crystal_show.rb +35 -0
  21. data/examples/shader_playground.rb +18 -0
  22. data/examples/unyo_liquid.rb +59 -0
  23. data/examples/vj_ambient_chill_room.rb +124 -0
  24. data/examples/vj_dnb_jungle.rb +170 -0
  25. data/examples/vj_festival_mainstage.rb +245 -0
  26. data/examples/vj_festival_mainstage.yml +17 -0
  27. data/examples/vj_glitch_industrial.rb +164 -0
  28. data/examples/vj_hiphop_cipher.rb +167 -0
  29. data/examples/vj_jpop_idol_live.rb +210 -0
  30. data/examples/vj_synthwave_retro.rb +173 -0
  31. data/examples/vj_techno_warehouse.rb +195 -0
  32. data/frontend/index.html +468 -2
  33. data/frontend/src/audio-inspector.js +40 -0
  34. data/frontend/src/live-controls.js +131 -0
  35. data/frontend/src/main.js +792 -16
  36. data/frontend/src/midi-learn.js +194 -0
  37. data/frontend/src/performance-monitor.js +183 -0
  38. data/frontend/src/plugin-runtime.js +130 -0
  39. data/frontend/src/projector-mode.js +56 -0
  40. data/frontend/src/renderer/engine.js +148 -3
  41. data/frontend/src/renderer/layer-manager.js +428 -30
  42. data/frontend/src/renderer/shader-manager.js +26 -0
  43. data/frontend/src/runtime-control-preset.js +11 -0
  44. data/frontend/src/shader-error-overlay.js +29 -0
  45. data/frontend/src/shader-param-controls.js +93 -0
  46. data/frontend/src/shaders/builtins.js +380 -2
  47. data/frontend/src/shaders/post-effects.js +52 -0
  48. data/frontend/src/visual-regression.js +67 -0
  49. data/frontend/src/visual-settings-preset.js +103 -0
  50. data/frontend/src/visuals/geometry.js +268 -0
  51. data/frontend/src/visuals/image-renderer.js +291 -0
  52. data/frontend/src/visuals/particle-system.js +56 -10
  53. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  54. data/frontend/src/visuals/text-renderer.js +112 -11
  55. data/frontend/src/websocket-client.js +12 -1
  56. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  57. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  58. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  59. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  60. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  61. data/lib/vizcore/analysis/pipeline.rb +235 -11
  62. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  63. data/lib/vizcore/analysis.rb +4 -0
  64. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  65. data/lib/vizcore/audio/fixture_input.rb +65 -0
  66. data/lib/vizcore/audio/input_manager.rb +4 -2
  67. data/lib/vizcore/audio/mic_input.rb +24 -8
  68. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  69. data/lib/vizcore/audio.rb +1 -0
  70. data/lib/vizcore/cli/doctor.rb +159 -0
  71. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  72. data/lib/vizcore/cli/layer_docs.rb +46 -0
  73. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  74. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  75. data/lib/vizcore/cli/scene_validator.rb +245 -0
  76. data/lib/vizcore/cli/shader_template.rb +68 -0
  77. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  78. data/lib/vizcore/cli.rb +689 -18
  79. data/lib/vizcore/config.rb +103 -2
  80. data/lib/vizcore/control_preset.rb +68 -0
  81. data/lib/vizcore/dsl/engine.rb +277 -5
  82. data/lib/vizcore/dsl/layer_builder.rb +491 -22
  83. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  84. data/lib/vizcore/dsl/mapping_resolver.rb +132 -3
  85. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  86. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  87. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  88. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  89. data/lib/vizcore/dsl/style_builder.rb +68 -0
  90. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  91. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  92. data/lib/vizcore/dsl.rb +5 -1
  93. data/lib/vizcore/layer_catalog.rb +273 -0
  94. data/lib/vizcore/project_manifest.rb +152 -0
  95. data/lib/vizcore/renderer/png_writer.rb +57 -0
  96. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  97. data/lib/vizcore/renderer/scene_frame_source.rb +119 -0
  98. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  99. data/lib/vizcore/renderer/snapshot.rb +38 -0
  100. data/lib/vizcore/renderer/snapshot_renderer.rb +446 -0
  101. data/lib/vizcore/renderer.rb +5 -0
  102. data/lib/vizcore/server/frame_broadcaster.rb +91 -5
  103. data/lib/vizcore/server/gallery_app.rb +155 -0
  104. data/lib/vizcore/server/gallery_page.rb +100 -0
  105. data/lib/vizcore/server/gallery_runner.rb +48 -0
  106. data/lib/vizcore/server/rack_app.rb +203 -4
  107. data/lib/vizcore/server/runner.rb +370 -22
  108. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  109. data/lib/vizcore/server/websocket_handler.rb +60 -10
  110. data/lib/vizcore/server.rb +4 -0
  111. data/lib/vizcore/sync/osc_message.rb +103 -0
  112. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  113. data/lib/vizcore/sync.rb +4 -0
  114. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  115. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  116. data/lib/vizcore/templates/plugin_readme.md +23 -0
  117. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  118. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  119. data/lib/vizcore/templates/project_readme.md +7 -23
  120. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  121. data/lib/vizcore/version.rb +1 -1
  122. data/lib/vizcore.rb +27 -0
  123. data/scripts/browser_capture.mjs +75 -0
  124. data/sig/vizcore.rbs +362 -0
  125. metadata +83 -3
  126. data/docs/GETTING_STARTED.md +0 -105
@@ -25,11 +25,13 @@ module Vizcore
25
25
  # @param frame_size [Integer] frame size used by capture loop
26
26
  # @param ring_buffer_size [Integer] stored sample capacity
27
27
  # @param file_path [String, nil] source file path for `:file`
28
- def initialize(source: :mic, sample_rate: DEFAULT_SAMPLE_RATE, frame_size: DEFAULT_FRAME_SIZE, ring_buffer_size: DEFAULT_RING_BUFFER_SIZE, file_path: nil)
28
+ # @param audio_device [String, Integer, nil] input device index/name for `:mic`
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)
29
30
  @source_name = source.to_sym
30
31
  @sample_rate = Integer(sample_rate)
31
32
  @frame_size = Integer(frame_size)
32
33
  @ring_buffer = RingBuffer.new(ring_buffer_size)
34
+ @audio_device = audio_device
33
35
  @input = build_input(file_path)
34
36
  @sample_rate = resolve_input_sample_rate(@input, fallback: @sample_rate)
35
37
  end
@@ -110,7 +112,7 @@ module Vizcore
110
112
  def build_input(file_path)
111
113
  case @source_name
112
114
  when :mic
113
- MicInput.new(sample_rate: sample_rate)
115
+ MicInput.new(sample_rate: sample_rate, device: @audio_device || :default)
114
116
  when :file
115
117
  FileInput.new(path: file_path, sample_rate: sample_rate)
116
118
  when :dummy
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_input"
4
- require_relative "dummy_sine_input"
5
4
  require_relative "../errors"
6
5
  require_relative "portaudio_ffi"
7
6
 
8
7
  module Vizcore
9
8
  module Audio
10
- # Microphone input using PortAudio, with automatic fallback to dummy source.
9
+ # Microphone input using PortAudio, with automatic fallback to silence.
11
10
  class MicInput < BaseInput
12
11
  attr_reader :device, :last_error
13
12
 
@@ -22,7 +21,7 @@ module Vizcore
22
21
  @device = device
23
22
  @channels = Integer(channels)
24
23
  @frames_per_buffer = Integer(frames_per_buffer)
25
- @fallback_input = fallback_input || DummySineInput.new(sample_rate: sample_rate)
24
+ @fallback_input = fallback_input || BaseInput.new(sample_rate: sample_rate)
26
25
  @portaudio_backend = portaudio_backend
27
26
  @stream = nil
28
27
  @using_fallback = false
@@ -75,11 +74,7 @@ module Vizcore
75
74
  private
76
75
 
77
76
  def open_stream
78
- stream = @portaudio_backend.open_default_input_stream(
79
- sample_rate: sample_rate,
80
- channels: @channels,
81
- frames_per_buffer: @frames_per_buffer
82
- )
77
+ stream = open_requested_stream
83
78
  return nil unless stream
84
79
  return stream if stream.start
85
80
 
@@ -90,6 +85,27 @@ module Vizcore
90
85
  nil
91
86
  end
92
87
 
88
+ def open_requested_stream
89
+ if default_device?
90
+ return @portaudio_backend.open_default_input_stream(
91
+ sample_rate: sample_rate,
92
+ channels: @channels,
93
+ frames_per_buffer: @frames_per_buffer
94
+ )
95
+ end
96
+
97
+ @portaudio_backend.open_input_stream(
98
+ device: @device,
99
+ sample_rate: sample_rate,
100
+ channels: @channels,
101
+ frames_per_buffer: @frames_per_buffer
102
+ )
103
+ end
104
+
105
+ def default_device?
106
+ @device.nil? || @device.to_s.empty? || @device.to_s == "default"
107
+ end
108
+
93
109
  def close_stream
94
110
  return unless @stream
95
111
 
@@ -6,6 +6,8 @@ module Vizcore
6
6
  module PortAudioFFI
7
7
  # Runtime wrapper for an opened PortAudio input stream.
8
8
  class Stream
9
+ PA_INPUT_OVERFLOWED = -9981
10
+
9
11
  # @param mod [Module] ffi-bound PortAudio module
10
12
  # @param pointer [FFI::Pointer] native stream pointer
11
13
  # @param channels [Integer] input channel count
@@ -37,7 +39,7 @@ module Vizcore
37
39
 
38
40
  buffer = ffi_module::MemoryPointer.new(:float, frames * @channels)
39
41
  result = @mod.Pa_ReadStream(@pointer, buffer, frames)
40
- return Array.new(frames, 0.0) unless ok?(result)
42
+ return Array.new(frames, 0.0) unless readable_result?(result)
41
43
 
42
44
  samples = buffer.read_array_of_float(frames * @channels)
43
45
  return samples if @channels == 1
@@ -83,6 +85,10 @@ module Vizcore
83
85
  result == self.class.pa_no_error
84
86
  end
85
87
 
88
+ def readable_result?(result)
89
+ ok?(result) || result == self.class.pa_input_overflowed
90
+ end
91
+
86
92
  def ffi_module
87
93
  self.class.ffi_module
88
94
  end
@@ -98,6 +104,12 @@ module Vizcore
98
104
  def pa_no_error
99
105
  0
100
106
  end
107
+
108
+ # PortAudio returns this when input data was dropped before this read,
109
+ # but the current read buffer can still contain usable samples.
110
+ def pa_input_overflowed
111
+ PA_INPUT_OVERFLOWED
112
+ end
101
113
  end
102
114
  end
103
115
 
@@ -109,6 +121,8 @@ module Vizcore
109
121
  PA_NO_ERROR = 0
110
122
  # PortAudio float sample format code.
111
123
  PA_FLOAT_32 = 0x0000_0001
124
+ # PortAudio stream flags.
125
+ PA_NO_FLAGS = 0
112
126
 
113
127
  # @return [Boolean] true when PortAudio native library can be loaded
114
128
  def available?
@@ -174,6 +188,50 @@ module Vizcore
174
188
  nil
175
189
  end
176
190
 
191
+ # @param device [String, Integer] PortAudio device index or case-insensitive name fragment
192
+ # @param sample_rate [Float]
193
+ # @param channels [Integer]
194
+ # @param frames_per_buffer [Integer]
195
+ # @return [Vizcore::Audio::PortAudioFFI::Stream, nil]
196
+ def open_input_stream(device:, sample_rate:, channels: DEFAULT_CHANNELS, frames_per_buffer: 1024)
197
+ mod = ffi_module
198
+ return nil unless mod
199
+ return nil unless ok?(mod.Pa_Initialize)
200
+
201
+ device_info = resolve_input_device(mod, device)
202
+ return terminate_with_nil(mod) unless device_info
203
+
204
+ device_index, info = device_info
205
+ actual_channels = [Integer(channels), info[:maxInputChannels]].min
206
+ input_params = mod::StreamParameters.new
207
+ input_params[:device] = device_index
208
+ input_params[:channelCount] = actual_channels
209
+ input_params[:sampleFormat] = PA_FLOAT_32
210
+ input_params[:suggestedLatency] = info[:defaultLowInputLatency].to_f
211
+ input_params[:hostApiSpecificStreamInfo] = nil
212
+
213
+ stream_ptr_ptr = ffi::MemoryPointer.new(:pointer)
214
+ result = mod.Pa_OpenStream(
215
+ stream_ptr_ptr,
216
+ input_params.to_ptr,
217
+ nil,
218
+ Float(sample_rate),
219
+ Integer(frames_per_buffer),
220
+ PA_NO_FLAGS,
221
+ nil,
222
+ nil
223
+ )
224
+ return terminate_with_nil(mod) unless ok?(result)
225
+
226
+ stream_pointer = stream_ptr_ptr.read_pointer
227
+ return terminate_with_nil(mod) if stream_pointer.null?
228
+
229
+ Stream.new(mod: mod, pointer: stream_pointer, channels: actual_channels)
230
+ rescue StandardError
231
+ mod&.Pa_Terminate
232
+ nil
233
+ end
234
+
177
235
  # @param stream [Vizcore::Audio::PortAudioFFI::Stream, nil]
178
236
  # @return [nil]
179
237
  def close_stream(stream)
@@ -190,6 +248,44 @@ module Vizcore
190
248
  nil
191
249
  end
192
250
 
251
+ def resolve_input_device(mod, device)
252
+ if integer_string?(device)
253
+ index = Integer(device)
254
+ pointer = mod.Pa_GetDeviceInfo(index)
255
+ return nil if pointer.null?
256
+
257
+ info = mod::DeviceInfo.new(pointer)
258
+ return nil unless info[:maxInputChannels].positive?
259
+
260
+ return [index, info]
261
+ end
262
+
263
+ find_input_device_by_name(mod, device.to_s)
264
+ end
265
+
266
+ def find_input_device_by_name(mod, query)
267
+ normalized_query = query.downcase
268
+ count = mod.Pa_GetDeviceCount
269
+ return nil if count <= 0
270
+
271
+ count.times do |index|
272
+ pointer = mod.Pa_GetDeviceInfo(index)
273
+ next if pointer.null?
274
+
275
+ info = mod::DeviceInfo.new(pointer)
276
+ next unless info[:maxInputChannels].positive?
277
+
278
+ name = info[:name].read_string
279
+ return [index, info] if name.downcase.include?(normalized_query)
280
+ end
281
+
282
+ nil
283
+ end
284
+
285
+ def integer_string?(value)
286
+ value.to_s.match?(/\A\d+\z/)
287
+ end
288
+
193
289
  def ffi
194
290
  @ffi ||= begin
195
291
  require "ffi"
@@ -225,11 +321,20 @@ module Vizcore
225
321
  :defaultSampleRate, :double
226
322
  mod.const_set(:DeviceInfo, device_info)
227
323
 
324
+ stream_parameters = Class.new(ffi::Struct)
325
+ stream_parameters.layout :device, :int,
326
+ :channelCount, :int,
327
+ :sampleFormat, :ulong,
328
+ :suggestedLatency, :double,
329
+ :hostApiSpecificStreamInfo, :pointer
330
+ mod.const_set(:StreamParameters, stream_parameters)
331
+
228
332
  mod.attach_function :Pa_Initialize, [], :int
229
333
  mod.attach_function :Pa_Terminate, [], :int
230
334
  mod.attach_function :Pa_GetDeviceCount, [], :int
231
335
  mod.attach_function :Pa_GetDeviceInfo, [:int], :pointer
232
336
  mod.attach_function :Pa_OpenDefaultStream, [:pointer, :int, :int, :ulong, :double, :ulong, :pointer, :pointer], :int
337
+ mod.attach_function :Pa_OpenStream, [:pointer, :pointer, :pointer, :double, :ulong, :ulong, :pointer, :pointer], :int
233
338
  mod.attach_function :Pa_StartStream, [:pointer], :int
234
339
  mod.attach_function :Pa_StopStream, [:pointer], :int
235
340
  mod.attach_function :Pa_CloseStream, [:pointer], :int
data/lib/vizcore/audio.rb CHANGED
@@ -9,6 +9,7 @@ end
9
9
  require_relative "audio/base_input"
10
10
  require_relative "audio/dummy_sine_input"
11
11
  require_relative "audio/file_input"
12
+ require_relative "audio/fixture_input"
12
13
  require_relative "audio/input_manager"
13
14
  require_relative "audio/mic_input"
14
15
  require_relative "audio/midi_input"
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require_relative "../analysis"
5
+ require_relative "../audio"
6
+ require_relative "../config"
7
+
8
+ module Vizcore
9
+ module CLISupport
10
+ # Environment preflight checks for local Vizcore development and live use.
11
+ class Doctor
12
+ REQUIRED_RUBY = Gem::Requirement.new(">= 3.2.0")
13
+
14
+ Check = Struct.new(:name, :status, :message, keyword_init: true) do
15
+ def ok?
16
+ status == :ok
17
+ end
18
+
19
+ def failure?
20
+ status == :fail
21
+ end
22
+ end
23
+
24
+ Report = Struct.new(:checks, keyword_init: true) do
25
+ def failure?
26
+ checks.any?(&:failure?)
27
+ end
28
+ end
29
+
30
+ def initialize(
31
+ ruby_version: RUBY_VERSION,
32
+ portaudio_available: -> { Vizcore::Audio::PortAudioFFI.available? },
33
+ audio_devices: -> { Vizcore::Audio::PortAudioFFI.input_devices },
34
+ midi_devices: -> { Vizcore::Audio::MidiInput.available_devices },
35
+ fftw_available: -> { Vizcore::Analysis::FFTProcessor.fftw_available? },
36
+ command_available: method(:command_available?),
37
+ port_available: method(:port_available?)
38
+ )
39
+ @ruby_version = ruby_version
40
+ @portaudio_available = portaudio_available
41
+ @audio_devices = audio_devices
42
+ @midi_devices = midi_devices
43
+ @fftw_available = fftw_available
44
+ @command_available = command_available
45
+ @port_available = port_available
46
+ end
47
+
48
+ def call
49
+ Report.new(
50
+ checks: [
51
+ ruby_check,
52
+ frontend_check,
53
+ portaudio_check,
54
+ audio_devices_check,
55
+ midi_check,
56
+ fftw_check,
57
+ ffmpeg_check,
58
+ port_check
59
+ ]
60
+ )
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :ruby_version
66
+
67
+ def ruby_check
68
+ version = Gem::Version.new(ruby_version)
69
+ return ok("Ruby", "#{ruby_version} satisfies #{REQUIRED_RUBY}") if REQUIRED_RUBY.satisfied_by?(version)
70
+
71
+ fail_check("Ruby", "#{ruby_version} is too old; Vizcore requires #{REQUIRED_RUBY}")
72
+ end
73
+
74
+ def frontend_check
75
+ index_path = Vizcore.frontend_root.join("index.html")
76
+ src_path = Vizcore.frontend_root.join("src")
77
+ return ok("Frontend assets", "browser runtime assets found") if index_path.file? && src_path.directory?
78
+
79
+ fail_check("Frontend assets", "missing browser runtime assets under #{Vizcore.frontend_root}")
80
+ end
81
+
82
+ def portaudio_check
83
+ return ok("PortAudio", "native audio input bridge is available") if @portaudio_available.call
84
+
85
+ warn("PortAudio", "native audio input unavailable; use --audio-source dummy or install PortAudio for mic input")
86
+ rescue StandardError => e
87
+ warn("PortAudio", "availability check failed: #{e.message}")
88
+ end
89
+
90
+ def audio_devices_check
91
+ devices = Array(@audio_devices.call)
92
+ return ok("Audio devices", "#{devices.length} input device(s) detected") unless devices.empty?
93
+
94
+ warn("Audio devices", "no native input devices detected; mic input may fall back to silence")
95
+ rescue StandardError => e
96
+ warn("Audio devices", "device scan failed: #{e.message}")
97
+ end
98
+
99
+ def midi_check
100
+ devices = Array(@midi_devices.call)
101
+ return ok("MIDI", "#{devices.length} MIDI input device(s) detected") unless devices.empty?
102
+
103
+ warn("MIDI", "no MIDI input devices detected; install unimidi and connect a controller to use midi_map")
104
+ rescue StandardError => e
105
+ warn("MIDI", "device scan failed: #{e.message}")
106
+ end
107
+
108
+ def fftw_check
109
+ return ok("FFTW3", "native FFT backend is available") if @fftw_available.call
110
+
111
+ warn("FFTW3", "native FFT backend unavailable; pure Ruby FFT fallback will be used")
112
+ rescue StandardError => e
113
+ warn("FFTW3", "availability check failed: #{e.message}")
114
+ end
115
+
116
+ def ffmpeg_check
117
+ return ok("ffmpeg", "compressed audio decoding is available") if @command_available.call("ffmpeg")
118
+
119
+ warn("ffmpeg", "not found on PATH; WAV input still works, MP3/FLAC file input will not")
120
+ end
121
+
122
+ def port_check
123
+ return ok("Default port", "#{Config::DEFAULT_HOST}:#{Config::DEFAULT_PORT} is available") if @port_available.call(Config::DEFAULT_HOST, Config::DEFAULT_PORT)
124
+
125
+ warn("Default port", "#{Config::DEFAULT_HOST}:#{Config::DEFAULT_PORT} is already in use")
126
+ rescue StandardError => e
127
+ warn("Default port", "availability check failed: #{e.message}")
128
+ end
129
+
130
+ def ok(name, message)
131
+ Check.new(name: name, status: :ok, message: message)
132
+ end
133
+
134
+ def warn(name, message)
135
+ Check.new(name: name, status: :warn, message: message)
136
+ end
137
+
138
+ def fail_check(name, message)
139
+ Check.new(name: name, status: :fail, message: message)
140
+ end
141
+
142
+ def command_available?(command)
143
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
144
+ path = File.join(directory, command)
145
+ File.file?(path) && File.executable?(path)
146
+ end
147
+ end
148
+
149
+ def port_available?(host, port)
150
+ server = TCPServer.new(host, port)
151
+ true
152
+ rescue Errno::EADDRINUSE
153
+ false
154
+ ensure
155
+ server&.close
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../layer_catalog"
4
+
5
+ module Vizcore
6
+ module CLISupport
7
+ # Produces a generated reference for the Ruby DSL entrypoints.
8
+ class DslReference
9
+ Entry = Struct.new(:syntax, :description, keyword_init: true)
10
+
11
+ TOP_LEVEL = [
12
+ Entry.new(syntax: "audio :mic, **options", description: "Register an audio input definition."),
13
+ Entry.new(syntax: "midi :controller, **options", description: "Register a MIDI input definition."),
14
+ Entry.new(syntax: "audio_normalize mode: :adaptive", description: "Configure analysis-level normalization."),
15
+ Entry.new(syntax: "bpm 128 / bpm_lock true", description: "Set and optionally lock the analysis BPM."),
16
+ Entry.new(syntax: "tap_tempo key: :space", description: "Enable browser tap tempo events."),
17
+ Entry.new(syntax: "set :global_intensity, 0.8", description: "Set a runtime global exposed to shaders."),
18
+ Entry.new(syntax: "style :name { ... } / theme :name { ... }", description: "Define reusable layer params or scene defaults."),
19
+ 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: "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."),
24
+ Entry.new(syntax: "key \"d\" { switch_scene :drop }", description: "Map browser keyboard shortcuts to runtime actions.")
25
+ ].freeze
26
+
27
+ SCENE = [
28
+ Entry.new(syntax: "use_theme :name", description: "Apply scene-wide layer defaults."),
29
+ Entry.new(syntax: "group :foreground { layer :name { ... } }", description: "Apply shared params to a related layer group."),
30
+ Entry.new(syntax: "layer :name { ... }", description: "Append a render layer to the scene.")
31
+ ].freeze
32
+
33
+ LAYER = [
34
+ Entry.new(syntax: "type :particle_field", description: "Set the layer renderer type."),
35
+ Entry.new(syntax: "shader :neon_grid / shader \"shaders/liquid.frag\"", description: "Use a built-in or custom fragment shader."),
36
+ Entry.new(syntax: "glsl \"shaders/liquid.frag\"", description: "Load a custom fragment shader file."),
37
+ Entry.new(syntax: "type :svg / file \"assets/logo.svg\"", description: "Render an SVG asset resolved relative to the scene file."),
38
+ Entry.new(syntax: "type :image / file \"assets/noise.png\"", description: "Render a PNG/JPEG/GIF/WebP asset resolved relative to the scene file."),
39
+ Entry.new(syntax: "type :video / file \"assets/loop.mp4\"", description: "Render a looping MP4/WebM/OGV video texture."),
40
+ Entry.new(syntax: "type :waveform / source :audio / style :ribbon", description: "Render an audio feature waveform layer."),
41
+ Entry.new(syntax: "type :spectrogram / scroll :vertical", description: "Render a scrolling FFT heatmap layer."),
42
+ Entry.new(syntax: "type :mesh / geometry :icosahedron / material :wireframe", description: "Render preset 3D wireframe geometry."),
43
+ Entry.new(syntax: "circle count: 8 { radius 100 } / line x1: 0, y1: 360, x2: 1280, y2: 360", description: "Render declarative 2D shape primitives."),
44
+ Entry.new(syntax: "font \"Inter\" / letter_spacing 4", description: "Set text presentation params."),
45
+ Entry.new(syntax: "palette \"#ff0055\", \"#00ffff\"", description: "Set ordered colors for supported layer renderers."),
46
+ Entry.new(syntax: "blend :add / effect :bloom / vj_effect :mirror", description: "Set compositing and browser effects."),
47
+ Entry.new(syntax: "param :wobble, default: 0.3, range: 0.0..2.0", description: "Declare numeric shader parameter metadata."),
48
+ Entry.new(syntax: "map amplitude, to: :speed, range: 0.0..2.0", description: "Map audio features to layer params."),
49
+ Entry.new(syntax: "react_to bass { change :size }", description: "Group mappings by source.")
50
+ ].freeze
51
+
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
55
+ ].freeze
56
+
57
+ TRANSFORMS = %w[
58
+ gain range min max curve deadzone smooth(attack:,release:)
59
+ ].freeze
60
+
61
+ # @return [Array<String>]
62
+ def lines
63
+ [
64
+ "# Vizcore Ruby DSL Reference",
65
+ "",
66
+ "## Top-level DSL",
67
+ *entry_lines(TOP_LEVEL),
68
+ "",
69
+ "## Scene DSL",
70
+ *entry_lines(SCENE),
71
+ "",
72
+ "## Layer DSL",
73
+ *entry_lines(LAYER),
74
+ "",
75
+ "Mapping sources: #{SOURCES.join(', ')}",
76
+ "Mapping transforms: #{TRANSFORMS.join(', ')}",
77
+ "",
78
+ "## Built-in Layer Capabilities",
79
+ *capability_lines
80
+ ]
81
+ end
82
+
83
+ private
84
+
85
+ def entry_lines(entries)
86
+ entries.map { |entry| "- `#{entry.syntax}` - #{entry.description}" }
87
+ end
88
+
89
+ def capability_lines
90
+ Vizcore::LayerCatalog.capabilities.map do |capability|
91
+ aliases = capability.aliases.empty? ? "" : " (aliases: #{capability.aliases.join(', ')})"
92
+ params = capability.params.keys.join(", ")
93
+ mappable = capability.mappable_params.empty? ? "(none)" : capability.mappable_params.join(", ")
94
+ "- `#{capability.type}`#{aliases}: params #{params}; mappable #{mappable}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../layer_catalog"
4
+
5
+ module Vizcore
6
+ module CLISupport
7
+ # Formats built-in layer capability metadata for CLI output.
8
+ class LayerDocs
9
+ def lines
10
+ [
11
+ "# Vizcore Layer Capabilities",
12
+ "",
13
+ *layer_lines,
14
+ "",
15
+ "Built-in shaders: #{Vizcore::LayerCatalog::BUILTIN_SHADERS.join(', ')}",
16
+ "Blend modes: #{Vizcore::LayerCatalog::BLEND_MODES.join(', ')}",
17
+ "Post effects: #{Vizcore::LayerCatalog::POST_EFFECTS.join(', ')}",
18
+ "VJ effects: #{Vizcore::LayerCatalog::VJ_EFFECTS.join(', ')}"
19
+ ]
20
+ end
21
+
22
+ private
23
+
24
+ def layer_lines
25
+ Vizcore::LayerCatalog.capabilities.flat_map do |capability|
26
+ aliases = capability.aliases.empty? ? "" : " (aliases: #{capability.aliases.join(', ')})"
27
+ [
28
+ "## #{capability.type}#{aliases}",
29
+ capability.description,
30
+ "Params: #{format_params(capability.params)}",
31
+ "Mappable params: #{format_list(capability.mappable_params)}",
32
+ ""
33
+ ]
34
+ end.tap(&:pop)
35
+ end
36
+
37
+ def format_params(params)
38
+ params.map { |name, type| "#{name}: #{type}" }.join(", ")
39
+ end
40
+
41
+ def format_list(values)
42
+ values.empty? ? "(none)" : values.join(", ")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "scene_inspector"
4
+ require_relative "scene_validator"
5
+
6
+ module Vizcore
7
+ module CLISupport
8
+ # Facade used by Thor commands for scene validation and inspection.
9
+ class SceneDiagnostics
10
+ def initialize(scene_file:)
11
+ @validator = SceneValidator.new(scene_file: scene_file)
12
+ end
13
+
14
+ def validate
15
+ @validator.call
16
+ end
17
+
18
+ def inspect_lines(definition)
19
+ SceneInspector.new(definition: definition).lines
20
+ end
21
+ end
22
+ end
23
+ end