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