lavende-core 0.1.6

Core in-process Discord voice connection and playback engine
Documentation
pub mod layer {
    use crate::audio::{RingBuffer, buffer::PooledBuffer, constants::LAYER_BUFFER_SIZE};
    use flume::Receiver;
    pub struct MixLayer {
        pub id: String,
        pub rx: Receiver<PooledBuffer>,
        pub ring_buffer: RingBuffer,
        pub volume: f32,
        pub finished: bool,
    }
    impl MixLayer {
        pub fn new(id: String, rx: Receiver<PooledBuffer>, volume: f32) -> Self {
            Self {
                id,
                rx,
                ring_buffer: RingBuffer::new(LAYER_BUFFER_SIZE),
                volume: volume.clamp(0.0, 1.0),
                finished: false,
            }
        }
        pub fn fill(&mut self) {
            while let Ok(pooled) = self.rx.try_recv() {
                let bytes = unsafe {
                    std::slice::from_raw_parts(pooled.as_ptr() as *const u8, pooled.len() * 2)
                };
                self.ring_buffer.write(bytes);
                crate::audio::buffer::release_buffer(pooled);
            }
            if self.rx.is_disconnected() {
                self.finished = true;
            }
        }
        pub fn is_dead(&self) -> bool {
            self.finished && self.ring_buffer.is_empty()
        }
        pub fn accumulate(&mut self, acc: &mut [i32]) {
            let byte_count = acc.len() * 2;
            if let Some(bytes) = self.ring_buffer.read(byte_count) {
                let samples = unsafe {
                    std::slice::from_raw_parts(bytes.as_ptr() as *const i16, bytes.len() / 2)
                };
                for (acc_val, &s) in acc.iter_mut().zip(samples.iter()) {
                    *acc_val += (s as f32 * self.volume).round() as i32;
                }
            }
        }
    }
}
pub mod mixer {
    use super::layer::MixLayer;
    use crate::{
        audio::{
            AudioFrame,
            buffer::PooledBuffer,
            constants::{MAX_LAYERS, MIXER_CHANNELS, TARGET_SAMPLE_RATE},
            flow::FlowController,
            playback::{StuckDetector, handle::PlaybackState},
        },
        config::player::PlayerConfig,
    };
    use flume::Receiver;
    use std::{
        collections::HashMap,
        sync::{
            Arc,
            atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering},
        },
    };
    pub struct AudioMixer {
        pub layers: HashMap<String, MixLayer>,
        pub max_layers: usize,
        pub enabled: bool,
        acc_buf: Vec<i32>,
    }
    impl Default for AudioMixer {
        fn default() -> Self {
            Self::new()
        }
    }
    impl AudioMixer {
        pub fn new() -> Self {
            Self {
                layers: HashMap::new(),
                max_layers: MAX_LAYERS,
                enabled: true,
                acc_buf: Vec::with_capacity(1920),
            }
        }
        pub fn add_layer(
            &mut self,
            id: String,
            rx: Receiver<PooledBuffer>,
            volume: f32,
        ) -> Result<(), &'static str> {
            if self.layers.len() >= MAX_LAYERS {
                return Err("Maximum mix layers reached");
            }
            self.layers
                .insert(id.clone(), MixLayer::new(id, rx, volume));
            Ok(())
        }
        pub fn remove_layer(&mut self, id: &str) {
            self.layers.remove(id);
        }
        pub fn set_layer_volume(&mut self, id: &str, volume: f32) {
            if let Some(layer) = self.layers.get_mut(id) {
                layer.volume = volume.clamp(0.0, 1.0);
            }
        }
        pub fn mix(&mut self, main_frame: &mut [i16]) {
            if !self.enabled || self.layers.is_empty() {
                return;
            }
            let out_len = main_frame.len();
            if self.acc_buf.len() != out_len {
                self.acc_buf.resize(out_len, 0);
            }
            for (acc, &sample) in self.acc_buf.iter_mut().zip(main_frame.iter()) {
                *acc = sample as i32;
            }
            self.layers.retain(|_, layer| {
                layer.fill();
                !layer.is_dead()
            });
            for layer in self.layers.values_mut() {
                layer.accumulate(&mut self.acc_buf);
            }
            for (out, &sum) in main_frame.iter_mut().zip(self.acc_buf.iter()) {
                *out = sum.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
            }
        }
    }
    pub struct Mixer {
        tracks: Vec<MixerTrack>,
        mix_buf: Vec<i32>,
        pub audio_mixer: AudioMixer,
        opus_passthrough_track: Option<usize>,
        final_pcm_buf: Vec<i16>,
        pub stuck_detector: Arc<StuckDetector>,
    }
    struct MixerTrack {
        flow: FlowController,
        pending: Vec<i16>,
        pending_pos: usize,
        state: Arc<AtomicU8>,
        volume: Arc<AtomicU32>,
        position: Arc<AtomicU64>,
        is_buffering: Arc<AtomicBool>,
        config: PlayerConfig,
        finished: bool,
    }
    impl Mixer {
        pub fn new(_sample_rate: u32) -> Self {
            Self {
                tracks: Vec::new(),
                mix_buf: Vec::with_capacity(1920),
                audio_mixer: AudioMixer::new(),
                opus_passthrough_track: None,
                final_pcm_buf: Vec::with_capacity(1920),
                stuck_detector: Arc::new(StuckDetector::new(10_000)),
            }
        }
        pub fn add_track(
            &mut self,
            rx: Receiver<AudioFrame>,
            state: Arc<AtomicU8>,
            volume: Arc<AtomicU32>,
            position: Arc<AtomicU64>,
            is_buffering: Arc<AtomicBool>,
            config: PlayerConfig,
        ) {
            let vol_raw = f32::from_bits(volume.load(Ordering::Acquire));
            let mut flow =
                FlowController::for_mixer(rx, TARGET_SAMPLE_RATE, MIXER_CHANNELS, vol_raw);
            flow.volume.set_volume_instant(vol_raw);
            self.tracks.push(MixerTrack {
                flow,
                pending: Vec::new(),
                pending_pos: 0,
                state,
                volume,
                position,
                is_buffering,
                config,
                finished: false,
            });
        }
        pub fn set_passthrough_track(&mut self, track_index: usize) {
            self.opus_passthrough_track = Some(track_index);
        }
        pub fn take_opus_frame(&mut self) -> Option<Vec<u8>> {
            for track in self.tracks.iter_mut() {
                let state = PlaybackState::from(track.state.load(Ordering::Acquire));
                if matches!(
                    state,
                    PlaybackState::Paused
                        | PlaybackState::Stopped
                        | PlaybackState::Stopping
                        | PlaybackState::Starting
                ) {
                    continue;
                }
                if let Some(packet) = track.flow.take_opus() {
                    track.position.fetch_add(960, Ordering::Relaxed);
                    return Some(packet);
                }
            }
            None
        }
        pub fn stop_all(&mut self) {
            for track in self.tracks.iter_mut() {
                track
                    .state
                    .store(PlaybackState::Stopped as u8, Ordering::Release);
            }
            self.tracks.clear();
            self.audio_mixer.enabled = false;
        }
        pub fn mix(&mut self, buf: &mut [i16]) -> bool {
            let out_len = buf.len();
            if self.mix_buf.len() != out_len {
                self.mix_buf.resize(out_len, 0);
            }
            self.mix_buf.fill(0);
            self.tracks
                .retain(|t| t.state.load(Ordering::Acquire) != PlaybackState::Stopped as u8);
            let mut has_audio = false;
            for track in self.tracks.iter_mut() {
                let state = PlaybackState::from(track.state.load(Ordering::Acquire));
                if matches!(state, PlaybackState::Paused | PlaybackState::Stopped) {
                    continue;
                }
                let vol_f = f32::from_bits(track.volume.load(Ordering::Acquire));
                if (vol_f - track.flow.volume.current_volume()).abs() > 0.001 {
                    track.flow.volume.set_volume(vol_f);
                }
                if state == PlaybackState::Stopping && !track.flow.tape.is_ramping() {
                    track.flow.tape.tape_to(
                        track.config.tape.tape_stop_duration_ms as f32,
                        false,
                        track.config.tape.curve,
                    );
                } else if state == PlaybackState::Starting && !track.flow.tape.is_ramping() {
                    track.flow.tape.tape_to(
                        track.config.tape.tape_stop_duration_ms as f32,
                        true,
                        track.config.tape.curve,
                    );
                }
                let mut filled = 0usize;
                if track.pending_pos < track.pending.len() {
                    let n = (out_len - filled).min(track.pending.len() - track.pending_pos);
                    for (acc, &s) in self.mix_buf[filled..filled + n]
                        .iter_mut()
                        .zip(&track.pending[track.pending_pos..track.pending_pos + n])
                    {
                        *acc += s as i32;
                    }
                    track.pending_pos += n;
                    filled += n;
                    if track.pending_pos >= track.pending.len() {
                        track.pending.clear();
                        track.pending_pos = 0;
                    }
                }
                'pull: while filled < out_len && !track.finished {
                    match track.flow.try_pop_frame() {
                        Ok(Some(frame)) => {
                            let n = frame.len().min(out_len - filled);
                            for (acc, &s) in
                                self.mix_buf[filled..filled + n].iter_mut().zip(&frame[..n])
                            {
                                *acc += s as i32;
                            }
                            if n < frame.len() {
                                track.pending.extend_from_slice(&frame[n..]);
                                track.pending_pos = 0;
                            }
                            filled += n;
                            crate::audio::buffer::release_buffer(frame);
                        }
                        Ok(None) => break 'pull,
                        Err(_) => {
                            track.finished = true;
                            break 'pull;
                        }
                    }
                }
                if filled > 0 {
                    has_audio = true;
                    track
                        .position
                        .fetch_add(filled as u64 / MIXER_CHANNELS as u64, Ordering::Relaxed);
                    track.is_buffering.store(false, Ordering::Release);
                    self.stuck_detector.record_frame_received();
                } else if !track.finished {
                    track.is_buffering.store(true, Ordering::Release);
                }
                if track.finished && track.pending.is_empty() && !track.flow.tape.is_active() {
                    track
                        .state
                        .store(PlaybackState::Stopped as u8, Ordering::Release);
                }
                if track.flow.tape.check_ramp_completed() {
                    match state {
                        PlaybackState::Stopping => {
                            track
                                .state
                                .store(PlaybackState::Paused as u8, Ordering::Release);
                        }
                        PlaybackState::Starting => {
                            track
                                .state
                                .store(PlaybackState::Playing as u8, Ordering::Release);
                        }
                        _ => {}
                    }
                }
            }
            if self.final_pcm_buf.len() != out_len {
                self.final_pcm_buf.resize(out_len, 0);
            }
            for (final_pcm, &sum) in self.final_pcm_buf.iter_mut().zip(self.mix_buf.iter()) {
                *final_pcm = sum.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
            }
            self.audio_mixer.mix(&mut self.final_pcm_buf);
            if !self.audio_mixer.layers.is_empty() {
                has_audio = true;
            }
            buf.copy_from_slice(&self.final_pcm_buf);
            has_audio
        }
    }
}
pub use layer::MixLayer;
pub use mixer::{AudioMixer, Mixer};