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

veridian_kernel/audio/
mixer.rs

1//! Fixed-point 16.16 audio mixer
2//!
3//! Provides software audio mixing using integer-only arithmetic suitable
4//! for kernel context where FPU state is not available. All volume and
5//! sample calculations use 16.16 fixed-point representation.
6//!
7//! ## Fixed-Point Format
8//!
9//! A `FixedPoint` value (i32) stores a number with 16 integer bits and
10//! 16 fractional bits. For example:
11//! - `0x0001_0000` = 1.0
12//! - `0x0000_8000` = 0.5
13//! - `0x0000_0000` = 0.0
14//! - `0xFFFF_0000` = -1.0
15
16#![allow(dead_code)]
17
18use alloc::{string::String, vec::Vec};
19use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
20
21use crate::error::KernelError;
22
23// ============================================================================
24// Fixed-Point Arithmetic (16.16 format)
25// ============================================================================
26
27/// Fixed-point 16.16 type: 16 integer bits + 16 fractional bits
28pub type FixedPoint = i32;
29
30/// Number of fractional bits in the fixed-point representation
31const FP_SHIFT: i32 = 16;
32
33/// Fixed-point representation of 1.0
34const FP_ONE: FixedPoint = 1 << FP_SHIFT;
35
36/// Maximum representable fixed-point value (before saturation)
37const FP_MAX: FixedPoint = i32::MAX;
38
39/// Minimum representable fixed-point value (before saturation)
40const FP_MIN: FixedPoint = i32::MIN;
41
42/// Convert a signed 16-bit sample to fixed-point 16.16
43///
44/// Maps the full i16 range (-32768..32767) into fixed-point. Since an i16
45/// fits in the integer portion of a 16.16 number, this is a simple shift.
46#[inline]
47pub fn fp_from_i16(sample: i16) -> FixedPoint {
48    (sample as i32) << FP_SHIFT
49}
50
51/// Convert a fixed-point 16.16 value back to signed 16-bit with saturation
52///
53/// Extracts the integer portion (upper 16 bits) and clamps to i16 range.
54#[inline]
55pub fn fp_to_i16(fp: FixedPoint) -> i16 {
56    let shifted = fp >> FP_SHIFT;
57    if shifted > i16::MAX as i32 {
58        i16::MAX
59    } else if shifted < i16::MIN as i32 {
60        i16::MIN
61    } else {
62        shifted as i16
63    }
64}
65
66/// Multiply two fixed-point values with saturation
67///
68/// Uses i64 intermediate to avoid overflow, then clamps to i32 range.
69#[inline]
70pub fn fp_mul(a: FixedPoint, b: FixedPoint) -> FixedPoint {
71    let result = (a as i64 * b as i64) >> FP_SHIFT;
72    if result > FP_MAX as i64 {
73        FP_MAX
74    } else if result < FP_MIN as i64 {
75        FP_MIN
76    } else {
77        result as i32
78    }
79}
80
81/// Convert a volume value (0..65535) to fixed-point (0.0..1.0)
82///
83/// 0 maps to 0x0000_0000 (0.0), 65535 maps to 0x0000_FFFF (~1.0).
84#[inline]
85pub fn fp_from_volume(volume: u16) -> FixedPoint {
86    volume as i32
87}
88
89/// Add two fixed-point values with saturation
90#[inline]
91fn fp_add_saturate(a: FixedPoint, b: FixedPoint) -> FixedPoint {
92    let result = (a as i64) + (b as i64);
93    if result > FP_MAX as i64 {
94        FP_MAX
95    } else if result < FP_MIN as i64 {
96        FP_MIN
97    } else {
98        result as i32
99    }
100}
101
102// ============================================================================
103// Mixer Channel
104// ============================================================================
105
106/// A single mixer channel representing one audio source
107pub struct MixerChannel {
108    /// Unique channel identifier
109    pub id: u16,
110    /// Channel volume (0..65535), stored atomically for lock-free reads
111    volume: AtomicU32,
112    /// Whether this channel is muted
113    muted: AtomicBool,
114    /// Ring buffer read index for this channel's audio data
115    buffer_index: u32,
116    /// Human-readable channel name
117    pub name: String,
118    /// Per-channel sample buffer for mixing
119    samples: Vec<i16>,
120}
121
122impl MixerChannel {
123    /// Create a new mixer channel
124    fn new(id: u16, name: String) -> Self {
125        Self {
126            id,
127            volume: AtomicU32::new(65535), // Full volume by default
128            muted: AtomicBool::new(false),
129            buffer_index: 0,
130            name,
131            samples: Vec::new(),
132        }
133    }
134
135    /// Get the current volume (0..65535)
136    pub fn get_volume(&self) -> u16 {
137        self.volume.load(Ordering::Relaxed) as u16
138    }
139
140    /// Set the volume (0..65535)
141    pub fn set_volume(&self, vol: u16) {
142        self.volume.store(vol as u32, Ordering::Relaxed);
143    }
144
145    /// Check if the channel is muted
146    pub fn is_muted(&self) -> bool {
147        self.muted.load(Ordering::Relaxed)
148    }
149
150    /// Set mute state
151    pub fn set_muted(&self, muted: bool) {
152        self.muted.store(muted, Ordering::Relaxed);
153    }
154
155    /// Write samples into this channel's buffer for the next mix cycle
156    pub fn write_samples(&mut self, data: &[i16]) {
157        self.samples.clear();
158        self.samples.extend_from_slice(data);
159        self.buffer_index = 0;
160    }
161
162    /// Read and consume up to `count` samples from this channel
163    fn read_samples(&mut self, count: usize) -> &[i16] {
164        let start = self.buffer_index as usize;
165        let available = self.samples.len().saturating_sub(start);
166        let to_read = count.min(available);
167        let end = start + to_read;
168        self.buffer_index = end as u32;
169        &self.samples[start..end]
170    }
171
172    /// Number of samples remaining in this channel's buffer
173    fn available_samples(&self) -> usize {
174        self.samples
175            .len()
176            .saturating_sub(self.buffer_index as usize)
177    }
178}
179
180// ============================================================================
181// Audio Mixer
182// ============================================================================
183
184/// Multi-channel audio mixer with fixed-point arithmetic
185pub struct AudioMixer {
186    /// Active mixer channels
187    channels: Vec<MixerChannel>,
188    /// Master volume (0..65535), atomically accessible
189    master_volume: AtomicU32,
190    /// Pre-allocated output buffer for mixed audio
191    output_buffer: Vec<i16>,
192    /// Output sample rate in Hz
193    sample_rate: u32,
194    /// Number of output channels (1=mono, 2=stereo)
195    output_channels: u8,
196    /// Next channel ID to assign
197    next_channel_id: u16,
198}
199
200impl AudioMixer {
201    /// Create a new audio mixer
202    ///
203    /// # Arguments
204    /// * `sample_rate` - Output sample rate in Hz (e.g., 48000)
205    /// * `channels` - Number of output channels (1=mono, 2=stereo)
206    pub fn new(sample_rate: u32, channels: u8) -> Self {
207        Self {
208            channels: Vec::new(),
209            master_volume: AtomicU32::new(65535),
210            output_buffer: Vec::new(),
211            sample_rate,
212            output_channels: channels,
213            next_channel_id: 1,
214        }
215    }
216
217    /// Add a new mixer channel and return its ID
218    pub fn add_channel(&mut self, name: &str) -> u16 {
219        let id = self.next_channel_id;
220        self.next_channel_id = self.next_channel_id.wrapping_add(1);
221        let channel = MixerChannel::new(id, String::from(name));
222        self.channels.push(channel);
223        println!("[AUDIO] Mixer: added channel {} (id={})", name, id);
224        id
225    }
226
227    /// Remove a mixer channel by ID
228    pub fn remove_channel(&mut self, id: u16) {
229        if let Some(pos) = self.channels.iter().position(|c| c.id == id) {
230            let name = self.channels[pos].name.clone();
231            self.channels.remove(pos);
232            println!("[AUDIO] Mixer: removed channel {} (id={})", name, id);
233        }
234    }
235
236    /// Set volume for a specific channel (0..65535)
237    pub fn set_volume(&self, channel_id: u16, volume: u16) {
238        if let Some(ch) = self.channels.iter().find(|c| c.id == channel_id) {
239            ch.set_volume(volume);
240        }
241    }
242
243    /// Set the master volume (0..65535)
244    pub fn set_master_volume(&self, volume: u16) {
245        self.master_volume.store(volume as u32, Ordering::Relaxed);
246    }
247
248    /// Get the master volume (0..65535)
249    pub fn get_master_volume(&self) -> u16 {
250        self.master_volume.load(Ordering::Relaxed) as u16
251    }
252
253    /// Mix all active channels into the provided output buffer
254    ///
255    /// For each sample position, reads from all channels, applies per-channel
256    /// volume and mute, sums with saturation, then applies master volume.
257    /// The result is written directly to `output`.
258    pub fn mix_to_output(&mut self, output: &mut [i16]) {
259        let master_vol = fp_from_volume(self.get_master_volume());
260        let num_samples = output.len();
261
262        // Clear output buffer
263        for sample in output.iter_mut() {
264            *sample = 0;
265        }
266
267        // If no channels or master muted, output silence
268        if self.channels.is_empty() || master_vol == 0 {
269            return;
270        }
271
272        // Mix each channel into the output
273        for channel in self.channels.iter_mut() {
274            if channel.is_muted() {
275                continue;
276            }
277
278            let ch_vol = fp_from_volume(channel.get_volume());
279            if ch_vol == 0 {
280                continue;
281            }
282
283            let samples = channel.read_samples(num_samples);
284            for (i, &sample) in samples.iter().enumerate() {
285                if i >= num_samples {
286                    break;
287                }
288
289                // Convert sample to fixed-point
290                let fp_sample = fp_from_i16(sample);
291
292                // Apply channel volume
293                let scaled = fp_mul(fp_sample, ch_vol);
294
295                // Apply master volume
296                let mastered = fp_mul(scaled, master_vol);
297
298                // Accumulate with saturation
299                let existing = fp_from_i16(output[i]);
300                let mixed = fp_add_saturate(existing, mastered);
301                output[i] = fp_to_i16(mixed);
302            }
303        }
304    }
305
306    /// Return the number of active channels
307    pub fn channel_count(&self) -> usize {
308        self.channels.len()
309    }
310
311    /// Get a mutable reference to a channel by ID
312    pub fn get_channel_mut(&mut self, id: u16) -> Option<&mut MixerChannel> {
313        self.channels.iter_mut().find(|c| c.id == id)
314    }
315
316    /// Write samples to a specific channel
317    pub fn write_channel_samples(&mut self, channel_id: u16, samples: &[i16]) {
318        if let Some(ch) = self.channels.iter_mut().find(|c| c.id == channel_id) {
319            ch.write_samples(samples);
320        }
321    }
322}
323
324// ============================================================================
325// Global Mixer State
326// ============================================================================
327
328static MIXER: spin::Mutex<Option<AudioMixer>> = spin::Mutex::new(None);
329
330/// Initialize the global audio mixer
331pub fn init(sample_rate: u32) -> Result<(), KernelError> {
332    let mut mixer = MIXER.lock();
333    if mixer.is_some() {
334        return Err(KernelError::InvalidState {
335            expected: "uninitialized",
336            actual: "already initialized",
337        });
338    }
339
340    *mixer = Some(AudioMixer::new(sample_rate, 2));
341    println!("[AUDIO] Mixer initialized at {} Hz, stereo", sample_rate);
342    Ok(())
343}
344
345/// Access the global mixer through a closure
346pub fn with_mixer<R, F: FnOnce(&mut AudioMixer) -> R>(f: F) -> Result<R, KernelError> {
347    let mut guard = MIXER.lock();
348    match guard.as_mut() {
349        Some(mixer) => Ok(f(mixer)),
350        None => Err(KernelError::NotInitialized {
351            subsystem: "audio mixer",
352        }),
353    }
354}
355
356// ============================================================================
357// Tests
358// ============================================================================
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_fp_from_i16() {
366        assert_eq!(fp_from_i16(0), 0);
367        assert_eq!(fp_from_i16(1), FP_ONE);
368        assert_eq!(fp_from_i16(-1), -FP_ONE);
369        assert_eq!(fp_from_i16(i16::MAX), (i16::MAX as i32) << FP_SHIFT);
370    }
371
372    #[test]
373    fn test_fp_to_i16_roundtrip() {
374        for val in [0i16, 1, -1, 100, -100, i16::MAX, i16::MIN] {
375            assert_eq!(fp_to_i16(fp_from_i16(val)), val);
376        }
377    }
378
379    #[test]
380    fn test_fp_to_i16_saturation() {
381        // Value larger than i16::MAX should saturate
382        assert_eq!(fp_to_i16(i32::MAX), i16::MAX);
383        // Value smaller than i16::MIN should saturate
384        assert_eq!(fp_to_i16(i32::MIN), i16::MIN);
385    }
386
387    #[test]
388    fn test_fp_mul_basic() {
389        // 1.0 * 1.0 = 1.0
390        assert_eq!(fp_mul(FP_ONE, FP_ONE), FP_ONE);
391        // 1.0 * 0.0 = 0.0
392        assert_eq!(fp_mul(FP_ONE, 0), 0);
393        // 2.0 * 0.5 = 1.0
394        let two = 2 << FP_SHIFT;
395        let half = FP_ONE / 2;
396        assert_eq!(fp_mul(two, half), FP_ONE);
397    }
398
399    #[test]
400    fn test_fp_mul_saturation() {
401        // Large values should saturate instead of wrapping
402        let large = i32::MAX;
403        let result = fp_mul(large, large);
404        assert_eq!(result, FP_MAX);
405    }
406
407    #[test]
408    fn test_fp_from_volume() {
409        assert_eq!(fp_from_volume(0), 0);
410        assert_eq!(fp_from_volume(65535), 65535);
411        assert_eq!(fp_from_volume(32768), 32768);
412    }
413
414    #[test]
415    fn test_mixer_add_remove_channel() {
416        let mut mixer = AudioMixer::new(48000, 2);
417        assert_eq!(mixer.channel_count(), 0);
418
419        let id1 = mixer.add_channel("test1");
420        assert_eq!(mixer.channel_count(), 1);
421
422        let id2 = mixer.add_channel("test2");
423        assert_eq!(mixer.channel_count(), 2);
424
425        mixer.remove_channel(id1);
426        assert_eq!(mixer.channel_count(), 1);
427
428        mixer.remove_channel(id2);
429        assert_eq!(mixer.channel_count(), 0);
430    }
431
432    #[test]
433    fn test_mixer_volume_scaling() {
434        let mut mixer = AudioMixer::new(48000, 1);
435        let ch_id = mixer.add_channel("test");
436
437        // Write a known sample pattern
438        let input = [16384i16; 4]; // ~0.5 amplitude
439        mixer.write_channel_samples(ch_id, &input);
440
441        // Full volume: output should be close to input
442        let mut output = [0i16; 4];
443        mixer.mix_to_output(&mut output);
444
445        // With full channel and master volume, samples pass through
446        // (volume is 65535/65536 ~= 1.0 so there may be tiny rounding)
447        for &sample in &output {
448            assert!(
449                sample > 16000 && sample < 16500,
450                "Expected ~16384, got {}",
451                sample
452            );
453        }
454    }
455
456    #[test]
457    fn test_mixer_two_channels() {
458        let mut mixer = AudioMixer::new(48000, 1);
459        let ch1 = mixer.add_channel("ch1");
460        let ch2 = mixer.add_channel("ch2");
461
462        // Two channels with the same data should sum
463        let samples1 = [8000i16; 4];
464        let samples2 = [8000i16; 4];
465        mixer.write_channel_samples(ch1, &samples1);
466        mixer.write_channel_samples(ch2, &samples2);
467
468        let mut output = [0i16; 4];
469        mixer.mix_to_output(&mut output);
470
471        // Sum of two channels should be roughly double (with volume scaling)
472        for &sample in &output {
473            assert!(sample > 14000, "Expected combined ~16000, got {}", sample);
474        }
475    }
476
477    #[test]
478    fn test_mixer_muted_channel() {
479        let mut mixer = AudioMixer::new(48000, 1);
480        let ch_id = mixer.add_channel("muted");
481
482        let input = [16384i16; 4];
483        mixer.write_channel_samples(ch_id, &input);
484
485        // Mute the channel
486        if let Some(ch) = mixer.get_channel_mut(ch_id) {
487            ch.set_muted(true);
488        }
489
490        let mut output = [0i16; 4];
491        mixer.mix_to_output(&mut output);
492
493        // Muted channel should produce silence
494        for &sample in &output {
495            assert_eq!(sample, 0);
496        }
497    }
498
499    #[test]
500    fn test_mixer_saturation() {
501        let mut mixer = AudioMixer::new(48000, 1);
502        let ch1 = mixer.add_channel("ch1");
503        let ch2 = mixer.add_channel("ch2");
504
505        // Two channels at near-max amplitude should saturate, not wrap
506        let max_samples = [i16::MAX; 4];
507        mixer.write_channel_samples(ch1, &max_samples);
508        mixer.write_channel_samples(ch2, &max_samples);
509
510        let mut output = [0i16; 4];
511        mixer.mix_to_output(&mut output);
512
513        // Should saturate at i16::MAX, not wrap to negative
514        for &sample in &output {
515            assert!(sample > 0, "Saturation failed: got {}", sample);
516        }
517    }
518}