Skip to main content

wavy/ffi/linux/
device_list.rs

1// Wavy
2// Copyright © 2019-2021 Jeron Aldaron Lau.
3//
4// Licensed under any of:
5// - Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
6// - MIT License (https://mit-license.org/)
7// - Boost Software License, Version 1.0 (https://www.boost.org/LICENSE_1_0.txt)
8// At your choosing (See accompanying files LICENSE_APACHE_2_0.txt,
9// LICENSE_MIT.txt and LICENSE_BOOST_1_0.txt).
10
11#![allow(unsafe_code)]
12
13use std::{
14    convert::TryInto,
15    ffi::CStr,
16    mem::MaybeUninit,
17    os::raw::{c_char, c_void},
18};
19
20use fon::chan::{Ch32, Channel};
21
22use super::{
23    free, pcm, Alsa, SndPcmAccess, SndPcmFormat, SndPcmMode, SndPcmStream,
24};
25
26pub(crate) const DEFAULT: &[u8] = b"default\0";
27
28/// Reset hardware parameters.
29pub(crate) unsafe fn reset_hwp(
30    pcm: *mut c_void,
31    hwp: *mut c_void,
32) -> Option<()> {
33    let format = if cfg!(target_endian = "little") {
34        SndPcmFormat::FloatLe
35    } else if cfg!(target_endian = "big") {
36        SndPcmFormat::FloatBe
37    } else {
38        unreachable!()
39    };
40    pcm::hw_params_any(pcm, hwp).ok()?;
41    pcm::hw_params_set_access(pcm, hwp, SndPcmAccess::RwInterleaved).ok()?;
42    pcm::hw_params_set_format(pcm, hwp, format).ok()?;
43    Some(())
44}
45
46/// Open a PCM Device.
47pub(crate) fn open(
48    name: *const c_char,
49    stream: SndPcmStream,
50) -> Option<(*mut c_void, *mut c_void, u8)> {
51    unsafe {
52        let pcm = pcm::open(name, stream, SndPcmMode::Nonblock).ok()?;
53        let hwp = pcm::hw_params_malloc().ok()?;
54        let mut channels = 0;
55        reset_hwp(pcm, hwp)?;
56        for i in 1..=8 {
57            if pcm::hw_test_channels(pcm, hwp, i).is_ok() {
58                channels |= 1 << (i - 1);
59            }
60        }
61        Some((pcm, hwp, channels))
62    }
63}
64
65pub(crate) trait SoundDevice:
66    std::fmt::Display + From<AudioDevice>
67{
68    const INPUT: bool;
69
70    fn pcm(&self) -> *mut c_void;
71    fn hwp(&self) -> *mut c_void;
72}
73
74/// An Audio Device (input or output).
75#[derive(Debug)]
76pub(crate) struct AudioDevice {
77    /// Human-readable name for the device.
78    pub(crate) name: String,
79    /// PCM For Device.
80    pub(crate) pcm: *mut c_void,
81    /// Hardware parameters for device.
82    pub(crate) hwp: *mut c_void,
83    /// Bitflags for numbers of channels (which of 1-8 are supported)
84    pub(crate) supported: u8,
85    /// File descriptors associated with this device.
86    pub(crate) fds: Vec<smelling_salts::Device>,
87}
88
89impl AudioDevice {
90    /// Generate file descriptors.
91    pub(crate) fn start(&mut self) -> Option<()> {
92        assert!(self.fds.is_empty());
93        // Get file descriptor.
94        let fd_list = unsafe { pcm::poll_descriptors(self.pcm).ok()? };
95        // Add to list.
96        for fd in fd_list {
97            self.fds.push(smelling_salts::Device::new(fd.fd, unsafe {
98                smelling_salts::Watcher::from_raw(fd.events as u32)
99            }));
100        }
101        Some(())
102    }
103}
104
105impl Drop for AudioDevice {
106    fn drop(&mut self) {
107        // Unregister async file descriptors before closing the PCM.
108        for fd in &mut self.fds {
109            fd.old();
110        }
111        // Free hardware parameters and close PCM
112        unsafe {
113            pcm::hw_params_free(self.hwp);
114            pcm::close(self.pcm).unwrap();
115        }
116    }
117}
118
119/// Return a list of available audio devices.
120pub(crate) fn device_list<D: SoundDevice, F: Fn(D) -> T, T>(
121    abstrakt: F,
122) -> Vec<T> {
123    super::ALSA.with(|alsa| {
124        if let Some(alsa) = alsa {
125            device_list_internal(&alsa, abstrakt)
126        } else {
127            Vec::new()
128        }
129    })
130}
131
132fn device_list_internal<D: SoundDevice, F: Fn(D) -> T, T>(
133    alsa: &Alsa,
134    abstrakt: F,
135) -> Vec<T> {
136    let tpcm = CStr::from_bytes_with_nul(b"pcm\0").unwrap();
137    let tname = CStr::from_bytes_with_nul(b"NAME\0").unwrap();
138    let tdesc = CStr::from_bytes_with_nul(b"DESC\0").unwrap();
139    let tioid = CStr::from_bytes_with_nul(b"IOID\0").unwrap();
140
141    let mut hints = MaybeUninit::uninit();
142    let mut devices = Vec::new();
143    unsafe {
144        if (alsa.snd_device_name_hint)(-1, tpcm.as_ptr(), hints.as_mut_ptr())
145            < 0
146        {
147            return Vec::new();
148        }
149        let hints = hints.assume_init();
150        let mut n = hints;
151        while !(*n).is_null() {
152            // Allocate 3 C Strings describing device.
153            let pcm_name = (alsa.snd_device_name_get_hint)(*n, tname.as_ptr());
154            let io = (alsa.snd_device_name_get_hint)(*n, tioid.as_ptr());
155            debug_assert_ne!(pcm_name, std::ptr::null_mut());
156
157            // Convert description to Rust String
158            let name = match CStr::from_ptr(pcm_name).to_str() {
159                Ok(x) if x.starts_with("sysdefault") => {
160                    n = n.offset(1);
161                    continue;
162                }
163                Ok("null") => {
164                    // Can't use epoll on null.
165                    n = n.offset(1);
166                    continue;
167                }
168                Ok("default") => "Default".to_string(),
169                _a => {
170                    let name =
171                        (alsa.snd_device_name_get_hint)(*n, tdesc.as_ptr());
172                    assert_ne!(name, std::ptr::null_mut());
173                    let rust =
174                        CStr::from_ptr(name).to_string_lossy().to_string();
175                    free(name.cast());
176                    rust.replace("\n", ": ")
177                }
178            };
179
180            // Check device io direction.
181            let is_input = io.is_null() || *(io.cast::<u8>()) == b'I';
182            let is_output = io.is_null() || *(io.cast::<u8>()) == b'O';
183            if !io.is_null() {
184                free(io.cast());
185            }
186
187            // Right input type?
188            if (D::INPUT && is_input) || (!D::INPUT && is_output) {
189                // Try to connect to PCM.
190                let dev = open(
191                    pcm_name,
192                    if D::INPUT {
193                        SndPcmStream::Capture
194                    } else {
195                        SndPcmStream::Playback
196                    },
197                );
198
199                if let Some((pcm, hwp, supported)) = dev {
200                    // Add device to list of devices.
201                    devices.push(abstrakt(D::from(AudioDevice {
202                        name,
203                        pcm,
204                        hwp,
205                        supported,
206                        fds: Vec::new(),
207                    })));
208                }
209            }
210            free(pcm_name.cast());
211            n = n.offset(1);
212        }
213        (alsa.snd_device_name_free_hint)(hints);
214    }
215    devices
216}
217
218#[allow(unsafe_code)]
219pub(crate) fn pcm_hw_params(
220    device: &AudioDevice,
221    channels: u8,
222    buffer: &mut Vec<Ch32>,
223    sample_rate: &mut Option<f64>,
224    period: &mut u16,
225) -> Option<()> {
226    unsafe {
227        // Reset hardware parameters to any interleaved native endian float32
228        reset_hwp(device.pcm, device.hwp)?;
229
230        // Set Hz near library target Hz.
231        pcm::hw_params_set_rate_near(
232            device.pcm,
233            device.hwp,
234            &mut crate::consts::SAMPLE_RATE.into(),
235            &mut 0,
236        )
237        .ok()?;
238        // Set the number of channels.
239        pcm::hw_set_channels(device.pcm, device.hwp, channels).ok()?;
240        // Set period near library target period.
241        let mut period_size = crate::consts::PERIOD.into();
242        pcm::hw_params_set_period_size_near(
243            device.pcm,
244            device.hwp,
245            &mut period_size,
246            &mut 0,
247        )
248        .ok()?;
249        // Some buffer size should always be available (match period).
250        pcm::hw_params_set_buffer_size_near(
251            device.pcm,
252            device.hwp,
253            &mut period_size,
254        )
255        .ok()?;
256        // Should always be able to apply parameters that succeeded
257        pcm::hw_params(device.pcm, device.hwp).ok()?;
258
259        // Now that a configuration has been chosen, we can retreive the actual
260        // exact sample rate.
261        *sample_rate = Some(pcm::hw_get_rate(device.hwp)?);
262
263        // Set the period of the buffer.
264        *period = period_size.try_into().ok()?;
265
266        // Resize the buffer
267        buffer.resize(*period as usize * channels as usize, Ch32::MID);
268
269        // Empty the audio buffer to avoid artifacts on startup.
270        let _ = pcm::drop(device.pcm);
271        // Should always be able to apply parameters that succeeded
272        pcm::prepare(device.pcm).ok()?;
273    }
274
275    Some(())
276}