aether_core/command.rs
1//! Lock-free command protocol between the control thread and the RT audio thread.
2//!
3//! Commands are sent via an SPSC ring buffer. The RT thread drains up to
4//! MAX_COMMANDS_PER_TICK per callback, bounding mutation cost.
5
6use crate::{arena::NodeId, param::Param};
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// All mutations the control thread can request.
12///
13/// Commands are sent from the control thread (UI, MIDI handler, etc.) to the
14/// real-time audio thread via an SPSC ring buffer. The audio thread drains
15/// commands at the start of each processing block.
16///
17/// # Design
18///
19/// - **Lock-free:** Commands are sent via SPSC ring buffer (no locks)
20/// - **Bounded:** Max `MAX_COMMANDS_PER_TICK` processed per audio block
21/// - **Clone:** Commands are cloned when sent (cheap - no heap allocations)
22/// - **Real-time safe:** All command processing is bounded and allocation-free
23///
24/// # Example
25///
26/// ```
27/// use aether_core::command::Command;
28/// use aether_core::arena::NodeId;
29///
30/// // Control thread sends commands
31/// let cmd = Command::AddNode {
32/// id: NodeId { index: 0, generation: 1 }
33/// };
34///
35/// // Audio thread receives and processes
36/// match cmd {
37/// Command::AddNode { id } => {
38/// // Add node to graph
39/// }
40/// _ => {}
41/// }
42/// ```
43///
44/// # Command Processing
45///
46/// Commands are processed in FIFO order. The audio thread processes up to
47/// `MAX_COMMANDS_PER_TICK` commands per block to bound latency impact.
48///
49/// # See Also
50///
51/// * [`Scheduler::process_block`](crate::scheduler::Scheduler::process_block) - Drains commands
52/// * [`DspGraph`](crate::graph::DspGraph) - Executes commands
53#[derive(Debug, Clone)]
54pub enum Command {
55 /// Add a new node to the graph.
56 ///
57 /// The node must already exist in the arena. This command makes it
58 /// visible to the scheduler for processing.
59 ///
60 /// # Fields
61 ///
62 /// * `id` - Node ID in the arena
63 ///
64 /// # Example
65 ///
66 /// ```
67 /// use aether_core::command::Command;
68 /// use aether_core::arena::NodeId;
69 ///
70 /// let cmd = Command::AddNode {
71 /// id: NodeId { index: 0, generation: 1 }
72 /// };
73 /// ```
74 AddNode { id: NodeId },
75
76 /// Remove a node from the graph and release its buffer.
77 ///
78 /// The node is removed from the processing order and its output buffer
79 /// is returned to the pool. All connections to/from this node are
80 /// automatically disconnected.
81 ///
82 /// # Fields
83 ///
84 /// * `id` - Node ID to remove
85 ///
86 /// # Example
87 ///
88 /// ```
89 /// use aether_core::command::Command;
90 /// use aether_core::arena::NodeId;
91 ///
92 /// let cmd = Command::RemoveNode {
93 /// id: NodeId { index: 0, generation: 1 }
94 /// };
95 /// ```
96 RemoveNode { id: NodeId },
97
98 /// Connect output of `src` to input slot `slot` of `dst`.
99 ///
100 /// Creates an audio connection between two nodes. The output of `src`
101 /// will be routed to input slot `slot` of `dst`. If the slot is already
102 /// connected, the old connection is replaced.
103 ///
104 /// # Fields
105 ///
106 /// * `src` - Source node ID (output)
107 /// * `dst` - Destination node ID (input)
108 /// * `slot` - Input slot index (0 to MAX_INPUTS-1)
109 ///
110 /// # Example
111 ///
112 /// ```
113 /// use aether_core::command::Command;
114 /// use aether_core::arena::NodeId;
115 ///
116 /// let osc = NodeId { index: 0, generation: 1 };
117 /// let filter = NodeId { index: 1, generation: 1 };
118 ///
119 /// // Connect oscillator output to filter input slot 0
120 /// let cmd = Command::Connect {
121 /// src: osc,
122 /// dst: filter,
123 /// slot: 0,
124 /// };
125 /// ```
126 Connect { src: NodeId, dst: NodeId, slot: usize },
127
128 /// Disconnect input slot `slot` of `dst`.
129 ///
130 /// Removes the connection to the specified input slot. The slot will
131 /// receive silence (None) in subsequent processing.
132 ///
133 /// # Fields
134 ///
135 /// * `dst` - Destination node ID
136 /// * `slot` - Input slot index to disconnect
137 ///
138 /// # Example
139 ///
140 /// ```
141 /// use aether_core::command::Command;
142 /// use aether_core::arena::NodeId;
143 ///
144 /// let filter = NodeId { index: 1, generation: 1 };
145 ///
146 /// // Disconnect input slot 0
147 /// let cmd = Command::Disconnect {
148 /// dst: filter,
149 /// slot: 0,
150 /// };
151 /// ```
152 Disconnect { dst: NodeId, slot: usize },
153
154 /// Update a parameter with a new smoothed value.
155 ///
156 /// Changes a node parameter with automatic smoothing to avoid clicks.
157 /// The parameter will ramp from its current value to the new target
158 /// over the configured smoothing time.
159 ///
160 /// # Fields
161 ///
162 /// * `node` - Node ID to update
163 /// * `param_index` - Parameter index (0-based)
164 /// * `new_param` - New parameter with target value and smoothing
165 ///
166 /// # Example
167 ///
168 /// ```
169 /// use aether_core::command::Command;
170 /// use aether_core::arena::NodeId;
171 /// use aether_core::param::Param;
172 ///
173 /// let filter = NodeId { index: 1, generation: 1 };
174 ///
175 /// // Update filter cutoff to 1000 Hz
176 /// let cmd = Command::UpdateParam {
177 /// node: filter,
178 /// param_index: 0,
179 /// new_param: Param::new(1000.0),
180 /// };
181 /// ```
182 UpdateParam { node: NodeId, param_index: usize, new_param: Param },
183
184 /// Swap the output node.
185 ///
186 /// Designates a new node as the graph output. The output node's buffer
187 /// is copied to the final output buffer each processing block.
188 ///
189 /// # Fields
190 ///
191 /// * `id` - Node ID to use as output
192 ///
193 /// # Example
194 ///
195 /// ```
196 /// use aether_core::command::Command;
197 /// use aether_core::arena::NodeId;
198 ///
199 /// let master = NodeId { index: 5, generation: 1 };
200 ///
201 /// let cmd = Command::SetOutputNode { id: master };
202 /// ```
203 SetOutputNode { id: NodeId },
204
205 /// Mute / unmute all audio output.
206 ///
207 /// When muted, the output buffer is filled with silence regardless of
208 /// graph processing. Processing continues normally - only the final
209 /// output is silenced.
210 ///
211 /// # Fields
212 ///
213 /// * `muted` - true to mute, false to unmute
214 ///
215 /// # Example
216 ///
217 /// ```
218 /// use aether_core::command::Command;
219 ///
220 /// // Mute output
221 /// let cmd = Command::SetMute { muted: true };
222 ///
223 /// // Unmute output
224 /// let cmd = Command::SetMute { muted: false };
225 /// ```
226 SetMute { muted: bool },
227
228 /// Remove all nodes and edges — silence the graph.
229 ///
230 /// Clears the entire graph, removing all nodes and connections. All
231 /// buffers are released back to the pool. The graph is left in an
232 /// empty state.
233 ///
234 /// # Example
235 ///
236 /// ```
237 /// use aether_core::command::Command;
238 ///
239 /// let cmd = Command::ClearGraph;
240 /// ```
241 ClearGraph,
242}
243
244/// Serializable graph description for UI ↔ host communication.
245#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
246#[derive(Debug, Clone)]
247pub struct GraphSnapshot {
248 pub nodes: Vec<NodeSnapshot>,
249 pub edges: Vec<EdgeSnapshot>,
250}
251
252#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
253#[derive(Debug, Clone)]
254pub struct NodeSnapshot {
255 pub id: u32,
256 pub generation: u32,
257 pub node_type: String,
258 pub params: Vec<f32>,
259}
260
261#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
262#[derive(Debug, Clone)]
263pub struct EdgeSnapshot {
264 pub src_id: u32,
265 pub dst_id: u32,
266 pub slot: usize,
267}