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.
- checksums.yaml +4 -4
- data/README.md +544 -9
- data/docs/.nojekyll +0 -0
- data/docs/assets/site.css +744 -0
- data/docs/assets/vizcore-demo.gif +0 -0
- data/docs/assets/vizcore-poster.png +0 -0
- data/docs/assets/vj-tunnel.js +159 -0
- data/docs/index.html +224 -0
- data/examples/README.md +59 -0
- data/examples/assets/README.md +19 -0
- data/examples/audio_inspector.rb +34 -0
- data/examples/club_intro_drop.rb +78 -0
- data/examples/kansai_rubykaigi_visual.rb +70 -0
- data/examples/live_coding_minimal.rb +22 -0
- data/examples/midi_controller_show.rb +78 -0
- data/examples/midi_scene_switch.rb +3 -1
- data/examples/parser_visualizer.rb +48 -0
- data/examples/readme_demo.rb +17 -0
- data/examples/rhythm_geometry.rb +34 -0
- data/examples/ruby_crystal_show.rb +35 -0
- data/examples/shader_playground.rb +18 -0
- data/examples/unyo_liquid.rb +59 -0
- data/examples/vj_ambient_chill_room.rb +124 -0
- data/examples/vj_dnb_jungle.rb +170 -0
- data/examples/vj_festival_mainstage.rb +245 -0
- data/examples/vj_festival_mainstage.yml +17 -0
- data/examples/vj_glitch_industrial.rb +164 -0
- data/examples/vj_hiphop_cipher.rb +167 -0
- data/examples/vj_jpop_idol_live.rb +210 -0
- data/examples/vj_synthwave_retro.rb +173 -0
- data/examples/vj_techno_warehouse.rb +195 -0
- data/frontend/index.html +468 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +792 -16
- data/frontend/src/midi-learn.js +194 -0
- data/frontend/src/performance-monitor.js +183 -0
- data/frontend/src/plugin-runtime.js +130 -0
- data/frontend/src/projector-mode.js +56 -0
- data/frontend/src/renderer/engine.js +148 -3
- data/frontend/src/renderer/layer-manager.js +428 -30
- data/frontend/src/renderer/shader-manager.js +26 -0
- data/frontend/src/runtime-control-preset.js +11 -0
- data/frontend/src/shader-error-overlay.js +29 -0
- data/frontend/src/shader-param-controls.js +93 -0
- data/frontend/src/shaders/builtins.js +380 -2
- data/frontend/src/shaders/post-effects.js +52 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +268 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/text-renderer.js +112 -11
- data/frontend/src/websocket-client.js +12 -1
- data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
- data/lib/vizcore/analysis/beat_detector.rb +4 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
- data/lib/vizcore/analysis/feature_recorder.rb +159 -0
- data/lib/vizcore/analysis/feature_replay.rb +84 -0
- data/lib/vizcore/analysis/pipeline.rb +235 -11
- data/lib/vizcore/analysis/tap_tempo.rb +74 -0
- data/lib/vizcore/analysis.rb +4 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
- data/lib/vizcore/audio/fixture_input.rb +65 -0
- data/lib/vizcore/audio/input_manager.rb +4 -2
- data/lib/vizcore/audio/mic_input.rb +24 -8
- data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/doctor.rb +159 -0
- data/lib/vizcore/cli/dsl_reference.rb +99 -0
- data/lib/vizcore/cli/layer_docs.rb +46 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
- data/lib/vizcore/cli/scene_inspector.rb +136 -0
- data/lib/vizcore/cli/scene_validator.rb +245 -0
- data/lib/vizcore/cli/shader_template.rb +68 -0
- data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
- data/lib/vizcore/cli.rb +689 -18
- data/lib/vizcore/config.rb +103 -2
- data/lib/vizcore/control_preset.rb +68 -0
- data/lib/vizcore/dsl/engine.rb +277 -5
- data/lib/vizcore/dsl/layer_builder.rb +491 -22
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +132 -3
- data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
- data/lib/vizcore/dsl/reaction_builder.rb +44 -0
- data/lib/vizcore/dsl/scene_builder.rb +61 -5
- data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
- data/lib/vizcore/dsl/style_builder.rb +68 -0
- data/lib/vizcore/dsl/timeline_builder.rb +138 -0
- data/lib/vizcore/dsl/transition_controller.rb +77 -0
- data/lib/vizcore/dsl.rb +5 -1
- data/lib/vizcore/layer_catalog.rb +273 -0
- data/lib/vizcore/project_manifest.rb +152 -0
- data/lib/vizcore/renderer/png_writer.rb +57 -0
- data/lib/vizcore/renderer/render_sequence.rb +153 -0
- data/lib/vizcore/renderer/scene_frame_source.rb +119 -0
- data/lib/vizcore/renderer/scene_serializer.rb +36 -3
- data/lib/vizcore/renderer/snapshot.rb +38 -0
- data/lib/vizcore/renderer/snapshot_renderer.rb +446 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +91 -5
- data/lib/vizcore/server/gallery_app.rb +155 -0
- data/lib/vizcore/server/gallery_page.rb +100 -0
- data/lib/vizcore/server/gallery_runner.rb +48 -0
- data/lib/vizcore/server/rack_app.rb +203 -4
- data/lib/vizcore/server/runner.rb +370 -22
- data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
- data/lib/vizcore/server/websocket_handler.rb +60 -10
- data/lib/vizcore/server.rb +4 -0
- data/lib/vizcore/sync/osc_message.rb +103 -0
- data/lib/vizcore/sync/osc_receiver.rb +68 -0
- data/lib/vizcore/sync.rb +4 -0
- data/lib/vizcore/templates/midi_control_scene.rb +3 -1
- data/lib/vizcore/templates/plugin_layer.rb +20 -0
- data/lib/vizcore/templates/plugin_readme.md +23 -0
- data/lib/vizcore/templates/plugin_renderer.js +43 -0
- data/lib/vizcore/templates/plugin_scene.rb +14 -0
- data/lib/vizcore/templates/project_readme.md +7 -23
- data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +27 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +362 -0
- metadata +83 -3
- 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
|
-
|
|
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
|
|
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 ||
|
|
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 =
|
|
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
|
|
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
|