Class: Vizcore::Server::FrameBroadcaster

Inherits:
Object
  • Object
show all
Defined in:
lib/vizcore/server/frame_broadcaster.rb

Overview

Produces audio-reactive frame payloads and broadcasts them over WebSocket.

Constant Summary collapse

FRAME_RATE =

Target broadcast frame rate.

60.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(scene_name: "basic", scene_layers: nil, input_manager: nil, analysis_pipeline: nil, mapping_resolver: nil, scene_serializer: nil, frame_scheduler: nil, scene_catalog: nil, transitions: nil, transition_controller: nil, noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, error_reporter: nil) ⇒ FrameBroadcaster

Returns a new instance of FrameBroadcaster.

Parameters:



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/vizcore/server/frame_broadcaster.rb', line 31

def initialize(
  scene_name: "basic",
  scene_layers: nil,
  input_manager: nil,
  analysis_pipeline: nil,
  mapping_resolver: nil,
  scene_serializer: nil,
  frame_scheduler: nil,
  scene_catalog: nil,
  transitions: nil,
  transition_controller: nil,
  noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE,
  audio_normalize: nil,
  bpm: nil,
  bpm_lock: false,
  error_reporter: nil
)
  @scene_name = scene_name
  @scene_layers = Array(scene_layers)
  @scene_mutex = Mutex.new
  @input_manager = input_manager || Vizcore::Audio::InputManager.new(source: :mic)
  fft_size = supported_fft_size(@input_manager.frame_size)
  @analysis_pipeline = analysis_pipeline || Vizcore::Analysis::Pipeline.new(
    sample_rate: @input_manager.sample_rate,
    fft_size: fft_size,
    noise_gate: noise_gate,
    audio_normalize: audio_normalize,
    bpm: bpm,
    bpm_lock: bpm_lock
  )
  @mapping_resolver = mapping_resolver || Vizcore::DSL::MappingResolver.new
  @scene_serializer = scene_serializer || Vizcore::Renderer::SceneSerializer.new
  @transition_controller = transition_controller || Vizcore::DSL::TransitionController.new(
    scenes: scene_catalog || [],
    transitions: transitions || []
  )
  @error_reporter = error_reporter || ->(_message) {}
  @last_error = nil
  @frame_count = 0
  @custom_shape_param_overrides = {}
  @custom_shape_param_mutex = Mutex.new
  @transport_playing = initial_transport_playing_state
  reset_transition_trigger_counters!
  @tap_tempo = Vizcore::Analysis::TapTempo.new
  @frame_scheduler = frame_scheduler || Vizcore::Renderer::FrameScheduler.new(frame_rate: FRAME_RATE) do |elapsed|
    tick(elapsed)
  end
end

Instance Attribute Details

#last_errorObject (readonly)

Returns the value of attribute last_error.



80
81
82
# File 'lib/vizcore/server/frame_broadcaster.rb', line 80

def last_error
  @last_error
end

Instance Method Details

#build_frame(elapsed_seconds, samples = nil) ⇒ Hash

Build one frame payload for transport to frontend.

Parameters:

  • _elapsed_seconds (Float)
  • samples (Array<Float>, nil) (defaults to: nil)

Returns:

  • (Hash)

Raises:



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/vizcore/server/frame_broadcaster.rb', line 238

def build_frame(elapsed_seconds, samples = nil)
  started_at_ms = monotonic_ms
  audio_samples, audio_capture_ms = capture_or_use_samples(samples)
  analyzed, audio_analysis_ms = measure_ms { @analysis_pipeline.call(audio_samples) }
  scene = current_scene
  layers, scene_build_ms = measure_ms { build_scene_layers(scene[:layers], analyzed, time: elapsed_seconds, frame: @frame_count) }

  @scene_serializer.audio_frame(
    timestamp: Time.now.to_f,
    audio: analyzed,
    scene_name: scene[:name],
    scene_layers: layers,
    transition: nil,
    metrics: {
      frame_id: @frame_count,
      audio_capture_ms: audio_capture_ms,
      audio_analysis_ms: audio_analysis_ms,
      scene_build_ms: scene_build_ms,
      server_frame_ms: monotonic_ms - started_at_ms
    }
  )
rescue StandardError => e
  report_error(e, context: "frame build failed")
  raise Vizcore::FrameBuildError, Vizcore::ErrorFormatting.summarize(e, context: "Frame build failed")
end

#current_scene_snapshotHash

Returns current scene snapshot (‘name`, `layers`).

Returns:

  • (Hash)

    current scene snapshot (‘name`, `layers`)



108
109
110
# File 'lib/vizcore/server/frame_broadcaster.rb', line 108

def current_scene_snapshot
  current_scene
end

#lock_bpm(bpm) ⇒ Float?

Lock analysis BPM from an external sync source.

Parameters:

  • bpm (Numeric)

Returns:

  • (Float, nil)


196
197
198
199
200
201
202
203
204
205
# File 'lib/vizcore/server/frame_broadcaster.rb', line 196

def lock_bpm(bpm)
  numeric = Float(bpm)
  return nil unless numeric.finite? && numeric.positive?
  return numeric unless @analysis_pipeline.respond_to?(:bpm_lock=)

  @analysis_pipeline.bpm_lock = { bpm: numeric, locked: true }
  numeric
rescue ArgumentError, TypeError
  nil
end

#running?Boolean

Returns:

  • (Boolean)


103
104
105
# File 'lib/vizcore/server/frame_broadcaster.rb', line 103

def running?
  @frame_scheduler.running?
end

#set_custom_shape_param(layer_name:, custom_shape_index:, param:, value:) ⇒ Object



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/vizcore/server/frame_broadcaster.rb', line 215

def set_custom_shape_param(layer_name:, custom_shape_index:, param:, value:)
  layer_key = layer_name.to_s
  param_key = param.to_s.strip
  index = Integer(custom_shape_index)
  numeric = finite_float(value)
  return custom_shape_param_overrides_snapshot if layer_key.empty? || param_key.empty? || index.negative? || numeric.nil?

  @custom_shape_param_mutex.synchronize do
    @custom_shape_param_overrides[layer_key] ||= {}
    @custom_shape_param_overrides[layer_key][index] ||= {}
    @custom_shape_param_overrides[layer_key][index][param_key] = numeric
    deep_dup(@custom_shape_param_overrides)
  end
rescue ArgumentError, TypeError
  custom_shape_param_overrides_snapshot
end

#startvoid

This method returns an undefined value.



83
84
85
86
87
88
89
90
91
92
# File 'lib/vizcore/server/frame_broadcaster.rb', line 83

def start
  return if running?

  @input_manager.start
  @frame_scheduler.start
rescue StandardError => e
  report_error(e, context: "frame broadcaster start failed")
  @input_manager.stop
  raise
end

#stopvoid

This method returns an undefined value.



95
96
97
98
99
100
# File 'lib/vizcore/server/frame_broadcaster.rb', line 95

def stop
  return unless running?

  @frame_scheduler.stop
  @input_manager.stop
end

#sync_transport(playing:, position_seconds:) ⇒ void

This method returns an undefined value.

Synchronize external playback transport (e.g. browser audio element) with the input source.

Parameters:

  • playing (Boolean)
  • position_seconds (Numeric)


117
118
119
120
121
122
123
124
125
126
127
# File 'lib/vizcore/server/frame_broadcaster.rb', line 117

def sync_transport(playing:, position_seconds:)
  @scene_mutex.synchronize do
    @transport_playing = !!playing
    reset_transition_trigger_counters! if transport_position_reset?(position_seconds)
  end
  return unless @input_manager.respond_to?(:sync_transport)

  @input_manager.sync_transport(playing: playing, position_seconds: position_seconds)
rescue StandardError => e
  report_error(e, context: "audio transport sync failed")
end

#tap_tempo(timestamp_ms:) ⇒ Float?

Apply a manual tap tempo event and lock analysis BPM when enough taps exist.

Parameters:

  • timestamp_ms (Numeric)

Returns:

  • (Float, nil)


183
184
185
186
187
188
189
190
# File 'lib/vizcore/server/frame_broadcaster.rb', line 183

def tap_tempo(timestamp_ms:)
  bpm = @tap_tempo.tap(timestamp_ms: timestamp_ms)
  return nil unless bpm
  return bpm unless @analysis_pipeline.respond_to?(:bpm_lock=)

  @analysis_pipeline.bpm_lock = { bpm: bpm, locked: true }
  bpm
end

#tick(elapsed_seconds, samples = nil) ⇒ Hash

Run one frame tick and broadcast it.

Parameters:

  • elapsed_seconds (Float)
  • samples (Array<Float>, nil) (defaults to: nil)

Returns:

  • (Hash)

    serialized frame



134
135
136
137
138
139
140
# File 'lib/vizcore/server/frame_broadcaster.rb', line 134

def tick(elapsed_seconds, samples = nil)
  @frame_count += 1
  frame = build_frame(elapsed_seconds, samples)
  WebSocketHandler.broadcast(type: "audio_frame", payload: frame)
  evaluate_transition(frame[:audio], frame_count: @frame_count)
  frame
end

#unlock_bpmBoolean

Unlock analysis BPM after an external sync lock.

Returns:

  • (Boolean)


210
211
212
213
# File 'lib/vizcore/server/frame_broadcaster.rb', line 210

def unlock_bpm
  @analysis_pipeline.bpm_lock = { bpm: nil, locked: false } if @analysis_pipeline.respond_to?(:bpm_lock=)
  true
end

#update_analysis_settings(audio_normalize:, bpm: nil, bpm_lock: false) ⇒ void

This method returns an undefined value.

Replace audio analysis settings after scene hot reload.

Parameters:

  • audio_normalize (Hash, nil)
  • bpm (Numeric, nil) (defaults to: nil)
  • bpm_lock (Boolean) (defaults to: false)


172
173
174
175
176
177
# File 'lib/vizcore/server/frame_broadcaster.rb', line 172

def update_analysis_settings(audio_normalize:, bpm: nil, bpm_lock: false)
  return unless @analysis_pipeline.respond_to?(:audio_normalize=)

  @analysis_pipeline.audio_normalize = audio_normalize
  @analysis_pipeline.bpm_lock = { bpm: bpm, locked: bpm_lock } if @analysis_pipeline.respond_to?(:bpm_lock=)
end

#update_scene(scene_name:, scene_layers:) ⇒ void

This method returns an undefined value.

Replace active scene and layers.

Parameters:

  • scene_name (String, Symbol)
  • scene_layers (Array<Hash>)


147
148
149
150
151
152
153
# File 'lib/vizcore/server/frame_broadcaster.rb', line 147

def update_scene(scene_name:, scene_layers:)
  @scene_mutex.synchronize do
    @scene_name = scene_name.to_s
    @scene_layers = Array(scene_layers)
    reset_transition_trigger_counters!
  end
end

#update_transition_definition(scenes:, transitions:) ⇒ void

This method returns an undefined value.

Replace transition catalog used by automatic scene switching.

Parameters:

  • scenes (Array<Hash>)
  • transitions (Array<Hash>)


160
161
162
163
164
# File 'lib/vizcore/server/frame_broadcaster.rb', line 160

def update_transition_definition(scenes:, transitions:)
  @scene_mutex.synchronize do
    @transition_controller.update(scenes: scenes, transitions: transitions)
  end
end