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
23struct MidiInputs {
31 voct: Arc<AtomicF64>,
33 gate: Arc<AtomicF64>,
35 velocity: Arc<AtomicF64>,
37 modulation: Arc<AtomicF64>,
39 bend: Arc<AtomicF64>,
41}
42
43impl MidiInputs {
44 fn new() -> Self {
45 Self {
46 voct: Arc::new(AtomicF64::new(0.0)),
47 gate: Arc::new(AtomicF64::new(0.0)),
48 velocity: Arc::new(AtomicF64::new(0.0)),
49 modulation: Arc::new(AtomicF64::new(0.0)),
50 bend: Arc::new(AtomicF64::new(0.0)),
51 }
52 }
53}
54
55#[wasm_bindgen]
57pub struct QuiverEngine {
58 patch: Patch,
59 registry: ModuleRegistry,
60 observer: StateObserver,
61 sample_rate: f64,
62
63 block_left: Vec<f64>,
67 block_right: Vec<f64>,
68 block_interleaved: Vec<f32>,
69
70 observer_interval: u32,
74 observer_countdown: u32,
75
76 midi: MidiInputs,
79
80 midi_note: Option<f64>,
82 midi_velocity: Option<f64>,
83 midi_gate: bool,
84 midi_cc_values: [f64; 128],
85 midi_pitch_bend_value: f64,
86
87 held_notes: Vec<(u8, u8)>,
94}
95
96#[wasm_bindgen]
97impl QuiverEngine {
98 #[wasm_bindgen(constructor)]
100 pub fn new(sample_rate: f64) -> Self {
101 console_error_panic_hook::set_once();
103
104 Self {
105 patch: Patch::new(sample_rate),
106 registry: ModuleRegistry::new(),
107 observer: StateObserver::new(),
108 sample_rate,
109 block_left: Vec::new(),
110 block_right: Vec::new(),
111 block_interleaved: Vec::new(),
112 observer_interval: 8,
116 observer_countdown: 0,
117 midi: MidiInputs::new(),
118 midi_note: None,
119 midi_velocity: None,
120 midi_gate: false,
121 midi_cc_values: [0.0; 128],
122 midi_pitch_bend_value: 0.0,
123 held_notes: Vec::new(),
124 }
125 }
126
127 #[wasm_bindgen(getter)]
129 pub fn sample_rate(&self) -> f64 {
130 self.sample_rate
131 }
132
133 pub fn get_catalog(&self) -> Result<JsValue, JsValue> {
139 let catalog = self.registry.catalog();
140 serde_wasm_bindgen::to_value(&catalog).map_err(|e| JsValue::from_str(&e.to_string()))
141 }
142
143 pub fn search_modules(&self, query: &str) -> Result<JsValue, JsValue> {
145 let results = self.registry.search(query);
146 serde_wasm_bindgen::to_value(&results).map_err(|e| JsValue::from_str(&e.to_string()))
147 }
148
149 pub fn get_modules_by_category(&self, category: &str) -> Result<JsValue, JsValue> {
151 let results = self.registry.by_category(category);
152 serde_wasm_bindgen::to_value(&results).map_err(|e| JsValue::from_str(&e.to_string()))
153 }
154
155 pub fn get_categories(&self) -> Result<JsValue, JsValue> {
157 let categories = self.registry.categories();
158 serde_wasm_bindgen::to_value(&categories).map_err(|e| JsValue::from_str(&e.to_string()))
159 }
160
161 pub fn get_signal_colors(&self) -> Result<JsValue, JsValue> {
167 let colors = SignalColors::default();
168 serde_wasm_bindgen::to_value(&colors).map_err(|e| JsValue::from_str(&e.to_string()))
169 }
170
171 pub fn check_compatibility(&self, from: &str, to: &str) -> Result<JsValue, JsValue> {
173 let from_kind = parse_signal_kind(from)?;
174 let to_kind = parse_signal_kind(to)?;
175 let compat = ports_compatible(from_kind, to_kind);
176 serde_wasm_bindgen::to_value(&compat).map_err(|e| JsValue::from_str(&e.to_string()))
177 }
178
179 pub fn load_patch(&mut self, patch_json: JsValue) -> Result<(), JsValue> {
185 let patch_def: PatchDef = serde_wasm_bindgen::from_value(patch_json)
186 .map_err(|e| JsValue::from_str(&e.to_string()))?;
187
188 self.patch = Patch::from_def(&patch_def, &self.registry, self.sample_rate)
189 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?;
190
191 Ok(())
192 }
193
194 pub fn save_patch(&self, name: &str) -> Result<JsValue, JsValue> {
196 let patch_def = self.patch.to_def(name);
197 serde_wasm_bindgen::to_value(&patch_def).map_err(|e| JsValue::from_str(&e.to_string()))
198 }
199
200 pub fn validate_patch(&self, patch_json: JsValue) -> Result<JsValue, JsValue> {
202 let patch_def: PatchDef = serde_wasm_bindgen::from_value(patch_json)
203 .map_err(|e| JsValue::from_str(&e.to_string()))?;
204
205 let result = patch_def.validate_with_registry(&self.registry);
206 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
207 }
208
209 pub fn clear_patch(&mut self) {
211 self.patch = Patch::new(self.sample_rate);
212 }
213
214 pub fn add_module(&mut self, type_id: &str, name: &str) -> Result<(), JsValue> {
220 let module = self
221 .registry
222 .instantiate(type_id, self.sample_rate)
223 .ok_or_else(|| JsValue::from_str(&format!("Unknown module type: {}", type_id)))?;
224
225 self.patch.add_boxed(name, module);
226 Ok(())
227 }
228
229 pub fn remove_module(&mut self, name: &str) -> Result<(), JsValue> {
231 let node_id = self
232 .get_node_id_by_name(name)
233 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
234
235 self.patch
236 .remove(node_id)
237 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
238 }
239
240 pub fn set_module_position(&mut self, name: &str, x: f32, y: f32) -> Result<(), JsValue> {
242 let node_id = self
243 .get_node_id_by_name(name)
244 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
245
246 self.patch.set_position(node_id, (x, y));
247 Ok(())
248 }
249
250 pub fn get_module_position(&self, name: &str) -> Result<JsValue, JsValue> {
252 let node_id = self
253 .get_node_id_by_name(name)
254 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
255
256 let position = self.patch.get_position(node_id);
257 serde_wasm_bindgen::to_value(&position).map_err(|e| JsValue::from_str(&e.to_string()))
258 }
259
260 pub fn module_count(&self) -> usize {
262 self.patch.node_count()
263 }
264
265 pub fn cable_count(&self) -> usize {
267 self.patch.cable_count()
268 }
269
270 pub fn set_output(&mut self, name: &str) -> Result<(), JsValue> {
275 let node_id = self
276 .get_node_id_by_name(name)
277 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", name)))?;
278
279 self.patch.set_output(node_id);
280 Ok(())
281 }
282
283 pub fn connect(&mut self, from: &str, to: &str) -> Result<usize, JsValue> {
293 let (from_ref, to_ref) = self.resolve_ports(from, to)?;
294 self.patch
295 .connect(from_ref, to_ref)
296 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
297 }
298
299 pub fn connect_attenuated(
301 &mut self,
302 from: &str,
303 to: &str,
304 attenuation: f64,
305 ) -> Result<usize, JsValue> {
306 let (from_ref, to_ref) = self.resolve_ports(from, to)?;
307 self.patch
308 .connect_attenuated(from_ref, to_ref, attenuation)
309 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
310 }
311
312 pub fn connect_modulated(
315 &mut self,
316 from: &str,
317 to: &str,
318 attenuation: f64,
319 offset: f64,
320 ) -> Result<usize, JsValue> {
321 let (from_ref, to_ref) = self.resolve_ports(from, to)?;
322 self.patch
323 .connect_modulated(from_ref, to_ref, attenuation, offset)
324 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
325 }
326
327 pub fn disconnect_cable(&mut self, cable_id: usize) -> Result<(), JsValue> {
332 self.patch
333 .disconnect(cable_id as CableId)
334 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
335 }
336
337 pub fn disconnect_by_index(&mut self, cable_index: usize) -> Result<(), JsValue> {
343 let cable_id = self
344 .patch
345 .cables()
346 .get(cable_index)
347 .map(|c| c.id)
348 .ok_or_else(|| JsValue::from_str(&format!("Invalid cable index: {}", cable_index)))?;
349 self.patch
350 .disconnect(cable_id)
351 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
352 }
353
354 pub fn disconnect(&mut self, from: &str, to: &str) -> Result<(), JsValue> {
356 let (from_module, from_port) = parse_port_ref(from)?;
357 let (to_module, to_port) = parse_port_ref(to)?;
358
359 let from_handle = self
360 .patch
361 .get_handle_by_name(from_module)
362 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", from_module)))?;
363 let to_handle = self
364 .patch
365 .get_handle_by_name(to_module)
366 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", to_module)))?;
367
368 self.patch
369 .disconnect_ports(from_handle.out(from_port), to_handle.in_(to_port))
370 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
371 }
372
373 pub fn get_module_names(&self) -> Result<JsValue, JsValue> {
375 let names = self.patch.module_names();
376 serde_wasm_bindgen::to_value(&names).map_err(|e| JsValue::from_str(&e.to_string()))
377 }
378
379 pub fn get_params(&self, node_name: &str) -> Result<JsValue, JsValue> {
388 let type_id = self
390 .patch
391 .nodes()
392 .find(|(_, name, _)| *name == node_name)
393 .map(|(_, _, module)| module.type_id())
394 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
395
396 let metadata = self
398 .registry
399 .get_metadata(type_id)
400 .ok_or_else(|| JsValue::from_str(&format!("Unknown module type: {}", type_id)))?;
401
402 serde_wasm_bindgen::to_value(&metadata.port_spec)
404 .map_err(|e| JsValue::from_str(&e.to_string()))
405 }
406
407 pub fn set_param(
409 &mut self,
410 node_name: &str,
411 param_index: u32,
412 value: f64,
413 ) -> Result<(), JsValue> {
414 let node_id = self
415 .get_node_id_by_name(node_name)
416 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
417
418 self.patch.set_param(node_id, param_index, value);
419 Ok(())
420 }
421
422 pub fn get_param(&self, node_name: &str, param_index: u32) -> Result<f64, JsValue> {
424 let node_id = self
425 .get_node_id_by_name(node_name)
426 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
427
428 self.patch
429 .get_param(node_id, param_index)
430 .ok_or_else(|| JsValue::from_str(&format!("Param {} not found", param_index)))
431 }
432
433 pub fn set_param_by_name(
437 &mut self,
438 node_name: &str,
439 param_name: &str,
440 value: f64,
441 ) -> Result<(), JsValue> {
442 let param_id = self
444 .patch
445 .nodes()
446 .find(|(_, name, _)| *name == node_name)
447 .and_then(|(_, _, module)| {
448 module
449 .params()
450 .iter()
451 .find(|p| p.name == param_name)
452 .map(|p| p.id)
453 })
454 .ok_or_else(|| {
455 JsValue::from_str(&format!(
456 "Unknown parameter '{}' on module '{}'",
457 param_name, node_name
458 ))
459 })?;
460
461 let node_id = self
463 .get_node_id_by_name(node_name)
464 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", node_name)))?;
465 self.patch.set_param(node_id, param_id, value);
466 Ok(())
467 }
468
469 pub fn subscribe(&mut self, targets: JsValue) -> Result<(), JsValue> {
475 let targets: Vec<SubscriptionTarget> = serde_wasm_bindgen::from_value(targets)
476 .map_err(|e| JsValue::from_str(&e.to_string()))?;
477
478 self.observer.add_subscriptions(targets);
479 Ok(())
480 }
481
482 pub fn unsubscribe(&mut self, target_ids: JsValue) -> Result<(), JsValue> {
484 let ids: Vec<String> = serde_wasm_bindgen::from_value(target_ids)
485 .map_err(|e| JsValue::from_str(&e.to_string()))?;
486
487 self.observer.remove_subscriptions(&ids);
488 Ok(())
489 }
490
491 pub fn clear_subscriptions(&mut self) {
493 self.observer.clear_subscriptions();
494 }
495
496 pub fn poll_updates(&mut self) -> Result<JsValue, JsValue> {
498 let updates = self.observer.drain_updates();
499 serde_wasm_bindgen::to_value(&updates).map_err(|e| JsValue::from_str(&e.to_string()))
500 }
501
502 pub fn pending_update_count(&self) -> usize {
504 self.observer.pending_count()
505 }
506
507 pub fn tick(&mut self) -> Box<[f64]> {
514 let (left, right) = self.patch.tick();
515 Box::new([left, right])
516 }
517
518 pub fn process_block(&mut self, num_samples: usize) -> js_sys::Float32Array {
537 const SAFETY_LIMIT: f64 = 10.0; if self.block_left.len() < num_samples {
541 self.block_left.resize(num_samples, 0.0);
542 self.block_right.resize(num_samples, 0.0);
543 }
544 let interleaved_len = num_samples * 2;
545 if self.block_interleaved.len() < interleaved_len {
546 self.block_interleaved.resize(interleaved_len, 0.0);
547 }
548
549 self.patch.tick_block(
551 &mut self.block_left[..num_samples],
552 &mut self.block_right[..num_samples],
553 );
554
555 for i in 0..num_samples {
557 let left = self.block_left[i].clamp(-SAFETY_LIMIT, SAFETY_LIMIT) as f32;
558 let right = self.block_right[i].clamp(-SAFETY_LIMIT, SAFETY_LIMIT) as f32;
559 self.block_interleaved[i * 2] = left;
560 self.block_interleaved[i * 2 + 1] = right;
561 }
562
563 if self.observer_interval <= 1 || self.observer_countdown == 0 {
567 self.observer.collect_from_patch(&self.patch);
568 self.observer_countdown = self.observer_interval.saturating_sub(1);
569 } else {
570 self.observer_countdown -= 1;
571 }
572
573 unsafe { js_sys::Float32Array::view(&self.block_interleaved[..interleaved_len]) }
577 }
578
579 pub fn set_observer_interval(&mut self, blocks: u32) {
584 self.observer_interval = blocks.max(1);
585 self.observer_countdown = 0;
587 }
588
589 pub fn reset(&mut self) {
591 self.patch.reset();
592 }
593
594 pub fn compile(&mut self) -> Result<(), JsValue> {
596 self.patch
597 .compile()
598 .map_err(|e| JsValue::from_str(&format!("{:?}", e)))
599 }
600
601 pub fn add_midi_inputs(&mut self) {
626 if self.patch.get_node_id_by_name(MIDI_VOCT_MODULE).is_none() {
627 self.patch.add_boxed(
628 MIDI_VOCT_MODULE,
629 Box::new(ExternalInput::voct(Arc::clone(&self.midi.voct))),
630 );
631 }
632 if self.patch.get_node_id_by_name(MIDI_GATE_MODULE).is_none() {
633 self.patch.add_boxed(
634 MIDI_GATE_MODULE,
635 Box::new(ExternalInput::gate(Arc::clone(&self.midi.gate))),
636 );
637 }
638 if self
639 .patch
640 .get_node_id_by_name(MIDI_VELOCITY_MODULE)
641 .is_none()
642 {
643 self.patch.add_boxed(
644 MIDI_VELOCITY_MODULE,
645 Box::new(ExternalInput::cv(Arc::clone(&self.midi.velocity))),
646 );
647 }
648 if self.patch.get_node_id_by_name(MIDI_MOD_MODULE).is_none() {
649 self.patch.add_boxed(
650 MIDI_MOD_MODULE,
651 Box::new(ExternalInput::cv(Arc::clone(&self.midi.modulation))),
652 );
653 }
654 if self.patch.get_node_id_by_name(MIDI_BEND_MODULE).is_none() {
655 self.patch.add_boxed(
656 MIDI_BEND_MODULE,
657 Box::new(ExternalInput::cv_bipolar(Arc::clone(&self.midi.bend))),
658 );
659 }
660 }
661
662 pub fn midi_note_on(&mut self, note: u8, velocity: u8) -> Result<(), JsValue> {
672 let v_oct = Self::note_to_voct(note);
674 let vel = velocity as f64 / 127.0;
676
677 self.held_notes.retain(|&(n, _)| n != note);
681 self.held_notes.push((note, velocity));
682
683 self.midi_note = Some(v_oct);
684 self.midi_velocity = Some(vel);
685 self.midi_gate = true;
686
687 self.midi.voct.set(v_oct);
689 self.midi.velocity.set(vel);
690 self.midi.gate.set(5.0);
691
692 Ok(())
693 }
694
695 pub fn midi_note_off(&mut self, note: u8, _velocity: u8) -> Result<(), JsValue> {
705 self.held_notes.retain(|&(n, _)| n != note);
707
708 match self.held_notes.last().copied() {
709 Some((held_note, held_velocity)) => {
711 let v_oct = Self::note_to_voct(held_note);
712 let vel = held_velocity as f64 / 127.0;
713
714 self.midi_note = Some(v_oct);
715 self.midi_velocity = Some(vel);
716 self.midi_gate = true;
717
718 self.midi.voct.set(v_oct);
719 self.midi.velocity.set(vel);
720 self.midi.gate.set(5.0);
723 }
724 None => {
726 self.midi_gate = false;
727 self.midi.gate.set(0.0);
728 }
729 }
730
731 Ok(())
732 }
733
734 fn note_to_voct(note: u8) -> f64 {
736 (note as f64 - 60.0) / 12.0
737 }
738
739 #[wasm_bindgen(getter)]
741 pub fn midi_note(&self) -> f64 {
742 self.midi_note.unwrap_or(0.0)
743 }
744
745 #[wasm_bindgen(getter)]
747 pub fn midi_velocity(&self) -> f64 {
748 self.midi_velocity.unwrap_or(0.0)
749 }
750
751 #[wasm_bindgen(getter)]
753 pub fn midi_gate(&self) -> bool {
754 self.midi_gate
755 }
756
757 pub fn midi_cc(&mut self, cc: u8, value: u8) -> Result<(), JsValue> {
762 let normalized = value as f64 / 127.0;
763 self.midi_cc_values[cc as usize] = normalized;
764 if cc == 1 {
765 self.midi.modulation.set(normalized);
766 }
767 Ok(())
768 }
769
770 pub fn get_midi_cc(&self, cc: u8) -> f64 {
772 self.midi_cc_values.get(cc as usize).copied().unwrap_or(0.0)
773 }
774
775 pub fn midi_pitch_bend(&mut self, value: f64) -> Result<(), JsValue> {
781 self.midi_pitch_bend_value = value;
782 self.midi.bend.set(value * (2.0 / 12.0));
784 Ok(())
785 }
786
787 #[wasm_bindgen(getter)]
789 pub fn pitch_bend(&self) -> f64 {
790 self.midi_pitch_bend_value
791 }
792
793 pub fn get_port_spec(&self, type_id: &str) -> Result<JsValue, JsValue> {
799 let metadata = self
800 .registry
801 .get_metadata(type_id)
802 .ok_or_else(|| JsValue::from_str(&format!("Unknown module type: {}", type_id)))?;
803
804 serde_wasm_bindgen::to_value(&metadata.port_spec)
805 .map_err(|e| JsValue::from_str(&e.to_string()))
806 }
807
808 fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
814 self.patch.get_node_id_by_name(name)
815 }
816
817 fn resolve_ports(
823 &self,
824 from: &str,
825 to: &str,
826 ) -> Result<(crate::graph::PortRef, crate::graph::PortRef), JsValue> {
827 let (from_module, from_port) = parse_port_ref(from)?;
828 let (to_module, to_port) = parse_port_ref(to)?;
829
830 let from_handle = self
831 .patch
832 .get_handle_by_name(from_module)
833 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", from_module)))?;
834 let to_handle = self
835 .patch
836 .get_handle_by_name(to_module)
837 .ok_or_else(|| JsValue::from_str(&format!("Unknown module: {}", to_module)))?;
838
839 let from_ref = from_handle
840 .output(from_port)
841 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
842 let to_ref = to_handle
843 .input(to_port)
844 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
845 Ok((from_ref, to_ref))
846 }
847}
848
849fn parse_port_ref(s: &str) -> Result<(&str, &str), JsValue> {
852 let parts: Vec<&str> = s.splitn(2, '.').collect();
853 if parts.len() != 2 {
854 return Err(JsValue::from_str(&format!(
855 "Invalid port reference: {} (expected 'module.port')",
856 s
857 )));
858 }
859 Ok((parts[0], parts[1]))
860}
861
862fn parse_signal_kind(s: &str) -> Result<SignalKind, JsValue> {
863 match s {
864 "audio" => Ok(SignalKind::Audio),
865 "cv_bipolar" => Ok(SignalKind::CvBipolar),
866 "cv_unipolar" => Ok(SignalKind::CvUnipolar),
867 "volt_per_octave" => Ok(SignalKind::VoltPerOctave),
868 "gate" => Ok(SignalKind::Gate),
869 "trigger" => Ok(SignalKind::Trigger),
870 "clock" => Ok(SignalKind::Clock),
871 _ => Err(JsValue::from_str(&format!("Unknown signal kind: {}", s))),
872 }
873}
874
875#[cfg(all(test, feature = "wasm"))]
888mod native_tests {
889 use super::*;
890 use crate::graph::PatchError;
891 use crate::wasm::QuiverError;
892
893 #[test]
894 fn new_engine_reports_sample_rate_and_empty_patch() {
895 let engine = QuiverEngine::new(48_000.0);
896 assert_eq!(engine.sample_rate(), 48_000.0);
897 assert_eq!(engine.module_count(), 0);
898 assert_eq!(engine.cable_count(), 0);
899 assert_eq!(engine.pending_update_count(), 0);
900 }
901
902 #[test]
903 fn add_module_updates_count_and_clear_resets() {
904 let mut engine = QuiverEngine::new(44_100.0);
905 assert!(engine.add_module("vco", "osc").is_ok());
906 assert!(engine.add_module("stereo_output", "out").is_ok());
907 assert_eq!(engine.module_count(), 2);
908 engine.clear_patch();
909 assert_eq!(engine.module_count(), 0);
910 assert_eq!(engine.cable_count(), 0);
911 }
912
913 #[test]
914 fn connect_returns_cable_id_and_updates_cable_count() {
915 let mut engine = QuiverEngine::new(44_100.0);
916 engine.add_module("vco", "osc").unwrap();
917 engine.add_module("stereo_output", "out").unwrap();
918 let id = engine
919 .connect("osc.saw", "out.left")
920 .expect("valid connection");
921 assert_eq!(id, 0, "first cable id should be 0");
922 assert_eq!(engine.cable_count(), 1);
923 }
924
925 #[test]
926 fn connect_then_disconnect_round_trips() {
927 let mut engine = QuiverEngine::new(44_100.0);
928 engine.add_module("vco", "osc").unwrap();
929 engine.add_module("stereo_output", "out").unwrap();
930 let id = engine.connect("osc.saw", "out.left").ok().unwrap();
931 assert_eq!(engine.cable_count(), 1);
932 assert!(engine.disconnect_cable(id).is_ok());
933 assert_eq!(engine.cable_count(), 0);
934 }
935
936 #[test]
937 fn attenuated_and_modulated_connections_succeed() {
938 let mut engine = QuiverEngine::new(44_100.0);
939 engine.add_module("lfo", "lfo").unwrap();
940 engine.add_module("svf", "flt").unwrap();
941 assert!(engine.connect_attenuated("lfo.sin", "flt.fm", 0.5).is_ok());
942 assert!(engine
943 .connect_modulated("lfo.tri", "flt.cutoff", 0.5, 0.1)
944 .is_ok());
945 assert_eq!(engine.cable_count(), 2);
946 }
947
948 #[test]
949 fn compile_and_tick_produce_audio() {
950 let mut engine = QuiverEngine::new(44_100.0);
951 engine.add_module("vco", "osc").unwrap();
952 engine.add_module("stereo_output", "out").unwrap();
953 engine.connect("osc.saw", "out.left").ok().unwrap();
954 engine.connect("osc.saw", "out.right").ok().unwrap();
955 engine.set_output("out").unwrap();
956 assert!(engine.compile().is_ok());
957
958 let mut nonzero = 0;
959 for _ in 0..2000 {
960 let frame = engine.tick();
961 assert_eq!(frame.len(), 2, "tick must return a stereo frame");
962 if frame[0].abs() > 1e-9 {
963 nonzero += 1;
964 }
965 }
966 assert!(nonzero > 1000, "compiled VCO patch should produce audio");
967 }
968
969 #[test]
970 fn set_param_on_valid_module_succeeds() {
971 let mut engine = QuiverEngine::new(44_100.0);
974 engine.add_module("vco", "osc").unwrap();
975 assert!(engine.set_param("osc", 0, 1.0).is_ok());
976 }
977
978 #[test]
979 fn midi_state_round_trips() {
980 let mut engine = QuiverEngine::new(44_100.0);
981 engine.add_midi_inputs();
982 assert_eq!(engine.module_count(), 5, "add_midi_inputs adds 5 modules");
983
984 assert!(engine.midi_note_on(60, 100).is_ok());
985 assert_eq!(engine.midi_note(), 0.0, "note 60 (C4) maps to 0V");
986 assert!(engine.midi_velocity() > 0.0);
987 assert!(engine.midi_gate());
988
989 assert!(engine.midi_note_off(60, 0).is_ok());
990 assert!(!engine.midi_gate());
991
992 assert!(engine.midi_cc(1, 127).is_ok());
993 assert!((engine.get_midi_cc(1) - 1.0).abs() < 1e-9);
994
995 assert!(engine.midi_pitch_bend(0.5).is_ok());
996 assert_eq!(engine.pitch_bend(), 0.5);
997 }
998
999 #[test]
1000 fn note_off_keeps_gate_while_another_note_is_held() {
1001 let mut engine = QuiverEngine::new(44_100.0);
1004 engine.midi_note_on(60, 100).unwrap();
1005 engine.midi_note_on(64, 100).unwrap();
1006
1007 engine.midi_note_off(60, 0).unwrap();
1008 assert!(engine.midi_gate(), "gate stays open while note 64 is held");
1009 assert!(
1010 (engine.midi_note() - (64.0 - 60.0) / 12.0).abs() < 1e-9,
1011 "pitch tracks the still-held note 64"
1012 );
1013
1014 engine.midi_note_off(64, 0).unwrap();
1016 assert!(!engine.midi_gate(), "gate closes once no notes remain");
1017 }
1018
1019 #[test]
1020 fn note_off_last_note_priority_reverts_pitch_on_top_release() {
1021 let mut engine = QuiverEngine::new(44_100.0);
1024 engine.midi_note_on(60, 100).unwrap();
1025 engine.midi_note_on(67, 100).unwrap();
1026 assert!((engine.midi_note() - (67.0 - 60.0) / 12.0).abs() < 1e-9);
1027
1028 engine.midi_note_off(67, 0).unwrap();
1029 assert!(engine.midi_gate(), "gate stays open, 60 still held");
1030 assert!(
1031 engine.midi_note().abs() < 1e-9,
1032 "pitch reverts to note 60 (0V) under last-note priority"
1033 );
1034
1035 engine.midi_note_off(60, 0).unwrap();
1036 assert!(!engine.midi_gate());
1037 }
1038
1039 #[test]
1040 fn note_off_single_note_and_stray_release_close_the_gate() {
1041 let mut engine = QuiverEngine::new(44_100.0);
1044 engine.midi_note_on(62, 100).unwrap();
1045 assert!(engine.midi_gate());
1046 engine.midi_note_off(62, 0).unwrap();
1047 assert!(!engine.midi_gate());
1048
1049 engine.midi_note_off(90, 0).unwrap();
1050 assert!(!engine.midi_gate(), "stray note-off keeps the gate closed");
1051 }
1052
1053 #[test]
1054 fn note_on_dedupes_repeated_note_so_one_release_clears_it() {
1055 let mut engine = QuiverEngine::new(44_100.0);
1058 engine.midi_note_on(60, 100).unwrap();
1059 engine.midi_note_on(60, 110).unwrap();
1060 engine.midi_note_off(60, 0).unwrap();
1061 assert!(
1062 !engine.midi_gate(),
1063 "one release clears a re-pressed note (no duplicate stack entry)"
1064 );
1065 }
1066
1067 #[test]
1068 fn reset_and_clear_subscriptions_do_not_panic() {
1069 let mut engine = QuiverEngine::new(44_100.0);
1070 engine.add_module("vco", "osc").unwrap();
1071 engine.reset();
1072 engine.clear_subscriptions();
1073 engine.set_observer_interval(4);
1074 assert_eq!(engine.pending_update_count(), 0);
1075 }
1076
1077 #[test]
1078 fn quiver_error_maps_from_patch_error_and_strings() {
1079 assert_eq!(QuiverError::from("boom").message(), "boom");
1081 assert_eq!(
1082 QuiverError::from(alloc::string::String::from("halp")).message(),
1083 "halp"
1084 );
1085 assert_eq!(
1086 QuiverError::from(PatchError::InvalidCable).message(),
1087 "InvalidCable"
1088 );
1089 }
1090}