maolan 0.2.0

Rust DAW application for recording, editing, routing, automation, export, and plugin hosting
use super::AudioDeviceOption;
use crate::consts::state_platform_freebsd::{
    AFMT_S8, AFMT_S16_BE, AFMT_S16_LE, AFMT_S24_BE, AFMT_S24_LE, AFMT_S32_BE, AFMT_S32_LE,
};
use nvtree::{Nvtree, Nvtvalue, nvtree_find, nvtree_unpack};
use std::{ffi::c_void, fs::File, os::fd::AsRawFd};

pub(crate) fn discover_freebsd_audio_devices() -> Vec<AudioDeviceOption> {
    let mut devices = discover_sndstat_dsp_devices().unwrap_or_default();
    devices.sort_by_key(|a| a.label.to_lowercase());
    devices.dedup_by(|a, b| a.id == b.id);
    devices
}

fn discover_sndstat_dsp_devices() -> Option<Vec<AudioDeviceOption>> {
    let file = File::open("/dev/sndstat").ok()?;
    let fd = file.as_raw_fd();

    unsafe {
        if sndst_refresh_devs(fd).is_err() {
            return None;
        }
    }

    let mut arg = SndstIoctlNvArg {
        nbytes: 0,
        buf: std::ptr::null_mut(),
    };
    unsafe {
        if sndst_get_devs(fd, &mut arg).is_err() {
            return None;
        }
    }
    if arg.nbytes == 0 {
        return None;
    }

    let mut buf = vec![0_u8; arg.nbytes];
    arg.buf = buf.as_mut_ptr().cast::<c_void>();
    unsafe {
        if sndst_get_devs(fd, &mut arg).is_err() {
            return None;
        }
    }
    if arg.nbytes == 0 || arg.nbytes > buf.len() {
        return None;
    }

    parse_sndstat_nvlist(&buf[..arg.nbytes])
}

fn parse_sndstat_nvlist(buf: &[u8]) -> Option<Vec<AudioDeviceOption>> {
    let root = nvtree_unpack(buf).ok()?;
    let dsps = if let Some(pair) = nvtree_find(&root, "dsps") {
        match &pair.value {
            Nvtvalue::NestedArray(arr) => arr,
            _ => return None,
        }
    } else {
        return None;
    };

    let out = dsps
        .iter()
        .filter_map(|dsp| {
            let devnode_pair = nvtree_find(dsp, "devnode")?;
            let Nvtvalue::String(devnode) = &devnode_pair.value else {
                return None;
            };

            let devpath = if devnode.starts_with('/') {
                devnode.to_string()
            } else {
                format!("/dev/{devnode}")
            };

            if !devpath.starts_with("/dev/dsp") {
                return None;
            }

            let label_prefix = nvtree_find(dsp, "desc")
                .and_then(|pair| match &pair.value {
                    Nvtvalue::String(s) if !s.is_empty() => Some(s.clone()),
                    _ => None,
                })
                .or_else(|| {
                    nvtree_find(dsp, "nameunit").and_then(|pair| match &pair.value {
                        Nvtvalue::String(s) if !s.is_empty() => Some(s.clone()),
                        _ => None,
                    })
                });
            let label = label_prefix
                .map(|prefix| format!("{prefix} ({devpath})"))
                .unwrap_or_else(|| devpath.clone());
            let mut supported_bits = decode_supported_bits_from_dsp(dsp);
            if supported_bits.is_empty() {
                supported_bits = probe_oss_supported_bits(&devpath);
            }
            let mut supported_sample_rates = decode_supported_sample_rates_from_dsp(dsp);
            if supported_sample_rates.is_empty() {
                supported_sample_rates = probe_oss_supported_sample_rates(&devpath);
            }
            Some(AudioDeviceOption::with_supported_caps(
                devpath,
                label,
                supported_bits,
                supported_sample_rates,
            ))
        })
        .collect::<Vec<_>>();

    (!out.is_empty()).then_some(out)
}

fn decode_supported_bits_from_dsp(dsp: &Nvtree) -> Vec<usize> {
    fn parse_number_text(s: &str) -> Option<u64> {
        let trimmed = s.trim();
        if let Some(hex) = trimmed
            .strip_prefix("0x")
            .or_else(|| trimmed.strip_prefix("0X"))
        {
            return u64::from_str_radix(hex, 16).ok();
        }
        trimmed.parse::<u64>().ok()
    }

    fn format_mask_from_value(value: &Nvtvalue) -> Option<u64> {
        match value {
            Nvtvalue::Number(n) => Some(*n),
            Nvtvalue::String(s) => parse_number_text(s),
            Nvtvalue::NumberArray(arr) => Some(arr.iter().copied().fold(0_u64, |acc, n| acc | n)),
            Nvtvalue::StringArray(arr) => Some(
                arr.iter()
                    .filter_map(|s| parse_number_text(s))
                    .fold(0_u64, |acc, n| acc | n),
            ),
            _ => None,
        }
    }

    fn format_mask_from_tree(tree: &Nvtree) -> Option<u64> {
        for key in crate::consts::state_platform_freebsd_lists::DIRECT_KEYS {
            if let Some(pair) = nvtree_find(tree, key)
                && let Some(mask) = format_mask_from_value(&pair.value)
            {
                return Some(mask);
            }
        }
        None
    }

    let mut mask = format_mask_from_tree(dsp).unwrap_or(0);
    for nested_name in ["play", "playback", "record", "capture"] {
        if let Some(pair) = nvtree_find(dsp, nested_name)
            && let Nvtvalue::Nested(nested) = &pair.value
        {
            mask |= format_mask_from_tree(nested).unwrap_or(0);
        }
    }

    bits_from_format_mask(mask)
}

fn bits_from_format_mask(mask: u64) -> Vec<usize> {
    let mut bits = Vec::with_capacity(4);
    if (mask & (AFMT_S32_LE | AFMT_S32_BE)) != 0 {
        bits.push(32);
    }
    if (mask & (AFMT_S24_LE | AFMT_S24_BE)) != 0 {
        bits.push(24);
    }
    if (mask & (AFMT_S16_LE | AFMT_S16_BE)) != 0 {
        bits.push(16);
    }
    if (mask & AFMT_S8) != 0 {
        bits.push(8);
    }
    bits
}

fn decode_supported_sample_rates_from_dsp(dsp: &Nvtree) -> Vec<i32> {
    use std::collections::BTreeSet;

    fn parse_number_text(s: &str) -> Option<i32> {
        s.trim().parse::<i32>().ok().filter(|v| *v > 0)
    }

    fn collect_rates_from_value(value: &Nvtvalue, rates: &mut BTreeSet<i32>) {
        match value {
            Nvtvalue::Number(n) => {
                if let Ok(rate) = i32::try_from(*n)
                    && rate > 0
                {
                    rates.insert(rate);
                }
            }
            Nvtvalue::String(s) => {
                if let Some(rate) = parse_number_text(s) {
                    rates.insert(rate);
                }
            }
            Nvtvalue::NumberArray(arr) => {
                for rate in arr.iter().copied().filter_map(|n| i32::try_from(n).ok()) {
                    if rate > 0 {
                        rates.insert(rate);
                    }
                }
            }
            Nvtvalue::StringArray(arr) => {
                for s in arr {
                    if let Some(rate) = parse_number_text(s) {
                        rates.insert(rate);
                    }
                }
            }
            _ => {}
        }
    }

    fn collect_rates_from_tree(tree: &Nvtree, rates: &mut BTreeSet<i32>) {
        for key in crate::consts::state_platform_freebsd_lists::RATE_KEYS {
            if let Some(pair) = nvtree_find(tree, key) {
                collect_rates_from_value(&pair.value, rates);
            }
        }
    }

    let mut rates = BTreeSet::new();
    collect_rates_from_tree(dsp, &mut rates);
    for nested_name in ["play", "playback", "record", "capture"] {
        if let Some(pair) = nvtree_find(dsp, nested_name)
            && let Nvtvalue::Nested(nested) = &pair.value
        {
            collect_rates_from_tree(nested, &mut rates);
        }
    }
    rates.into_iter().collect()
}

fn probe_oss_supported_bits(devpath: &str) -> Vec<usize> {
    let Ok(file) = std::fs::OpenOptions::new().read(true).open(devpath) else {
        return Vec::new();
    };
    let fd = file.as_raw_fd();
    let mut formats = 0_i32;
    let ok = unsafe { oss_get_formats(fd, &mut formats).is_ok() };
    if !ok {
        return Vec::new();
    }
    bits_from_format_mask(formats as u64)
}

fn probe_oss_supported_sample_rates(devpath: &str) -> Vec<i32> {
    let Ok(file) = std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open(devpath)
    else {
        return Vec::new();
    };
    let fd = file.as_raw_fd();
    let mut supported = std::collections::BTreeSet::new();
    for candidate in crate::consts::state_platform_freebsd_lists::SAMPLE_RATE_CANDIDATES {
        let mut rate = candidate;
        let ok = unsafe { oss_set_speed(fd, &mut rate).is_ok() };
        if ok && rate > 0 {
            supported.insert(rate);
        }
    }
    supported.into_iter().collect()
}

#[repr(C)]
struct SndstIoctlNvArg {
    nbytes: usize,
    buf: *mut c_void,
}

nix::ioctl_none!(sndst_refresh_devs, b'D', 100);
nix::ioctl_readwrite!(sndst_get_devs, b'D', 101, SndstIoctlNvArg);
nix::ioctl_read!(oss_get_formats, b'P', 11, i32);
nix::ioctl_readwrite!(oss_set_speed, b'P', 2, i32);