Skip to main content

sim_lib_audio_graph_core/
port.rs

1use std::{fmt, str::FromStr};
2
3use sim_kernel::{Error, Result};
4use sim_lib_stream_core::RateContract;
5
6/// Kind of signal a port carries.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum PortMedia {
9    /// Sample-rate audio.
10    Audio,
11    /// Control-rate parameter signals.
12    Control,
13    /// Discrete events (for example MIDI).
14    Event,
15}
16
17/// Direction of a port relative to its node.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum PortDir {
20    /// Input port.
21    In,
22    /// Output port.
23    Out,
24}
25
26/// Declaration of one port on a processor: name, media, direction, channel
27/// count, and rate contract.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct PortDecl {
30    /// Port name, unique among a node's ports of the same direction.
31    pub name: String,
32    /// Media kind carried by the port.
33    pub media: PortMedia,
34    /// Port direction.
35    pub dir: PortDir,
36    /// Channel count.
37    pub channels: u16,
38    /// Rate contract the port advertises.
39    pub rate_contract: RateContract,
40}
41
42impl PortDecl {
43    /// Creates a port declaration with the media kind's default rate contract.
44    pub fn new(name: impl Into<String>, media: PortMedia, dir: PortDir, channels: u16) -> Self {
45        Self {
46            name: name.into(),
47            media,
48            dir,
49            channels,
50            rate_contract: media.default_rate_contract(),
51        }
52    }
53
54    /// Returns the declaration with its rate contract overridden.
55    pub fn with_rate_contract(mut self, rate_contract: RateContract) -> Self {
56        self.rate_contract = rate_contract;
57        self
58    }
59}
60
61impl PortMedia {
62    /// Returns the default rate contract for this media kind: sample-exact for
63    /// audio, control-rate for control and event media.
64    pub fn default_rate_contract(self) -> RateContract {
65        match self {
66            Self::Audio => RateContract::sample_exact(None),
67            Self::Control | Self::Event => RateContract::control(),
68        }
69    }
70}
71
72/// A structured URI identifying a single channel of a port.
73///
74/// Graph ports use the `sim-node://graph/<graph-id>/<node-id>/<port-name>:<index>`
75/// form; [`PortUri::node`] builds that shape and [`PortUri::node_id`] /
76/// [`PortUri::node_port_name`] read it back.
77#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
78pub struct PortUri {
79    /// URI scheme (for example `sim-node`).
80    pub scheme: String,
81    /// URI authority (for example `graph`).
82    pub authority: String,
83    /// Path segments after the authority.
84    pub path: Vec<String>,
85    /// Channel index within the port.
86    pub index: u32,
87}
88
89impl PortUri {
90    /// Creates and validates a port URI from its parts.
91    pub fn new(
92        scheme: impl Into<String>,
93        authority: impl Into<String>,
94        path: Vec<String>,
95        index: u32,
96    ) -> Result<Self> {
97        let uri = Self {
98            scheme: scheme.into(),
99            authority: authority.into(),
100            path,
101            index,
102        };
103        uri.validate()?;
104        Ok(uri)
105    }
106
107    /// Builds a graph node port URI in the `sim-node://graph/...` form.
108    pub fn node(
109        graph_id: impl Into<String>,
110        node_id: impl Into<String>,
111        port_name: impl Into<String>,
112        index: u32,
113    ) -> Result<Self> {
114        Self::new(
115            "sim-node",
116            "graph",
117            vec![graph_id.into(), node_id.into(), port_name.into()],
118            index,
119        )
120    }
121
122    /// Returns the node id if this is a graph node port URI.
123    pub fn node_id(&self) -> Option<&str> {
124        (self.scheme == "sim-node" && self.authority == "graph" && self.path.len() >= 3)
125            .then_some(self.path[1].as_str())
126    }
127
128    /// Returns the port name if this is a graph node port URI.
129    pub fn node_port_name(&self) -> Option<&str> {
130        (self.scheme == "sim-node" && self.authority == "graph" && self.path.len() >= 3)
131            .then_some(self.path[2].as_str())
132    }
133
134    fn validate(&self) -> Result<()> {
135        if self.scheme.is_empty() {
136            return Err(Error::Eval("port URI scheme cannot be empty".to_owned()));
137        }
138        if self.scheme.contains(':') || self.scheme.contains('/') {
139            return Err(Error::Eval(format!(
140                "port URI scheme contains an invalid separator: {}",
141                self.scheme
142            )));
143        }
144        if self.authority.is_empty() {
145            return Err(Error::Eval("port URI authority cannot be empty".to_owned()));
146        }
147        if self.authority.contains('/') {
148            return Err(Error::Eval(format!(
149                "port URI authority contains an invalid separator: {}",
150                self.authority
151            )));
152        }
153        if let Some(segment) = self.path.iter().find(|segment| segment.is_empty()) {
154            return Err(Error::Eval(format!(
155                "port URI path segment cannot be empty: {segment}"
156            )));
157        }
158        if let Some(segment) = self.path.iter().find(|segment| segment.contains('/')) {
159            return Err(Error::Eval(format!(
160                "port URI path segment contains an invalid separator: {segment}"
161            )));
162        }
163        Ok(())
164    }
165}
166
167impl fmt::Display for PortUri {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}://{}", self.scheme, self.authority)?;
170        for segment in &self.path {
171            write!(f, "/{segment}")?;
172        }
173        write!(f, ":{}", self.index)
174    }
175}
176
177impl FromStr for PortUri {
178    type Err = Error;
179
180    fn from_str(text: &str) -> Result<Self> {
181        let (scheme, rest) = text
182            .split_once("://")
183            .ok_or_else(|| Error::Eval(format!("port URI is missing scheme: {text}")))?;
184        let (address, index_text) = rest
185            .rsplit_once(':')
186            .ok_or_else(|| Error::Eval(format!("port URI is missing port index: {text}")))?;
187        let index = index_text
188            .parse::<u32>()
189            .map_err(|_| Error::Eval(format!("port URI has invalid port index: {text}")))?;
190        let mut segments = address.split('/');
191        let authority = segments
192            .next()
193            .ok_or_else(|| Error::Eval(format!("port URI is missing authority: {text}")))?;
194        let path = segments.map(str::to_owned).collect::<Vec<_>>();
195        Self::new(scheme, authority, path, index)
196    }
197}