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 {
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}