MayaFlux 0.2.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Node.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "GpuContext.hpp"
4#include "NodeOperators.hpp"
5
6/**
7 * @namespace MayaFlux::Nodes
8 * @brief Contains the node-based computational processing system components
9 *
10 * The Nodes namespace provides a flexible, composable processing architecture
11 * based on the concept of interconnected computational nodes. Each node represents a
12 * discrete transformation unit that can be connected to other nodes to form
13 * complex processing networks and computational graphs.
14 */
15namespace MayaFlux::Nodes {
16
17/**
18 * @class NodeContext
19 * @brief Base context class for node callbacks
20 *
21 * Provides basic context information for callbacks and can be
22 * extended by specific node types to include additional context.
23 * The NodeContext serves as a container for node state information
24 * that is passed to callback functions, allowing callbacks to
25 * access relevant node data during execution.
26 *
27 * Node implementations can extend this class to provide type-specific
28 * context information, which can be safely accessed using the as<T>() method.
29 */
30class MAYAFLUX_API NodeContext {
31public:
32 virtual ~NodeContext() = default;
33
34 /**
35 * @brief Current sample value
36 *
37 * The most recent output value produced by the node.
38 * This is the primary data point that callbacks will typically use.
39 */
40 double value;
41
42 /**
43 * @brief Type identifier for runtime type checking
44 *
45 * String identifier that represents the concrete type of the context.
46 * Used for safe type casting with the as<T>() method.
47 */
48 std::string type_id;
49
50 /**
51 * @brief Safely cast to a derived context type
52 * @tparam T The derived context type to cast to
53 * @return Pointer to the derived context or nullptr if types don't match
54 *
55 * Provides type-safe access to derived context classes. If the requested
56 * type matches the actual type of this context, returns a properly cast
57 * pointer. Otherwise, returns nullptr to prevent unsafe access.
58 *
59 * Example:
60 * ```cpp
61 * if (auto filter_ctx = ctx.as<FilterContext>()) {
62 * // Access filter-specific context members
63 * double gain = filter_ctx->gain;
64 * }
65 * ```
66 */
67 template <typename T>
68 T* as()
69 {
70 if (typeid(T).name() == type_id) {
71 return static_cast<T*>(this);
72 }
73 return nullptr;
74 }
75
76protected:
77 /**
78 * @brief Protected constructor for NodeContext
79 * @param value The current sample value
80 * @param type String identifier for the context type
81 *
82 * This constructor is protected to ensure that only derived classes
83 * can create context objects, with proper type identification.
84 */
85 NodeContext(double value, std::string type)
86 : value(value)
87 , type_id(std::move(type))
88 {
89 }
90};
91
92/**
93 * @class Node
94 * @brief Base interface for all computational processing nodes
95 *
96 * The Node class defines the fundamental interface for all processing components
97 * in the MayaFlux engine. Nodes are the basic building blocks of transformation chains
98 * and can be connected together to create complex computational graphs.
99 *
100 * Each node processes data on a sample-by-sample basis, allowing for flexible
101 * real-time processing. Nodes can be:
102 * - Connected in series (output of one feeding into input of another)
103 * - Combined in parallel (outputs mixed together)
104 * - Multiplied (outputs multiplied together)
105 *
106 * The node system supports both single-sample processing for real-time applications
107 * and batch processing for more efficient offline processing.
108 */
109class MAYAFLUX_API Node {
110
111public:
112 /**
113 * @brief Virtual destructor for proper cleanup of derived classes
114 */
115 virtual ~Node() = default;
116
117 /**
118 * @brief Processes a single data sample
119 * @param input The input sample value
120 * @return The processed output sample value
121 *
122 * This is the core processing method that all nodes must implement.
123 * It takes a single input value, applies the node's transformation algorithm,
124 * and returns the resulting output value.
125 *
126 * For generator nodes that don't require input (like oscillators or stochastic generators),
127 * the input parameter may be ignored.
128 * Note: This method does NOT mark the node as processed. That responsibility
129 * belongs to the caller, typically a chained parent node or the root node.
130 */
131 virtual double process_sample(double input = 0.) = 0;
132
133 /**
134 * @brief Processes multiple samples at once
135 * @param num_samples Number of samples to process
136 * @return Vector containing the processed samples
137 *
138 * This method provides batch processing capability for more efficient
139 * processing of multiple samples. The default implementation typically
140 * calls process_sample() for each sample, but specialized nodes can
141 * override this with more optimized batch processing algorithms.
142 */
143 virtual std::vector<double> process_batch(unsigned int num_samples) = 0;
144
145 /**
146 * @brief Registers a callback to be called on each tick
147 * @param callback Function to call with the current node context
148 *
149 * Registers a callback function that will be called each time the node
150 * produces a new output value. The callback receives a NodeContext object
151 * containing information about the node's current state.
152 *
153 * This mechanism enables external components to monitor and react to
154 * the node's activity without interrupting the processing flow.
155 *
156 * Example:
157 * ```cpp
158 * node->on_tick([](NodeContext& ctx) {
159 * std::cout << "Node produced value: " << ctx.value << std::endl;
160 * });
161 * ```
162 */
163 virtual void on_tick(const NodeHook& callback);
164
165 /**
166 * @brief Registers a conditional callback
167 * @param condition Predicate that determines when callback should be triggered
168 * @param callback Function to call when condition is met
169 *
170 * Registers a callback function that will be called only when the specified
171 * condition is met. The condition is evaluated each time the node produces
172 * a new output value, and the callback is triggered only if the condition
173 * returns true.
174 *
175 * This mechanism enables selective monitoring and reaction to specific
176 * node states or events, such as threshold crossings or pattern detection.
177 *
178 * Example:
179 * ```cpp
180 * node->on_tick_if(
181 * [](NodeContext& ctx) { return ctx.value > 0.8; },
182 * [](NodeContext& ctx) { std::cout << "Threshold exceeded!" << std::endl; }
183 * );
184 * ```
185 */
186 virtual void on_tick_if(const NodeCondition& condition, const NodeHook& callback);
187
188 /**
189 * @brief Removes a previously registered callback
190 * @param callback The callback to remove
191 * @return True if the callback was found and removed, false otherwise
192 *
193 * Unregisters a callback that was previously registered with on_tick().
194 * After removal, the callback will no longer be triggered when the node
195 * produces new output values.
196 *
197 * This method is useful for cleaning up callbacks when they are no longer
198 * needed, preventing memory leaks and unnecessary processing.
199 */
200 virtual bool remove_hook(const NodeHook& callback);
201
202 /**
203 * @brief Removes a previously registered conditional callback
204 * @param callback The callback part of the conditional callback to remove
205 * @return True if the callback was found and removed, false otherwise
206 *
207 * Unregisters a conditional callback that was previously registered with
208 * on_tick_if(). After removal, the callback will no longer be triggered
209 * even when its condition is met.
210 *
211 * This method is useful for cleaning up conditional callbacks when they
212 * are no longer needed, preventing memory leaks and unnecessary processing.
213 */
214 virtual bool remove_conditional_hook(const NodeCondition& callback);
215
216 /**
217 * @brief Removes all registered callbacks
218 *
219 * Unregisters all callbacks that were previously registered with on_tick()
220 * and on_tick_if(). After calling this method, no callbacks will be triggered
221 * when the node produces new output values.
222 *
223 * This method is useful for completely resetting the node's callback system,
224 * such as when repurposing a node or preparing for cleanup.
225 */
226 virtual void remove_all_hooks();
227
228 /**
229 * @brief Resets the processed state of the node and any attached input nodes
230 *
231 * This method is used by the processing system to reset the processed state
232 * of the node at the end of each processing cycle. This ensures that
233 * all nodes are marked as unprocessed before the cycle next begins, allowing
234 * the system to correctly identify which nodes need to be processed.
235 */
236 virtual void reset_processed_state();
237
238 /**
239 * @brief Retrieves the most recent output value produced by the node
240 * @return The last output sample value
241 *
242 * This method provides access to the node's most recent output without
243 * triggering additional processing. It's useful for monitoring node state,
244 * debugging, and for implementing feedback loops where a node needs to
245 * access its previous output.
246 *
247 * The returned value represents the last sample that was produced by
248 * the node's process_sample() method.
249 */
250 inline virtual double get_last_output() { return m_last_output; }
251
252 /**
253 * @brief Mark the specificed channel as a processor/user
254 * @param channel_id The ID of the channel to register
255 *
256 * This method uses a bitmask to track which channels are currently using this node.
257 * It allows the node to manage its state based on channel usage, which is important
258 * for the audio engine's processing lifecycle. When a channel registers usage,
259 * the node can adjust its processing state accordingly, such as preventing state resets
260 * within the same cycle, or use the same output for multiple channels
261 */
262 void register_channel_usage(uint32_t channel_id);
263
264 /**
265 * @brief Removes the specified channel from the usage tracking
266 * @param channel_id The ID of the channel to unregister
267 */
268 void unregister_channel_usage(uint32_t channel_id);
269
270 /**
271 * @brief Checks if the node is currently used by a specific channel
272 * @param channel_id The ID of the channel to check
273 */
274 [[nodiscard]] bool is_used_by_channel(uint32_t channel_id) const;
275
276 /**
277 * @brief Requests a reset of the processed state from a specific channel
278 * @param channel_id The ID of the channel requesting the reset
279 *
280 * This method is called by channels to signal that they have completed their
281 * processing and that the node's processed state should be reset. It uses a bitmask
282 * to track pending resets and ensures that all channels have completed before
283 * actually resetting the node's state.
284 */
285 void request_reset_from_channel(uint32_t channel_id);
286
287 /**
288 * @brief Retrieves the current bitmask of active channels using this node
289 * @return Bitmask where each bit represents an active channel
290 *
291 * This method returns the current bitmask that tracks which channels
292 * are actively using this node. Each bit in the mask corresponds to a
293 * specific channel ID, allowing the node to manage its state based on
294 * channel usage.
295 */
296 [[nodiscard]] const inline std::atomic<uint32_t>& get_channel_mask() const { return m_active_channels_mask; }
297
298 /**
299 * @brief Updates the context object with the current node state
300 * @param value The current sample value
301 *
302 * This method is responsible for updating the NodeContext object
303 * with the latest state information from the node. It is called
304 * internally whenever a new output value is produced, ensuring that
305 * the context reflects the current state of the node for use in callbacks.
306 */
307 virtual void update_context(double value) = 0;
308
309 /**
310 * @brief Retrieves the last created context object
311 * @return Reference to the last NodeContext object
312 *
313 * This method provides access to the most recent NodeContext object
314 * created by the node. This context contains information about the
315 * node's state at the time of the last output generation.
316 */
318
319 /**
320 * @brief Sets whether the node is compatible with GPU processing
321 * @param compatible True if the node supports GPU processing, false otherwise
322 */
323 void set_gpu_compatible(bool compatible) { m_gpu_compatible = compatible; }
324
325 /**
326 * @brief Checks if the node supports GPU processing
327 * @return True if the node is GPU compatible, false otherwise
328 */
329 [[nodiscard]] bool is_gpu_compatible() const { return m_gpu_compatible; }
330
331 /**
332 * @brief Provides access to the GPU data buffer
333 * @return Span of floats representing the GPU data buffer
334 * This method returns a span of floats that represents the GPU data buffer
335 * associated with this node. The buffer contains data that can be uploaded to the GPU
336 * for processing, enabling efficient execution in GPU-accelerated pipelines.
337 */
338 [[nodiscard]] std::span<const float> get_gpu_data_buffer() const;
339
340protected:
341 /**
342 * @brief Notifies all registered callbacks with the current context
343 * @param value The current sample value
344 *
345 * This method is called by the node implementation when a new output value
346 * is produced. It creates a context object using create_context(), then
347 * calls all registered callbacks with that context.
348 *
349 * For unconditional callbacks (registered with on_tick()), the callback
350 * is always called. For conditional callbacks (registered with on_tick_if()),
351 * the callback is called only if its condition returns true.
352 *
353 * Node implementations should call this method at appropriate points in their
354 * processing flow to trigger callbacks.
355 */
356 virtual void notify_tick(double value) = 0;
357
358 /**
359 * @brief Resets the processed state of the node directly
360 *
361 * Unlike reset_processed_state(), this method is called internally
362 * and does not perform any checks or state transitions.
363 */
364 virtual void reset_processed_state_internal();
365
366 /**
367 * @brief The most recent sample value generated by this oscillator
368 *
369 * This value is updated each time process_sample() is called and can be
370 * accessed via get_last_output() without triggering additional processing.
371 * It's useful for monitoring the oscillator's state and for implementing
372 * feedback loops.
373 */
374 double m_last_output { 0 };
375
376 /**
377 * @brief Flag indicating if the node supports GPU processing
378 * This flag is set by derived classes to indicate whether
379 * the node can be processed on the GPU. Nodes that support GPU
380 * processing can provide GPU-compatible context data for
381 * efficient execution in GPU-accelerated pipelines.
382 */
383 bool m_gpu_compatible {};
384
385 /**
386 * @brief GPU data buffer for context objects
387 *
388 * This buffer is used to store float data that can be uploaded
389 * to the GPU for nodes that support GPU processing. It provides
390 * a contiguous array of floats that can be bound to GPU descriptors,
391 * enabling efficient data transfer and processing on the GPU.
392 */
393 std::vector<float> m_gpu_data_buffer;
394
395 /**
396 * @brief Collection of standard callback functions
397 *
398 * Stores the registered callback functions that will be notified
399 * whenever the binary operation produces a new output value. These callbacks
400 * enable external components to monitor and react to the combined output
401 * without interrupting the processing flow.
402 */
403 std::vector<NodeHook> m_callbacks;
404
405 /**
406 * @brief Collection of conditional callback functions with their predicates
407 *
408 * Stores pairs of callback functions and their associated condition predicates.
409 * These callbacks are only invoked when their condition evaluates to true
410 * for a combined output value, enabling selective monitoring of specific
411 * conditions or patterns in the combined signal.
412 */
413 std::vector<std::pair<NodeHook, NodeCondition>> m_conditional_callbacks;
414
415 /**
416 * @brief Flag indicating if the node is part of a NodeNetwork
417 * This flag is used to disable event firing when the node is
418 * managed within a NodeNetwork, preventing redundant or conflicting
419 * event notifications.
420 */
421 bool m_networked_node {};
422
423 /**
424 @brief tracks if the node's state has been saved by a snapshot operation
425 */
426 bool m_state_saved {};
427
428public:
429 /**
430 * @brief Saves the node's current state for later restoration
431 * Recursively cascades through all connected modulator nodes
432 * Protected - only NodeSourceProcessor and NodeBuffer can call
433 */
434 virtual void save_state() = 0;
435
436 /**
437 * @brief Restores the node's state from the last save
438 * Recursively cascades through all connected modulator nodes
439 * Protected - only NodeSourceProcessor and NodeBuffer can call
440 */
441 virtual void restore_state() = 0;
442
443 /**
444 * @brief Internal flag controlling whether notify_tick fires during state snapshots
445 * Default: false (events don't fire during isolated buffer processing)
446 * Can be exposed in future if needed via concrete implementation in parent
447 */
448 bool m_fire_events_during_snapshot = false;
449
450 /**
451 * @brief Atomic state flag tracking the node's processing status
452 *
453 * This atomic state variable tracks the node's current operational status using
454 * bit flags defined in Utils::NodeState. It indicates whether the node is:
455 * - ACTIVE: Currently part of the processing graph
456 * - PROCESSED: Has been processed in the current cycle
457 * - PENDING_REMOVAL: Marked for removal from the processing graph
458 * - MOCK_PROCESS: Should be processed but output ignored
459 *
460 * The atomic nature ensures thread-safe state transitions, allowing the audio
461 * engine to safely coordinate processing across multiple threads without data races.
462 */
463 std::atomic<Utils::NodeState> m_state { Utils::NodeState::INACTIVE };
464
465 /**
466 * @brief Counter tracking how many other nodes are using this node as a modulator
467 *
468 * This counter is incremented when another node begins using this node as a
469 * modulation source, and decremented when that relationship ends. It's critical
470 * for the node lifecycle management system, as it prevents premature state resets
471 * when a node's output is still needed by downstream nodes.
472 *
473 * When this counter is non-zero, the node's processed state will not be reset
474 * automatically after processing, ensuring that all dependent nodes can access
475 * its output before it's cleared.
476 */
477 std::atomic<uint32_t> m_modulator_count { 0 };
478
479 /**
480 * @brief Attempt to claim snapshot context for this processing cycle
481 * @param context_id Unique context identifier for this buffer processing
482 * @return true if this caller claimed the context (should call save_state)
483 *
484 * This method enables multiple NodeBuffers referencing the same node to
485 * coordinate save/restore state operations. Only the first caller per
486 * processing context will claim the snapshot, preventing nested state saves.
487 */
488 bool try_claim_snapshot_context(uint64_t context_id);
489
490 /**
491 * @brief Check if currently in a snapshot context
492 * @param context_id Context to check
493 * @return true if this context is active
494 *
495 * Used by secondary callers to detect when the primary snapshot holder
496 * has completed processing and released the context.
497 */
498 [[nodiscard]] bool is_in_snapshot_context(uint64_t context_id) const;
499
500 /**
501 * @brief Release snapshot context
502 * @param context_id Context to release
503 *
504 * Called by the snapshot owner after restore_state() completes,
505 * allowing other buffers to proceed with their own snapshots.
506 */
507 void release_snapshot_context(uint64_t context_id);
508
509 /**
510 * @brief Check if node is currently being snapshotted by any context
511 * @return true if a snapshot is in progress
512 */
513 [[nodiscard]] bool has_active_snapshot() const;
514
515 /**
516 * @brief Get the active snapshot context ID
517 * @return The current active snapshot context ID, or 0 if none
518 */
519 [[nodiscard]] inline uint64_t get_active_snapshot_context() const
520 {
521 return m_snapshot_context_id.load(std::memory_order_acquire);
522 }
523
524 /**
525 * @brief Increments the buffer reference count
526 * This method is called when a new buffer starts using this node
527 * to ensure proper lifecycle management.
528 */
529 void add_buffer_reference();
530
531 /**
532 * @brief Decrements the buffer reference count
533 * This method is called when a buffer stops using this node
534 * to ensure proper lifecycle management.
535 */
536 void remove_buffer_reference();
537
538 /**
539 * @brief Marks the node as having been processed by a buffer
540 * @return true if the buffer was successfully marked as processed
541 *
542 * This method checks if the node can be marked as processed based on
543 * the current buffer count and node state. If conditions are met,
544 * it updates the processed flag and increments the reset counter.
545 */
546 bool mark_buffer_processed();
547
548 /**
549 * @brief Requests a reset of the buffer state
550 *
551 * This method is called to signal that the buffer's processed state
552 * should be reset. It increments the reset counter, which is used to
553 * determine when it's safe to clear the processed state.
554 */
555 void request_buffer_reset();
556
557 /**
558 * @brief Checks if the buffer has been processed
559 * @return true if the buffer is marked as processed
560 */
561 [[nodiscard]] inline bool is_buffer_processed() const
562 {
563 return m_buffer_processed.load(std::memory_order_acquire);
564 }
565
566 /**
567 * @brief Sets whether the node is part of a NodeNetwork
568 * @param networked True if the node is managed within a NodeNetwork
569 *
570 * This method sets a flag indicating whether the node is part of a
571 * NodeNetwork. When set, certain behaviors such as event firing
572 * may be disabled to prevent redundant or conflicting notifications.
573 */
574 [[nodiscard]] inline bool is_in_network() const { return m_networked_node; }
575
576 /**
577 * @brief Marks the node as being part of a NodeNetwork
578 * @param networked True if the node is managed within a NodeNetwork
579 *
580 * This method sets a flag indicating whether the node is part of a
581 * NodeNetwork. When set, certain behaviors such as event firing
582 * may be disabled to prevent redundant or conflicting notifications.
583 */
584 void set_in_network(bool networked) { m_networked_node = networked; }
585
586private:
587 /**
588 * @brief Bitmask tracking which channels are currently using this node
589 */
590 std::atomic<uint32_t> m_active_channels_mask { 0 };
591
592 /**
593 * @brief Bitmask tracking which channels have requested a reset
594 *
595 * This mask is used to track which channels have requested the node's processed
596 * state to be reset. When all channels that are currently using the node have
597 * requested a reset, the node can safely clear its processed state.
598 */
599 std::atomic<uint32_t> m_pending_reset_mask { 0 };
600
601 /**
602 * @brief Unique identifier for the current snapshot context
603 *
604 * This atomic variable holds the unique identifier of the current
605 * snapshot context that has claimed ownership of this node's state.
606 * It ensures that only one processing context can perform save/restore
607 * operations at a time, preventing nested snapshots and ensuring
608 * consistent state management.
609 */
610 std::atomic<uint64_t> m_snapshot_context_id { 0 };
611
612 /**
613 * @brief Counter tracking how many buffers are using this node
614 * This counter is incremented when a buffer starts using this node
615 * and decremented when the buffer stops using it. It helps manage
616 * the node's lifecycle in relation to buffer usage.
617 */
618 std::atomic<uint32_t> m_buffer_count { 0 };
619
620 /**
621 * @brief Flag indicating whether the buffer has been processed
622 * This atomic flag is set when the buffer has been successfully
623 * processed and is used to prevent redundant processing.
624 */
625 std::atomic<bool> m_buffer_processed { false };
626
627 /**
628 * @brief Counter tracking how many buffers have requested a reset
629 *
630 * When all buffers using this node have requested a reset, the node's
631 * processed state can be safely cleared. This counter helps coordinate
632 * that process.
633 */
634 std::atomic<uint32_t> m_buffer_reset_count { 0 };
635};
636}
std::string type_id
Type identifier for runtime type checking.
Definition Node.hpp:48
virtual ~NodeContext()=default
NodeContext(double value, std::string type)
Protected constructor for NodeContext.
Definition Node.hpp:85
double value
Current sample value.
Definition Node.hpp:40
T * as()
Safely cast to a derived context type.
Definition Node.hpp:68
Base context class for node callbacks.
Definition Node.hpp:30
virtual double process_sample(double input=0.)=0
Processes a single data sample.
virtual double get_last_output()
Retrieves the most recent output value produced by the node.
Definition Node.hpp:250
virtual std::vector< double > process_batch(unsigned int num_samples)=0
Processes multiple samples at once.
bool is_buffer_processed() const
Checks if the buffer has been processed.
Definition Node.hpp:561
virtual void save_state()=0
Saves the node's current state for later restoration Recursively cascades through all connected modul...
bool is_in_network() const
Sets whether the node is part of a NodeNetwork.
Definition Node.hpp:574
void set_gpu_compatible(bool compatible)
Sets whether the node is compatible with GPU processing.
Definition Node.hpp:323
std::vector< NodeHook > m_callbacks
Collection of standard callback functions.
Definition Node.hpp:403
void set_in_network(bool networked)
Marks the node as being part of a NodeNetwork.
Definition Node.hpp:584
virtual void restore_state()=0
Restores the node's state from the last save Recursively cascades through all connected modulator nod...
uint64_t get_active_snapshot_context() const
Get the active snapshot context ID.
Definition Node.hpp:519
const std::atomic< uint32_t > & get_channel_mask() const
Retrieves the current bitmask of active channels using this node.
Definition Node.hpp:296
virtual NodeContext & get_last_context()=0
Retrieves the last created context object.
virtual void notify_tick(double value)=0
Notifies all registered callbacks with the current context.
virtual void update_context(double value)=0
Updates the context object with the current node state.
bool is_gpu_compatible() const
Checks if the node supports GPU processing.
Definition Node.hpp:329
std::vector< std::pair< NodeHook, NodeCondition > > m_conditional_callbacks
Collection of conditional callback functions with their predicates.
Definition Node.hpp:413
virtual ~Node()=default
Virtual destructor for proper cleanup of derived classes.
std::vector< float > m_gpu_data_buffer
GPU data buffer for context objects.
Definition Node.hpp:393
Base interface for all computational processing nodes.
Definition Node.hpp:109
std::function< void(NodeContext &)> NodeHook
Callback function type for node processing events.
Definition NodeUtils.hpp:25
std::function< bool(NodeContext &)> NodeCondition
Predicate function type for conditional callbacks.
Definition NodeUtils.hpp:43
Contains the node-based computational processing system components.
Definition Chronie.hpp:5