Class: Vizcore::DSL::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/vizcore/dsl/engine.rb

Overview

Evaluates and stores scene definitions built with the Vizcore Ruby DSL.

Defined Under Namespace

Classes: KeyBindingBuilder, TransitionBuilder

Constant Summary collapse

THREAD_KEY =

Thread-local key used when evaluating scene files.

:vizcore_current_dsl_engine

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeEngine

Returns a new instance of Engine.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/vizcore/dsl/engine.rb', line 72

def initialize
  @audio_inputs = []
  @midi_inputs = []
  @scenes = []
  @transitions = []
  @midi_mappings = []
  @key_mappings = []
  @global_params = {}
  @mapping_presets = {}
  @analysis_settings = {}
  @section_tail = nil
  @timelines = []
  @styles = {}
  @themes = {}
  @scene_registry = {}
  @strict = false
  @seed = nil
end

Class Method Details

.currentVizcore::DSL::Engine?

Returns current thread-local DSL engine.

Returns:



57
58
59
# File 'lib/vizcore/dsl/engine.rb', line 57

def current
  Thread.current[THREAD_KEY]
end

.define { ... } ⇒ Hash

Evaluate a DSL block using the current thread-local engine, or a new engine.

Yields:

  • Scene/audio/midi DSL configuration block

Returns:

  • (Hash)

    serialized DSL definition



22
23
24
25
# File 'lib/vizcore/dsl/engine.rb', line 22

def define(&block)
  engine = current || new
  engine.evaluate(&block)
end

.load_file(path) ⇒ Hash

Load and evaluate a scene file.

Parameters:

  • path (String, Pathname)

    scene file path

Returns:

  • (Hash)

    serialized DSL definition

Raises:

  • (ArgumentError)

    when the scene file does not exist



32
33
34
35
36
37
38
39
# File 'lib/vizcore/dsl/engine.rb', line 32

def load_file(path)
  scene_path = Pathname.new(path.to_s).expand_path
  raise ArgumentError, "Scene file not found: #{scene_path}" unless scene_path.file?

  engine = new
  with_current(engine) { Kernel.load(scene_path.to_s) }
  engine.result
end

.watch_file(path, poll_interval: FileWatcher::DEFAULT_POLL_INTERVAL, listener_factory: nil) {|definition, changed_path| ... } ⇒ Vizcore::DSL::FileWatcher

Build a file watcher that reloads and yields definitions on change.

Parameters:

  • path (String, Pathname)

    scene file path to watch

  • poll_interval (Float) (defaults to: FileWatcher::DEFAULT_POLL_INTERVAL)

    watcher poll interval in seconds

  • listener_factory (#call, nil) (defaults to: nil)

    optional listener factory for tests

Yield Parameters:

  • definition (Hash)

    reloaded DSL definition

  • changed_path (Pathname)

    path reported by the watcher

Returns:



49
50
51
52
53
54
# File 'lib/vizcore/dsl/engine.rb', line 49

def watch_file(path, poll_interval: FileWatcher::DEFAULT_POLL_INTERVAL, listener_factory: nil, &on_change)
  FileWatcher.new(path: path, poll_interval: poll_interval, listener_factory: listener_factory) do |changed_path|
    definition = load_file(changed_path.to_s)
    on_change&.call(definition, changed_path)
  end
end

Instance Method Details

#audio(name, **options) ⇒ void

This method returns an undefined value.

Register an audio input definition.

Parameters:

  • name (Symbol, String)

    input name

  • options (Hash)

    input options



105
106
107
# File 'lib/vizcore/dsl/engine.rb', line 105

def audio(name, **options)
  @audio_inputs << { name: name.to_sym, options: symbolize_keys(options) }
end

#audio_analysis(**options) ⇒ Hash

Configure analysis feature extraction behavior.

Parameters:

  • options (Hash)

    optional onset/FFT/silence/peak-hold settings

Returns:

  • (Hash)

    normalized analysis settings



182
183
184
185
# File 'lib/vizcore/dsl/engine.rb', line 182

def audio_analysis(**options)
  settings = normalize_audio_analysis(options)
  @analysis_settings.merge!(settings)
end

#audio_normalize(mode: :adaptive, **options) ⇒ Hash

Configure analysis-level audio feature normalization.

Parameters:

  • mode (Symbol, String) (defaults to: :adaptive)

    ‘:off` or `:adaptive`

  • options (Hash)

    optional ‘window`, `target`, and `floor` values

Returns:

  • (Hash)

    normalized audio normalization settings



173
174
175
176
# File 'lib/vizcore/dsl/engine.rb', line 173

def audio_normalize(mode: :adaptive, **options)
  settings = normalize_audio_normalize(mode: mode, **options)
  @analysis_settings[:audio_normalize] = settings
end

#bpm(value) ⇒ Float

Set a fixed BPM value for analysis output.

Parameters:

  • value (Numeric)

Returns:

  • (Float)


191
192
193
# File 'lib/vizcore/dsl/engine.rb', line 191

def bpm(value)
  @analysis_settings[:bpm] = positive_float(value, "bpm")
end

#bpm_lock(value = true) ⇒ Boolean

Enable or disable fixed BPM output.

Parameters:

  • value (Boolean) (defaults to: true)

Returns:

  • (Boolean)


199
200
201
# File 'lib/vizcore/dsl/engine.rb', line 199

def bpm_lock(value = true)
  @analysis_settings[:bpm_lock] = !!value
end

#evaluate { ... } ⇒ Hash

Evaluate DSL methods on this engine instance.

Yields:

  • DSL configuration block

Returns:

  • (Hash)

    serialized DSL definition



95
96
97
98
# File 'lib/vizcore/dsl/engine.rb', line 95

def evaluate(&block)
  instance_eval(&block) if block
  result
end

#key(value) { ... } ⇒ void

This method returns an undefined value.

Register a browser keyboard shortcut for runtime controls.

Parameters:

  • value (Symbol, String)

    browser KeyboardEvent key value

Yields:

  • Action block (‘switch_scene`, `blackout`, or `freeze`)

Raises:

  • (ArgumentError)

    when the key or action is missing



328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/vizcore/dsl/engine.rb', line 328

def key(value, &block)
  binding_key = normalize_keyboard_key(value)
  builder = KeyBindingBuilder.new
  builder.instance_eval(&block) if block
  action = builder.to_h
  raise ArgumentError, "key #{binding_key.inspect} requires an action" if action.empty?

  @key_mappings << {
    key: binding_key,
    action: action
  }
end

#mapping(name) { ... } ⇒ void

This method returns an undefined value.

Register reusable mapping behavior for layer-level targets.

Parameters:

  • name (Symbol, String)

    mapping preset identifier

Yields:

  • Mapping preset block



136
137
138
139
140
# File 'lib/vizcore/dsl/engine.rb', line 136

def mapping(name, &block)
  builder = MappingPresetBuilder.new(name: name, strict: @strict)
  preset_definition = builder.evaluate(&block).to_h
  @mapping_presets[preset_definition[:name]] = deep_dup(preset_definition[:mappings])
end

#midi(name, **options) ⇒ void

This method returns an undefined value.

Register a MIDI input definition.

Parameters:

  • name (Symbol, String)

    input name

  • options (Hash)

    input options



147
148
149
# File 'lib/vizcore/dsl/engine.rb', line 147

def midi(name, **options)
  @midi_inputs << { name: name.to_sym, options: symbolize_keys(options) }
end

#midi_map(note: nil, cc: nil, pc: nil, channel: nil, relative: false, deadband: nil, smooth: nil, pickup: nil, allow_multiple: false) { ... } ⇒ void

This method returns an undefined value.

Register a MIDI trigger/action mapping.

Parameters:

  • note (Integer, nil) (defaults to: nil)

    note number trigger

  • cc (Integer, nil) (defaults to: nil)

    control-change trigger

  • pc (Integer, nil) (defaults to: nil)

    program-change trigger

  • channel (Integer, nil) (defaults to: nil)

    optional MIDI channel condition (1..16; 0 aliases channel 1)

  • relative (Boolean) (defaults to: false)

    true when CC values should be treated as relative encoder deltas

  • deadband (Numeric, nil) (defaults to: nil)

    minimum CC value change required to emit an action

  • smooth (Numeric, Boolean, nil) (defaults to: nil)

    optional CC smoothing alpha

  • pickup (Boolean, nil) (defaults to: nil)

    when true, waits for CC to reach local pickup point before emitting updates

Yields:

  • Action block executed by midi runtime

Raises:

  • (ArgumentError)

    when no trigger is supplied



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/vizcore/dsl/engine.rb', line 303

def midi_map(note: nil, cc: nil, pc: nil, channel: nil, relative: false, deadband: nil, smooth: nil, pickup: nil, allow_multiple: false, &block)
  trigger = {}
  trigger[:note] = Integer(note) unless note.nil?
  trigger[:cc] = Integer(cc) unless cc.nil?
  trigger[:pc] = Integer(pc) unless pc.nil?
  raise ArgumentError, "midi_map requires note, cc or pc" if trigger.empty?
  trigger[:channel] = normalize_midi_channel(channel) unless channel.nil?
  trigger[:relative] = true if relative && trigger.key?(:cc)
  trigger[:deadband] = non_negative_float(deadband, "midi deadband") unless deadband.nil?
  trigger[:smooth] = normalize_midi_smooth(smooth) unless smooth.nil? || smooth == false
  trigger[:pickup] = pickup if trigger.key?(:cc) && trigger[:cc].between?(0, 127) && !!pickup
  trigger[:allow_multiple] = !!allow_multiple

  @midi_mappings << {
    trigger: trigger,
    action: block
  }
end

#resultHash

Returns deep-copied definition payload for renderer/runtime.

Returns:

  • (Hash)

    deep-copied definition payload for renderer/runtime.



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/vizcore/dsl/engine.rb', line 351

def result
  append_pending_section_transition
  definition = {
    audio: @audio_inputs.map { |item| deep_dup(item) },
    midi: @midi_inputs.map { |item| deep_dup(item) },
    scenes: @scenes.map { |scene| deep_dup(scene) },
    transitions: @transitions.map { |transition| deep_dup(transition) },
    midi_maps: @midi_mappings.map { |mapping| deep_dup(mapping) },
    key_mappings: @key_mappings.map { |mapping| deep_dup(mapping) },
    mapping_presets: @mapping_presets.map { |name, mappings| { name: name, mappings: deep_dup(mappings) } },
    globals: deep_dup(@global_params),
    analysis: deep_dup(@analysis_settings),
    styles: @styles.map { |name, params| { name: name, params: deep_dup(params) } },
    themes: @themes.map { |name, params| { name: name, params: deep_dup(params) } }
  }
  definition[:strict] = true if @strict
  definition[:seed] = @seed unless @seed.nil?
  definition[:timelines] = @timelines.map { |timeline| deep_dup(timeline) } unless @timelines.empty?
  definition
end

#scene(name, extends: nil) { ... } ⇒ void

This method returns an undefined value.

Define a scene and its layers.

Parameters:

  • name (Symbol, String)

    scene identifier

  • extends (Symbol, String, nil) (defaults to: nil)

    optional base scene to copy layers from

Yields:

  • Scene definition block



220
221
222
223
224
225
226
# File 'lib/vizcore/dsl/engine.rb', line 220

def scene(name, extends: nil, &block)
  builder = SceneBuilder.new(name: name, styles: @styles, themes: @themes, mapping_presets: @mapping_presets, layers: inherited_layers(extends), strict: @strict)
  builder.evaluate(&block)
  scene_definition = builder.to_h
  @scenes << scene_definition
  @scene_registry[scene_definition[:name]] = deep_dup(scene_definition)
end

#section(name, bars:, beats_per_bar: 4, loop: false, hold: 0, outro: false) { ... } ⇒ void

This method returns an undefined value.

Define a beat-counted song section as a scene and auto-transition to the following section.

Parameters:

  • name (Symbol, String)

    scene/section identifier

  • bars (Integer)

    section duration in bars

  • beats_per_bar (Integer) (defaults to: 4)

    meter used to convert bars into beats

  • loop (Boolean) (defaults to: false)

    whether the section should loop to itself

  • hold (Numeric) (defaults to: 0)

    optional additional beats to wait before transitioning

  • outro (Boolean) (defaults to: false)

    whether to skip auto-transitioning to the next section

Yields:

  • Scene definition block



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/vizcore/dsl/engine.rb', line 239

def section(name, bars:, beats_per_bar: 4, loop: false, hold: 0, outro: false, &block)
  section_name = name.to_sym
  section_beats = positive_integer(bars, "section bars") * positive_integer(beats_per_bar, "beats_per_bar")
  normalized_hold = non_negative_float(hold, "section hold")
  is_loop = !!loop
  is_outro = !!outro
  if is_loop && is_outro
    raise ArgumentError, "section cannot be both loop and outro"
  end

  scene(section_name, &block)
  add_section_transition(to: section_name) if @section_tail
  @section_tail = {
    name: section_name,
    beats: section_beats,
    hold: normalized_hold,
    loop: is_loop,
    outro: is_outro
  }
end

#seed(value) ⇒ Integer

Set a deterministic Ruby random seed for offline rendering.

Parameters:

  • value (Integer)

Returns:

  • (Integer)


162
163
164
165
166
# File 'lib/vizcore/dsl/engine.rb', line 162

def seed(value)
  @seed = Integer(value)
rescue ArgumentError, TypeError
  raise ArgumentError, "seed must be an integer"
end

#set(key, value) ⇒ Object

Set a mutable global value shared with scene/runtime logic.

Parameters:

  • key (Symbol, String)

    global key

  • value (Object)

    global value

Returns:

  • (Object)

    assigned value



346
347
348
# File 'lib/vizcore/dsl/engine.rb', line 346

def set(key, value)
  @global_params[key.to_sym] = value
end

#strict!Boolean

Enable strict DSL validation while the file is evaluated.

Returns:

  • (Boolean)


154
155
156
# File 'lib/vizcore/dsl/engine.rb', line 154

def strict!
  @strict = true
end

#style(name) { ... } ⇒ void

This method returns an undefined value.

Register a reusable layer parameter style.

Parameters:

  • name (Symbol, String)

    style identifier

Yields:

  • Style parameter block



114
115
116
117
118
# File 'lib/vizcore/dsl/engine.rb', line 114

def style(name, &block)
  builder = StyleBuilder.new(name: name)
  style_definition = builder.evaluate(&block).to_h
  @styles[style_definition[:name]] = deep_dup(style_definition[:params])
end

#tap_tempo(key: :t) ⇒ Hash

Enable browser keyboard tap tempo.

Parameters:

  • key (Symbol, String) (defaults to: :t)

    key that should send tap tempo events

Returns:

  • (Hash)

Raises:

  • (ArgumentError)


207
208
209
210
211
212
# File 'lib/vizcore/dsl/engine.rb', line 207

def tap_tempo(key: :t)
  normalized_key = key.to_s.strip.downcase
  raise ArgumentError, "tap_tempo key must not be empty" if normalized_key.empty?

  @analysis_settings[:tap_tempo] = { key: normalized_key }
end

#theme(name) { ... } ⇒ void

This method returns an undefined value.

Register a reusable scene-wide layer parameter theme.

Parameters:

  • name (Symbol, String)

    theme identifier

Yields:

  • Theme parameter block



125
126
127
128
129
# File 'lib/vizcore/dsl/engine.rb', line 125

def theme(name, &block)
  builder = StyleBuilder.new(name: name, kind: "theme")
  theme_definition = builder.evaluate(&block).to_h
  @themes[theme_definition[:name]] = deep_dup(theme_definition[:params])
end

#timeline(beats_per_bar: TimelineBuilder::DEFAULT_BEATS_PER_BAR) { ... } ⇒ void

This method returns an undefined value.

Define ordered scene markers and derive transitions between them.

Parameters:

  • beats_per_bar (Integer) (defaults to: TimelineBuilder::DEFAULT_BEATS_PER_BAR)

    meter used by timeline ‘bars(…)` markers

Yields:

  • Timeline marker block

Raises:

  • (ArgumentError)


265
266
267
268
269
270
271
272
# File 'lib/vizcore/dsl/engine.rb', line 265

def timeline(beats_per_bar: TimelineBuilder::DEFAULT_BEATS_PER_BAR, &block)
  raise ArgumentError, "timeline requires a block" unless block

  builder = TimelineBuilder.new(beats_per_bar: beats_per_bar, bpm: @analysis_settings[:bpm]).evaluate(&block)
  entries = builder.to_h
  @timelines << entries unless entries.empty?
  @transitions.concat(builder.transitions)
end

#transition(from:, to:) { ... } ⇒ void

This method returns an undefined value.

Define a transition between scenes.

Parameters:

  • from (Symbol, String)

    source scene name

  • to (Symbol, String)

    target scene name

Yields:

  • Optional transition block (‘effect`, `trigger`)



280
281
282
283
284
285
286
287
288
# File 'lib/vizcore/dsl/engine.rb', line 280

def transition(from:, to:, &block)
  definition = {
    from: from.to_sym,
    to: to.to_sym
  }
  builder = TransitionBuilder.new
  builder.instance_eval(&block) if block
  @transitions << definition.merge(builder.to_h)
end