1use super::{Brick, BrickAssertion, BrickBudget, BrickVerification};
27use std::time::Duration;
28
29#[derive(Debug, Clone)]
31pub struct AudioParam {
32 pub name: String,
34 pub default_value: f64,
36 pub min_value: f64,
38 pub max_value: f64,
40 pub automation_rate: String,
42}
43
44impl AudioParam {
45 #[must_use]
47 pub fn new(name: impl Into<String>, default_value: f64) -> Self {
48 Self {
49 name: name.into(),
50 default_value,
51 min_value: f64::MIN,
52 max_value: f64::MAX,
53 automation_rate: "k-rate".into(),
54 }
55 }
56
57 #[must_use]
59 pub fn range(mut self, min: f64, max: f64) -> Self {
60 self.min_value = min;
61 self.max_value = max;
62 self
63 }
64
65 #[must_use]
67 pub fn a_rate(mut self) -> Self {
68 self.automation_rate = "a-rate".into();
69 self
70 }
71
72 #[must_use]
74 pub fn k_rate(mut self) -> Self {
75 self.automation_rate = "k-rate".into();
76 self
77 }
78
79 #[must_use]
81 pub fn to_js_descriptor(&self) -> String {
82 format!(
83 "{{ name: '{}', defaultValue: {}, minValue: {}, maxValue: {}, automationRate: '{}' }}",
84 self.name, self.default_value, self.min_value, self.max_value, self.automation_rate
85 )
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct RingBufferConfig {
92 pub size: usize,
94 pub channels: usize,
96 pub use_atomics: bool,
98}
99
100impl Default for RingBufferConfig {
101 fn default() -> Self {
102 Self {
103 size: 48000, channels: 1,
105 use_atomics: true,
106 }
107 }
108}
109
110impl RingBufferConfig {
111 #[must_use]
113 pub fn new(size: usize) -> Self {
114 Self {
115 size,
116 ..Default::default()
117 }
118 }
119
120 #[must_use]
122 pub fn channels(mut self, channels: usize) -> Self {
123 self.channels = channels;
124 self
125 }
126
127 #[must_use]
129 pub fn without_atomics(mut self) -> Self {
130 self.use_atomics = false;
131 self
132 }
133}
134
135#[derive(Debug, Clone)]
137pub struct AudioBrick {
138 name: String,
140 inputs: usize,
142 outputs: usize,
144 params: Vec<AudioParam>,
146 ring_buffer: Option<RingBufferConfig>,
148 sample_rate: u32,
150}
151
152impl AudioBrick {
153 #[must_use]
155 pub fn new(name: impl Into<String>) -> Self {
156 Self {
157 name: name.into(),
158 inputs: 1,
159 outputs: 1,
160 params: Vec::new(),
161 ring_buffer: None,
162 sample_rate: 48000,
163 }
164 }
165
166 #[must_use]
168 pub fn inputs(mut self, count: usize) -> Self {
169 self.inputs = count;
170 self
171 }
172
173 #[must_use]
175 pub fn outputs(mut self, count: usize) -> Self {
176 self.outputs = count;
177 self
178 }
179
180 #[must_use]
182 pub fn param(mut self, param: AudioParam) -> Self {
183 self.params.push(param);
184 self
185 }
186
187 #[must_use]
189 pub fn with_ring_buffer(mut self, config: RingBufferConfig) -> Self {
190 self.ring_buffer = Some(config);
191 self
192 }
193
194 #[must_use]
196 pub fn sample_rate(mut self, rate: u32) -> Self {
197 self.sample_rate = rate;
198 self
199 }
200
201 #[must_use]
203 pub fn class_name(&self) -> String {
204 let mut result = String::new();
205 let mut capitalize_next = true;
206
207 for c in self.name.chars() {
208 if c == '-' || c == '_' {
209 capitalize_next = true;
210 } else if capitalize_next {
211 result.push(c.to_ascii_uppercase());
212 capitalize_next = false;
213 } else {
214 result.push(c);
215 }
216 }
217
218 result.push_str("Processor");
219 result
220 }
221
222 #[must_use]
224 pub fn to_worklet_js(&self) -> String {
225 let mut js = String::new();
226 let class_name = self.class_name();
227
228 js.push_str(&format!("// {} AudioWorklet Processor\n", class_name));
230 js.push_str("// Generated by probar - DO NOT EDIT MANUALLY\n\n");
231
232 if let Some(ref rb) = self.ring_buffer {
234 if rb.use_atomics {
235 js.push_str(&self.generate_ring_buffer_class(rb));
236 js.push('\n');
237 }
238 }
239
240 js.push_str(&format!(
242 "class {} extends AudioWorkletProcessor {{\n",
243 class_name
244 ));
245
246 if !self.params.is_empty() {
248 js.push_str(" static get parameterDescriptors() {\n");
249 js.push_str(" return [\n");
250 for param in &self.params {
251 js.push_str(&format!(" {},\n", param.to_js_descriptor()));
252 }
253 js.push_str(" ];\n");
254 js.push_str(" }\n\n");
255 }
256
257 js.push_str(" constructor() {\n");
259 js.push_str(" super();\n");
260
261 if self.ring_buffer.is_some() {
262 js.push_str(" this.ringBuffer = null;\n");
263 js.push_str(" this.port.onmessage = (e) => {\n");
264 js.push_str(" if (e.data.type === 'init' && e.data.ringBuffer) {\n");
265 js.push_str(" this.ringBuffer = new RingBuffer(e.data.ringBuffer);\n");
266 js.push_str(" }\n");
267 js.push_str(" };\n");
268 }
269
270 js.push_str(" }\n\n");
271
272 js.push_str(" process(inputs, outputs, parameters) {\n");
274 js.push_str(" const input = inputs[0];\n");
275 js.push_str(" if (!input || !input[0]) return true;\n\n");
276
277 if self.outputs > 0 {
279 js.push_str(" const output = outputs[0];\n");
280 js.push_str(" for (let channel = 0; channel < input.length; channel++) {\n");
281 js.push_str(" if (output[channel]) {\n");
282 js.push_str(" output[channel].set(input[channel]);\n");
283 js.push_str(" }\n");
284 js.push_str(" }\n\n");
285 }
286
287 if self.ring_buffer.is_some() {
289 js.push_str(" // Write to ring buffer for worker consumption\n");
290 js.push_str(" if (this.ringBuffer) {\n");
291 js.push_str(" this.ringBuffer.write(input[0]);\n");
292 js.push_str(" }\n\n");
293 }
294
295 js.push_str(" return true; // Keep processor alive\n");
296 js.push_str(" }\n");
297 js.push_str("}\n\n");
298
299 js.push_str(&format!(
301 "registerProcessor('{}', {});\n",
302 self.name, class_name
303 ));
304
305 js
306 }
307
308 fn generate_ring_buffer_class(&self, config: &RingBufferConfig) -> String {
310 format!(
311 r#"// Lock-free ring buffer using SharedArrayBuffer + Atomics
312class RingBuffer {{
313 constructor(sab) {{
314 this.buffer = new Float32Array(sab, 8, {size});
315 this.state = new Int32Array(sab, 0, 2); // [writeIdx, readIdx]
316 }}
317
318 write(samples) {{
319 const writeIdx = Atomics.load(this.state, 0);
320 const len = samples.length;
321 const bufferLen = this.buffer.length;
322
323 for (let i = 0; i < len; i++) {{
324 this.buffer[(writeIdx + i) % bufferLen] = samples[i];
325 }}
326
327 Atomics.store(this.state, 0, (writeIdx + len) % bufferLen);
328 Atomics.notify(this.state, 0);
329 }}
330
331 read(samples) {{
332 const readIdx = Atomics.load(this.state, 1);
333 const writeIdx = Atomics.load(this.state, 0);
334 const bufferLen = this.buffer.length;
335
336 let available = writeIdx - readIdx;
337 if (available < 0) available += bufferLen;
338
339 const toRead = Math.min(samples.length, available);
340
341 for (let i = 0; i < toRead; i++) {{
342 samples[i] = this.buffer[(readIdx + i) % bufferLen];
343 }}
344
345 Atomics.store(this.state, 1, (readIdx + toRead) % bufferLen);
346 return toRead;
347 }}
348
349 available() {{
350 const readIdx = Atomics.load(this.state, 1);
351 const writeIdx = Atomics.load(this.state, 0);
352 let available = writeIdx - readIdx;
353 if (available < 0) available += {size};
354 return available;
355 }}
356}}
357"#,
358 size = config.size
359 )
360 }
361
362 #[must_use]
364 pub fn to_audio_init_js(&self) -> String {
365 let mut js = String::new();
366
367 js.push_str("// Audio Pipeline Initialization\n");
368 js.push_str("// Generated by probar - DO NOT EDIT MANUALLY\n\n");
369
370 js.push_str("async function initAudio(workletUrl) {\n");
371 js.push_str(" const audioContext = new AudioContext();\n");
372 js.push_str(" await audioContext.audioWorklet.addModule(workletUrl);\n\n");
373
374 if let Some(ref rb) = self.ring_buffer {
376 let buffer_bytes = rb.size * 4 + 8; js.push_str(&format!(
378 " // Ring buffer: {} samples ({} bytes)\n",
379 rb.size, buffer_bytes
380 ));
381 js.push_str(&format!(
382 " const ringBufferSab = new SharedArrayBuffer({});\n\n",
383 buffer_bytes
384 ));
385 }
386
387 js.push_str(&format!(
389 " const workletNode = new AudioWorkletNode(audioContext, '{}');\n",
390 self.name
391 ));
392
393 if self.ring_buffer.is_some() {
395 js.push_str(
396 " workletNode.port.postMessage({ type: 'init', ringBuffer: ringBufferSab });\n",
397 );
398 }
399
400 js.push_str("\n return { audioContext, workletNode");
401 if self.ring_buffer.is_some() {
402 js.push_str(", ringBufferSab");
403 }
404 js.push_str(" };\n");
405 js.push_str("}\n");
406
407 js
408 }
409
410 #[must_use]
412 pub fn to_rust_bindings(&self) -> String {
413 let mut rust = String::new();
414
415 rust.push_str(&format!("//! {} Audio Bindings\n", self.class_name()));
416 rust.push_str("//! Generated by probar - DO NOT EDIT MANUALLY\n\n");
417
418 if let Some(ref rb) = self.ring_buffer {
419 rust.push_str("use std::sync::atomic::{AtomicI32, Ordering};\n\n");
420
421 rust.push_str(&format!(
422 "pub const RING_BUFFER_SIZE: usize = {};\n",
423 rb.size
424 ));
425 rust.push_str(&format!(
426 "pub const RING_BUFFER_CHANNELS: usize = {};\n\n",
427 rb.channels
428 ));
429
430 rust.push_str("/// Lock-free ring buffer for audio data transfer\n");
431 rust.push_str("pub struct AudioRingBuffer {\n");
432 rust.push_str(" buffer: js_sys::Float32Array,\n");
433 rust.push_str(" state: js_sys::Int32Array,\n");
434 rust.push_str("}\n\n");
435
436 rust.push_str("impl AudioRingBuffer {\n");
437 rust.push_str(" /// Create from SharedArrayBuffer\n");
438 rust.push_str(" pub fn new(sab: js_sys::SharedArrayBuffer) -> Self {\n");
439 rust.push_str(&format!(
440 " let buffer = js_sys::Float32Array::new_with_byte_offset_and_length(&sab, 8, {});\n",
441 rb.size
442 ));
443 rust.push_str(" let state = js_sys::Int32Array::new_with_byte_offset_and_length(&sab, 0, 2);\n");
444 rust.push_str(" Self { buffer, state }\n");
445 rust.push_str(" }\n\n");
446
447 rust.push_str(" /// Read available samples\n");
448 rust.push_str(" pub fn read(&self, output: &mut [f32]) -> usize {\n");
449 rust.push_str(" // Implementation uses Atomics for thread-safe access\n");
450 rust.push_str(" let read_idx = self.state.get_index(1) as usize;\n");
451 rust.push_str(" let write_idx = self.state.get_index(0) as usize;\n");
452 rust.push_str(&format!(" let buffer_len = {};\n", rb.size));
453 rust.push_str(" \n");
454 rust.push_str(" let mut available = write_idx as i32 - read_idx as i32;\n");
455 rust.push_str(" if available < 0 { available += buffer_len as i32; }\n");
456 rust.push_str(" \n");
457 rust.push_str(" let to_read = output.len().min(available as usize);\n");
458 rust.push_str(" for i in 0..to_read {\n");
459 rust.push_str(" output[i] = self.buffer.get_index(((read_idx + i) % buffer_len) as u32);\n");
460 rust.push_str(" }\n");
461 rust.push_str(" \n");
462 rust.push_str(
463 " self.state.set_index(1, ((read_idx + to_read) % buffer_len) as i32);\n",
464 );
465 rust.push_str(" to_read\n");
466 rust.push_str(" }\n\n");
467
468 rust.push_str(" /// Get number of available samples\n");
469 rust.push_str(" pub fn available(&self) -> usize {\n");
470 rust.push_str(" let read_idx = self.state.get_index(1) as i32;\n");
471 rust.push_str(" let write_idx = self.state.get_index(0) as i32;\n");
472 rust.push_str(" let mut available = write_idx - read_idx;\n");
473 rust.push_str(&format!(
474 " if available < 0 {{ available += {}; }}\n",
475 rb.size
476 ));
477 rust.push_str(" available as usize\n");
478 rust.push_str(" }\n");
479 rust.push_str("}\n");
480 }
481
482 rust
483 }
484}
485
486impl Brick for AudioBrick {
487 fn brick_name(&self) -> &'static str {
488 "AudioBrick"
489 }
490
491 fn assertions(&self) -> &[BrickAssertion] {
492 &[]
493 }
494
495 fn budget(&self) -> BrickBudget {
496 BrickBudget::uniform(3)
499 }
500
501 fn verify(&self) -> BrickVerification {
502 let mut passed = Vec::new();
503 let mut failed = Vec::new();
504
505 if let Some(ref rb) = self.ring_buffer {
507 if rb.size >= 128 && rb.size <= 48000 * 10 {
508 passed.push(BrickAssertion::Custom {
509 name: "ring_buffer_size_valid".into(),
510 validator_id: 20,
511 });
512 } else {
513 failed.push((
514 BrickAssertion::Custom {
515 name: "ring_buffer_size_valid".into(),
516 validator_id: 20,
517 },
518 format!("Ring buffer size {} out of range (128-480000)", rb.size),
519 ));
520 }
521 }
522
523 for param in &self.params {
525 if param.min_value < param.max_value {
526 passed.push(BrickAssertion::Custom {
527 name: format!("param_{}_range_valid", param.name),
528 validator_id: 21,
529 });
530 } else {
531 failed.push((
532 BrickAssertion::Custom {
533 name: format!("param_{}_range_valid", param.name),
534 validator_id: 21,
535 },
536 "min >= max".into(),
537 ));
538 }
539 }
540
541 BrickVerification {
542 passed,
543 failed,
544 verification_time: Duration::from_micros(50),
545 }
546 }
547
548 fn to_html(&self) -> String {
549 String::new()
550 }
551
552 fn to_css(&self) -> String {
553 String::new()
554 }
555}
556
557#[cfg(test)]
558#[allow(clippy::unwrap_used, clippy::expect_used)]
559mod tests {
560 use super::*;
561
562 #[test]
567 fn test_audio_param_new() {
568 let param = AudioParam::new("gain", 1.0);
569
570 assert_eq!(param.name, "gain");
571 assert_eq!(param.default_value, 1.0);
572 assert_eq!(param.min_value, f64::MIN);
573 assert_eq!(param.max_value, f64::MAX);
574 assert_eq!(param.automation_rate, "k-rate");
575 }
576
577 #[test]
578 fn test_audio_param() {
579 let param = AudioParam::new("gain", 1.0).range(0.0, 2.0).a_rate();
580
581 assert_eq!(param.name, "gain");
582 assert_eq!(param.default_value, 1.0);
583 assert_eq!(param.min_value, 0.0);
584 assert_eq!(param.max_value, 2.0);
585 assert_eq!(param.automation_rate, "a-rate");
586 }
587
588 #[test]
589 fn test_audio_param_k_rate() {
590 let param = AudioParam::new("frequency", 440.0).k_rate();
591 assert_eq!(param.automation_rate, "k-rate");
592 }
593
594 #[test]
595 fn test_audio_param_js_descriptor() {
596 let param = AudioParam::new("volume", 0.5).range(0.0, 1.0);
597 let js = param.to_js_descriptor();
598
599 assert!(js.contains("name: 'volume'"));
600 assert!(js.contains("defaultValue: 0.5"));
601 assert!(js.contains("minValue: 0"));
602 assert!(js.contains("maxValue: 1"));
603 assert!(js.contains("automationRate: 'k-rate'"));
604 }
605
606 #[test]
607 fn test_audio_param_js_descriptor_a_rate() {
608 let param = AudioParam::new("pan", 0.0).range(-1.0, 1.0).a_rate();
609 let js = param.to_js_descriptor();
610
611 assert!(js.contains("automationRate: 'a-rate'"));
612 }
613
614 #[test]
615 fn test_audio_param_debug_and_clone() {
616 let param = AudioParam::new("test", 0.5);
617 let cloned = param;
618
619 assert_eq!(cloned.name, "test");
620 assert!(format!("{:?}", cloned).contains("AudioParam"));
621 }
622
623 #[test]
628 fn test_ring_buffer_config_default() {
629 let config = RingBufferConfig::default();
630
631 assert_eq!(config.size, 48000);
632 assert_eq!(config.channels, 1);
633 assert!(config.use_atomics);
634 }
635
636 #[test]
637 fn test_ring_buffer_config_new() {
638 let config = RingBufferConfig::new(24000);
639
640 assert_eq!(config.size, 24000);
641 assert_eq!(config.channels, 1);
642 assert!(config.use_atomics);
643 }
644
645 #[test]
646 fn test_ring_buffer_config() {
647 let config = RingBufferConfig::new(48000).channels(2);
648
649 assert_eq!(config.size, 48000);
650 assert_eq!(config.channels, 2);
651 assert!(config.use_atomics);
652 }
653
654 #[test]
655 fn test_ring_buffer_config_without_atomics() {
656 let config = RingBufferConfig::new(16000).without_atomics();
657
658 assert!(!config.use_atomics);
659 }
660
661 #[test]
662 fn test_ring_buffer_config_chained() {
663 let config = RingBufferConfig::new(96000).channels(4).without_atomics();
664
665 assert_eq!(config.size, 96000);
666 assert_eq!(config.channels, 4);
667 assert!(!config.use_atomics);
668 }
669
670 #[test]
671 fn test_ring_buffer_config_debug_and_clone() {
672 let config = RingBufferConfig::new(48000);
673 let cloned = config;
674
675 assert_eq!(cloned.size, 48000);
676 assert!(format!("{:?}", cloned).contains("RingBufferConfig"));
677 }
678
679 #[test]
684 fn test_audio_brick_basic() {
685 let audio = AudioBrick::new("whisper-capture");
686 assert_eq!(audio.name, "whisper-capture");
687 assert_eq!(audio.inputs, 1);
688 assert_eq!(audio.outputs, 1);
689 }
690
691 #[test]
692 fn test_audio_brick_inputs_outputs() {
693 let audio = AudioBrick::new("mixer").inputs(4).outputs(2);
694
695 assert_eq!(audio.inputs, 4);
696 assert_eq!(audio.outputs, 2);
697 }
698
699 #[test]
700 fn test_audio_brick_sample_rate() {
701 let audio = AudioBrick::new("test").sample_rate(44100);
702 assert_eq!(audio.sample_rate, 44100);
703 }
704
705 #[test]
706 fn test_audio_brick_with_param() {
707 let audio = AudioBrick::new("processor").param(AudioParam::new("gain", 1.0));
708
709 assert_eq!(audio.params.len(), 1);
710 assert_eq!(audio.params[0].name, "gain");
711 }
712
713 #[test]
714 fn test_audio_brick_with_multiple_params() {
715 let audio = AudioBrick::new("eq")
716 .param(AudioParam::new("low", 0.0).range(-12.0, 12.0))
717 .param(AudioParam::new("mid", 0.0).range(-12.0, 12.0))
718 .param(AudioParam::new("high", 0.0).range(-12.0, 12.0));
719
720 assert_eq!(audio.params.len(), 3);
721 }
722
723 #[test]
724 fn test_audio_brick_with_ring_buffer() {
725 let audio =
726 AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(48000).channels(2));
727
728 assert!(audio.ring_buffer.is_some());
729 let rb = audio.ring_buffer.unwrap();
730 assert_eq!(rb.size, 48000);
731 assert_eq!(rb.channels, 2);
732 }
733
734 #[test]
735 fn test_audio_brick_class_name() {
736 let audio = AudioBrick::new("whisper-capture");
737 assert_eq!(audio.class_name(), "WhisperCaptureProcessor");
738
739 let audio2 = AudioBrick::new("my_processor");
740 assert_eq!(audio2.class_name(), "MyProcessorProcessor");
741 }
742
743 #[test]
744 fn test_audio_brick_class_name_single_word() {
745 let audio = AudioBrick::new("processor");
746 assert_eq!(audio.class_name(), "ProcessorProcessor");
747 }
748
749 #[test]
750 fn test_audio_brick_class_name_complex() {
751 let audio = AudioBrick::new("my-complex_audio-processor");
752 assert_eq!(audio.class_name(), "MyComplexAudioProcessorProcessor");
753 }
754
755 #[test]
756 fn test_audio_brick_debug_and_clone() {
757 let audio = AudioBrick::new("test")
758 .param(AudioParam::new("gain", 1.0))
759 .with_ring_buffer(RingBufferConfig::default());
760
761 let cloned = audio;
762 assert_eq!(cloned.name, "test");
763 assert!(format!("{:?}", cloned).contains("AudioBrick"));
764 }
765
766 #[test]
771 fn test_worklet_js_generation() {
772 let audio = AudioBrick::new("test-processor")
773 .param(AudioParam::new("gain", 1.0))
774 .with_ring_buffer(RingBufferConfig::new(24000));
775
776 let js = audio.to_worklet_js();
777
778 assert!(js.contains("Generated by probar"));
779 assert!(js.contains("class TestProcessorProcessor"));
780 assert!(js.contains("extends AudioWorkletProcessor"));
781 assert!(js.contains("parameterDescriptors"));
782 assert!(js.contains("process(inputs, outputs, parameters)"));
783 assert!(js.contains("registerProcessor('test-processor'"));
784 assert!(js.contains("RingBuffer"));
785 }
786
787 #[test]
788 fn test_worklet_js_without_params() {
789 let audio = AudioBrick::new("simple").with_ring_buffer(RingBufferConfig::new(24000));
790
791 let js = audio.to_worklet_js();
792
793 assert!(!js.contains("parameterDescriptors"));
795 assert!(js.contains("class SimpleProcessor"));
796 }
797
798 #[test]
799 fn test_worklet_js_without_ring_buffer() {
800 let audio = AudioBrick::new("passthrough").param(AudioParam::new("gain", 1.0));
801
802 let js = audio.to_worklet_js();
803
804 assert!(!js.contains("class RingBuffer"));
806 assert!(!js.contains("this.ringBuffer"));
807 assert!(js.contains("parameterDescriptors"));
808 }
809
810 #[test]
811 fn test_worklet_js_no_outputs() {
812 let audio = AudioBrick::new("sink")
813 .outputs(0)
814 .with_ring_buffer(RingBufferConfig::new(24000));
815
816 let js = audio.to_worklet_js();
817
818 assert!(!js.contains("const output = outputs[0]"));
820 }
821
822 #[test]
823 fn test_worklet_js_ring_buffer_class() {
824 let audio = AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(48000));
825
826 let js = audio.to_worklet_js();
827
828 assert!(js.contains("class RingBuffer"));
830 assert!(js.contains("constructor(sab)"));
831 assert!(js.contains("write(samples)"));
832 assert!(js.contains("read(samples)"));
833 assert!(js.contains("available()"));
834 assert!(js.contains("Atomics.load"));
835 assert!(js.contains("Atomics.store"));
836 assert!(js.contains("Atomics.notify"));
837 }
838
839 #[test]
840 fn test_worklet_js_ring_buffer_without_atomics() {
841 let audio = AudioBrick::new("simple")
842 .with_ring_buffer(RingBufferConfig::new(24000).without_atomics());
843
844 let js = audio.to_worklet_js();
845
846 assert!(!js.contains("class RingBuffer"));
848 }
849
850 #[test]
855 fn test_audio_init_js_generation() {
856 let audio = AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(48000));
857
858 let js = audio.to_audio_init_js();
859
860 assert!(js.contains("AudioContext"));
861 assert!(js.contains("audioWorklet.addModule"));
862 assert!(js.contains("SharedArrayBuffer"));
863 assert!(js.contains("AudioWorkletNode"));
864 }
865
866 #[test]
867 fn test_audio_init_js_without_ring_buffer() {
868 let audio = AudioBrick::new("passthrough");
869
870 let js = audio.to_audio_init_js();
871
872 assert!(js.contains("AudioContext"));
873 assert!(js.contains("AudioWorkletNode"));
874 assert!(!js.contains("SharedArrayBuffer"));
875 assert!(!js.contains("ringBufferSab"));
876 }
877
878 #[test]
879 fn test_audio_init_js_ring_buffer_size_calculation() {
880 let audio = AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(48000));
881
882 let js = audio.to_audio_init_js();
883
884 assert!(js.contains("Ring buffer: 48000 samples"));
886 assert!(js.contains("SharedArrayBuffer(192008)"));
887 }
888
889 #[test]
890 fn test_audio_init_js_posts_ring_buffer() {
891 let audio = AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(24000));
892
893 let js = audio.to_audio_init_js();
894
895 assert!(js.contains("workletNode.port.postMessage"));
896 assert!(js.contains("type: 'init'"));
897 assert!(js.contains("ringBuffer: ringBufferSab"));
898 }
899
900 #[test]
905 fn test_rust_bindings_generation() {
906 let audio =
907 AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(48000).channels(2));
908
909 let rust = audio.to_rust_bindings();
910
911 assert!(rust.contains("CaptureProcessor Audio Bindings"));
912 assert!(rust.contains("Generated by probar"));
913 assert!(rust.contains("RING_BUFFER_SIZE: usize = 48000"));
914 assert!(rust.contains("RING_BUFFER_CHANNELS: usize = 2"));
915 assert!(rust.contains("struct AudioRingBuffer"));
916 assert!(rust.contains("fn new(sab: js_sys::SharedArrayBuffer)"));
917 assert!(rust.contains("fn read(&self, output: &mut [f32])"));
918 assert!(rust.contains("fn available(&self)"));
919 }
920
921 #[test]
922 fn test_rust_bindings_without_ring_buffer() {
923 let audio = AudioBrick::new("passthrough");
924
925 let rust = audio.to_rust_bindings();
926
927 assert!(rust.contains("PassthroughProcessor Audio Bindings"));
928 assert!(!rust.contains("RING_BUFFER_SIZE"));
929 assert!(!rust.contains("struct AudioRingBuffer"));
930 }
931
932 #[test]
937 fn test_audio_brick_brick_name() {
938 let audio = AudioBrick::new("test");
939 assert_eq!(audio.brick_name(), "AudioBrick");
940 }
941
942 #[test]
943 fn test_audio_brick_assertions() {
944 let audio = AudioBrick::new("test");
945 assert!(audio.assertions().is_empty());
946 }
947
948 #[test]
949 fn test_audio_brick_budget() {
950 let audio = AudioBrick::new("test");
951 let budget = audio.budget();
952 assert_eq!(budget.as_duration(), Duration::from_millis(3));
954 }
955
956 #[test]
957 fn test_audio_brick_to_html() {
958 let audio = AudioBrick::new("test");
959 assert!(audio.to_html().is_empty());
960 }
961
962 #[test]
963 fn test_audio_brick_to_css() {
964 let audio = AudioBrick::new("test");
965 assert!(audio.to_css().is_empty());
966 }
967
968 #[test]
973 fn test_verification_valid() {
974 let audio = AudioBrick::new("test")
975 .param(AudioParam::new("gain", 1.0).range(0.0, 2.0))
976 .with_ring_buffer(RingBufferConfig::new(24000));
977
978 let result = audio.verify();
979 assert!(result.is_valid());
980 }
981
982 #[test]
983 fn test_verification_invalid_param() {
984 let audio = AudioBrick::new("test").param(AudioParam::new("bad", 1.0).range(2.0, 1.0)); let result = audio.verify();
987 assert!(!result.is_valid());
988 }
989
990 #[test]
991 fn test_verification_ring_buffer_too_small() {
992 let audio = AudioBrick::new("test").with_ring_buffer(RingBufferConfig {
993 size: 64, channels: 1,
995 use_atomics: true,
996 });
997
998 let result = audio.verify();
999 assert!(!result.is_valid());
1000 }
1001
1002 #[test]
1003 fn test_verification_ring_buffer_too_large() {
1004 let audio = AudioBrick::new("test").with_ring_buffer(RingBufferConfig {
1005 size: 48000 * 11, channels: 1,
1007 use_atomics: true,
1008 });
1009
1010 let result = audio.verify();
1011 assert!(!result.is_valid());
1012 }
1013
1014 #[test]
1015 fn test_verification_ring_buffer_edge_cases() {
1016 let audio_min = AudioBrick::new("test").with_ring_buffer(RingBufferConfig {
1018 size: 128,
1019 channels: 1,
1020 use_atomics: true,
1021 });
1022 assert!(audio_min.verify().is_valid());
1023
1024 let audio_max = AudioBrick::new("test").with_ring_buffer(RingBufferConfig {
1026 size: 48000 * 10,
1027 channels: 1,
1028 use_atomics: true,
1029 });
1030 assert!(audio_max.verify().is_valid());
1031 }
1032
1033 #[test]
1034 fn test_verification_no_ring_buffer() {
1035 let audio = AudioBrick::new("test").param(AudioParam::new("gain", 1.0).range(0.0, 2.0));
1036
1037 let result = audio.verify();
1038 assert!(result.is_valid());
1039 }
1040
1041 #[test]
1042 fn test_verification_multiple_params() {
1043 let audio = AudioBrick::new("eq")
1044 .param(AudioParam::new("low", 0.0).range(-12.0, 12.0))
1045 .param(AudioParam::new("mid", 0.0).range(-12.0, 12.0))
1046 .param(AudioParam::new("high", 0.0).range(-12.0, 12.0));
1047
1048 let result = audio.verify();
1049 assert!(result.is_valid());
1050 assert_eq!(result.passed.len(), 3);
1052 }
1053
1054 #[test]
1055 fn test_verification_mixed_valid_invalid_params() {
1056 let audio = AudioBrick::new("test")
1057 .param(AudioParam::new("good", 0.5).range(0.0, 1.0))
1058 .param(AudioParam::new("bad", 0.5).range(1.0, 0.0)); let result = audio.verify();
1061 assert!(!result.is_valid());
1062 assert_eq!(result.passed.len(), 1);
1064 assert_eq!(result.failed.len(), 1);
1065 }
1066
1067 #[test]
1072 fn test_full_audio_brick_workflow() {
1073 let audio = AudioBrick::new("whisper-capture")
1074 .inputs(1)
1075 .outputs(1)
1076 .sample_rate(16000)
1077 .param(AudioParam::new("gain", 1.0).range(0.0, 2.0).a_rate())
1078 .param(
1079 AudioParam::new("threshold", -40.0)
1080 .range(-60.0, 0.0)
1081 .k_rate(),
1082 )
1083 .with_ring_buffer(RingBufferConfig::new(144000).channels(1));
1084
1085 assert_eq!(audio.name, "whisper-capture");
1087 assert_eq!(audio.inputs, 1);
1088 assert_eq!(audio.outputs, 1);
1089 assert_eq!(audio.sample_rate, 16000);
1090 assert_eq!(audio.params.len(), 2);
1091 assert!(audio.ring_buffer.is_some());
1092
1093 assert_eq!(audio.class_name(), "WhisperCaptureProcessor");
1095
1096 assert!(audio.verify().is_valid());
1098
1099 let worklet_js = audio.to_worklet_js();
1101 let init_js = audio.to_audio_init_js();
1102 let rust_bindings = audio.to_rust_bindings();
1103
1104 assert!(!worklet_js.is_empty());
1106 assert!(!init_js.is_empty());
1107 assert!(!rust_bindings.is_empty());
1108
1109 assert!(worklet_js.contains("class WhisperCaptureProcessor"));
1111 assert!(worklet_js.contains("gain"));
1112 assert!(worklet_js.contains("threshold"));
1113 assert!(init_js.contains("whisper-capture"));
1114 assert!(rust_bindings.contains("RING_BUFFER_SIZE: usize = 144000"));
1115 }
1116
1117 #[test]
1118 fn test_minimal_audio_brick() {
1119 let audio = AudioBrick::new("minimal");
1120
1121 assert_eq!(audio.inputs, 1);
1123 assert_eq!(audio.outputs, 1);
1124 assert_eq!(audio.sample_rate, 48000);
1125 assert!(audio.params.is_empty());
1126 assert!(audio.ring_buffer.is_none());
1127
1128 assert!(audio.verify().is_valid());
1130
1131 let js = audio.to_worklet_js();
1133 assert!(js.contains("class MinimalProcessor"));
1134 assert!(js.contains("registerProcessor('minimal'"));
1135 }
1136}