rill_core/traits/port.rs
1//! Port types and identifiers for the Rill ecosystem
2//!
3//! Ports are the connection points between nodes in the signal graph.
4//! Each output port owns a `Buffer<T, BUF_SIZE>` and an optional `Action`
5//! that defines how data is produced. Input ports are connection endpoints
6//! that receive data from upstream output ports.
7
8use crate::buffer::Buffer;
9use crate::math::Transcendental;
10use crate::time::ClockTick;
11use crate::traits::algorithm::Algorithm;
12use crate::traits::node::{SignalNode, NodeId};
13use crate::traits::processable::NodeVariant;
14use crate::traits::PortError;
15use std::fmt;
16
17// ============================================================================
18// Port Type
19// ============================================================================
20
21/// Type of a port - what kind of signal it carries
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum PortType {
24 /// Signal port - carries signal blocks (audio, sensor, etc.)
25 Signal,
26
27 /// Control signal port - carries modulation/automation
28 Control,
29
30 /// Clock signal port - carries timing information
31 Clock,
32
33 /// Feedback port - stores state between blocks
34 Feedback,
35
36 /// Parameter port - for node parameters (special)
37 Param,
38}
39
40impl PortType {
41 /// Get the name of the port type
42 pub const fn name(&self) -> &'static str {
43 match self {
44 Self::Signal => "signal",
45 Self::Control => "control",
46 Self::Clock => "clock",
47 Self::Feedback => "feedback",
48 Self::Param => "param",
49 }
50 }
51
52 /// Check if this port carries audio-rate signals
53 pub const fn is_audio_rate(&self) -> bool {
54 matches!(self, Self::Signal)
55 }
56
57 /// Check if this port carries control-rate signals
58 pub const fn is_control_rate(&self) -> bool {
59 matches!(self, Self::Control)
60 }
61
62 /// Check if this port carries clock signals
63 pub const fn is_clock(&self) -> bool {
64 matches!(self, Self::Clock)
65 }
66}
67
68impl fmt::Display for PortType {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{}", self.name())
71 }
72}
73
74// ============================================================================
75// Port Direction
76// ============================================================================
77
78/// Direction of a port (input or output)
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub enum PortDirection {
81 /// Input port (receives data into the node)
82 Input,
83
84 /// Output port (sends data out of the node)
85 Output,
86}
87
88impl PortDirection {
89 /// Get the name of the direction
90 pub const fn name(&self) -> &'static str {
91 match self {
92 Self::Input => "input",
93 Self::Output => "output",
94 }
95 }
96
97 /// Check if this is an input port
98 pub const fn is_input(&self) -> bool {
99 matches!(self, Self::Input)
100 }
101
102 /// Check if this is an output port
103 pub const fn is_output(&self) -> bool {
104 matches!(self, Self::Output)
105 }
106}
107
108impl fmt::Display for PortDirection {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "{}", self.name())
111 }
112}
113
114// ============================================================================
115// Port ID
116// ============================================================================
117
118/// Unique identifier for a port within a graph
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub struct PortId {
121 node: NodeId,
122 port_type: PortType,
123 direction: PortDirection,
124 index: u16,
125}
126
127impl PortId {
128 /// Create a new port ID
129 pub const fn new(
130 node: NodeId,
131 port_type: PortType,
132 direction: PortDirection,
133 index: u16,
134 ) -> Self {
135 Self {
136 node,
137 port_type,
138 direction,
139 index,
140 }
141 }
142
143 // ========================================================================
144 // Audio Port Constructors
145 // ========================================================================
146
147 /// Create a new signal input port
148 pub const fn audio_in(node: NodeId, index: u16) -> Self {
149 Self::new(node, PortType::Signal, PortDirection::Input, index)
150 }
151
152 /// Create a new signal output port
153 pub const fn audio_out(node: NodeId, index: u16) -> Self {
154 Self::new(node, PortType::Signal, PortDirection::Output, index)
155 }
156
157 // ========================================================================
158 // Control Port Constructors
159 // ========================================================================
160
161 /// Create a new control input port
162 pub const fn control_in(node: NodeId, index: u16) -> Self {
163 Self::new(node, PortType::Control, PortDirection::Input, index)
164 }
165
166 /// Create a new control output port
167 pub const fn control_out(node: NodeId, index: u16) -> Self {
168 Self::new(node, PortType::Control, PortDirection::Output, index)
169 }
170
171 // ========================================================================
172 // Clock Port Constructors
173 // ========================================================================
174
175 /// Create a new clock input port
176 pub const fn clock_in(node: NodeId, index: u16) -> Self {
177 Self::new(node, PortType::Clock, PortDirection::Input, index)
178 }
179
180 /// Create a new clock output port
181 pub const fn clock_out(node: NodeId, index: u16) -> Self {
182 Self::new(node, PortType::Clock, PortDirection::Output, index)
183 }
184
185 // ========================================================================
186 // Feedback Port Constructors
187 // ========================================================================
188
189 /// Create a new feedback input port
190 pub const fn feedback_in(node: NodeId, index: u16) -> Self {
191 Self::new(node, PortType::Feedback, PortDirection::Input, index)
192 }
193
194 /// Create a new feedback output port
195 pub const fn feedback_out(node: NodeId, index: u16) -> Self {
196 Self::new(node, PortType::Feedback, PortDirection::Output, index)
197 }
198
199 // ========================================================================
200 // Parameter Port Constructors
201 // ========================================================================
202
203 /// Create a new parameter port (always input)
204 pub const fn param(node: NodeId, index: u16) -> Self {
205 Self::new(node, PortType::Param, PortDirection::Input, index)
206 }
207
208 // ========================================================================
209 // Getters
210 // ========================================================================
211
212 /// Get the node ID
213 pub const fn node_id(&self) -> NodeId {
214 self.node
215 }
216
217 /// Get the port type
218 pub const fn port_type(&self) -> PortType {
219 self.port_type
220 }
221
222 /// Get the port direction
223 pub const fn direction(&self) -> PortDirection {
224 self.direction
225 }
226
227 /// Get the port index
228 pub const fn index(&self) -> u16 {
229 self.index
230 }
231
232 // ========================================================================
233 // Predicates
234 // ========================================================================
235
236 /// Check if this is an input port
237 pub const fn is_input(&self) -> bool {
238 self.direction.is_input()
239 }
240
241 /// Check if this is an output port
242 pub const fn is_output(&self) -> bool {
243 self.direction.is_output()
244 }
245
246 /// Check if this is an audio port
247 pub const fn is_audio(&self) -> bool {
248 matches!(self.port_type, PortType::Signal)
249 }
250
251 /// Check if this is a control port
252 pub const fn is_control(&self) -> bool {
253 matches!(self.port_type, PortType::Control)
254 }
255
256 /// Check if this is a clock port
257 pub const fn is_clock(&self) -> bool {
258 matches!(self.port_type, PortType::Clock)
259 }
260
261 /// Check if this is a feedback port
262 pub const fn is_feedback(&self) -> bool {
263 matches!(self.port_type, PortType::Feedback)
264 }
265
266 /// Check if this is a parameter port
267 pub const fn is_param(&self) -> bool {
268 matches!(self.port_type, PortType::Param)
269 }
270}
271
272impl fmt::Display for PortId {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 write!(
275 f,
276 "Node({}).{}_{}[{}]",
277 self.node.inner(),
278 self.port_type.name(),
279 self.direction.name(),
280 self.index
281 )
282 }
283}
284
285// ============================================================================
286// Port Structure
287// ============================================================================
288
289/// A port on a node.
290///
291/// Each port has an owned `Buffer<T, BUF_SIZE>` for its data and an optional
292/// `Action` that defines per-port processing. Output ports typically have
293/// an action; input ports may have one for preprocessing.
294///
295/// Ports can optionally participate in feedback edges:
296/// - On an output port in a feedback edge, `feedback_buffer` stores the
297/// previous block's output, snapshotted after DSP via `snapshot_feedback()`.
298/// - On an input port in a feedback edge, `feedback_buffer` holds the delayed
299/// feedback value that gets mixed into `buffer` by `pre_process()`.
300/// - `downstream` lists audio connections from this output port to input ports
301/// of other nodes, populated at build time by the graph builder.
302/// - `upstream_buffer` on input ports: direct pointer to the upstream output
303/// port's buffer for zero-copy routing. `None` for fan-in/feedback ports.
304///
305/// # Safety
306/// `upstream_buffer` is safe because the graph topology is immutable and
307/// processing is strictly single-threaded in topological order. The
308/// upstream output buffer is guaranteed to outlive the downstream input
309/// port that references it.
310pub struct Port<T: Transcendental, const BUF_SIZE: usize> {
311 /// Port identifier
312 pub id: PortId,
313 /// Port name
314 pub name: String,
315 /// Port direction (input/output)
316 pub direction: PortDirection,
317 /// Per-port processing algorithm (None for simple input ports)
318 pub action: Option<Box<dyn Algorithm<T>>>,
319 /// Pending command value from the control path
320 pub pending_command: Option<T>,
321 /// Owned audio buffer (for output ports and input ports without upstream)
322 pub buffer: Buffer<T, BUF_SIZE>,
323 /// Delayed feedback state (None if not on a feedback edge)
324 pub feedback_buffer: Option<Buffer<T, BUF_SIZE>>,
325 /// Downstream audio connections: (target_node_index, target_port_index)
326 pub downstream: Vec<(usize, usize)>,
327 /// Direct pointer to upstream output buffer for zero-copy routing.
328 /// `Some` for input ports with exactly one upstream (1:1 connection).
329 /// `None` for output ports, fan-in, feedback, or unconnected input ports.
330 ///
331 /// # Safety
332 /// Valid for the engine's lifetime: the graph topology is static and
333 /// processing is single-threaded in topological order.
334 pub upstream_buffer: Option<*const Buffer<T, BUF_SIZE>>,
335 /// Feedback edge targets from this output port
336 pub feedback_downstream: Vec<(usize, usize)>,
337}
338
339impl<T: Transcendental, const BUF_SIZE: usize> fmt::Debug for Port<T, BUF_SIZE> {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 f.debug_struct("Port")
342 .field("id", &self.id)
343 .field("name", &self.name)
344 .field("direction", &self.direction)
345 .field("has_action", &self.action.is_some())
346 .field("has_feedback", &self.feedback_buffer.is_some())
347 .field("downstream_len", &self.downstream.len())
348 .finish()
349 }
350}
351
352impl<T: Transcendental, const BUF_SIZE: usize> Port<T, BUF_SIZE> {
353 /// Create a new signal output port
354 pub fn output(node_id: NodeId, index: u16, name: &str) -> Self {
355 Self {
356 id: PortId::audio_out(node_id, index),
357 name: name.to_string(),
358 direction: PortDirection::Output,
359 action: None,
360 pending_command: None,
361 buffer: Buffer::new(),
362 feedback_buffer: None,
363 downstream: Vec::new(),
364 feedback_downstream: Vec::new(),
365 upstream_buffer: None,
366 }
367 }
368
369 /// Create a new signal output port with an algorithm
370 pub fn output_with_action(
371 node_id: NodeId,
372 index: u16,
373 name: &str,
374 action: Box<dyn Algorithm<T>>,
375 ) -> Self {
376 Self {
377 id: PortId::audio_out(node_id, index),
378 name: name.to_string(),
379 direction: PortDirection::Output,
380 action: Some(action),
381 pending_command: None,
382 buffer: Buffer::new(),
383 feedback_buffer: None,
384 downstream: Vec::new(),
385 feedback_downstream: Vec::new(),
386 upstream_buffer: None,
387 }
388 }
389
390 /// Create a new signal input port
391 pub fn input(node_id: NodeId, index: u16, name: &str) -> Self {
392 Self {
393 id: PortId::audio_in(node_id, index),
394 name: name.to_string(),
395 direction: PortDirection::Input,
396 action: None,
397 pending_command: None,
398 buffer: Buffer::new(),
399 feedback_buffer: None,
400 downstream: Vec::new(),
401 feedback_downstream: Vec::new(),
402 upstream_buffer: None,
403 }
404 }
405
406 /// Create a new control output port
407 pub fn control_output(node_id: NodeId, index: u16, name: &str) -> Self {
408 Self {
409 id: PortId::control_out(node_id, index),
410 name: name.to_string(),
411 direction: PortDirection::Output,
412 action: None,
413 pending_command: None,
414 buffer: Buffer::new(),
415 feedback_buffer: None,
416 downstream: Vec::new(),
417 feedback_downstream: Vec::new(),
418 upstream_buffer: None,
419 }
420 }
421
422 /// Create a new control output port with an algorithm
423 pub fn control_output_with_action(
424 node_id: NodeId,
425 index: u16,
426 name: &str,
427 action: Box<dyn Algorithm<T>>,
428 ) -> Self {
429 Self {
430 id: PortId::control_out(node_id, index),
431 name: name.to_string(),
432 direction: PortDirection::Output,
433 action: Some(action),
434 pending_command: None,
435 buffer: Buffer::new(),
436 feedback_buffer: None,
437 downstream: Vec::new(),
438 feedback_downstream: Vec::new(),
439 upstream_buffer: None,
440 }
441 }
442
443 /// Create a new control input port
444 pub fn control_input(node_id: NodeId, index: u16, name: &str) -> Self {
445 Self {
446 id: PortId::control_in(node_id, index),
447 name: name.to_string(),
448 direction: PortDirection::Input,
449 action: None,
450 pending_command: None,
451 buffer: Buffer::new(),
452 feedback_buffer: None,
453 downstream: Vec::new(),
454 feedback_downstream: Vec::new(),
455 upstream_buffer: None,
456 }
457 }
458
459 /// Get the port ID
460 pub fn id(&self) -> PortId {
461 self.id
462 }
463
464 /// Get the port name
465 pub fn name(&self) -> &str {
466 &self.name
467 }
468
469 /// Check if port is an input
470 pub fn is_input(&self) -> bool {
471 self.direction.is_input()
472 }
473
474 /// Check if port is an output
475 pub fn is_output(&self) -> bool {
476 self.direction.is_output()
477 }
478
479 /// Get a reference to the buffer
480 pub fn buffer(&self) -> &Buffer<T, BUF_SIZE> {
481 &self.buffer
482 }
483
484 /// Get a mutable reference to the buffer
485 pub fn buffer_mut(&mut self) -> &mut Buffer<T, BUF_SIZE> {
486 &mut self.buffer
487 }
488
489 /// Get the effective audio buffer for an input port.
490 ///
491 /// Returns a reference to the upstream output buffer when this port has
492 /// a direct 1:1 connection (zero-copy), or the port's own buffer for
493 /// fan-in/feedback/unconnected ports (copy-based).
494 ///
495 /// # Safety
496 /// The upstream pointer is valid because the graph topology is static
497 /// and processing is single-threaded in topological order. The upstream
498 /// output buffer is owned by another port in the same graph and lives
499 /// for the entire processing session.
500 pub fn audio_buffer(&self) -> &Buffer<T, BUF_SIZE> {
501 match self.upstream_buffer {
502 Some(ptr) => {
503 #[allow(unsafe_code)]
504 unsafe {
505 &*ptr
506 }
507 }
508 None => &self.buffer,
509 }
510 }
511
512 /// Directly dereference an upstream buffer pointer to a `&Buffer`.
513 /// Used by the engine to avoid borrowing `self` (and thus the node)
514 /// when building audio input references for zero-copy processing.
515 ///
516 /// # Safety
517 /// `ptr` must be a valid, aligned pointer to a `Buffer<T, BUF_SIZE>`
518 /// that lives for the entire processing session.
519 #[allow(unsafe_code)]
520 pub unsafe fn upstream_ref(ptr: *const Buffer<T, BUF_SIZE>) -> &'static Buffer<T, BUF_SIZE> {
521 &*ptr
522 }
523
524 /// Pre-process this port before node DSP.
525 ///
526 /// For input ports on a feedback edge, mixes the delayed feedback
527 /// (from `feedback_buffer`) into the current `buffer`.
528 /// No-op when `feedback_buffer` is `None`.
529 ///
530 /// `tick` is the current clock tick, available for future
531 /// sample-accurate or time-varying port-level processing.
532 pub fn pre_process(&mut self, _tick: &ClockTick) {
533 if let Some(ref fb) = self.feedback_buffer {
534 let arr = self.buffer.as_mut_array();
535 let fb_arr = fb.as_array();
536 for i in 0..BUF_SIZE {
537 arr[i] = arr[i] + fb_arr[i];
538 }
539 }
540 }
541
542 /// Snapshot the buffer into `feedback_buffer` after node DSP.
543 ///
544 /// For output ports on a feedback edge, saves the current buffer
545 /// so it can be used as delayed feedback in the next block.
546 /// No-op when `feedback_buffer` is `None`.
547 pub fn snapshot_feedback(&mut self) {
548 if let Some(ref mut fb) = self.feedback_buffer {
549 fb.copy_from(self.buffer.as_array());
550 }
551 }
552
553 /// Propagate this port's buffer to all downstream input ports.
554 ///
555 /// Iterates over `downstream` and copies `buffer` into each target
556 /// input port's buffer. The caller must ensure no aliasing between
557 /// this port's node and any target node (guaranteed by DAG topology).
558 ///
559 /// `tick` is the current clock tick, available for future
560 /// sample-accurate or time-varying port-level propagation.
561 pub fn propagate(&self, _tick: &ClockTick, nodes: &mut [NodeVariant<T, BUF_SIZE>]) {
562 for &(target_node, target_port) in &self.downstream {
563 if let Some(p) = nodes[target_node].input_port_mut(target_port) {
564 p.buffer.copy_from(self.buffer.as_array());
565 }
566 }
567 }
568
569 /// Run the port's algorithm.
570 ///
571 /// Delivers any pending command via `Algorithm::apply_command()`, then
572 /// calls `Algorithm::process()` with the input and output slices.
573 /// When no algorithm is attached, the pending command value (if any)
574 /// is written directly into the buffer; otherwise input is passed
575 /// through or zero-filled.
576 pub fn run_action(
577 &mut self,
578 input: Option<&[T; BUF_SIZE]>,
579 ctx: &crate::traits::algorithm::ActionContext,
580 ) -> crate::traits::ProcessResult<()> {
581 match &mut self.action {
582 Some(action) => {
583 // Deliver any pending command to the algorithm
584 if let Some(cmd) = self.pending_command.take() {
585 action.apply_command(cmd);
586 }
587 let input_slice = input.map(|arr| arr.as_slice());
588 action.process(input_slice, self.buffer.as_mut_slice(), ctx)
589 }
590 None => {
591 // No algorithm — use pending command value if set,
592 // otherwise pass through input or zero-fill.
593 if let Some(cmd) = self.pending_command.take() {
594 self.buffer.fill(cmd);
595 } else if let Some(input_data) = input {
596 self.buffer.copy_from(input_data);
597 } else {
598 self.buffer.fill(T::ZERO);
599 }
600 Ok(())
601 }
602 }
603 }
604
605 /// Set a command value for this port.
606 ///
607 /// The value is stored as a pending command and delivered to the
608 /// algorithm (or written directly to the buffer) on the next
609 /// `run_action()` call.
610 pub fn set_value(&mut self, value: T) {
611 self.pending_command = Some(value);
612 }
613}
614
615// ============================================================================
616// Active Port Trait
617// ============================================================================
618
619/// Trait for ports that can actively pull/push data.
620pub trait ActivePort<T: Transcendental, const BUF_SIZE: usize> {
621 /// Pull data from the port (for input ports).
622 fn pull(&mut self) -> Option<[T; BUF_SIZE]>;
623
624 /// Push data into the port (for output ports).
625 fn push(&mut self, data: [T; BUF_SIZE]) -> Result<(), PortError>;
626
627 /// Check if the port is connected.
628 fn is_connected(&self) -> bool;
629
630 /// Called on each clock tick (optional).
631 fn on_tick(&mut self, _tick: &ClockTick) {}
632}
633
634impl<T: Transcendental, const BUF_SIZE: usize> ActivePort<T, BUF_SIZE> for Port<T, BUF_SIZE> {
635 #[inline]
636 fn pull(&mut self) -> Option<[T; BUF_SIZE]> {
637 if self.is_input() {
638 Some(*self.buffer.as_array())
639 } else {
640 None
641 }
642 }
643
644 #[inline]
645 fn push(&mut self, data: [T; BUF_SIZE]) -> Result<(), PortError> {
646 if self.is_output() {
647 self.buffer = Buffer::from_array(data);
648 Ok(())
649 } else {
650 Err(PortError::NotFound(self.id.to_string()))
651 }
652 }
653
654 #[inline]
655 fn is_connected(&self) -> bool {
656 self.action.is_some()
657 }
658
659 #[inline]
660 fn on_tick(&mut self, _tick: &ClockTick) {}
661}
662
663// SAFETY: `upstream_buffer` is a raw pointer to a buffer owned by another
664// Port in the same static graph. The graph is immutable during processing
665// and runs single-threaded in topological order. The pointer target
666// outlives the pointer for the entire processing session.
667#[allow(unsafe_code)]
668unsafe impl<T: Transcendental + Send, const BUF_SIZE: usize> Send for Port<T, BUF_SIZE> {}
669#[allow(unsafe_code)]
670unsafe impl<T: Transcendental + Sync, const BUF_SIZE: usize> Sync for Port<T, BUF_SIZE> {}
671
672// ============================================================================
673// Tests
674// ============================================================================
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn test_port_id_creation() {
682 let node = NodeId(42);
683
684 let audio_in = PortId::audio_in(node, 0);
685 assert_eq!(audio_in.port_type(), PortType::Signal);
686 assert!(audio_in.is_input());
687
688 let clock_out = PortId::clock_out(node, 0);
689 assert_eq!(clock_out.port_type(), PortType::Clock);
690 assert!(clock_out.is_output());
691
692 let feedback_in = PortId::feedback_in(node, 0);
693 assert_eq!(feedback_in.port_type(), PortType::Feedback);
694 assert!(feedback_in.is_input());
695 }
696}