⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/audio/
pipeline.rs

1//! Audio output pipeline
2//!
3//! Manages the flow of mixed audio data to the output device. The pipeline
4//! periodically calls the mixer to produce output frames and tracks
5//! statistics such as total frames processed and buffer underrun events.
6
7#![allow(dead_code)]
8
9use alloc::vec::Vec;
10use core::sync::atomic::{AtomicU64, Ordering};
11
12use crate::{audio::AudioConfig, error::KernelError};
13
14// ============================================================================
15// Pipeline State
16// ============================================================================
17
18/// Current state of the audio output pipeline
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum PipelineState {
21    /// Pipeline is idle (not producing output)
22    Idle,
23    /// Pipeline is actively processing and outputting audio
24    Running,
25    /// Pipeline is draining remaining buffered data before stopping
26    Draining,
27}
28
29/// Pipeline performance statistics
30#[derive(Debug, Clone, Copy)]
31pub struct PipelineStats {
32    /// Total number of audio frames processed since pipeline start
33    pub frames_processed: u64,
34    /// Number of buffer underrun events (mixer had no data)
35    pub underruns: u64,
36    /// Current pipeline state
37    pub state: PipelineState,
38}
39
40// ============================================================================
41// Audio Pipeline
42// ============================================================================
43
44/// Audio output pipeline
45///
46/// Coordinates the mixer with the output device, managing buffering,
47/// underrun detection, and output timing.
48pub struct AudioPipeline {
49    /// Current pipeline state
50    state: PipelineState,
51    /// Pre-allocated output buffer for mixed audio (one period)
52    output_buffer: Vec<i16>,
53    /// Output audio configuration
54    output_config: AudioConfig,
55    /// Total frames processed
56    frames_processed: AtomicU64,
57    /// Total underrun events
58    underruns: AtomicU64,
59}
60
61impl AudioPipeline {
62    /// Create a new audio pipeline with the given output configuration
63    pub fn new(config: AudioConfig) -> Self {
64        let buffer_size = config.buffer_frames as usize * config.channels as usize;
65        Self {
66            state: PipelineState::Idle,
67            output_buffer: alloc::vec![0i16; buffer_size],
68            output_config: config,
69            frames_processed: AtomicU64::new(0),
70            underruns: AtomicU64::new(0),
71        }
72    }
73
74    /// Process one frame period: mix all active channels into the output buffer
75    ///
76    /// Calls the mixer to fill the output buffer with mixed audio. Returns
77    /// a reference to the buffer containing the mixed samples.
78    pub fn process_frame(&mut self) -> &[i16] {
79        if self.state != PipelineState::Running {
80            // Not running: return silence
81            for sample in self.output_buffer.iter_mut() {
82                *sample = 0;
83            }
84            return &self.output_buffer;
85        }
86
87        // Ask the mixer to fill our output buffer
88        let result = crate::audio::mixer::with_mixer(|mixer| {
89            mixer.mix_to_output(&mut self.output_buffer);
90        });
91
92        match result {
93            Ok(()) => {
94                // Check if the output is all silence (possible underrun)
95                let all_silence = self.output_buffer.iter().all(|&s| s == 0);
96                if all_silence {
97                    self.underruns.fetch_add(1, Ordering::Relaxed);
98                }
99
100                // Submit mixed audio to VirtIO-Sound hardware (if present)
101                if !all_silence {
102                    let _ = crate::audio::virtio_sound::with_device(|dev| {
103                        let _ = dev.write_pcm(0, &self.output_buffer);
104                    });
105                }
106
107                let frames = self.output_config.buffer_frames as u64;
108                self.frames_processed.fetch_add(frames, Ordering::Relaxed);
109            }
110            Err(_) => {
111                // Mixer not available: output silence
112                for sample in self.output_buffer.iter_mut() {
113                    *sample = 0;
114                }
115                self.underruns.fetch_add(1, Ordering::Relaxed);
116            }
117        }
118
119        &self.output_buffer
120    }
121
122    /// Start the pipeline
123    pub fn start(&mut self) {
124        if self.state == PipelineState::Idle {
125            self.state = PipelineState::Running;
126            self.frames_processed.store(0, Ordering::Relaxed);
127            self.underruns.store(0, Ordering::Relaxed);
128            println!("[AUDIO] Pipeline started");
129        }
130    }
131
132    /// Stop the pipeline immediately
133    pub fn stop(&mut self) {
134        self.state = PipelineState::Idle;
135        // Clear output buffer
136        for sample in self.output_buffer.iter_mut() {
137            *sample = 0;
138        }
139        println!("[AUDIO] Pipeline stopped");
140    }
141
142    /// Drain the pipeline: process remaining buffered data then stop
143    pub fn drain(&mut self) {
144        if self.state == PipelineState::Running {
145            self.state = PipelineState::Draining;
146            println!("[AUDIO] Pipeline draining...");
147
148            // Process one last frame to flush any remaining data
149            self.process_frame_internal();
150
151            self.state = PipelineState::Idle;
152            println!("[AUDIO] Pipeline drain complete");
153        }
154    }
155
156    /// Internal frame processing (used during drain)
157    fn process_frame_internal(&mut self) {
158        let _ = crate::audio::mixer::with_mixer(|mixer| {
159            mixer.mix_to_output(&mut self.output_buffer);
160        });
161
162        let frames = self.output_config.buffer_frames as u64;
163        self.frames_processed.fetch_add(frames, Ordering::Relaxed);
164    }
165
166    /// Get pipeline statistics
167    pub fn stats(&self) -> PipelineStats {
168        PipelineStats {
169            frames_processed: self.frames_processed.load(Ordering::Relaxed),
170            underruns: self.underruns.load(Ordering::Relaxed),
171            state: self.state,
172        }
173    }
174
175    /// Get the current pipeline state
176    pub fn state(&self) -> PipelineState {
177        self.state
178    }
179
180    /// Get the output configuration
181    pub fn config(&self) -> &AudioConfig {
182        &self.output_config
183    }
184}
185
186// ============================================================================
187// Global Pipeline State
188// ============================================================================
189
190static PIPELINE: spin::Mutex<Option<AudioPipeline>> = spin::Mutex::new(None);
191
192/// Initialize the global audio pipeline
193pub fn init(config: AudioConfig) -> Result<(), KernelError> {
194    let mut pipeline = PIPELINE.lock();
195    if pipeline.is_some() {
196        return Err(KernelError::InvalidState {
197            expected: "uninitialized",
198            actual: "already initialized",
199        });
200    }
201
202    *pipeline = Some(AudioPipeline::new(config));
203    println!(
204        "[AUDIO] Pipeline initialized: {} Hz, {} ch, {} frames/period",
205        config.sample_rate, config.channels, config.buffer_frames
206    );
207    Ok(())
208}
209
210/// Access the global pipeline through a closure
211pub fn with_pipeline<R, F: FnOnce(&mut AudioPipeline) -> R>(f: F) -> Result<R, KernelError> {
212    let mut guard = PIPELINE.lock();
213    match guard.as_mut() {
214        Some(pipeline) => Ok(f(pipeline)),
215        None => Err(KernelError::NotInitialized {
216            subsystem: "audio pipeline",
217        }),
218    }
219}
220
221// ============================================================================
222// Tests
223// ============================================================================
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::audio::SampleFormat;
229
230    fn test_config() -> AudioConfig {
231        AudioConfig {
232            sample_rate: 48000,
233            channels: 2,
234            format: SampleFormat::S16Le,
235            buffer_frames: 256,
236        }
237    }
238
239    #[test]
240    fn test_pipeline_creation() {
241        let config = test_config();
242        let pipeline = AudioPipeline::new(config);
243        assert_eq!(pipeline.state(), PipelineState::Idle);
244        assert_eq!(pipeline.output_buffer.len(), 256 * 2); // 256 frames * 2
245                                                           // channels
246    }
247
248    #[test]
249    fn test_pipeline_start_stop() {
250        let config = test_config();
251        let mut pipeline = AudioPipeline::new(config);
252
253        assert_eq!(pipeline.state(), PipelineState::Idle);
254
255        pipeline.start();
256        assert_eq!(pipeline.state(), PipelineState::Running);
257
258        pipeline.stop();
259        assert_eq!(pipeline.state(), PipelineState::Idle);
260    }
261
262    #[test]
263    fn test_pipeline_stats_initial() {
264        let config = test_config();
265        let pipeline = AudioPipeline::new(config);
266        let stats = pipeline.stats();
267
268        assert_eq!(stats.frames_processed, 0);
269        assert_eq!(stats.underruns, 0);
270        assert_eq!(stats.state, PipelineState::Idle);
271    }
272
273    #[test]
274    fn test_pipeline_idle_silence() {
275        let config = test_config();
276        let mut pipeline = AudioPipeline::new(config);
277
278        // Pipeline is idle, should output silence
279        let output = pipeline.process_frame();
280        for &sample in output {
281            assert_eq!(sample, 0);
282        }
283    }
284}