Class: Vizcore::Server::FrameBroadcaster
- Inherits:
-
Object
- Object
- Vizcore::Server::FrameBroadcaster
- 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
-
#last_error ⇒ Object
readonly
Returns the value of attribute last_error.
Instance Method Summary collapse
-
#build_frame(elapsed_seconds, samples = nil) ⇒ Hash
Build one frame payload for transport to frontend.
-
#current_scene_snapshot ⇒ Hash
Current scene snapshot (‘name`, `layers`).
-
#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, initial_timeline_entry: nil, noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES, error_reporter: nil) ⇒ FrameBroadcaster
constructor
A new instance of FrameBroadcaster.
-
#lock_bpm(bpm) ⇒ Float?
Lock analysis BPM from an external sync source.
- #running? ⇒ Boolean
-
#runtime_status ⇒ Hash
Runtime health details for control/status endpoints.
- #set_custom_shape_param(layer_name:, custom_shape_index:, param:, value:) ⇒ Object
- #set_layer_param(layer_name:, param:, value:) ⇒ Object
- #start ⇒ void
- #stop ⇒ void
-
#sync_transport(playing:, position_seconds:) ⇒ void
Synchronize external playback transport (e.g. browser audio element) with the input source.
-
#tap_tempo(timestamp_ms:) ⇒ Float?
Apply a manual tap tempo event and lock analysis BPM when enough taps exist.
-
#tick(elapsed_seconds, samples = nil) ⇒ Hash
Run one frame tick and broadcast it.
-
#unlock_bpm ⇒ Boolean
Unlock analysis BPM after an external sync lock.
-
#update_analysis_settings(audio_normalize:, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES) ⇒ void
Replace audio analysis settings after scene hot reload.
-
#update_scene(scene_name:, scene_layers:) ⇒ void
Replace active scene and layers.
-
#update_transition_definition(scenes:, transitions:) ⇒ void
Replace transition catalog used by automatic scene switching.
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, initial_timeline_entry: nil, noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES, error_reporter: nil) ⇒ FrameBroadcaster
Returns a new instance of FrameBroadcaster.
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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 37 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, initial_timeline_entry: nil, noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES, error_reporter: nil ) @scene_name = scene_name.to_s @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, onset_sensitivity: onset_sensitivity, fft_preview_bins: fft_preview_bins, peak_hold_frames: peak_hold_frames, silence_reset_frames: silence_reset_frames ) @mapping_resolver = mapping_resolver || Vizcore::DSL::MappingResolver.new @scene_serializer = scene_serializer || Vizcore::Renderer::SceneSerializer.new @error_reporter = error_reporter || ->() {} @transition_controller = transition_controller || Vizcore::DSL::TransitionController.new( scenes: scene_catalog || [], transitions: transitions || [], error_reporter: lambda do || @error_reporter.call() ( , context: "transition trigger failed", source: "transition", event: "transition_failed" ) end ) @last_error = nil @frame_count = 0 @last_frame_metrics = {} @scene_version = 1 @last_sent_scene_version = nil @last_sent_scene_payload = nil @connected_client_count = 0 @custom_shape_param_overrides = {} @layer_param_overrides = {} @custom_shape_param_mutex = Mutex.new @transport_reference_position = nil @transport_reference_wall_seconds = nil @transport_drift_seconds = 0.0 @transport_drift_threshold_seconds = 0.08 @transport_playing = reset_transition_trigger_counters! apply_initial_timeline_entry(initial_timeline_entry) @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_error ⇒ Object (readonly)
Returns the value of attribute last_error.
115 116 117 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 115 def last_error @last_error end |
Instance Method Details
#build_frame(elapsed_seconds, samples = nil) ⇒ Hash
Build one frame payload for transport to frontend.
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 324 def build_frame(elapsed_seconds, samples = nil) started_at_ms = monotonic_ms apply_transport_drift_correction if file_transport_source? audio_samples, audio_capture_ms = capture_or_use_samples(samples) sync_last_scene_state_with_connections 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) } frame = @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 } ) frame[:scene_version] = scene[:version] full_scene = deep_dup(frame[:scene]) send_full_scene = scene[:version] != @last_sent_scene_version if send_full_scene full_scene[:version] = scene[:version] frame[:scene] = full_scene @last_sent_scene_payload = deep_dup(full_scene) @last_sent_scene_version = scene[:version] else patch = scene_delta(previous: @last_sent_scene_payload, current: full_scene, scene_name: scene[:name], scene_version: scene[:version]) if patch frame[:scene] = patch else frame.delete(:scene) end end @last_sent_scene_payload = deep_dup(full_scene) if scene[:version] == @last_sent_scene_version @last_frame_metrics = frame[:metrics] || {} frame rescue StandardError => e report_error(e, context: "frame build failed") raise Vizcore::FrameBuildError, Vizcore::ErrorFormatting.summarize(e, context: "Frame build failed") end |
#current_scene_snapshot ⇒ Hash
Returns current scene snapshot (‘name`, `layers`).
143 144 145 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 143 def current_scene_snapshot current_scene end |
#lock_bpm(bpm) ⇒ Float?
Lock analysis BPM from an external sync source.
270 271 272 273 274 275 276 277 278 279 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 270 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
138 139 140 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 138 def running? @frame_scheduler.running? end |
#runtime_status ⇒ Hash
Returns runtime health details for control/status endpoints.
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 148 def runtime_status scene = current_scene { current_scene: scene[:name].to_s, scene_version: @scene_version, fps: FRAME_RATE, frame_id: @frame_count, sample_rate: input_manager_value(:sample_rate), frame_size: input_manager_value(:frame_size), input: input_manager_status, transport_playing: @scene_mutex.synchronize { @transport_playing }, transport_drift: transport_drift_status, websocket_clients: WebSocketHandler.connection_count, dropped_frames: WebSocketHandler.dropped_frame_count, websocket_backpressure: WebSocketHandler.backpressure_status, last_error: formatted_last_error, metrics: deep_dup(@last_frame_metrics) }.compact end |
#set_custom_shape_param(layer_name:, custom_shape_index:, param:, value:) ⇒ Object
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 289 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 |
#set_layer_param(layer_name:, param:, value:) ⇒ Object
306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 306 def set_layer_param(layer_name:, param:, value:) layer_key = layer_name.to_s param_key = param.to_s.tr("/", ".").strip return layer_param_overrides_snapshot if layer_key.empty? || param_key.empty? @custom_shape_param_mutex.synchronize do @layer_param_overrides[layer_key] ||= {} @layer_param_overrides[layer_key][param_key] = value deep_dup(@layer_param_overrides) end end |
#start ⇒ void
This method returns an undefined value.
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 118 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 |
#stop ⇒ void
This method returns an undefined value.
130 131 132 133 134 135 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 130 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.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 173 def sync_transport(playing:, position_seconds:) position = finite_float(position_seconds) @scene_mutex.synchronize do @transport_playing = !! reset_transition_trigger_counters! if transport_position_reset?(position) if file_transport_source? @transport_reference_position = position @transport_reference_wall_seconds = wall_clock_seconds end end return unless @input_manager.respond_to?(:sync_transport) @input_manager.sync_transport(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.
257 258 259 260 261 262 263 264 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 257 def tap_tempo(timestamp_ms:) bpm = @tap_tempo.tap(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.
195 196 197 198 199 200 201 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 195 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, elapsed_seconds: elapsed_seconds) frame end |
#unlock_bpm ⇒ Boolean
Unlock analysis BPM after an external sync lock.
284 285 286 287 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 284 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, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES) ⇒ void
This method returns an undefined value.
Replace audio analysis settings after scene hot reload.
242 243 244 245 246 247 248 249 250 251 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 242 def update_analysis_settings(audio_normalize:, bpm: nil, bpm_lock: false, onset_sensitivity: 1.0, fft_preview_bins: Vizcore::Analysis::Pipeline::DEFAULT_FFT_PREVIEW_BINS, peak_hold_frames: 0, silence_reset_frames: Vizcore::Analysis::Pipeline::SILENCE_RESET_FRAMES) 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=) @analysis_pipeline.onset_sensitivity = onset_sensitivity if @analysis_pipeline.respond_to?(:onset_sensitivity=) @analysis_pipeline.fft_preview_bins = fft_preview_bins if @analysis_pipeline.respond_to?(:fft_preview_bins=) @analysis_pipeline.peak_hold_frames = peak_hold_frames if @analysis_pipeline.respond_to?(:peak_hold_frames=) @analysis_pipeline.silence_reset_frames = silence_reset_frames if @analysis_pipeline.respond_to?(:silence_reset_frames=) end |
#update_scene(scene_name:, scene_layers:) ⇒ void
This method returns an undefined value.
Replace active scene and layers.
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 208 def update_scene(scene_name:, scene_layers:) @scene_mutex.synchronize do next_scene_name = scene_name.to_s next_scene_layers = Array(scene_layers) same_scene = @scene_name == next_scene_name && deep_layers_equal?(@scene_layers, next_scene_layers) @scene_name = next_scene_name @scene_layers = next_scene_layers @scene_version += 1 unless same_scene @last_sent_scene_version = nil unless same_scene @last_sent_scene_payload = nil unless same_scene @mapping_resolver.reset! if @mapping_resolver.respond_to?(:reset!) 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.
230 231 232 233 234 |
# File 'lib/vizcore/server/frame_broadcaster.rb', line 230 def update_transition_definition(scenes:, transitions:) @scene_mutex.synchronize do @transition_controller.update(scenes: scenes, transitions: transitions) end end |