wavecraft_dev_server/audio/server.rs
1//! Audio server for full-duplex audio I/O in dev mode.
2//!
3//! This module provides an audio server that captures microphone input,
4//! processes it through a `DevAudioProcessor` (typically an `FfiProcessor`
5//! loaded from the user's cdylib), and sends the processed audio to the
6//! output device (speakers/headphones). Meter data is communicated back
7//! via a callback channel.
8//!
9//! # Architecture
10//!
11//! ```text
12//! OS Mic → cpal input callback → deinterleave → FfiProcessor::process()
13//! │
14//! ┌─────────┴──────────┐
15//! │ │
16//! meter compute interleave
17//! │ → SPSC ring
18//! ▼ │
19//! WebSocket broadcast │
20//! ▼
21//! cpal output callback → Speakers
22//! ```
23
24mod device_setup;
25mod input_pipeline;
26mod metering;
27mod output_modifiers;
28mod output_routing;
29mod startup_wiring;
30
31use std::sync::Arc;
32
33use anyhow::Result;
34use cpal::{Device, Stream, StreamConfig};
35use wavecraft_processors::OscilloscopeFrameConsumer;
36use wavecraft_protocol::MeterUpdateNotification;
37
38use super::atomic_params::AtomicParameterBridge;
39use super::ffi_processor::DevAudioProcessor;
40
41/// Configuration for audio server.
42#[derive(Debug, Clone)]
43pub struct AudioConfig {
44 /// Desired sample rate (e.g., 44100.0). Falls back to system default.
45 pub sample_rate: f32,
46 /// Buffer size in samples.
47 pub buffer_size: u32,
48}
49
50/// Handle returned by `AudioServer::start()` that keeps both audio
51/// streams alive. Drop this handle to stop audio capture and playback.
52pub struct AudioHandle {
53 _input_stream: Stream,
54 _output_stream: Option<Stream>,
55}
56
57/// Audio server that processes OS input through a `DevAudioProcessor`
58/// and routes the processed audio to the output device.
59pub struct AudioServer {
60 processor: Box<dyn DevAudioProcessor>,
61 config: AudioConfig,
62 input_device: Device,
63 output_device: Device,
64 input_config: StreamConfig,
65 output_config: StreamConfig,
66 param_bridge: Arc<AtomicParameterBridge>,
67}
68
69impl AudioServer {
70 /// Create a new audio server with the given processor, config, and
71 /// parameter bridge for lock-free audio-thread parameter reads.
72 pub fn new(
73 processor: Box<dyn DevAudioProcessor>,
74 config: AudioConfig,
75 param_bridge: Arc<AtomicParameterBridge>,
76 ) -> Result<Self> {
77 let negotiated = device_setup::negotiate_default_devices_and_configs()?;
78
79 Ok(Self {
80 processor,
81 config,
82 input_device: negotiated.input_device,
83 output_device: negotiated.output_device,
84 input_config: negotiated.input_config,
85 output_config: negotiated.output_config,
86 param_bridge,
87 })
88 }
89
90 /// Start audio capture, processing, and playback.
91 ///
92 /// Returns an `AudioHandle` that keeps both streams alive, plus a
93 /// `MeterConsumer` for draining meter frames from a lock-free ring
94 /// buffer (RT-safe: no allocations on the audio thread).
95 ///
96 /// Drop the handle to stop audio.
97 pub fn start(
98 mut self,
99 ) -> Result<(
100 AudioHandle,
101 rtrb::Consumer<MeterUpdateNotification>,
102 OscilloscopeFrameConsumer,
103 )> {
104 // Set sample rate from the actual input device config
105 let actual_sample_rate = self.input_config.sample_rate.0 as f32;
106 self.processor.set_sample_rate(actual_sample_rate);
107
108 let processor = self.processor;
109 let buffer_size = self.config.buffer_size as usize;
110 let input_channels = self.input_config.channels as usize;
111 let output_channels = self.output_config.channels as usize;
112 let param_bridge = Arc::clone(&self.param_bridge);
113
114 startup_wiring::start_audio_io(startup_wiring::StartAudioIoContext {
115 input_device: &self.input_device,
116 input_config: &self.input_config,
117 output_device: &self.output_device,
118 output_config: &self.output_config,
119 processor,
120 buffer_size,
121 input_channels,
122 output_channels,
123 param_bridge,
124 actual_sample_rate,
125 })
126 }
127
128 /// Returns true if an output device is available for audio playback.
129 pub fn has_output(&self) -> bool {
130 true
131 }
132}