1use crate::graph::{CableId, NodeId, Patch};
4use crate::io::{AtomicF64, ExternalInput};
5use crate::observer::{StateObserver, SubscriptionTarget};
6use crate::port::{ports_compatible, SignalColors, SignalKind};
7use crate::serialize::{ModuleRegistry, PatchDef};
8use alloc::boxed::Box;
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::sync::Arc;
12use alloc::vec::Vec;
13use wasm_bindgen::prelude::*;
14
15pub const MIDI_VOCT_MODULE: &str = "midi_voct";
18pub const MIDI_GATE_MODULE: &str = "midi_gate";
19pub const MIDI_VELOCITY_MODULE: &str = "midi_velocity";
20pub const MIDI_MOD_MODULE: &str = "midi_mod";
21pub const MIDI_BEND_MODULE: &str = "midi_bend";
22
23pub const AUDIO_IN_MODULE: &str = "audio_in";
27
28struct MidiInputs {
36 voct: Arc<AtomicF64>,
38 gate: Arc<AtomicF64>,
40 velocity: Arc<AtomicF64>,
42 modulation: Arc<AtomicF64>,
44 bend: Arc<AtomicF64>,
46}
47
48impl MidiInputs {
49 fn new() -> Self {
50 Self {
51 voct: Arc::new(AtomicF64::new(0.0)),
52 gate: Arc::new(AtomicF64::new(0.0)),
53 velocity: Arc::new(AtomicF64::new(0.0)),
54 modulation: Arc::new(AtomicF64::new(0.0)),
55 bend: Arc::new(AtomicF64::new(0.0)),
56 }
57 }
58}
59
60#[wasm_bindgen]
62pub struct QuiverEngine {
63 patch: Patch,
64 registry: ModuleRegistry,
65 observer: StateObserver,
66 sample_rate: f64,
67
68 block_left: Vec<f64>,
72 block_right: Vec<f64>,
73 block_interleaved: Vec<f32>,
74
75 observer_interval: u32,
79 observer_countdown: u32,
80
81 midi: MidiInputs,
84
85 audio_in: Arc<AtomicF64>,
90
91 midi_note: Option<f64>,
93 midi_velocity: Option<f64>,
94 midi_gate: bool,
95 midi_cc_values: [f64; 128],
96 midi_pitch_bend_value: f64,
97
98 held_notes: Vec<(u8, u8)>,
105}
106
107#[wasm_bindgen]
108impl QuiverEngine {
109 #[wasm_bindgen(constructor)]
111 pub fn new(sample_rate: f64) -> Self {
112 console_error_panic_hook::set_once();
114
115 Self {
116 patch: Patch::new(sample_rate),
117 registry: ModuleRegistry::new(),
118 observer: StateObserver::new(),
119 sample_rate,
120 block_left: Vec::new(),
121 block_right: Vec::new(),
122 block_interleaved: Vec::new(),
123 observer_interval: 8,
127 observer_countdown: 0,
128 midi: MidiInputs::new(),
129 audio_in: Arc::new(AtomicF64::new(0.0)),
130 midi_note: None,
131 midi_velocity: None,
132 midi_gate: false,
133 midi_cc_values: [0.0; 128],
134 midi_pitch_bend_value: 0.0,
135 held_notes: Vec::new(),
136 }
137 }
138
139 #[wasm_bindgen(getter)]
141 pub fn sample_rate(&self) -> f64 {
142 self.sample_rate
143 }
144
145 pub fn get_catalog(&self) -> Result<JsValue, JsValue> {
151 let catalog = self.registry.catalog();
152 serde_wasm_bindgen::to_value(&catalog).map_err(|e| JsValue::from_str(&e.to_string()))
153 }
154
155 pub fn search_modules(&self, query: &str) -> Result<JsValue, JsValue> {
157 let results = self.registry.search(query);
158 serde_wasm_bindgen::to_value(&results).map_err(|e| JsValue::from_str(&e.to_string()))
159 }
160
161 pub fn get_modules_by_category(&self, category: &str) -> Result<JsValue, JsValue> {
163 let results = self.registry.by_category(category);
164 serde_wasm_bindgen::to_value(&results).map_err(|e| JsValue::from_str(&e.to_string()))
165 }
166
167 pub fn get_categories(&self) -> Result<JsValue, JsValue> {
169 let categories = self.registry.categories();
170 serde_wasm_bindgen::to_value(&categories).map_err(|e| JsValue::from_str(&e.to_string()))
171 }
172
173 pub fn get_signal_colors(&self) -> Result<JsValue, JsValue> {
179 let colors = SignalColors::default();
180 serde_wasm_bindgen::to_value(&colors).map_err(|e| JsValue::from_str(&e.to_string()))
181 }
182
183 pub fn check_compatibility(&self, from: &str, to: &str) -> Result<JsValue, JsValue> {
185 let from_kind = parse_signal_kind(from)?;
186 let to_kind = parse_signal_kind(to)?;
187 let compat = ports_compatible(from_kind, to_kind);
188 serde_wasm_bindgen::to_value(&compat).map_err(|e| JsValue::from_str(&e.to_string()))
189 }
190
191 pub fn load_patch(&mut self, patch_json: JsValue) -> Result<(), JsValue> {
197 let patch_def: PatchDef = serde_wasm_bindgen::from_value(patch_json)
198 .map_err(|e| JsValue::from_str(&e.to_string()))?;
199
200 self.patch = Patch::from_def(&patch_def, &self.registry, self.sample_rate)
201 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?;
202
203 Ok(())
204 }
205
206 pub fn save_patch(&self, name: &str) -> Result<JsValue, JsValue> {
208 let patch_def = self.patch.to_def(name);
209 serde_wasm_bindgen::to_value(&patch_def).map_err(|e| JsValue::from_str(&e.to_string()))
210 }
211
212 pub fn validate_patch(&self, patch_json: JsValue) -> Result<JsValue, JsValue> {
214 let patch_def: PatchDef = serde_wasm_bindgen::from_value(patch_json)
215 .map_err(|e| JsValue::from_str(&e.to_string()))?;
216
217 let result = patch_def.validate_with_registry(&self.registry);
218 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
219 }
220
221 pub fn clear_patch(&mut self) {
223 self.patch = Patch::new(self.sample_rate);
224 }
225
226 pub fn add_module(&mut self, type_id: &str, name: &str) -> Result<(), JsValue> {
232 let module = self
233 .registry
234 .instantiate(type_id, self.sample_rate)
235 .ok_or_else(|| JsValue::from_str(&format!("Unknown module type: {}", type_id)))?;
236
237 self.patch.add_boxed(name, module);
238 Ok(())
239 }
240
241 pub fn remove_module(&mut self, name: &str) -> Result<(), JsValue> {
243 let node_id = self
244 .get_node_id_by_name(name)
245 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
246
247 self.patch
248 .remove(node_id)
249 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
250 }
251
252 pub fn set_module_position(&mut self, name: &str, x: f32, y: f32) -> Result<(), JsValue> {
254 let node_id = self
255 .get_node_id_by_name(name)
256 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
257
258 self.patch.set_position(node_id, (x, y));
259 Ok(())
260 }
261
262 pub fn get_module_position(&self, name: &str) -> Result<JsValue, JsValue> {
264 let node_id = self
265 .get_node_id_by_name(name)
266 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
267
268 let position = self.patch.get_position(node_id);
269 serde_wasm_bindgen::to_value(&position).map_err(|e| JsValue::from_str(&e.to_string()))
270 }
271
272 pub fn module_count(&self) -> usize {
274 self.patch.node_count()
275 }
276
277 pub fn cable_count(&self) -> usize {
279 self.patch.cable_count()
280 }
281
282 pub fn set_output(&mut self, name: &str) -> Result<(), JsValue> {
287 let node_id = self
288 .get_node_id_by_name(name)
289 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
290
291 self.patch.set_output(node_id);
292 Ok(())
293 }
294
295 pub fn connect(&mut self, from: &str, to: &str) -> Result<usize, JsValue> {
305 let (from_ref, to_ref) = self.resolve_ports(from, to)?;
306 self.patch
307 .connect(from_ref, to_ref)
308 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
309 }
310
311 pub fn connect_attenuated(
313 &mut self,
314 from: &str,
315 to: &str,
316 attenuation: f64,
317 ) -> Result<usize, JsValue> {
318 let (from_ref, to_ref) = self.resolve_ports(from, to)?;
319 self.patch
320 .connect_attenuated(from_ref, to_ref, attenuation)
321 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
322 }
323
324 pub fn connect_modulated(
327 &mut self,
328 from: &str,
329 to: &str,
330 attenuation: f64,
331 offset: f64,
332 ) -> Result<usize, JsValue> {
333 let (from_ref, to_ref) = self.resolve_ports(from, to)?;
334 self.patch
335 .connect_modulated(from_ref, to_ref, attenuation, offset)
336 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
337 }
338
339 pub fn disconnect_cable(&mut self, cable_id: usize) -> Result<(), JsValue> {
344 self.patch
345 .disconnect(cable_id as CableId)
346 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
347 }
348
349 pub fn disconnect_by_index(&mut self, cable_index: usize) -> Result<(), JsValue> {
355 let cable_id = self
356 .patch
357 .cables()
358 .get(cable_index)
359 .map(|c| c.id)
360 .ok_or_else(|| JsValue::from_str(&format!("Invalid cable index: {}", cable_index)))?;
361 self.patch
362 .disconnect(cable_id)
363 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
364 }
365
366 pub fn disconnect(&mut self, from: &str, to: &str) -> Result<(), JsValue> {
368 let (from_module, from_port) = parse_port_ref(from)?;
369 let (to_module, to_port) = parse_port_ref(to)?;
370
371 let from_handle = self
372 .patch
373 .get_handle_by_name(from_module)
374 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", from_module)))?;
375 let to_handle = self
376 .patch
377 .get_handle_by_name(to_module)
378 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", to_module)))?;
379
380 self.patch
381 .disconnect_ports(from_handle.out(from_port), to_handle.in_(to_port))
382 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
383 }
384
385 pub fn get_module_names(&self) -> Result<JsValue, JsValue> {
387 let names = self.patch.module_names();
388 serde_wasm_bindgen::to_value(&names).map_err(|e| JsValue::from_str(&e.to_string()))
389 }
390
391 pub fn get_params(&self, node_name: &str) -> Result<JsValue, JsValue> {
400 let type_id = self
402 .patch
403 .nodes()
404 .find(|(_, name, _)| *name == node_name)
405 .map(|(_, _, module)| module.type_id())
406 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
407
408 let metadata = self
410 .registry
411 .get_metadata(type_id)
412 .ok_or_else(|| JsValue::from_str(&format!("Unknown module type: {}", type_id)))?;
413
414 serde_wasm_bindgen::to_value(&metadata.port_spec)
416 .map_err(|e| JsValue::from_str(&e.to_string()))
417 }
418
419 pub fn set_param(
421 &mut self,
422 node_name: &str,
423 param_index: u32,
424 value: f64,
425 ) -> Result<(), JsValue> {
426 let node_id = self
427 .get_node_id_by_name(node_name)
428 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
429
430 self.patch.set_param(node_id, param_index, value);
431 Ok(())
432 }
433
434 pub fn get_param(&self, node_name: &str, param_index: u32) -> Result<f64, JsValue> {
436 let node_id = self
437 .get_node_id_by_name(node_name)
438 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
439
440 self.patch
441 .get_param(node_id, param_index)
442 .ok_or_else(|| JsValue::from_str(&format!("Param {} not found", param_index)))
443 }
444
445 pub fn set_param_by_name(
449 &mut self,
450 node_name: &str,
451 param_name: &str,
452 value: f64,
453 ) -> Result<(), JsValue> {
454 let param_id = self
456 .patch
457 .nodes()
458 .find(|(_, name, _)| *name == node_name)
459 .and_then(|(_, _, module)| {
460 module
461 .params()
462 .iter()
463 .find(|p| p.name == param_name)
464 .map(|p| p.id)
465 })
466 .ok_or_else(|| {
467 JsValue::from_str(&format!(
468 "Unknown parameter '{}' on module '{}'",
469 param_name, node_name
470 ))
471 })?;
472
473 let node_id = self
475 .get_node_id_by_name(node_name)
476 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
477 self.patch.set_param(node_id, param_id, value);
478 Ok(())
479 }
480
481 pub fn subscribe(&mut self, targets: JsValue) -> Result<(), JsValue> {
487 let targets: Vec<SubscriptionTarget> = serde_wasm_bindgen::from_value(targets)
488 .map_err(|e| JsValue::from_str(&e.to_string()))?;
489
490 self.observer.add_subscriptions(targets);
491 self.sync_metering_keepalive();
492 Ok(())
493 }
494
495 pub fn unsubscribe(&mut self, target_ids: JsValue) -> Result<(), JsValue> {
497 let ids: Vec<String> = serde_wasm_bindgen::from_value(target_ids)
498 .map_err(|e| JsValue::from_str(&e.to_string()))?;
499
500 self.observer.remove_subscriptions(&ids);
501 self.sync_metering_keepalive();
502 Ok(())
503 }
504
505 pub fn clear_subscriptions(&mut self) {
507 self.observer.clear_subscriptions();
508 self.sync_metering_keepalive();
509 }
510
511 pub fn poll_updates(&mut self) -> Result<JsValue, JsValue> {
513 let updates = self.observer.drain_updates();
514 serde_wasm_bindgen::to_value(&updates).map_err(|e| JsValue::from_str(&e.to_string()))
515 }
516
517 pub fn pending_update_count(&self) -> usize {
519 self.observer.pending_count()
520 }
521
522 pub fn tick(&mut self) -> Box<[f64]> {
529 let (left, right) = self.patch.tick();
530 Box::new([left, right])
531 }
532
533 pub fn process_block(&mut self, num_samples: usize) -> js_sys::Float32Array {
552 const SAFETY_LIMIT: f64 = 10.0; if self.block_left.len() < num_samples {
556 self.block_left.resize(num_samples, 0.0);
557 self.block_right.resize(num_samples, 0.0);
558 }
559 let interleaved_len = num_samples * 2;
560 if self.block_interleaved.len() < interleaved_len {
561 self.block_interleaved.resize(interleaved_len, 0.0);
562 }
563
564 self.patch.tick_block(
566 &mut self.block_left[..num_samples],
567 &mut self.block_right[..num_samples],
568 );
569
570 for i in 0..num_samples {
572 let left = self.block_left[i].clamp(-SAFETY_LIMIT, SAFETY_LIMIT) as f32;
573 let right = self.block_right[i].clamp(-SAFETY_LIMIT, SAFETY_LIMIT) as f32;
574 self.block_interleaved[i * 2] = left;
575 self.block_interleaved[i * 2 + 1] = right;
576 }
577
578 if self.observer_interval <= 1 || self.observer_countdown == 0 {
582 self.observer.collect_from_patch(&self.patch);
583 self.observer_countdown = self.observer_interval.saturating_sub(1);
584 } else {
585 self.observer_countdown -= 1;
586 }
587
588 unsafe { js_sys::Float32Array::view(&self.block_interleaved[..interleaved_len]) }
592 }
593
594 fn tick_with_input(&mut self, sample: f64) -> (f64, f64) {
598 self.audio_in.set(sample);
599 self.patch.tick()
600 }
601
602 pub fn process_block_with_input(&mut self, input: &[f32]) -> js_sys::Float32Array {
615 const SAFETY_LIMIT: f64 = 10.0; let num_samples = input.len();
618 let interleaved_len = num_samples * 2;
619 if self.block_interleaved.len() < interleaved_len {
620 self.block_interleaved.resize(interleaved_len, 0.0);
621 }
622
623 for (i, &sample) in input.iter().enumerate() {
624 let (left, right) = self.tick_with_input(f64::from(sample));
625 self.block_interleaved[i * 2] = left.clamp(-SAFETY_LIMIT, SAFETY_LIMIT) as f32;
626 self.block_interleaved[i * 2 + 1] = right.clamp(-SAFETY_LIMIT, SAFETY_LIMIT) as f32;
627 }
628
629 if self.observer_interval <= 1 || self.observer_countdown == 0 {
631 self.observer.collect_from_patch(&self.patch);
632 self.observer_countdown = self.observer_interval.saturating_sub(1);
633 } else {
634 self.observer_countdown -= 1;
635 }
636
637 unsafe { js_sys::Float32Array::view(&self.block_interleaved[..interleaved_len]) }
640 }
641
642 pub fn set_observer_interval(&mut self, blocks: u32) {
647 self.observer_interval = blocks.max(1);
648 self.observer_countdown = 0;
650 }
651
652 pub fn reset(&mut self) {
654 self.patch.reset();
655 }
656
657 pub fn compile(&mut self) -> Result<(), JsValue> {
659 self.sync_metering_keepalive();
664 self.patch
665 .compile()
666 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
667 }
668
669 pub fn add_audio_input(&mut self) {
686 if self.patch.get_node_id_by_name(AUDIO_IN_MODULE).is_none() {
687 self.patch.add_boxed(
688 AUDIO_IN_MODULE,
689 Box::new(ExternalInput::audio(Arc::clone(&self.audio_in))),
690 );
691 }
692 }
693
694 pub fn add_midi_inputs(&mut self) {
715 if self.patch.get_node_id_by_name(MIDI_VOCT_MODULE).is_none() {
716 self.patch.add_boxed(
717 MIDI_VOCT_MODULE,
718 Box::new(ExternalInput::voct(Arc::clone(&self.midi.voct))),
719 );
720 }
721 if self.patch.get_node_id_by_name(MIDI_GATE_MODULE).is_none() {
722 self.patch.add_boxed(
723 MIDI_GATE_MODULE,
724 Box::new(ExternalInput::gate(Arc::clone(&self.midi.gate))),
725 );
726 }
727 if self
728 .patch
729 .get_node_id_by_name(MIDI_VELOCITY_MODULE)
730 .is_none()
731 {
732 self.patch.add_boxed(
733 MIDI_VELOCITY_MODULE,
734 Box::new(ExternalInput::cv(Arc::clone(&self.midi.velocity))),
735 );
736 }
737 if self.patch.get_node_id_by_name(MIDI_MOD_MODULE).is_none() {
738 self.patch.add_boxed(
739 MIDI_MOD_MODULE,
740 Box::new(ExternalInput::cv(Arc::clone(&self.midi.modulation))),
741 );
742 }
743 if self.patch.get_node_id_by_name(MIDI_BEND_MODULE).is_none() {
744 self.patch.add_boxed(
745 MIDI_BEND_MODULE,
746 Box::new(ExternalInput::cv_bipolar(Arc::clone(&self.midi.bend))),
747 );
748 }
749 }
750
751 pub fn midi_note_on(&mut self, note: u8, velocity: u8) -> Result<(), JsValue> {
761 let v_oct = Self::note_to_voct(note);
763 let vel = velocity as f64 / 127.0;
765
766 self.held_notes.retain(|&(n, _)| n != note);
770 self.held_notes.push((note, velocity));
771
772 self.midi_note = Some(v_oct);
773 self.midi_velocity = Some(vel);
774 self.midi_gate = true;
775
776 self.midi.voct.set(v_oct);
778 self.midi.velocity.set(vel);
779 self.midi.gate.set(5.0);
780
781 Ok(())
782 }
783
784 pub fn midi_note_off(&mut self, note: u8, _velocity: u8) -> Result<(), JsValue> {
794 self.held_notes.retain(|&(n, _)| n != note);
796
797 match self.held_notes.last().copied() {
798 Some((held_note, held_velocity)) => {
800 let v_oct = Self::note_to_voct(held_note);
801 let vel = held_velocity as f64 / 127.0;
802
803 self.midi_note = Some(v_oct);
804 self.midi_velocity = Some(vel);
805 self.midi_gate = true;
806
807 self.midi.voct.set(v_oct);
808 self.midi.velocity.set(vel);
809 self.midi.gate.set(5.0);
812 }
813 None => {
815 self.midi_gate = false;
816 self.midi.gate.set(0.0);
817 }
818 }
819
820 Ok(())
821 }
822
823 fn note_to_voct(note: u8) -> f64 {
825 (note as f64 - 60.0) / 12.0
826 }
827
828 #[wasm_bindgen(getter)]
830 pub fn midi_note(&self) -> f64 {
831 self.midi_note.unwrap_or(0.0)
832 }
833
834 #[wasm_bindgen(getter)]
836 pub fn midi_velocity(&self) -> f64 {
837 self.midi_velocity.unwrap_or(0.0)
838 }
839
840 #[wasm_bindgen(getter)]
842 pub fn midi_gate(&self) -> bool {
843 self.midi_gate
844 }
845
846 pub fn midi_cc(&mut self, cc: u8, value: u8) -> Result<(), JsValue> {
851 let normalized = value as f64 / 127.0;
852 self.midi_cc_values[cc as usize] = normalized;
853 if cc == 1 {
854 self.midi.modulation.set(normalized);
855 }
856 Ok(())
857 }
858
859 pub fn get_midi_cc(&self, cc: u8) -> f64 {
861 self.midi_cc_values.get(cc as usize).copied().unwrap_or(0.0)
862 }
863
864 pub fn midi_pitch_bend(&mut self, value: f64) -> Result<(), JsValue> {
870 self.midi_pitch_bend_value = value;
871 self.midi.bend.set(value * (2.0 / 12.0));
873 Ok(())
874 }
875
876 #[wasm_bindgen(getter)]
878 pub fn pitch_bend(&self) -> f64 {
879 self.midi_pitch_bend_value
880 }
881
882 pub fn get_port_spec(&self, type_id: &str) -> Result<JsValue, JsValue> {
888 let metadata = self
889 .registry
890 .get_metadata(type_id)
891 .ok_or_else(|| JsValue::from_str(&format!("Unknown module type: {}", type_id)))?;
892
893 serde_wasm_bindgen::to_value(&metadata.port_spec)
894 .map_err(|e| JsValue::from_str(&e.to_string()))
895 }
896
897 fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
903 self.patch.get_node_id_by_name(name)
904 }
905
906 fn resolve_ports(
912 &self,
913 from: &str,
914 to: &str,
915 ) -> Result<(crate::graph::PortRef, crate::graph::PortRef), JsValue> {
916 let (from_module, from_port) = parse_port_ref(from)?;
917 let (to_module, to_port) = parse_port_ref(to)?;
918
919 let from_handle = self
920 .patch
921 .get_handle_by_name(from_module)
922 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", from_module)))?;
923 let to_handle = self
924 .patch
925 .get_handle_by_name(to_module)
926 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", to_module)))?;
927
928 let from_ref = from_handle
929 .output(from_port)
930 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
931 let to_ref = to_handle
932 .input(to_port)
933 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
934 Ok((from_ref, to_ref))
935 }
936}
937
938impl QuiverEngine {
941 fn sync_metering_keepalive(&mut self) {
953 self.observer.sync_output_keepalive(&mut self.patch);
955 }
956}
957
958fn parse_port_ref(s: &str) -> Result<(&str, &str), JsValue> {
961 let parts: Vec<&str> = s.splitn(2, '.').collect();
962 if parts.len() != 2 {
963 return Err(JsValue::from_str(&format!(
964 "Invalid port reference: {} (expected 'module.port')",
965 s
966 )));
967 }
968 Ok((parts[0], parts[1]))
969}
970
971fn parse_signal_kind(s: &str) -> Result<SignalKind, JsValue> {
972 match s {
973 "audio" => Ok(SignalKind::Audio),
974 "cv_bipolar" => Ok(SignalKind::CvBipolar),
975 "cv_unipolar" => Ok(SignalKind::CvUnipolar),
976 "volt_per_octave" => Ok(SignalKind::VoltPerOctave),
977 "gate" => Ok(SignalKind::Gate),
978 "trigger" => Ok(SignalKind::Trigger),
979 "clock" => Ok(SignalKind::Clock),
980 _ => Err(JsValue::from_str(&format!("Unknown signal kind: {}", s))),
981 }
982}
983
984#[cfg(all(test, feature = "wasm"))]
997mod native_tests {
998 use super::*;
999 use crate::graph::PatchError;
1000 use crate::wasm::QuiverError;
1001
1002 #[test]
1003 fn new_engine_reports_sample_rate_and_empty_patch() {
1004 let engine = QuiverEngine::new(48_000.0);
1005 assert_eq!(engine.sample_rate(), 48_000.0);
1006 assert_eq!(engine.module_count(), 0);
1007 assert_eq!(engine.cable_count(), 0);
1008 assert_eq!(engine.pending_update_count(), 0);
1009 }
1010
1011 #[test]
1012 fn audio_input_path_feeds_the_patch_per_sample() {
1013 let mut engine = QuiverEngine::new(44_100.0);
1014 engine.add_audio_input();
1015 engine.add_audio_input(); assert_eq!(engine.module_count(), 1);
1017
1018 assert!(engine.set_output(AUDIO_IN_MODULE).is_ok());
1019 assert!(engine.compile().is_ok());
1020
1021 let (left, _right) = engine.tick_with_input(0.7);
1023 assert!((left - 0.7).abs() < 1e-12);
1024 let (left, _right) = engine.tick_with_input(-0.25);
1025 assert!((left + 0.25).abs() < 1e-12);
1026 }
1027
1028 #[test]
1029 fn add_module_updates_count_and_clear_resets() {
1030 let mut engine = QuiverEngine::new(44_100.0);
1031 assert!(engine.add_module("vco", "osc").is_ok());
1032 assert!(engine.add_module("stereo_output", "out").is_ok());
1033 assert_eq!(engine.module_count(), 2);
1034 engine.clear_patch();
1035 assert_eq!(engine.module_count(), 0);
1036 assert_eq!(engine.cable_count(), 0);
1037 }
1038
1039 #[test]
1040 fn connect_returns_cable_id_and_updates_cable_count() {
1041 let mut engine = QuiverEngine::new(44_100.0);
1042 engine.add_module("vco", "osc").unwrap();
1043 engine.add_module("stereo_output", "out").unwrap();
1044 let id = engine
1045 .connect("osc.saw", "out.left")
1046 .expect("valid connection");
1047 assert_eq!(id, 0, "first cable id should be 0");
1048 assert_eq!(engine.cable_count(), 1);
1049 }
1050
1051 #[test]
1052 fn connect_then_disconnect_round_trips() {
1053 let mut engine = QuiverEngine::new(44_100.0);
1054 engine.add_module("vco", "osc").unwrap();
1055 engine.add_module("stereo_output", "out").unwrap();
1056 let id = engine.connect("osc.saw", "out.left").ok().unwrap();
1057 assert_eq!(engine.cable_count(), 1);
1058 assert!(engine.disconnect_cable(id).is_ok());
1059 assert_eq!(engine.cable_count(), 0);
1060 }
1061
1062 #[test]
1063 fn attenuated_and_modulated_connections_succeed() {
1064 let mut engine = QuiverEngine::new(44_100.0);
1065 engine.add_module("lfo", "lfo").unwrap();
1066 engine.add_module("svf", "flt").unwrap();
1067 assert!(engine.connect_attenuated("lfo.sin", "flt.fm", 0.5).is_ok());
1068 assert!(engine
1069 .connect_modulated("lfo.tri", "flt.cutoff", 0.5, 0.1)
1070 .is_ok());
1071 assert_eq!(engine.cable_count(), 2);
1072 }
1073
1074 #[test]
1075 fn compile_and_tick_produce_audio() {
1076 let mut engine = QuiverEngine::new(44_100.0);
1077 engine.add_module("vco", "osc").unwrap();
1078 engine.add_module("stereo_output", "out").unwrap();
1079 engine.connect("osc.saw", "out.left").ok().unwrap();
1080 engine.connect("osc.saw", "out.right").ok().unwrap();
1081 engine.set_output("out").unwrap();
1082 assert!(engine.compile().is_ok());
1083
1084 let mut nonzero = 0;
1085 for _ in 0..2000 {
1086 let frame = engine.tick();
1087 assert_eq!(frame.len(), 2, "tick must return a stereo frame");
1088 if frame[0].abs() > 1e-9 {
1089 nonzero += 1;
1090 }
1091 }
1092 assert!(nonzero > 1000, "compiled VCO patch should produce audio");
1093 }
1094
1095 #[test]
1096 fn set_param_on_valid_module_succeeds() {
1097 let mut engine = QuiverEngine::new(44_100.0);
1100 engine.add_module("vco", "osc").unwrap();
1101 assert!(engine.set_param("osc", 0, 1.0).is_ok());
1102 }
1103
1104 #[test]
1105 fn midi_state_round_trips() {
1106 let mut engine = QuiverEngine::new(44_100.0);
1107 engine.add_midi_inputs();
1108 assert_eq!(engine.module_count(), 5, "add_midi_inputs adds 5 modules");
1109
1110 assert!(engine.midi_note_on(60, 100).is_ok());
1111 assert_eq!(engine.midi_note(), 0.0, "note 60 (C4) maps to 0V");
1112 assert!(engine.midi_velocity() > 0.0);
1113 assert!(engine.midi_gate());
1114
1115 assert!(engine.midi_note_off(60, 0).is_ok());
1116 assert!(!engine.midi_gate());
1117
1118 assert!(engine.midi_cc(1, 127).is_ok());
1119 assert!((engine.get_midi_cc(1) - 1.0).abs() < 1e-9);
1120
1121 assert!(engine.midi_pitch_bend(0.5).is_ok());
1122 assert_eq!(engine.pitch_bend(), 0.5);
1123 }
1124
1125 #[test]
1126 fn note_off_keeps_gate_while_another_note_is_held() {
1127 let mut engine = QuiverEngine::new(44_100.0);
1130 engine.midi_note_on(60, 100).unwrap();
1131 engine.midi_note_on(64, 100).unwrap();
1132
1133 engine.midi_note_off(60, 0).unwrap();
1134 assert!(engine.midi_gate(), "gate stays open while note 64 is held");
1135 assert!(
1136 (engine.midi_note() - (64.0 - 60.0) / 12.0).abs() < 1e-9,
1137 "pitch tracks the still-held note 64"
1138 );
1139
1140 engine.midi_note_off(64, 0).unwrap();
1142 assert!(!engine.midi_gate(), "gate closes once no notes remain");
1143 }
1144
1145 #[test]
1146 fn note_off_last_note_priority_reverts_pitch_on_top_release() {
1147 let mut engine = QuiverEngine::new(44_100.0);
1150 engine.midi_note_on(60, 100).unwrap();
1151 engine.midi_note_on(67, 100).unwrap();
1152 assert!((engine.midi_note() - (67.0 - 60.0) / 12.0).abs() < 1e-9);
1153
1154 engine.midi_note_off(67, 0).unwrap();
1155 assert!(engine.midi_gate(), "gate stays open, 60 still held");
1156 assert!(
1157 engine.midi_note().abs() < 1e-9,
1158 "pitch reverts to note 60 (0V) under last-note priority"
1159 );
1160
1161 engine.midi_note_off(60, 0).unwrap();
1162 assert!(!engine.midi_gate());
1163 }
1164
1165 #[test]
1166 fn note_off_single_note_and_stray_release_close_the_gate() {
1167 let mut engine = QuiverEngine::new(44_100.0);
1170 engine.midi_note_on(62, 100).unwrap();
1171 assert!(engine.midi_gate());
1172 engine.midi_note_off(62, 0).unwrap();
1173 assert!(!engine.midi_gate());
1174
1175 engine.midi_note_off(90, 0).unwrap();
1176 assert!(!engine.midi_gate(), "stray note-off keeps the gate closed");
1177 }
1178
1179 #[test]
1180 fn note_on_dedupes_repeated_note_so_one_release_clears_it() {
1181 let mut engine = QuiverEngine::new(44_100.0);
1184 engine.midi_note_on(60, 100).unwrap();
1185 engine.midi_note_on(60, 110).unwrap();
1186 engine.midi_note_off(60, 0).unwrap();
1187 assert!(
1188 !engine.midi_gate(),
1189 "one release clears a re-pressed note (no duplicate stack entry)"
1190 );
1191 }
1192
1193 #[test]
1194 fn reset_and_clear_subscriptions_do_not_panic() {
1195 let mut engine = QuiverEngine::new(44_100.0);
1196 engine.add_module("vco", "osc").unwrap();
1197 engine.reset();
1198 engine.clear_subscriptions();
1199 engine.set_observer_interval(4);
1200 assert_eq!(engine.pending_update_count(), 0);
1201 }
1202
1203 #[test]
1204 fn quiver_error_maps_from_patch_error_and_strings() {
1205 assert_eq!(QuiverError::from("boom").message(), "boom");
1207 assert_eq!(
1208 QuiverError::from(alloc::string::String::from("halp")).message(),
1209 "halp"
1210 );
1211 assert_eq!(
1212 QuiverError::from(PatchError::InvalidCable).message(),
1213 "InvalidCable"
1214 );
1215 }
1216}