From 771987063de9dcc33547b65bf9e0b1c93d3e08dc Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 18 Mar 2024 03:36:38 +0100 Subject: [PATCH] feat: add cpal source, refactor sources now splitting stream in channels and parsing stream format are separate but handled by the source, so that cpal source can skip format parsing. added some nicer types, and also range now is +-1 because way easier than 32k sorry this is a huge commit, ive been messing with it for a while and changed a lot across whole project, at this point i'm just committing it because it can only get worse ehe --- Cargo.toml | 1 + src/app.rs | 36 +++++++-------- src/display/mod.rs | 6 ++- src/display/oscilloscope.rs | 9 ++-- src/display/spectroscope.rs | 4 +- src/display/vectorscope.rs | 4 +- src/input/cpal.rs | 67 ++++++++++++++++++++++++++++ src/input/file.rs | 42 ++++++++++++++++++ src/input/format/mod.rs | 11 +++++ src/input/mod.rs | 31 +++++++++++++ src/input/pulse.rs | 61 +++++++++++++++++++++++++ src/main.rs | 52 +++++++++++++++++----- src/music.rs | 2 +- src/parser.rs | 31 ------------- src/source.rs | 88 ------------------------------------- 15 files changed, 283 insertions(+), 162 deletions(-) create mode 100644 src/input/cpal.rs create mode 100644 src/input/file.rs create mode 100644 src/input/format/mod.rs create mode 100644 src/input/mod.rs create mode 100644 src/input/pulse.rs delete mode 100644 src/parser.rs delete mode 100644 src/source.rs diff --git a/Cargo.toml b/Cargo.toml index 49133f6..752dec1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ crossterm = { version = "0.27", optional = true } # for pulseaudio libpulse-binding = { version = "2.0", optional = true } libpulse-simple-binding = { version = "2.25", optional = true } +cpal = "0.15.3" [features] default = ["tui", "pulseaudio"] diff --git a/src/app.rs b/src/app.rs index 5410ce6..42749e1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ -use std::{io, time::{Duration, Instant}, ops::Range}; +use std::{io, ops::Range, time::{Duration, Instant}}; use ratatui::{ style::Color, widgets::{Table, Row, Cell}, symbols::Marker, backend::Backend, @@ -8,8 +8,7 @@ use ratatui::{ }; use crossterm::event::{self, Event, KeyCode, KeyModifiers}; -use crate::{source::DataSource, display::{GraphConfig, oscilloscope::Oscilloscope, DisplayMode, Dimension, vectorscope::Vectorscope, spectroscope::Spectroscope}}; -use crate::parser::{SampleParser, Signed16PCM}; +use crate::{display::{oscilloscope::Oscilloscope, spectroscope::Spectroscope, vectorscope::Vectorscope, Dimension, DisplayMode, GraphConfig}, input::{Matrix, DataSource}}; pub enum CurrentDisplayMode { Oscilloscope, @@ -18,7 +17,7 @@ pub enum CurrentDisplayMode { } pub struct App { - channels: u8, + #[allow(unused)] channels: u8, graph: GraphConfig, oscilloscope: Oscilloscope, vectorscope: Vectorscope, @@ -33,9 +32,9 @@ impl From::<&crate::ScopeArgs> for App { axis_color: Color::DarkGray, labels_color: Color::Cyan, palette: vec![Color::Red, Color::Yellow, Color::Green, Color::Magenta], - scale: args.range, - width: args.buffer / (2 * args.channels as u32), // TODO also make bit depth customizable - samples: args.buffer / (2 * args.channels as u32), + scale: args.scale as f64, + width: args.buffer, // TODO also make bit depth customizable + samples: args.buffer, sampling_rate: args.sample_rate, references: !args.no_reference, show_ui: !args.no_ui, @@ -55,27 +54,24 @@ impl From::<&crate::ScopeArgs> for App { App { graph, oscilloscope, vectorscope, spectroscope, mode: CurrentDisplayMode::Oscilloscope, - channels: args.channels, + channels: args.channels as u8, } } } impl App { - pub fn run(&mut self, mut source: Box, terminal: &mut Terminal) -> Result<(), io::Error> { - // prepare globals - let fmt = Signed16PCM{}; // TODO some way to choose this? - + pub fn run(&mut self, mut source: Box>, terminal: &mut Terminal) -> Result<(), io::Error> { let mut fps = 0; let mut framerate = 0; let mut last_poll = Instant::now(); - let mut channels = vec![]; + let mut channels = Matrix::default(); loop { let data = source.recv() .ok_or(io::Error::new(io::ErrorKind::BrokenPipe, "data source returned null"))?; if !self.graph.pause { - channels = fmt.oscilloscope(data, self.channels); + channels = data; } fps += 1; @@ -107,7 +103,7 @@ impl App { .x_axis(self.current_display().axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes? .y_axis(self.current_display().axis(&self.graph, Dimension::Y)); f.render_widget(chart, size) - }).unwrap(); + })?; } while event::poll(Duration::from_millis(0))? { // process all enqueued events @@ -151,8 +147,8 @@ impl App { _ => 1.0, }; match key.code { - KeyCode::Up => update_value_i(&mut self.graph.scale, true, 250, magnitude, 0..65535), // inverted to act as zoom - KeyCode::Down => update_value_i(&mut self.graph.scale, false, 250, magnitude, 0..65535), // inverted to act as zoom + KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..1.5), // inverted to act as zoom + KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..1.5), // inverted to act as zoom KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width*2), KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width*2), KeyCode::Char('q') => quit = true, @@ -169,7 +165,7 @@ impl App { }, KeyCode::Esc => { self.graph.samples = self.graph.width; - self.graph.scale = 20000; + self.graph.scale = 1.; }, _ => {}, } @@ -212,9 +208,9 @@ fn make_header<'a>(cfg: &GraphConfig, module_header: &'a str, kind_o_scope: &'st vec![ Row::new( vec![ - Cell::from(format!("{}::scope-tui", kind_o_scope)).style(Style::default().fg(*cfg.palette.get(0).expect("empty palette?")).add_modifier(Modifier::BOLD)), + Cell::from(format!("{}::scope-tui", kind_o_scope)).style(Style::default().fg(*cfg.palette.first().expect("empty palette?")).add_modifier(Modifier::BOLD)), Cell::from(module_header), - Cell::from(format!("-{}+", cfg.scale)), + Cell::from(format!("-{:.2}x+", cfg.scale)), Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)), Cell::from(format!("{}fps", fps)), Cell::from(if cfg.scatter { "***" } else { "---" }), diff --git a/src/display/mod.rs b/src/display/mod.rs index 481c0d4..18fa72a 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -5,6 +5,8 @@ pub mod spectroscope; use crossterm::event::Event; use ratatui::{widgets::{Dataset, Axis, GraphType}, style::{Style, Color}, symbols::Marker}; +use crate::input::Matrix; + pub enum Dimension { X, Y } @@ -14,7 +16,7 @@ pub struct GraphConfig { pub pause: bool, pub samples: u32, pub sampling_rate: u32, - pub scale: u32, + pub scale: f64, pub width: u32, pub scatter: bool, pub references: bool, @@ -36,7 +38,7 @@ pub trait DisplayMode { // MUST define fn from_args(args: &crate::ScopeArgs) -> Self where Self : Sized; fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this - fn process(&mut self, cfg: &GraphConfig, data: &Vec>) -> Vec; + fn process(&mut self, cfg: &GraphConfig, data: &Matrix) -> Vec; fn mode_str(&self) -> &'static str; // SHOULD override diff --git a/src/display/oscilloscope.rs b/src/display/oscilloscope.rs index 99cb019..f0eff28 100644 --- a/src/display/oscilloscope.rs +++ b/src/display/oscilloscope.rs @@ -1,7 +1,7 @@ use crossterm::event::{Event, KeyModifiers, KeyCode}; use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span}; -use crate::app::{update_value_f, update_value_i}; +use crate::{app::{update_value_f, update_value_i}, input::Matrix}; use super::{DisplayMode, GraphConfig, DataSet, Dimension}; @@ -47,7 +47,7 @@ impl DisplayMode for Oscilloscope { fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis { let (name, bounds) = match dimension { Dimension::X => ("time -", [0.0, cfg.samples as f64]), - Dimension::Y => ("| amplitude", [-(cfg.scale as f64), cfg.scale as f64]), + Dimension::Y => ("| amplitude", [-cfg.scale, cfg.scale]), }; let mut a = Axis::default(); if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here @@ -62,7 +62,7 @@ impl DisplayMode for Oscilloscope { ] } - fn process(&mut self, cfg: &GraphConfig, data: &Vec>) -> Vec { + fn process(&mut self, cfg: &GraphConfig, data: &Matrix) -> Vec { let mut out = Vec::new(); let mut trigger_offset = 0; @@ -71,9 +71,8 @@ impl DisplayMode for Oscilloscope { for i in 0..data[0].len() { if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { // triggered break; - } else { - trigger_offset += 1; } + trigger_offset += 1; } } diff --git a/src/display/spectroscope.rs b/src/display/spectroscope.rs index 00280f0..da99133 100644 --- a/src/display/spectroscope.rs +++ b/src/display/spectroscope.rs @@ -3,7 +3,7 @@ use std::collections::VecDeque; use crossterm::event::{Event, KeyCode}; use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span}; -use crate::app::update_value_i; +use crate::{app::update_value_i, input::Matrix}; use super::{DisplayMode, GraphConfig, DataSet, Dimension}; @@ -90,7 +90,7 @@ impl DisplayMode for Spectroscope { a.style(Style::default().fg(cfg.axis_color)).bounds(bounds) } - fn process(&mut self, cfg: &GraphConfig, data: &Vec>) -> Vec { + fn process(&mut self, cfg: &GraphConfig, data: &Matrix) -> Vec { if self.average == 0 { self.average = 1 } // otherwise fft breaks if !cfg.pause { for (i, chan) in data.iter().enumerate() { diff --git a/src/display/vectorscope.rs b/src/display/vectorscope.rs index c8c95a1..1d2d9c1 100644 --- a/src/display/vectorscope.rs +++ b/src/display/vectorscope.rs @@ -1,5 +1,7 @@ use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span}; +use crate::input::Matrix; + use super::{DisplayMode, GraphConfig, DataSet, Dimension}; #[derive(Default)] @@ -41,7 +43,7 @@ impl DisplayMode for Vectorscope { ] } - fn process(&mut self, cfg: &GraphConfig, data: &Vec>) -> Vec { + fn process(&mut self, cfg: &GraphConfig, data: &Matrix) -> Vec { let mut out = Vec::new(); for (n, chunk) in data.chunks(2).enumerate() { diff --git a/src/input/cpal.rs b/src/input/cpal.rs new file mode 100644 index 0000000..eb70cba --- /dev/null +++ b/src/input/cpal.rs @@ -0,0 +1,67 @@ +use std::sync::mpsc; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; + +use super::{stream_to_matrix, Matrix}; + +pub struct DefaultAudioDeviceWithCPAL { + rx: mpsc::Receiver>, + #[allow(unused)] + stream: cpal::Stream, +} + +#[derive(Debug, thiserror::Error)] +pub enum AudioDeviceErrors { + #[error("{0}")] + Device(#[from] cpal::DevicesError), + + #[error("device not found")] + NotFound, + + #[error("{0}")] + BuildStream(#[from] cpal::BuildStreamError), + + #[error("{0}")] + PlayStream(#[from] cpal::PlayStreamError), +} + +impl DefaultAudioDeviceWithCPAL { + pub fn new(device: Option<&str>, channels: u32, sample_rate: u32, buffer: u32, timeout_secs: u64) -> Result>, AudioDeviceErrors> { + let host = cpal::default_host(); + let device = match device { + Some(name) => host + .input_devices()? + .find(|x| x.name().as_deref().unwrap_or("") == name) + .ok_or(AudioDeviceErrors::NotFound)?, + None => host + .default_input_device() + .ok_or(AudioDeviceErrors::NotFound)?, + }; + let cfg = cpal::StreamConfig { + channels: channels as u16, + buffer_size: cpal::BufferSize::Fixed(buffer * channels * 2), + sample_rate: cpal::SampleRate(sample_rate), + }; + let (tx, rx) = mpsc::channel(); + let stream = device.build_input_stream( + &cfg, + move |data:&[f32], _info| tx.send(stream_to_matrix(data.iter().cloned(), channels as usize, 1.)).unwrap_or(()), + |e| eprintln!("error in input stream: {e}"), + Some(std::time::Duration::from_secs(timeout_secs)), + )?; + stream.play()?; + + Ok(Box::new(DefaultAudioDeviceWithCPAL { stream, rx })) + } +} + +impl super::DataSource for DefaultAudioDeviceWithCPAL { + fn recv(&mut self) -> Option> { + match self.rx.recv() { + Ok(x) => Some(x), + Err(e) => { + println!("error receiving from source? {e}"); + None + }, + } + } +} diff --git a/src/input/file.rs b/src/input/file.rs new file mode 100644 index 0000000..3aa3980 --- /dev/null +++ b/src/input/file.rs @@ -0,0 +1,42 @@ +use std::{fs::File, io::Read}; + +use super::{format::{SampleParser, Signed16PCM}, stream_to_matrix, Matrix}; + +pub struct FileSource { + file: File, + buffer: Vec, + channels: usize, + sample_rate: usize, + limit_rate: bool, + // TODO when all data is available (eg, file) limit data flow to make it + // somehow visualizable. must be optional because named pipes block + // TODO support more formats +} + +impl FileSource { + #[allow(clippy::new_ret_no_self)] + pub fn new(path: &str, channels: usize, sample_rate: usize, buffer: usize, limit_rate: bool) -> Result>, std::io::Error> { + Ok(Box::new( + FileSource { + channels, sample_rate, limit_rate, + file: File::open(path)?, + buffer: vec![0u8; buffer * channels], + } + )) + } +} + +impl super::DataSource for FileSource { + fn recv(&mut self) -> Option> { + match self.file.read_exact(&mut self.buffer) { + Ok(()) => Some( + stream_to_matrix( + self.buffer.chunks(2).map(Signed16PCM::parse), + self.channels, + 32768.0, + ) + ), + Err(_e) => None, // TODO log it + } + } +} diff --git a/src/input/format/mod.rs b/src/input/format/mod.rs new file mode 100644 index 0000000..f823218 --- /dev/null +++ b/src/input/format/mod.rs @@ -0,0 +1,11 @@ + +pub trait SampleParser { + fn parse(data: &[u8]) -> T; +} + +pub struct Signed16PCM; +impl SampleParser for Signed16PCM { + fn parse(chunk: &[u8]) -> f64 { + (chunk[0] as i16 | (chunk[1] as i16) << 8) as f64 + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..4e4e61a --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,31 @@ +pub mod format; + +#[cfg(feature = "pulseaudio")] +pub mod pulse; + +pub mod file; + +pub mod cpal; + +pub type Matrix = Vec>; + +pub trait DataSource { + fn recv(&mut self) -> Option>; // TODO convert in Result and make generic error +} + +/// separate a stream of alternating channels into a matrix of channel streams: +/// L R L R L R L R L R +/// becomes +/// L L L L L +/// R R R R R +pub fn stream_to_matrix(stream: impl Iterator, channels: usize, norm: O) -> Matrix +where I : Copy + Into, O : Copy + std::ops::Div +{ + let mut out = vec![vec![]; channels]; + let mut channel = 0; + for sample in stream { + out[channel].push(sample.into() / norm); + channel = (channel + 1) % channels; + } + out +} diff --git a/src/input/pulse.rs b/src/input/pulse.rs new file mode 100644 index 0000000..dfe8e33 --- /dev/null +++ b/src/input/pulse.rs @@ -0,0 +1,61 @@ +use libpulse_binding::{sample::{Spec, Format}, def::BufferAttr, error::PAErr, stream::Direction}; +use libpulse_simple_binding::Simple; + +use super::{format::{SampleParser, Signed16PCM}, stream_to_matrix}; + +pub struct PulseAudioSimpleDataSource { + simple: Simple, + buffer: Vec, + channels: usize, +} + +impl PulseAudioSimpleDataSource { + #[allow(clippy::new_ret_no_self)] + pub fn new( + device: Option<&str>, channels: u8, rate: u32, buffer: u32, server_buffer: u32 + ) -> Result>, PAErr> { + let spec = Spec { + format: Format::S16NE, // TODO allow more formats? + channels, rate, + }; + if !spec.is_valid() { + return Err(PAErr(0)); // TODO what error number should we throw? + } + let attrs = BufferAttr { + maxlength: server_buffer * buffer * channels as u32 * 2, + fragsize: buffer, + ..Default::default() + }; + let simple = Simple::new( + None, // Use the default server + "scope-tui", // Our application’s name + Direction::Record, // We want a record stream + device, // Use requested device, or default + "data", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + Some(&attrs), // Our hints on how to handle client/server buffers + )?; + Ok(Box::new(Self { + simple, + buffer: vec![0; buffer as usize * channels as usize * 2], + channels: channels as usize + })) + } +} + +impl super::DataSource for PulseAudioSimpleDataSource { + fn recv(&mut self) -> Option> { + match self.simple.read(&mut self.buffer) { + Ok(()) => Some(stream_to_matrix( + self.buffer.chunks(2).map(Signed16PCM::parse), + self.channels, + 32768.0, + )), + Err(e) => { + eprintln!("[!] could not receive from pulseaudio: {}", e); + None + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 07e9690..50cad92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ -mod parser; mod app; mod music; -mod source; +mod input; mod display; use app::App; @@ -37,23 +36,23 @@ pub struct ScopeArgs { /// number of channels to open #[arg(long, value_name = "N", default_value_t = 2)] - channels: u8, + channels: usize, /// tune buffer size to be in tune with given note (overrides buffer option) #[arg(long, value_name = "NOTE")] tune: Option, /// size of audio buffer, and width of scope - #[arg(short, long, value_name = "SIZE", default_value_t = 8192)] + #[arg(short, long, value_name = "SIZE", default_value_t = 2048)] buffer: u32, /// sample rate to use - #[arg(long, value_name = "HZ", default_value_t = 44100)] + #[arg(long, value_name = "HZ", default_value_t = 48000)] sample_rate: u32, - /// max value, positive and negative, on amplitude scale - #[arg(short, long, value_name = "SIZE", default_value_t = 20000)] - range: u32, // TODO counterintuitive, improve this + /// floating point vertical scale, from 0 to 1 + #[arg(short, long, value_name = "x", default_value_t = 1.0)] + scale: f32, /// use vintage looking scatter mode instead of line mode #[arg(long, default_value_t = false)] @@ -90,7 +89,21 @@ pub enum ScopeSource { File { /// path on filesystem of file or pipe path: String, + + /// limit data flow to match requested sample rate (UNIMPLEMENTED) + #[arg(short, long, default_value_t = false)] + limit_rate: bool, }, + + /// use new experimental CPAL backend + Audio { + /// source device to attach to + device: Option, + + /// timeout (in seconds) waiting for audio stream + #[arg(long, default_value_t = 60)] + timeout: u64, + } } fn main() -> Result<(), Box> { @@ -111,19 +124,34 @@ fn main() -> Result<(), Box> { #[cfg(feature = "pulseaudio")] ScopeSource::Pulse { device, server_buffer } => { - source::pulseaudio::PulseAudioSimpleDataSource::new( + input::pulse::PulseAudioSimpleDataSource::new( device.as_deref(), - args.channels, + args.channels as u8, args.sample_rate, args.buffer, *server_buffer, )? }, - ScopeSource::File { path } => { - source::file::FileSource::new(path, args.buffer)? + ScopeSource::File { path, limit_rate } => { + input::file::FileSource::new( + path, + args.channels, + args.sample_rate as usize, + args.buffer as usize, + *limit_rate + )? }, + ScopeSource::Audio { device, timeout } => { + input::cpal::DefaultAudioDeviceWithCPAL::new( + device.as_deref(), + args.channels as u32, + args.sample_rate, + args.buffer, + *timeout, + )? + } }; let mut app = App::from(&args); diff --git a/src/music.rs b/src/music.rs index 56f5f13..e46938a 100644 --- a/src/music.rs +++ b/src/music.rs @@ -68,7 +68,7 @@ impl Note { pub fn tune_buffer_size(&self, sample_rate: u32) -> u32 { let t = 1.0 / self.tone.freq(self.octave); // periodo ? let buf = (sample_rate as f32) * t; - (buf * 4.0).round() as u32 + buf.round() as u32 } } diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index aa4ae3b..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,31 +0,0 @@ -// use libpulse_binding::sample::Format; - -// pub fn parser(fmt: Format) -> impl SampleParser { -// match fmt { -// Format::S16NE => Signed16PCM {}, -// _ => panic!("parser not implemented for this format") -// } -// } - -pub trait SampleParser { - fn oscilloscope(&self, data: &[u8], channels: u8) -> Vec>; - fn sample_size(&self) -> usize; -} - -pub struct Signed16PCM(); - -/// TODO these are kinda inefficient, can they be faster? -impl SampleParser for Signed16PCM { - fn oscilloscope(&self, data: &[u8], channels: u8) -> Vec> { - let mut out = vec![vec![]; channels as usize]; - let mut channel = 0; - for chunk in data.chunks(2) { - let buf = chunk[0] as i16 | (chunk[1] as i16) << 8; - out[channel].push(buf as f64); - channel = (channel + 1 ) % channels as usize; - } - out - } - - fn sample_size(&self) -> usize { 2 } // 16 bit, thus 2 bytes -} diff --git a/src/source.rs b/src/source.rs deleted file mode 100644 index ff214a8..0000000 --- a/src/source.rs +++ /dev/null @@ -1,88 +0,0 @@ -pub trait DataSource { - fn recv(&mut self) -> Option<&[u8]>; // TODO convert in Result and make generic error -} - -pub mod file { - use std::{fs::File, io::Read}; - - pub struct FileSource { - file: File, - buffer: Vec, - } - - impl FileSource { - #[allow(clippy::new_ret_no_self)] - pub fn new(path: &str, buffer: u32) -> Result, std::io::Error> { - Ok(Box::new( - FileSource { - file: File::open(path)?, - buffer: vec![0u8; buffer as usize], - } - )) - } - } - - impl super::DataSource for FileSource { - fn recv(&mut self) -> Option<&[u8]> { - match self.file.read_exact(&mut self.buffer) { - Ok(()) => Some(self.buffer.as_slice()), - Err(_e) => None, // TODO log it - } - } - } -} - -#[cfg(feature = "pulseaudio")] -pub mod pulseaudio { - use libpulse_binding::{sample::{Spec, Format}, def::BufferAttr, error::PAErr, stream::Direction}; - use libpulse_simple_binding::Simple; - - pub struct PulseAudioSimpleDataSource { - simple: Simple, - buffer: Vec, - } - - impl PulseAudioSimpleDataSource { - #[allow(clippy::new_ret_no_self)] - pub fn new( - device: Option<&str>, channels: u8, rate: u32, buffer: u32, server_buffer: u32 - ) -> Result, PAErr> { - let spec = Spec { - format: Format::S16NE, // TODO allow more formats? - channels, rate, - }; - if !spec.is_valid() { - return Err(PAErr(0)); // TODO what error number should we throw? - } - let attrs = BufferAttr { - maxlength: server_buffer * buffer, - fragsize: buffer, - ..Default::default() - }; - let simple = Simple::new( - None, // Use the default server - "scope-tui", // Our application’s name - Direction::Record, // We want a record stream - device, // Use requested device, or default - "data", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - Some(&attrs), // Our hints on how to handle client/server buffers - )?; - Ok(Box::new(Self { simple, buffer: vec![0; buffer as usize] })) - } - } - - impl super::DataSource for PulseAudioSimpleDataSource { - fn recv(&mut self) -> Option<&[u8]> { - match self.simple.read(&mut self.buffer) { - Ok(()) => Some(&self.buffer), - Err(e) => { - eprintln!("[!] could not receive from pulseaudio: {}", e); - None - } - } - } - } - -}