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 {
127 src: NodeId,
128 dst: NodeId,
129 slot: usize,
130 },
131
132 /// Disconnect input slot `slot` of `dst`.
133 ///
134 /// Removes the connection to the specified input slot. The slot will
135 /// receive silence (None) in subsequent processing.
136 ///
137 /// # Fields
138 ///
139 /// * `dst` - Destination node ID
140 /// * `slot` - Input slot index to disconnect
141 ///
142 /// # Example
143 ///
144 /// ```
145 /// use aether_core::command::Command;
146 /// use aether_core::arena::NodeId;
147 ///
148 /// let filter = NodeId { index: 1, generation: 1 };
149 ///
150 /// // Disconnect input slot 0
151 /// let cmd = Command::Disconnect {
152 /// dst: filter,
153 /// slot: 0,
154 /// };
155 /// ```
156 Disconnect { dst: NodeId, slot: usize },
157
158 /// Update a parameter with a new smoothed value.
159 ///
160 /// Changes a node parameter with automatic smoothing to avoid clicks.
161 /// The parameter will ramp from its current value to the new target
162 /// over the configured smoothing time.
163 ///
164 /// # Fields
165 ///
166 /// * `node` - Node ID to update
167 /// * `param_index` - Parameter index (0-based)
168 /// * `new_param` - New parameter with target value and smoothing
169 ///
170 /// # Example
171 ///
172 /// ```
173 /// use aether_core::command::Command;
174 /// use aether_core::arena::NodeId;
175 /// use aether_core::param::Param;
176 ///
177 /// let filter = NodeId { index: 1, generation: 1 };
178 ///
179 /// // Update filter cutoff to 1000 Hz
180 /// let cmd = Command::UpdateParam {
181 /// node: filter,
182 /// param_index: 0,
183 /// new_param: Param::new(1000.0),
184 /// };
185 /// ```
186 UpdateParam {
187 node: NodeId,
188 param_index: usize,
189 new_param: Param,
190 },
191
192 /// Swap the output node.
193 ///
194 /// Designates a new node as the graph output. The output node's buffer
195 /// is copied to the final output buffer each processing block.
196 ///
197 /// # Fields
198 ///
199 /// * `id` - Node ID to use as output
200 ///
201 /// # Example
202 ///
203 /// ```
204 /// use aether_core::command::Command;
205 /// use aether_core::arena::NodeId;
206 ///
207 /// let master = NodeId { index: 5, generation: 1 };
208 ///
209 /// let cmd = Command::SetOutputNode { id: master };
210 /// ```
211 SetOutputNode { id: NodeId },
212
213 /// Mute / unmute all audio output.
214 ///
215 /// When muted, the output buffer is filled with silence regardless of
216 /// graph processing. Processing continues normally - only the final
217 /// output is silenced.
218 ///
219 /// # Fields
220 ///
221 /// * `muted` - true to mute, false to unmute
222 ///
223 /// # Example
224 ///
225 /// ```
226 /// use aether_core::command::Command;
227 ///
228 /// // Mute output
229 /// let cmd = Command::SetMute { muted: true };
230 ///
231 /// // Unmute output
232 /// let cmd = Command::SetMute { muted: false };
233 /// ```
234 SetMute { muted: bool },
235
236 /// Remove all nodes and edges — silence the graph.
237 ///
238 /// Clears the entire graph, removing all nodes and connections. All
239 /// buffers are released back to the pool. The graph is left in an
240 /// empty state.
241 ///
242 /// # Example
243 ///
244 /// ```
245 /// use aether_core::command::Command;
246 ///
247 /// let cmd = Command::ClearGraph;
248 /// ```
249 ClearGraph,
250}
251
252/// Serializable graph description for UI ↔ host communication.
253#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
254#[derive(Debug, Clone)]
255pub struct GraphSnapshot {
256 pub nodes: Vec<NodeSnapshot>,
257 pub edges: Vec<EdgeSnapshot>,
258}
259
260#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
261#[derive(Debug, Clone)]
262pub struct NodeSnapshot {
263 pub id: u32,
264 pub generation: u32,
265 pub node_type: String,
266 pub params: Vec<f32>,
267}
268
269#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
270#[derive(Debug, Clone)]
271pub struct EdgeSnapshot {
272 pub src_id: u32,
273 pub dst_id: u32,
274 pub slot: usize,
275}