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