Skip to main content

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}