wayle-audio 0.1.4

PulseAudio service with reactive state
Documentation
use libpulse_binding::{
    context::introspect::{SinkInfo as PulseSinkInfo, SourceInfo as PulseSourceInfo},
    def::{SinkState, SourceState},
};

use super::{
    format::{convert_channel_map, convert_sample_spec},
    pulse::{active_port_name, collect_proplist, convert_formats, convert_ports, cow_to_string},
    volume::{from_pulse, from_pulse_single},
};
use crate::types::device::{DeviceInfo, DeviceState, SinkInfo, SourceInfo};

pub(crate) fn from_sink(sink: &PulseSinkInfo) -> SinkInfo {
    SinkInfo {
        device: DeviceInfo {
            index: sink.index,
            name: cow_to_string(sink.name.as_ref()),
            description: cow_to_string(sink.description.as_ref()),
            card_index: sink.card,
            owner_module: sink.owner_module,
            driver: cow_to_string(sink.driver.as_ref()),
            state: convert_sink_state(sink.state),
            volume: from_pulse(&sink.volume),
            base_volume: from_pulse_single(sink.base_volume),
            n_volume_steps: sink.n_volume_steps,
            muted: sink.mute,
            properties: collect_proplist(&sink.proplist),
            ports: convert_ports(&sink.ports),
            active_port: active_port_name(&sink.active_port),
            formats: convert_formats(&sink.formats),
            sample_spec: convert_sample_spec(&sink.sample_spec),
            channel_map: convert_channel_map(&sink.channel_map),
            latency: sink.latency,
            configured_latency: sink.configured_latency,
            flags: sink.flags.bits(),
        },
        monitor_source: sink.monitor_source,
        monitor_source_name: cow_to_string(sink.monitor_source_name.as_ref()),
    }
}

pub(crate) fn from_source(source: &PulseSourceInfo) -> SourceInfo {
    SourceInfo {
        device: DeviceInfo {
            index: source.index,
            name: cow_to_string(source.name.as_ref()),
            description: cow_to_string(source.description.as_ref()),
            card_index: source.card,
            owner_module: source.owner_module,
            driver: cow_to_string(source.driver.as_ref()),
            state: convert_source_state(source.state),
            volume: from_pulse(&source.volume),
            base_volume: from_pulse_single(source.base_volume),
            n_volume_steps: source.n_volume_steps,
            muted: source.mute,
            properties: collect_proplist(&source.proplist),
            ports: convert_ports(&source.ports),
            active_port: active_port_name(&source.active_port),
            formats: convert_formats(&source.formats),
            sample_spec: convert_sample_spec(&source.sample_spec),
            channel_map: convert_channel_map(&source.channel_map),
            latency: source.latency,
            configured_latency: source.configured_latency,
            flags: source.flags.bits(),
        },
        monitor_of_sink: source.monitor_of_sink,
        monitor_of_sink_name: source
            .monitor_of_sink
            .map(|_| cow_to_string(source.monitor_of_sink_name.as_ref())),
        is_monitor: source.monitor_of_sink.is_some(),
    }
}

fn convert_sink_state(state: SinkState) -> DeviceState {
    match state {
        SinkState::Running => DeviceState::Running,
        SinkState::Idle => DeviceState::Idle,
        SinkState::Suspended => DeviceState::Suspended,
        _ => DeviceState::Offline,
    }
}

fn convert_source_state(state: SourceState) -> DeviceState {
    match state {
        SourceState::Running => DeviceState::Running,
        SourceState::Idle => DeviceState::Idle,
        SourceState::Suspended => DeviceState::Suspended,
        _ => DeviceState::Offline,
    }
}

#[cfg(test)]
mod tests {
    use libpulse_binding::{
        sample::Format as PulseFormat,
        volume::{ChannelVolumes, Volume as PulseVolume},
    };

    use super::*;

    #[test]
    fn sink_running_state() {
        let sink = create_minimal_sink(SinkState::Running);
        assert_eq!(from_sink(&sink).device.state, DeviceState::Running);
    }

    #[test]
    fn sink_idle_state() {
        let sink = create_minimal_sink(SinkState::Idle);
        assert_eq!(from_sink(&sink).device.state, DeviceState::Idle);
    }

    #[test]
    fn sink_suspended_state() {
        let sink = create_minimal_sink(SinkState::Suspended);
        assert_eq!(from_sink(&sink).device.state, DeviceState::Suspended);
    }

    fn create_minimal_sink(state: SinkState) -> PulseSinkInfo<'static> {
        let mut channel_map = libpulse_binding::channelmap::Map::default();
        channel_map.init_stereo();

        PulseSinkInfo {
            name: Some("test-sink".into()),
            index: 0,
            description: Some("Test Sink".into()),
            sample_spec: libpulse_binding::sample::Spec {
                format: PulseFormat::S16le,
                channels: 2,
                rate: 44100,
            },
            channel_map,
            owner_module: None,
            volume: ChannelVolumes::default(),
            mute: false,
            monitor_source: 1,
            monitor_source_name: Some("test-sink.monitor".into()),
            latency: libpulse_binding::time::MicroSeconds(0),
            driver: Some("test-driver".into()),
            flags: libpulse_binding::def::SinkFlagSet::empty(),
            proplist: libpulse_binding::proplist::Proplist::new().unwrap(),
            configured_latency: libpulse_binding::time::MicroSeconds(0),
            base_volume: PulseVolume::NORMAL,
            state,
            n_volume_steps: 65536,
            card: None,
            ports: vec![],
            active_port: None,
            formats: vec![],
        }
    }
}