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.



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

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

Class Method Details

.currentVizcore::DSL::Engine?

Returns current thread-local DSL engine.

Returns:



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

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



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

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



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

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:



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

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



101
102
103
# File 'lib/vizcore/dsl/engine.rb', line 101

def audio(name, **options)
  @audio_inputs << { name: name.to_sym, options: symbolize_keys(options) }
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



141
142
143
144
# File 'lib/vizcore/dsl/engine.rb', line 141

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)


150
151
152
# File 'lib/vizcore/dsl/engine.rb', line 150

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)


158
159
160
# File 'lib/vizcore/dsl/engine.rb', line 158

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



91
92
93
94
# File 'lib/vizcore/dsl/engine.rb', line 91

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



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

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

#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



132
133
134
# File 'lib/vizcore/dsl/engine.rb', line 132

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

#midi_map(note: nil, cc: nil, pc: nil) { ... } ⇒ 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

Yields:

  • Action block executed by midi runtime

Raises:

  • (ArgumentError)

    when no trigger is supplied



242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/vizcore/dsl/engine.rb', line 242

def midi_map(note: nil, cc: nil, pc: nil, &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?

  @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.



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/vizcore/dsl/engine.rb', line 284

def result
  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) },
    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[: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



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

def scene(name, extends: nil, &block)
  builder = SceneBuilder.new(name: name, styles: @styles, themes: @themes, layers: inherited_layers(extends))
  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) { ... } ⇒ 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

Yields:

  • Scene definition block



195
196
197
198
199
200
201
202
# File 'lib/vizcore/dsl/engine.rb', line 195

def section(name, bars:, beats_per_bar: 4, &block)
  section_name = name.to_sym
  section_beats = positive_integer(bars, "section bars") * positive_integer(beats_per_bar, "beats_per_bar")

  scene(section_name, &block)
  add_section_transition(to: section_name) if @section_tail
  @section_tail = { name: section_name, beats: section_beats }
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



279
280
281
# File 'lib/vizcore/dsl/engine.rb', line 279

def set(key, value)
  @global_params[key.to_sym] = value
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



110
111
112
113
114
# File 'lib/vizcore/dsl/engine.rb', line 110

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)


166
167
168
169
170
171
# File 'lib/vizcore/dsl/engine.rb', line 166

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



121
122
123
124
125
# File 'lib/vizcore/dsl/engine.rb', line 121

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)


209
210
211
212
213
214
215
216
# File 'lib/vizcore/dsl/engine.rb', line 209

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).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`)



224
225
226
227
228
229
230
231
232
# File 'lib/vizcore/dsl/engine.rb', line 224

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