elemaudio_rs/
runtime.rs

1//! Safe wrapper around the Elementary runtime handle and instruction batches.
2
3use crate::error::{describe_return_code, Error, Result};
4use crate::ffi;
5use crate::resource::{AudioBuffer, Resource, ResourceManager};
6use bon::bon;
7use serde_json::Value as JsonValue;
8use std::cell::Cell;
9use std::cell::{Ref, RefCell};
10use std::convert::TryFrom;
11use std::ffi::{c_void, CString};
12use std::marker::PhantomData;
13use std::ptr::NonNull;
14use std::sync::Arc;
15
16/// Runtime node identifier used by instruction batches and GC results.
17pub type NodeId = i32;
18
19/// Instruction supported by the current wrapper surface.
20#[derive(Debug, Clone)]
21pub enum Instruction {
22    /// Create a runtime node with the given identifier and type.
23    CreateNode {
24        /// Node identifier assigned by the caller.
25        node_id: NodeId,
26        /// Native node type name.
27        node_type: String,
28    },
29    /// Set a property on an existing node.
30    SetProperty {
31        /// Target node identifier.
32        node_id: NodeId,
33        /// Property name.
34        property: String,
35        /// JSON value to assign to the property.
36        value: JsonValue,
37    },
38    /// Append one node as a child of another node.
39    AppendChild {
40        /// Parent node identifier.
41        parent_id: NodeId,
42        /// Child node identifier.
43        child_id: NodeId,
44        /// Output channel index on the child node.
45        child_output_channel: i32,
46    },
47    /// Activate the provided root nodes.
48    ActivateRoots {
49        /// Root node identifiers.
50        roots: Vec<NodeId>,
51    },
52    /// Commit a pending set of runtime updates.
53    CommitUpdates,
54}
55
56impl Instruction {
57    fn to_json_value(&self) -> JsonValue {
58        match self {
59            Self::CreateNode { node_id, node_type } => JsonValue::Array(vec![
60                JsonValue::from(0),
61                JsonValue::from(*node_id),
62                JsonValue::from(node_type.clone()),
63            ]),
64            Self::SetProperty {
65                node_id,
66                property,
67                value,
68            } => JsonValue::Array(vec![
69                JsonValue::from(3),
70                JsonValue::from(*node_id),
71                JsonValue::from(property.clone()),
72                value.clone(),
73            ]),
74            Self::AppendChild {
75                parent_id,
76                child_id,
77                child_output_channel,
78            } => JsonValue::Array(vec![
79                JsonValue::from(2),
80                JsonValue::from(*parent_id),
81                JsonValue::from(*child_id),
82                JsonValue::from(*child_output_channel),
83            ]),
84            Self::ActivateRoots { roots } => JsonValue::Array(vec![
85                JsonValue::from(4),
86                JsonValue::Array(roots.iter().copied().map(JsonValue::from).collect()),
87            ]),
88            Self::CommitUpdates => JsonValue::Array(vec![JsonValue::from(5)]),
89        }
90    }
91}
92
93/// A batch of instructions serialized to the runtime JSON array shape.
94#[derive(Debug, Clone, Default)]
95pub struct InstructionBatch {
96    instructions: Vec<Instruction>,
97}
98
99impl InstructionBatch {
100    /// Creates an empty instruction batch.
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Appends one instruction to the batch.
106    pub fn push(&mut self, instruction: Instruction) {
107        self.instructions.push(instruction);
108    }
109
110    /// Serializes the batch to the JSON payload expected by the native runtime.
111    pub fn to_json_string(&self) -> String {
112        let payload = JsonValue::Array(
113            self.instructions
114                .iter()
115                .map(Instruction::to_json_value)
116                .collect(),
117        );
118        serde_json::to_string(&payload).expect("instruction batch serialization is infallible")
119    }
120}
121
122/// Safe owner for a native runtime handle.
123pub struct Runtime {
124    handle: NonNull<ffi::ElementaryRuntimeHandle>,
125    resources: RefCell<ResourceManager>,
126    retired_resources: RefCell<Vec<Resource>>,
127    buffer_size: usize,
128    _not_send_or_sync: PhantomData<Cell<()>>, // keep Runtime movable but not shareable
129}
130
131unsafe impl Send for Runtime {}
132
133#[bon]
134impl Runtime {
135    /// Starts building a runtime.
136    pub fn builder() -> RuntimeConstructBuilder {
137        Self::create()
138    }
139
140    /// Backward-compatible constructor entrypoint used by examples and tests.
141    pub fn new() -> RuntimeConstructBuilder {
142        Self::builder()
143    }
144
145    /// Creates a runtime for the given sample rate and buffer size.
146    #[builder(start_fn(name = create, vis = ""))]
147    pub fn construct(sample_rate: f64, buffer_size: usize) -> Result<Self> {
148        let block_size = i32::try_from(buffer_size)
149            .map_err(|_| Error::InvalidArgument("buffer_size must fit in i32"))?;
150        let handle = unsafe { ffi::elementary_runtime_new(sample_rate, block_size) };
151        let handle = NonNull::new(handle).ok_or(Error::NullHandle)?;
152
153        Ok(Self {
154            handle,
155            resources: RefCell::new(ResourceManager::new()),
156            retired_resources: RefCell::new(Vec::new()),
157            buffer_size,
158            _not_send_or_sync: PhantomData,
159        })
160    }
161
162    /// Applies a serialized batch of instructions to the runtime.
163    pub fn apply_instructions(&self, batch: &InstructionBatch) -> Result<()> {
164        let json = CString::new(batch.to_json_string())?;
165        let code = unsafe {
166            ffi::elementary_runtime_apply_instructions(self.handle.as_ptr(), json.as_ptr())
167        };
168
169        if code == 0 {
170            return Ok(());
171        }
172
173        Err(Error::Native {
174            operation: "apply_instructions",
175            code,
176            message: describe_return_code(code).to_string(),
177        })
178    }
179
180    /// Resets the runtime state.
181    pub fn reset(&self) {
182        unsafe { ffi::elementary_runtime_reset(self.handle.as_ptr()) }
183    }
184
185    /// Sets the current runtime time in samples.
186    pub fn set_current_time_samples(&self, sample_time: i64) {
187        unsafe {
188            ffi::elementary_runtime_set_current_time_samples(self.handle.as_ptr(), sample_time)
189        }
190    }
191
192    /// Sets the current runtime time in milliseconds.
193    pub fn set_current_time_ms(&self, sample_time_ms: f64) {
194        unsafe { ffi::elementary_runtime_set_current_time_ms(self.handle.as_ptr(), sample_time_ms) }
195    }
196
197    /// Returns the current resource registry.
198    pub fn resources(&self) -> Ref<'_, ResourceManager> {
199        self.resources.borrow()
200    }
201
202    /// Adds a resource to the registry if the id is unused.
203    pub fn add_resource(&self, name: impl AsRef<str>, resource: Resource) -> Result<()> {
204        self.resources.borrow_mut().add(name, resource)
205    }
206
207    /// Inserts or replaces a resource in the registry.
208    pub fn set_resource(
209        &self,
210        name: impl AsRef<str>,
211        resource: Resource,
212    ) -> Result<Option<Resource>> {
213        self.resources.borrow_mut().insert(name, resource)
214    }
215
216    /// Replaces an existing resource and returns the previous value.
217    pub fn replace_resource(&self, name: impl AsRef<str>, resource: Resource) -> Result<Resource> {
218        self.resources.borrow_mut().replace(name, resource)
219    }
220
221    /// Removes a resource from the registry.
222    pub fn remove_resource(&self, name: impl AsRef<str>) -> Result<Resource> {
223        let removed = self.resources.borrow_mut().remove(name)?;
224        self.retired_resources.borrow_mut().push(removed.clone());
225        Ok(removed)
226    }
227
228    /// Renames a resource without changing its value.
229    pub fn rename_resource(&self, from: impl AsRef<str>, to: impl AsRef<str>) -> Result<()> {
230        let resource = self
231            .resource(from.as_ref())
232            .ok_or_else(|| Error::ResourceNotFound(from.as_ref().to_string()))?;
233
234        self.resources
235            .borrow_mut()
236            .rename(from.as_ref(), to.as_ref())?;
237
238        if let Resource::F32(samples) = &resource {
239            self.add_shared_resource_f32(to.as_ref(), samples.as_ref())?;
240        } else if let Resource::Audio(buffer) = &resource {
241            self.add_audio_resource(to.as_ref(), buffer.clone())?;
242        }
243
244        self.retired_resources.borrow_mut().push(resource);
245        Ok(())
246    }
247
248    /// Returns a cloned resource by name.
249    pub fn resource(&self, name: impl AsRef<str>) -> Option<Resource> {
250        self.resources.borrow().get_cloned(name)
251    }
252
253    /// Adds a shared `f32` resource by name and mirrors it into the Rust registry.
254    pub fn add_shared_resource_f32(&self, name: &str, data: &[f32]) -> Result<()> {
255        if name.trim().is_empty() {
256            return Err(Error::InvalidArgument("resource id cannot be empty"));
257        }
258
259        let resource = Resource::f32(Arc::from(data.to_vec().into_boxed_slice()));
260        let resource_name = CString::new(name)?;
261        let code = unsafe {
262            ffi::elementary_runtime_add_shared_resource_f32(
263                self.handle.as_ptr(),
264                resource_name.as_ptr(),
265                data.as_ptr(),
266                data.len(),
267            )
268        };
269
270        if code == 0 {
271            self.resources.borrow_mut().insert(name, resource)?;
272            return Ok(());
273        }
274
275        Err(Error::Native {
276            operation: "add_shared_resource_f32",
277            code,
278            message: "native runtime rejected the shared resource".to_string(),
279        })
280    }
281
282    /// Adds a decoded mono audio buffer as a shared resource.
283    pub fn add_audio_resource(&self, name: &str, buffer: AudioBuffer) -> Result<()> {
284        if buffer.channels == 1 {
285            self.add_shared_resource_f32(name, buffer.samples.as_ref())?;
286        } else {
287            self.add_shared_resource_f32_multi(name, &buffer)?;
288        }
289        self.resources
290            .borrow_mut()
291            .insert(name, Resource::audio(buffer))?;
292        Ok(())
293    }
294
295    /// Adds a decoded multichannel audio buffer as a shared resource.
296    fn add_shared_resource_f32_multi(&self, name: &str, buffer: &AudioBuffer) -> Result<()> {
297        if name.trim().is_empty() {
298            return Err(Error::InvalidArgument("resource id cannot be empty"));
299        }
300
301        let channels = buffer.channels as usize;
302        let frames = buffer.frames();
303        let samples = buffer.samples.as_ref();
304        let mut channel_slices: Vec<Vec<f32>> =
305            (0..channels).map(|_| Vec::with_capacity(frames)).collect();
306
307        for frame in 0..frames {
308            let base = frame * channels;
309            for channel in 0..channels {
310                channel_slices[channel].push(samples[base + channel]);
311            }
312        }
313
314        let channel_ptrs: Vec<*const f32> = channel_slices
315            .iter()
316            .map(|channel| channel.as_ptr())
317            .collect();
318        let resource_name = CString::new(name)?;
319        let code = unsafe {
320            ffi::elementary_runtime_add_shared_resource_f32_multi(
321                self.handle.as_ptr(),
322                resource_name.as_ptr(),
323                channel_ptrs.as_ptr(),
324                channel_ptrs.len(),
325                frames,
326            )
327        };
328
329        if code == 0 {
330            self.resources
331                .borrow_mut()
332                .insert(name, Resource::audio(buffer.clone()))?;
333            return Ok(());
334        }
335
336        Err(Error::Native {
337            operation: "add_shared_resource_f32_multi",
338            code,
339            message: "native runtime rejected the shared multichannel resource".to_string(),
340        })
341    }
342
343    /// Prunes native shared resources and releases retired Rust buffers.
344    pub fn prune_shared_resources(&self) {
345        unsafe { ffi::elementary_runtime_prune_shared_resources(self.handle.as_ptr()) }
346        self.retired_resources.borrow_mut().clear();
347    }
348
349    /// Processes one audio block.
350    ///
351    /// Every input and output channel must have at least `num_samples` samples.
352    pub fn process(
353        &self,
354        num_samples: usize,
355        inputs: &[&[f64]],
356        outputs: &mut [&mut [f64]],
357    ) -> Result<()> {
358        if num_samples > self.buffer_size {
359            return Err(Error::InvalidArgument(
360                "num_samples exceeds the configured buffer_size",
361            ));
362        }
363
364        if inputs.iter().any(|channel| channel.len() < num_samples) {
365            return Err(Error::InvalidArgument(
366                "an input channel is shorter than num_samples",
367            ));
368        }
369
370        if outputs.iter().any(|channel| channel.len() < num_samples) {
371            return Err(Error::InvalidArgument(
372                "an output channel is shorter than num_samples",
373            ));
374        }
375
376        let input_ptrs: Vec<*const f64> = inputs.iter().map(|channel| channel.as_ptr()).collect();
377        let mut output_ptrs: Vec<*mut f64> = outputs
378            .iter_mut()
379            .map(|channel| channel.as_mut_ptr())
380            .collect();
381
382        let code = unsafe {
383            ffi::elementary_runtime_process(
384                self.handle.as_ptr(),
385                input_ptrs.as_ptr(),
386                input_ptrs.len(),
387                output_ptrs.as_mut_ptr(),
388                output_ptrs.len(),
389                num_samples,
390            )
391        };
392
393        if code == 0 {
394            return Ok(());
395        }
396
397        Err(Error::Native {
398            operation: "process",
399            code,
400            message: describe_return_code(code).to_string(),
401        })
402    }
403
404    /// Runs garbage collection and returns the collected node identifiers.
405    pub fn gc(&self) -> Vec<NodeId> {
406        unsafe extern "C" fn collect(node_id: i32, user_data: *mut c_void) {
407            // The pointer comes from `gc` below and remains valid for the duration of the call.
408            let ids = unsafe { &mut *(user_data as *mut Vec<NodeId>) };
409            ids.push(node_id);
410        }
411
412        let mut ids = Vec::new();
413        unsafe {
414            ffi::elementary_runtime_gc(
415                self.handle.as_ptr(),
416                Some(collect),
417                &mut ids as *mut _ as *mut c_void,
418            );
419        }
420        ids
421    }
422}
423
424impl Drop for Runtime {
425    /// Releases the native runtime handle.
426    fn drop(&mut self) {
427        unsafe { ffi::elementary_runtime_free(self.handle.as_ptr()) }
428    }
429}