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)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_audio_brick_basic() {
563 let audio = AudioBrick::new("whisper-capture");
564 assert_eq!(audio.name, "whisper-capture");
565 assert_eq!(audio.inputs, 1);
566 assert_eq!(audio.outputs, 1);
567 }
568
569 #[test]
570 fn test_audio_brick_class_name() {
571 let audio = AudioBrick::new("whisper-capture");
572 assert_eq!(audio.class_name(), "WhisperCaptureProcessor");
573
574 let audio2 = AudioBrick::new("my_processor");
575 assert_eq!(audio2.class_name(), "MyProcessorProcessor");
576 }
577
578 #[test]
579 fn test_audio_param() {
580 let param = AudioParam::new("gain", 1.0).range(0.0, 2.0).a_rate();
581
582 assert_eq!(param.name, "gain");
583 assert_eq!(param.default_value, 1.0);
584 assert_eq!(param.min_value, 0.0);
585 assert_eq!(param.max_value, 2.0);
586 assert_eq!(param.automation_rate, "a-rate");
587 }
588
589 #[test]
590 fn test_audio_param_js_descriptor() {
591 let param = AudioParam::new("volume", 0.5).range(0.0, 1.0);
592 let js = param.to_js_descriptor();
593
594 assert!(js.contains("name: 'volume'"));
595 assert!(js.contains("defaultValue: 0.5"));
596 assert!(js.contains("minValue: 0"));
597 assert!(js.contains("maxValue: 1"));
598 }
599
600 #[test]
601 fn test_ring_buffer_config() {
602 let config = RingBufferConfig::new(48000).channels(2);
603
604 assert_eq!(config.size, 48000);
605 assert_eq!(config.channels, 2);
606 assert!(config.use_atomics);
607 }
608
609 #[test]
610 fn test_worklet_js_generation() {
611 let audio = AudioBrick::new("test-processor")
612 .param(AudioParam::new("gain", 1.0))
613 .with_ring_buffer(RingBufferConfig::new(24000));
614
615 let js = audio.to_worklet_js();
616
617 assert!(js.contains("Generated by probar"));
618 assert!(js.contains("class TestProcessorProcessor"));
619 assert!(js.contains("extends AudioWorkletProcessor"));
620 assert!(js.contains("parameterDescriptors"));
621 assert!(js.contains("process(inputs, outputs, parameters)"));
622 assert!(js.contains("registerProcessor('test-processor'"));
623 assert!(js.contains("RingBuffer"));
624 }
625
626 #[test]
627 fn test_audio_init_js_generation() {
628 let audio = AudioBrick::new("capture").with_ring_buffer(RingBufferConfig::new(48000));
629
630 let js = audio.to_audio_init_js();
631
632 assert!(js.contains("AudioContext"));
633 assert!(js.contains("audioWorklet.addModule"));
634 assert!(js.contains("SharedArrayBuffer"));
635 assert!(js.contains("AudioWorkletNode"));
636 }
637
638 #[test]
639 fn test_verification_valid() {
640 let audio = AudioBrick::new("test")
641 .param(AudioParam::new("gain", 1.0).range(0.0, 2.0))
642 .with_ring_buffer(RingBufferConfig::new(24000));
643
644 let result = audio.verify();
645 assert!(result.is_valid());
646 }
647
648 #[test]
649 fn test_verification_invalid_param() {
650 let audio = AudioBrick::new("test").param(AudioParam::new("bad", 1.0).range(2.0, 1.0)); let result = audio.verify();
653 assert!(!result.is_valid());
654 }
655}