diff options
Diffstat (limited to 'third_party/rust/alsa/src/direct/pcm.rs')
-rw-r--r-- | third_party/rust/alsa/src/direct/pcm.rs | 630 |
1 files changed, 630 insertions, 0 deletions
diff --git a/third_party/rust/alsa/src/direct/pcm.rs b/third_party/rust/alsa/src/direct/pcm.rs new file mode 100644 index 0000000000..ccea666145 --- /dev/null +++ b/third_party/rust/alsa/src/direct/pcm.rs @@ -0,0 +1,630 @@ +/*! +This module bypasses alsa-lib and directly read and write into memory mapped kernel memory. +In case of the sample memory, this is in many cases the DMA buffers that is transferred to the sound card. + +The reasons for doing this are: + + * Minimum overhead where it matters most: let alsa-lib do the code heavy setup - + then steal its file descriptor and deal with sample streaming from Rust. + * RT-safety to the maximum extent possible. Creating/dropping any of these structs causes syscalls, + but function calls on these are just read and write from memory. No syscalls, no memory allocations, + not even loops (with the exception of `MmapPlayback::write` that loops over samples to write). + * Possibility to allow Send + Sync for structs + * It's a fun experiment and an interesting deep dive into how alsa-lib does things. + +Note: Not all sound card drivers support this direct method of communication; although almost all +modern/common ones do. It only works with hardware devices though (such as "hw:xxx" device strings), +don't expect it to work with, e g, the PulseAudio plugin or so. + +For an example of how to use this mode, look in the "synth-example" directory. +*/ + +use libc; +use std::{mem, ptr, fmt, cmp}; +use crate::error::{Error, Result}; +use std::os::unix::io::RawFd; +use crate::{pcm, PollDescriptors, Direction}; +use crate::pcm::Frames; +use std::marker::PhantomData; + +use super::ffi::*; + +/// Read PCM status via a simple kernel syscall, bypassing alsa-lib. +/// +/// If Status is not available on your architecture, this is the second best option. +pub struct SyncPtrStatus(snd_pcm_mmap_status); + +impl SyncPtrStatus { + /// Executes sync_ptr syscall. + /// + /// Unsafe because + /// - setting appl_ptr and avail_min might make alsa-lib confused + /// - no check that the fd is really a PCM + pub unsafe fn sync_ptr(fd: RawFd, hwsync: bool, appl_ptr: Option<pcm::Frames>, avail_min: Option<pcm::Frames>) -> Result<Self> { + let mut data = snd_pcm_sync_ptr { + flags: (if hwsync { SNDRV_PCM_SYNC_PTR_HWSYNC } else { 0 }) + + (if appl_ptr.is_some() { SNDRV_PCM_SYNC_PTR_APPL } else { 0 }) + + (if avail_min.is_some() { SNDRV_PCM_SYNC_PTR_AVAIL_MIN } else { 0 }), + c: snd_pcm_mmap_control_r { + control: snd_pcm_mmap_control { + appl_ptr: appl_ptr.unwrap_or(0) as snd_pcm_uframes_t, + avail_min: avail_min.unwrap_or(0) as snd_pcm_uframes_t, + } + }, + s: mem::zeroed() + }; + + sndrv_pcm_ioctl_sync_ptr(fd, &mut data).map_err(|_| + Error::new("SNDRV_PCM_IOCTL_SYNC_PTR", nix::errno::Errno::last() as i32))?; + + let i = data.s.status.state; + if (i >= (pcm::State::Open as snd_pcm_state_t)) && (i <= (pcm::State::Disconnected as snd_pcm_state_t)) { + Ok(SyncPtrStatus(data.s.status)) + } else { + Err(Error::unsupported("SNDRV_PCM_IOCTL_SYNC_PTR returned broken state")) + } + } + + pub fn hw_ptr(&self) -> pcm::Frames { self.0.hw_ptr as pcm::Frames } + pub fn state(&self) -> pcm::State { unsafe { mem::transmute(self.0.state as u8) } /* valid range checked in sync_ptr */ } + pub fn htstamp(&self) -> libc::timespec { self.0.tstamp } +} + + + +/// Read PCM status directly from memory, bypassing alsa-lib. +/// +/// This means that it's +/// 1) less overhead for reading status (no syscall, no allocations, no virtual dispatch, just a read from memory) +/// 2) Send + Sync, and +/// 3) will only work for "hw" / "plughw" devices (not e g PulseAudio plugins), and not +/// all of those are supported, although all common ones are (as of 2017, and a kernel from the same decade). +/// Kernel supported archs are: x86, PowerPC, Alpha. Use "SyncPtrStatus" for other archs. +/// +/// The values are updated every now and then by the kernel. Many functions will force an update to happen, +/// e g `PCM::avail()` and `PCM::delay()`. +/// +/// Note: Even if you close the original PCM device, ALSA will not actually close the device until all +/// Status structs are dropped too. +/// +#[derive(Debug)] +pub struct Status(DriverMemory<snd_pcm_mmap_status>); + +fn pcm_to_fd(p: &pcm::PCM) -> Result<RawFd> { + let mut fds: [libc::pollfd; 1] = unsafe { mem::zeroed() }; + let c = PollDescriptors::fill(p, &mut fds)?; + if c != 1 { + return Err(Error::unsupported("snd_pcm_poll_descriptors returned wrong number of fds")) + } + Ok(fds[0].fd) +} + +impl Status { + pub fn new(p: &pcm::PCM) -> Result<Self> { Status::from_fd(pcm_to_fd(p)?) } + + pub fn from_fd(fd: RawFd) -> Result<Self> { + DriverMemory::new(fd, 1, SNDRV_PCM_MMAP_OFFSET_STATUS as libc::off_t, false).map(Status) + } + + /// Current PCM state. + pub fn state(&self) -> pcm::State { + unsafe { + let i = ptr::read_volatile(&(*self.0.ptr).state); + assert!((i >= (pcm::State::Open as snd_pcm_state_t)) && (i <= (pcm::State::Disconnected as snd_pcm_state_t))); + mem::transmute(i as u8) + } + } + + /// Number of frames hardware has read or written + /// + /// This number is updated every now and then by the kernel. + /// Calling most functions on the PCM will update it, so will usually a period interrupt. + /// No guarantees given. + /// + /// This value wraps at "boundary" (a large value you can read from SwParams). + pub fn hw_ptr(&self) -> pcm::Frames { + unsafe { + ptr::read_volatile(&(*self.0.ptr).hw_ptr) as pcm::Frames + } + } + + /// Timestamp - fast version of alsa-lib's Status::get_htstamp + /// + /// Note: This just reads the actual value in memory. + /// Unfortunately, the timespec is too big to be read atomically on most archs. + /// Therefore, this function can potentially give bogus result at times, at least in theory...? + pub fn htstamp(&self) -> libc::timespec { + unsafe { + ptr::read_volatile(&(*self.0.ptr).tstamp) + } + } + + /// Audio timestamp - fast version of alsa-lib's Status::get_audio_htstamp + /// + /// Note: This just reads the actual value in memory. + /// Unfortunately, the timespec is too big to be read atomically on most archs. + /// Therefore, this function can potentially give bogus result at times, at least in theory...? + pub fn audio_htstamp(&self) -> libc::timespec { + unsafe { + ptr::read_volatile(&(*self.0.ptr).audio_tstamp) + } + } +} + +/// Write PCM appl ptr directly, bypassing alsa-lib. +/// +/// Provides direct access to appl ptr and avail min, without the overhead of +/// alsa-lib or a syscall. Caveats that apply to Status applies to this struct too. +#[derive(Debug)] +pub struct Control(DriverMemory<snd_pcm_mmap_control>); + +impl Control { + pub fn new(p: &pcm::PCM) -> Result<Self> { Self::from_fd(pcm_to_fd(p)?) } + + pub fn from_fd(fd: RawFd) -> Result<Self> { + DriverMemory::new(fd, 1, SNDRV_PCM_MMAP_OFFSET_CONTROL as libc::off_t, true).map(Control) + } + + /// Read number of frames application has read or written + /// + /// This value wraps at "boundary" (a large value you can read from SwParams). + pub fn appl_ptr(&self) -> pcm::Frames { + unsafe { + ptr::read_volatile(&(*self.0.ptr).appl_ptr) as pcm::Frames + } + } + + /// Set number of frames application has read or written + /// + /// When the kernel wakes up due to a period interrupt, this value will + /// be checked by the kernel. An XRUN will happen in case the application + /// has not read or written enough data. + pub fn set_appl_ptr(&self, value: pcm::Frames) { + unsafe { + ptr::write_volatile(&mut (*self.0.ptr).appl_ptr, value as snd_pcm_uframes_t) + } + } + + /// Read minimum number of frames in buffer in order to wakeup process + pub fn avail_min(&self) -> pcm::Frames { + unsafe { + ptr::read_volatile(&(*self.0.ptr).avail_min) as pcm::Frames + } + } + + /// Write minimum number of frames in buffer in order to wakeup process + pub fn set_avail_min(&self, value: pcm::Frames) { + unsafe { + ptr::write_volatile(&mut (*self.0.ptr).avail_min, value as snd_pcm_uframes_t) + } + } +} + +struct DriverMemory<S> { + ptr: *mut S, + size: libc::size_t, +} + +impl<S> fmt::Debug for DriverMemory<S> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "DriverMemory({:?})", self.ptr) } +} + +impl<S> DriverMemory<S> { + fn new(fd: RawFd, count: usize, offs: libc::off_t, writable: bool) -> Result<Self> { + let mut total = count * mem::size_of::<S>(); + let ps = pagesize(); + assert!(total > 0); + if total % ps != 0 { total += ps - total % ps }; + let flags = if writable { libc::PROT_WRITE | libc::PROT_READ } else { libc::PROT_READ }; + let p = unsafe { libc::mmap(ptr::null_mut(), total, flags, libc::MAP_FILE | libc::MAP_SHARED, fd, offs) }; + if p.is_null() || p == libc::MAP_FAILED { + Err(Error::new("mmap (of driver memory)", nix::errno::Errno::last() as i32)) + } else { + Ok(DriverMemory { ptr: p as *mut S, size: total }) + } + } +} + +unsafe impl<S> Send for DriverMemory<S> {} +unsafe impl<S> Sync for DriverMemory<S> {} + +impl<S> Drop for DriverMemory<S> { + fn drop(&mut self) { + unsafe {{ libc::munmap(self.ptr as *mut libc::c_void, self.size); } } + } +} + +#[derive(Debug)] +struct SampleData<S> { + mem: DriverMemory<S>, + frames: pcm::Frames, + channels: u32, +} + +impl<S> SampleData<S> { + pub fn new(p: &pcm::PCM) -> Result<Self> { + let params = p.hw_params_current()?; + let bufsize = params.get_buffer_size()?; + let channels = params.get_channels()?; + if params.get_access()? != pcm::Access::MMapInterleaved { + return Err(Error::unsupported("Not MMAP interleaved data")) + } + + let fd = pcm_to_fd(p)?; + let info = unsafe { + let mut info: snd_pcm_channel_info = mem::zeroed(); + sndrv_pcm_ioctl_channel_info(fd, &mut info).map_err(|_| + Error::new("SNDRV_PCM_IOCTL_CHANNEL_INFO", nix::errno::Errno::last() as i32))?; + info + }; + // println!("{:?}", info); + if (info.step != channels * mem::size_of::<S>() as u32 * 8) || (info.first != 0) { + return Err(Error::unsupported("MMAP data size mismatch")) + } + Ok(SampleData { + mem: DriverMemory::new(fd, (bufsize as usize) * (channels as usize), info.offset as libc::off_t, true)?, + frames: bufsize, + channels, + }) + } +} + + +/// Dummy trait for better generics +pub trait MmapDir: fmt::Debug { + const DIR: Direction; + fn avail(hwptr: Frames, applptr: Frames, buffersize: Frames, boundary: Frames) -> Frames; +} + +/// Dummy struct for better generics +#[derive(Copy, Clone, Debug)] +pub struct Playback; + +impl MmapDir for Playback { + const DIR: Direction = Direction::Playback; + #[inline] + fn avail(hwptr: Frames, applptr: Frames, buffersize: Frames, boundary: Frames) -> Frames { + let r = hwptr.wrapping_add(buffersize).wrapping_sub(applptr); + let r = if r < 0 { r.wrapping_add(boundary) } else { r }; + if r as usize >= boundary as usize { r.wrapping_sub(boundary) } else { r } + } +} + +/// Dummy struct for better generics +#[derive(Copy, Clone, Debug)] +pub struct Capture; + +impl MmapDir for Capture { + const DIR: Direction = Direction::Capture; + #[inline] + fn avail(hwptr: Frames, applptr: Frames, _buffersize: Frames, boundary: Frames) -> Frames { + let r = hwptr.wrapping_sub(applptr); + if r < 0 { r.wrapping_add(boundary) } else { r } + } +} + +pub type MmapPlayback<S> = MmapIO<S, Playback>; + +pub type MmapCapture<S> = MmapIO<S, Capture>; + +#[derive(Debug)] +/// Struct containing direct I/O functions shared between playback and capture. +pub struct MmapIO<S, D> { + data: SampleData<S>, + c: Control, + ss: Status, + bound: Frames, + dir: PhantomData<*const D>, +} + +#[derive(Debug, Clone, Copy)] +/// A raw pointer to samples, and the amount of samples readable or writable. +pub struct RawSamples<S> { + pub ptr: *mut S, + pub frames: Frames, + pub channels: u32, +} + +impl<S> RawSamples<S> { + #[inline] + /// Returns `frames` * `channels`, i e the amount of samples (of type `S`) that can be read/written. + pub fn samples(&self) -> isize { self.frames as isize * (self.channels as isize) } + + /// Writes samples from an iterator. + /// + /// Returns true if iterator was depleted, and the number of samples written. + /// This is just raw read/write of memory. + pub unsafe fn write_samples<I: Iterator<Item=S>>(&self, i: &mut I) -> (bool, isize) { + let mut z = 0; + let max_samples = self.samples(); + while z < max_samples { + let b = if let Some(b) = i.next() { b } else { return (true, z) }; + ptr::write_volatile(self.ptr.offset(z), b); + z += 1; + }; + (false, z) + } + +} + +impl<S, D: MmapDir> MmapIO<S, D> { + fn new(p: &pcm::PCM) -> Result<Self> { + if p.info()?.get_stream() != D::DIR { + return Err(Error::unsupported("Wrong direction")); + } + let boundary = p.sw_params_current()?.get_boundary()?; + Ok(MmapIO { + data: SampleData::new(p)?, + c: Control::new(p)?, + ss: Status::new(p)?, + bound: boundary, + dir: PhantomData, + }) + } +} + +pub (crate) fn new_mmap<S, D: MmapDir>(p: &pcm::PCM) -> Result<MmapIO<S, D>> { MmapIO::new(p) } + +impl<S, D: MmapDir> MmapIO<S, D> { + /// Read current status + pub fn status(&self) -> &Status { &self.ss } + + /// Read current number of frames committed by application + /// + /// This number wraps at 'boundary'. + #[inline] + pub fn appl_ptr(&self) -> Frames { self.c.appl_ptr() } + + /// Read current number of frames read / written by hardware + /// + /// This number wraps at 'boundary'. + #[inline] + pub fn hw_ptr(&self) -> Frames { self.ss.hw_ptr() } + + /// The number at which hw_ptr and appl_ptr wraps. + #[inline] + pub fn boundary(&self) -> Frames { self.bound } + + /// Total number of frames in hardware buffer + #[inline] + pub fn buffer_size(&self) -> Frames { self.data.frames } + + /// Number of channels in stream + #[inline] + pub fn channels(&self) -> u32 { self.data.channels } + + /// Notifies the kernel that frames have now been read / written by the application + /// + /// This will allow the kernel to write new data into this part of the buffer. + pub fn commit(&self, v: Frames) { + let mut z = self.appl_ptr() + v; + if z + v >= self.boundary() { z -= self.boundary() }; + self.c.set_appl_ptr(z) + } + + /// Number of frames available to read / write. + /// + /// In case of an underrun, this value might be bigger than the buffer size. + pub fn avail(&self) -> Frames { D::avail(self.hw_ptr(), self.appl_ptr(), self.buffer_size(), self.boundary()) } + + /// Returns raw pointers to data to read / write. + /// + /// Use this if you want to read/write data yourself (instead of using iterators). If you do, + /// using `write_volatile` or `read_volatile` is recommended, since it's DMA memory and can + /// change at any time. + /// + /// Since this is a ring buffer, there might be more data to read/write in the beginning + /// of the buffer as well. If so this is returned as the second return value. + pub fn data_ptr(&self) -> (RawSamples<S>, Option<RawSamples<S>>) { + let (hwptr, applptr) = (self.hw_ptr(), self.appl_ptr()); + let c = self.channels(); + let bufsize = self.buffer_size(); + + // These formulas mostly mimic the behaviour of + // snd_pcm_mmap_begin (in alsa-lib/src/pcm/pcm.c). + let offs = applptr % bufsize; + let mut a = D::avail(hwptr, applptr, bufsize, self.boundary()); + a = cmp::min(a, bufsize); + let b = bufsize - offs; + let more_data = if b < a { + let z = a - b; + a = b; + Some( RawSamples { ptr: self.data.mem.ptr, frames: z, channels: c }) + } else { None }; + + let p = unsafe { self.data.mem.ptr.offset(offs as isize * self.data.channels as isize) }; + (RawSamples { ptr: p, frames: a, channels: c }, more_data) + } +} + +impl<S> MmapPlayback<S> { + /// Write samples to the kernel ringbuffer. + pub fn write<I: Iterator<Item=S>>(&mut self, i: &mut I) -> Frames { + let (data, more_data) = self.data_ptr(); + let (iter_end, samples) = unsafe { data.write_samples(i) }; + let mut z = samples / data.channels as isize; + if !iter_end { + if let Some(data2) = more_data { + let (_, samples2) = unsafe { data2.write_samples(i) }; + z += samples2 / data2.channels as isize; + } + } + let z = z as Frames; + self.commit(z); + z + } +} + +impl<S> MmapCapture<S> { + /// Read samples from the kernel ringbuffer. + /// + /// When the iterator is dropped or depleted, the read samples will be committed, i e, + /// the kernel can then write data to the location again. So do this ASAP. + pub fn iter(&mut self) -> CaptureIter<S> { + let (data, more_data) = self.data_ptr(); + CaptureIter { + m: self, + samples: data, + p_offs: 0, + read_samples: 0, + next_p: more_data, + } + } +} + +/// Iterator over captured samples +pub struct CaptureIter<'a, S: 'static> { + m: &'a MmapCapture<S>, + samples: RawSamples<S>, + p_offs: isize, + read_samples: isize, + next_p: Option<RawSamples<S>>, +} + +impl<'a, S: 'static + Copy> CaptureIter<'a, S> { + fn handle_max(&mut self) { + self.p_offs = 0; + if let Some(p2) = self.next_p.take() { + self.samples = p2; + } else { + self.m.commit((self.read_samples / self.samples.channels as isize) as Frames); + self.read_samples = 0; + self.samples.frames = 0; // Shortcut to "None" in case anyone calls us again + } + } +} + +impl<'a, S: 'static + Copy> Iterator for CaptureIter<'a, S> { + type Item = S; + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + if self.p_offs >= self.samples.samples() { + self.handle_max(); + if self.samples.frames <= 0 { return None; } + } + let s = unsafe { ptr::read_volatile(self.samples.ptr.offset(self.p_offs)) }; + self.p_offs += 1; + self.read_samples += 1; + Some(s) + } +} + +impl<'a, S: 'static> Drop for CaptureIter<'a, S> { + fn drop(&mut self) { + self.m.commit((self.read_samples / self.m.data.channels as isize) as Frames); + } +} + + +#[test] +#[ignore] // Not everyone has a recording device on plughw:1. So let's ignore this test by default. +fn record_from_plughw_rw() { + use crate::pcm::*; + use crate::{ValueOr, Direction}; + use std::ffi::CString; + let pcm = PCM::open(&*CString::new("plughw:1").unwrap(), Direction::Capture, false).unwrap(); + let ss = self::Status::new(&pcm).unwrap(); + let c = self::Control::new(&pcm).unwrap(); + let hwp = HwParams::any(&pcm).unwrap(); + hwp.set_channels(2).unwrap(); + hwp.set_rate(44100, ValueOr::Nearest).unwrap(); + hwp.set_format(Format::s16()).unwrap(); + hwp.set_access(Access::RWInterleaved).unwrap(); + pcm.hw_params(&hwp).unwrap(); + + { + let swp = pcm.sw_params_current().unwrap(); + swp.set_tstamp_mode(true).unwrap(); + pcm.sw_params(&swp).unwrap(); + } + assert_eq!(ss.state(), State::Prepared); + pcm.start().unwrap(); + assert_eq!(c.appl_ptr(), 0); + println!("{:?}, {:?}", ss, c); + let mut buf = [0i16; 512*2]; + assert_eq!(pcm.io_i16().unwrap().readi(&mut buf).unwrap(), 512); + assert_eq!(c.appl_ptr(), 512); + + assert_eq!(ss.state(), State::Running); + assert!(ss.hw_ptr() >= 512); + let t2 = ss.htstamp(); + assert!(t2.tv_sec > 0 || t2.tv_nsec > 0); +} + + +#[test] +#[ignore] // Not everyone has a record device on plughw:1. So let's ignore this test by default. +fn record_from_plughw_mmap() { + use crate::pcm::*; + use crate::{ValueOr, Direction}; + use std::ffi::CString; + use std::{thread, time}; + + let pcm = PCM::open(&*CString::new("plughw:1").unwrap(), Direction::Capture, false).unwrap(); + let hwp = HwParams::any(&pcm).unwrap(); + hwp.set_channels(2).unwrap(); + hwp.set_rate(44100, ValueOr::Nearest).unwrap(); + hwp.set_format(Format::s16()).unwrap(); + hwp.set_access(Access::MMapInterleaved).unwrap(); + pcm.hw_params(&hwp).unwrap(); + + let ss = unsafe { SyncPtrStatus::sync_ptr(pcm_to_fd(&pcm).unwrap(), false, None, None).unwrap() }; + assert_eq!(ss.state(), State::Prepared); + + let mut m = pcm.direct_mmap_capture::<i16>().unwrap(); + + assert_eq!(m.status().state(), State::Prepared); + assert_eq!(m.appl_ptr(), 0); + assert_eq!(m.hw_ptr(), 0); + + + println!("{:?}", m); + + let now = time::Instant::now(); + pcm.start().unwrap(); + while m.avail() < 256 { thread::sleep(time::Duration::from_millis(1)) }; + assert!(now.elapsed() >= time::Duration::from_millis(256 * 1000 / 44100)); + let (ptr1, md) = m.data_ptr(); + assert_eq!(ptr1.channels, 2); + assert!(ptr1.frames >= 256); + assert!(md.is_none()); + println!("Has {:?} frames at {:?} in {:?}", m.avail(), ptr1.ptr, now.elapsed()); + let samples: Vec<i16> = m.iter().collect(); + assert!(samples.len() >= ptr1.frames as usize * 2); + println!("Collected {} samples", samples.len()); + let (ptr2, _md) = m.data_ptr(); + assert!(unsafe { ptr1.ptr.offset(256 * 2) } <= ptr2.ptr); +} + +#[test] +#[ignore] +fn playback_to_plughw_mmap() { + use crate::pcm::*; + use crate::{ValueOr, Direction}; + use std::ffi::CString; + + let pcm = PCM::open(&*CString::new("plughw:1").unwrap(), Direction::Playback, false).unwrap(); + let hwp = HwParams::any(&pcm).unwrap(); + hwp.set_channels(2).unwrap(); + hwp.set_rate(44100, ValueOr::Nearest).unwrap(); + hwp.set_format(Format::s16()).unwrap(); + hwp.set_access(Access::MMapInterleaved).unwrap(); + pcm.hw_params(&hwp).unwrap(); + let mut m = pcm.direct_mmap_playback::<i16>().unwrap(); + + assert_eq!(m.status().state(), State::Prepared); + assert_eq!(m.appl_ptr(), 0); + assert_eq!(m.hw_ptr(), 0); + + println!("{:?}", m); + let mut i = (0..(m.buffer_size() * 2)).map(|i| + (((i / 2) as f32 * 2.0 * ::std::f32::consts::PI / 128.0).sin() * 8192.0) as i16); + m.write(&mut i); + assert_eq!(m.appl_ptr(), m.buffer_size()); + + pcm.start().unwrap(); + pcm.drain().unwrap(); + assert_eq!(m.appl_ptr(), m.buffer_size()); + assert!(m.hw_ptr() >= m.buffer_size()); +} |