1use 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
16pub type NodeId = i32;
18
19#[derive(Debug, Clone)]
21pub enum Instruction {
22 CreateNode {
24 node_id: NodeId,
26 node_type: String,
28 },
29 SetProperty {
31 node_id: NodeId,
33 property: String,
35 value: JsonValue,
37 },
38 AppendChild {
40 parent_id: NodeId,
42 child_id: NodeId,
44 child_output_channel: i32,
46 },
47 ActivateRoots {
49 roots: Vec<NodeId>,
51 },
52 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#[derive(Debug, Clone, Default)]
95pub struct InstructionBatch {
96 instructions: Vec<Instruction>,
97}
98
99impl InstructionBatch {
100 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn push(&mut self, instruction: Instruction) {
107 self.instructions.push(instruction);
108 }
109
110 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
122pub 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<()>>, }
130
131unsafe impl Send for Runtime {}
132
133#[bon]
134impl Runtime {
135 pub fn builder() -> RuntimeConstructBuilder {
137 Self::create()
138 }
139
140 pub fn new() -> RuntimeConstructBuilder {
142 Self::builder()
143 }
144
145 #[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 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 pub fn reset(&self) {
182 unsafe { ffi::elementary_runtime_reset(self.handle.as_ptr()) }
183 }
184
185 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 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 pub fn resources(&self) -> Ref<'_, ResourceManager> {
199 self.resources.borrow()
200 }
201
202 pub fn add_resource(&self, name: impl AsRef<str>, resource: Resource) -> Result<()> {
204 self.resources.borrow_mut().add(name, resource)
205 }
206
207 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 pub fn replace_resource(&self, name: impl AsRef<str>, resource: Resource) -> Result<Resource> {
218 self.resources.borrow_mut().replace(name, resource)
219 }
220
221 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 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 pub fn resource(&self, name: impl AsRef<str>) -> Option<Resource> {
250 self.resources.borrow().get_cloned(name)
251 }
252
253 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 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 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 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 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 pub fn gc(&self) -> Vec<NodeId> {
406 unsafe extern "C" fn collect(node_id: i32, user_data: *mut c_void) {
407 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 fn drop(&mut self) {
427 unsafe { ffi::elementary_runtime_free(self.handle.as_ptr()) }
428 }
429}