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, noise_gate: Vizcore::Analysis::Pipeline::DEFAULT_NOISE_GATE, audio_normalize: nil, bpm: nil, bpm_lock: false, error_reporter: nil) ⇒ FrameBroadcaster
constructor
A new instance of FrameBroadcaster.
-
#lock_bpm(bpm) ⇒ Float?
Lock analysis BPM from an external sync source.
- #running? ⇒ Boolean
- #set_custom_shape_param(layer_name:, custom_shape_index:, 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) ⇒ 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, 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.
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 || ->() {} @last_error = nil @frame_count = 0 @custom_shape_param_overrides = {} @custom_shape_param_mutex = Mutex.new @transport_playing = 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_error ⇒ Object (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.
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_snapshot ⇒ Hash
Returns 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.
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
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 |
#start ⇒ void
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 |
#stop ⇒ void
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.
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 = !! reset_transition_trigger_counters! if transport_position_reset?(position_seconds) 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.
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: ) 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.
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_bpm ⇒ Boolean
Unlock analysis BPM after an external sync lock.
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.
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.
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.
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 |