mirror of
https://github.com/alemidev/scope-tui.git
synced 2024-11-23 14:14:48 +01:00
feat: added crude file source, gated pulseaudio
now requires feature pulseaudio to build libpulse and pulse source. file source is always included. the cli got reworked a little bit but I'm not sure I like it this way, we'll see
This commit is contained in:
parent
e559798385
commit
9378621a26
8 changed files with 187 additions and 134 deletions
19
Cargo.toml
19
Cargo.toml
|
@ -3,8 +3,8 @@ name = "scope-tui"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [ "alemi <me@alemi.dev>" ]
|
authors = [ "alemi <me@alemi.dev>" ]
|
||||||
description = "A simple oscilloscope/vectorscope for the terminal, inspired by cava"
|
description = "A simple oscilloscope/vectorscope/spectroscope for your terminal"
|
||||||
keywords = ["tui", "terminal", "audio", "visualization", "scope", "dashboard"]
|
keywords = ["tui", "terminal", "audio", "visualization", "scope", "dashboard", "oscilloscope", "spectroscope"]
|
||||||
repository = "https://github.com/alemidev/scope-tui"
|
repository = "https://github.com/alemidev/scope-tui"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
# documentation =
|
# documentation =
|
||||||
|
@ -12,11 +12,18 @@ readme = "README.md"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = { version = "0.23.0", features = ["all-widgets"] }
|
|
||||||
crossterm = "0.25"
|
|
||||||
libpulse-binding = "2.0"
|
|
||||||
libpulse-simple-binding = "2.25"
|
|
||||||
clap = { version = "4.0.32", features = ["derive"] }
|
clap = { version = "4.0.32", features = ["derive"] }
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
thiserror = "1.0.48"
|
thiserror = "1.0.48"
|
||||||
rustfft = "6.1.0"
|
rustfft = "6.1.0"
|
||||||
|
# for TUI backend
|
||||||
|
ratatui = { version = "0.23.0", features = ["all-widgets"], optional = true }
|
||||||
|
crossterm = { version = "0.25", optional = true }
|
||||||
|
# for pulseaudio
|
||||||
|
libpulse-binding = { version = "2.0", optional = true }
|
||||||
|
libpulse-simple-binding = { version = "2.25", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["tui"]
|
||||||
|
tui = ["dep:ratatui", "dep:crossterm"]
|
||||||
|
pulseaudio = ["dep:libpulse-binding", "dep:libpulse-simple-binding"]
|
||||||
|
|
29
src/app.rs
29
src/app.rs
|
@ -27,8 +27,9 @@ pub struct App {
|
||||||
mode: CurrentDisplayMode,
|
mode: CurrentDisplayMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From::<&crate::Args> for App {
|
// TODO another way to build this that doesn't require getting cli args directly!!!
|
||||||
fn from(args: &crate::Args) -> Self {
|
impl From::<&crate::ScopeArgs> for App {
|
||||||
|
fn from(args: &crate::ScopeArgs) -> Self {
|
||||||
let graph = GraphConfig {
|
let graph = GraphConfig {
|
||||||
axis_color: Color::DarkGray,
|
axis_color: Color::DarkGray,
|
||||||
labels_color: Color::Cyan,
|
labels_color: Color::Cyan,
|
||||||
|
@ -36,6 +37,7 @@ impl From::<&crate::Args> for App {
|
||||||
scale: args.range,
|
scale: args.range,
|
||||||
width: args.buffer / (2 * args.channels as u32), // TODO also make bit depth customizable
|
width: args.buffer / (2 * args.channels as u32), // TODO also make bit depth customizable
|
||||||
samples: args.buffer / (2 * args.channels as u32),
|
samples: args.buffer / (2 * args.channels as u32),
|
||||||
|
sampling_rate: args.sample_rate,
|
||||||
references: !args.no_reference,
|
references: !args.no_reference,
|
||||||
show_ui: !args.no_ui,
|
show_ui: !args.no_ui,
|
||||||
scatter: args.scatter,
|
scatter: args.scatter,
|
||||||
|
@ -46,21 +48,9 @@ impl From::<&crate::Args> for App {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let oscilloscope = Oscilloscope {
|
let oscilloscope = Oscilloscope::from_args(args);
|
||||||
triggering: args.triggering,
|
let vectorscope = Vectorscope::from_args(args);
|
||||||
depth: args.check_depth,
|
let spectroscope = Spectroscope::from_args(args);
|
||||||
threshold: args.threshold,
|
|
||||||
falling_edge: args.falling_edge,
|
|
||||||
peaks: args.show_peaks,
|
|
||||||
};
|
|
||||||
|
|
||||||
let vectorscope = Vectorscope::default();
|
|
||||||
let spectroscope = Spectroscope {
|
|
||||||
sampling_rate: args.sample_rate,
|
|
||||||
buffer_size: graph.width,
|
|
||||||
average: 1,
|
|
||||||
buf: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
App {
|
App {
|
||||||
graph, oscilloscope, vectorscope, spectroscope,
|
graph, oscilloscope, vectorscope, spectroscope,
|
||||||
|
@ -72,7 +62,7 @@ impl From::<&crate::Args> for App {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn run<T : Backend>(&mut self, mut source: impl DataSource, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
pub fn run<T : Backend>(&mut self, mut source: Box<dyn DataSource>, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
||||||
// prepare globals
|
// prepare globals
|
||||||
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
||||||
|
|
||||||
|
@ -82,7 +72,8 @@ impl App {
|
||||||
let mut channels = vec![];
|
let mut channels = vec![];
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let data = source.recv().unwrap();
|
let data = source.recv()
|
||||||
|
.ok_or(io::Error::new(io::ErrorKind::BrokenPipe, "data source returned null"))?;
|
||||||
|
|
||||||
if !self.pause {
|
if !self.pause {
|
||||||
channels = fmt.oscilloscope(data, self.channels);
|
channels = fmt.oscilloscope(data, self.channels);
|
||||||
|
|
|
@ -12,6 +12,7 @@ pub enum Dimension {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GraphConfig {
|
pub struct GraphConfig {
|
||||||
pub samples: u32,
|
pub samples: u32,
|
||||||
|
pub sampling_rate: u32,
|
||||||
pub scale: u32,
|
pub scale: u32,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub scatter: bool,
|
pub scatter: bool,
|
||||||
|
@ -32,6 +33,7 @@ impl GraphConfig {
|
||||||
#[allow(clippy::ptr_arg)] // TODO temporarily! it's a shitty solution
|
#[allow(clippy::ptr_arg)] // TODO temporarily! it's a shitty solution
|
||||||
pub trait DisplayMode {
|
pub trait DisplayMode {
|
||||||
// MUST define
|
// MUST define
|
||||||
|
fn from_args(args: &crate::ScopeArgs) -> Self where Self : Sized;
|
||||||
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
|
||||||
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet>;
|
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet>;
|
||||||
fn mode_str(&self) -> &'static str;
|
fn mode_str(&self) -> &'static str;
|
||||||
|
|
|
@ -15,6 +15,10 @@ pub struct Oscilloscope {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMode for Oscilloscope {
|
impl DisplayMode for Oscilloscope {
|
||||||
|
fn from_args(_args: &crate::ScopeArgs) -> Self {
|
||||||
|
Oscilloscope::default()
|
||||||
|
}
|
||||||
|
|
||||||
fn mode_str(&self) -> &'static str {
|
fn mode_str(&self) -> &'static str {
|
||||||
"oscillo"
|
"oscillo"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,14 @@ fn complex_to_magnitude(c: Complex<f64>) -> f64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMode for Spectroscope {
|
impl DisplayMode for Spectroscope {
|
||||||
|
fn from_args(args: &crate::ScopeArgs) -> Self {
|
||||||
|
Spectroscope {
|
||||||
|
sampling_rate: args.sample_rate,
|
||||||
|
buffer_size: args.buffer / (2 * args.channels as u32),
|
||||||
|
average: 1, buf: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn mode_str(&self) -> &'static str {
|
fn mode_str(&self) -> &'static str {
|
||||||
"spectro"
|
"spectro"
|
||||||
}
|
}
|
||||||
|
@ -61,6 +69,7 @@ impl DisplayMode for Spectroscope {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
|
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
|
||||||
|
if self.average == 0 { self.average = 1 } // otherwise fft breaks
|
||||||
for (i, chan) in data.iter().enumerate() {
|
for (i, chan) in data.iter().enumerate() {
|
||||||
if self.buf.len() <= i {
|
if self.buf.len() <= i {
|
||||||
self.buf.push(VecDeque::new());
|
self.buf.push(VecDeque::new());
|
||||||
|
|
|
@ -6,6 +6,10 @@ use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
pub struct Vectorscope {}
|
pub struct Vectorscope {}
|
||||||
|
|
||||||
impl DisplayMode for Vectorscope {
|
impl DisplayMode for Vectorscope {
|
||||||
|
fn from_args(_args: &crate::ScopeArgs) -> Self {
|
||||||
|
Vectorscope::default()
|
||||||
|
}
|
||||||
|
|
||||||
fn mode_str(&self) -> &'static str {
|
fn mode_str(&self) -> &'static str {
|
||||||
"vector"
|
"vector"
|
||||||
}
|
}
|
||||||
|
|
135
src/main.rs
135
src/main.rs
|
@ -5,7 +5,6 @@ mod source;
|
||||||
mod display;
|
mod display;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use source::PulseAudioSimple;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
Terminal,
|
Terminal,
|
||||||
|
@ -16,7 +15,7 @@ use crossterm::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
use crate::music::Note;
|
use crate::music::Note;
|
||||||
|
|
||||||
|
@ -29,80 +28,73 @@ const HELP_TEMPLATE : &str = "{before-help}\
|
||||||
{all-args}{after-help}
|
{all-args}{after-help}
|
||||||
";
|
";
|
||||||
|
|
||||||
/// A simple oscilloscope/vectorscope for your terminal
|
/// a simple oscilloscope/vectorscope for your terminal
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None, help_template = HELP_TEMPLATE)]
|
#[command(author, version, about, long_about = None, help_template = HELP_TEMPLATE)]
|
||||||
pub struct Args {
|
pub struct ScopeArgs {
|
||||||
/// Audio device to attach to
|
#[clap(subcommand)]
|
||||||
device: Option<String>,
|
source: ScopeSource,
|
||||||
|
|
||||||
/// Size of audio buffer, and width of scope
|
/// number of channels to open
|
||||||
#[arg(short, long, value_name = "SIZE", default_value_t = 8192)]
|
|
||||||
buffer: 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
|
|
||||||
|
|
||||||
/// Use vintage looking scatter mode instead of line mode
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
scatter: bool,
|
|
||||||
|
|
||||||
/// Combine left and right channels into vectorscope view
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
vectorscope: bool,
|
|
||||||
|
|
||||||
/// Show peaks for each channel as dots
|
|
||||||
#[arg(long, default_value_t = true)]
|
|
||||||
show_peaks: bool,
|
|
||||||
|
|
||||||
/// Tune buffer size to be in tune with given note (overrides buffer option)
|
|
||||||
#[arg(long, value_name = "NOTE")]
|
|
||||||
tune: Option<String>,
|
|
||||||
|
|
||||||
/// Number of channels to open
|
|
||||||
#[arg(long, value_name = "N", default_value_t = 2)]
|
#[arg(long, value_name = "N", default_value_t = 2)]
|
||||||
channels: u8,
|
channels: u8,
|
||||||
|
|
||||||
/// Sample rate to use
|
/// tune buffer size to be in tune with given note (overrides buffer option)
|
||||||
|
#[arg(long, value_name = "NOTE")]
|
||||||
|
tune: Option<String>,
|
||||||
|
|
||||||
|
/// size of audio buffer, and width of scope
|
||||||
|
#[arg(short, long, value_name = "SIZE", default_value_t = 8192)]
|
||||||
|
buffer: u32,
|
||||||
|
|
||||||
|
/// sample rate to use
|
||||||
#[arg(long, value_name = "HZ", default_value_t = 44100)]
|
#[arg(long, value_name = "HZ", default_value_t = 44100)]
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
|
|
||||||
/// Pulseaudio server buffer size, in block number
|
/// max value, positive and negative, on amplitude scale
|
||||||
#[arg(long, value_name = "N", default_value_t = 32)]
|
#[arg(short, long, value_name = "SIZE", default_value_t = 20000)]
|
||||||
server_buffer: u32,
|
range: u32, // TODO counterintuitive, improve this
|
||||||
|
|
||||||
/// Start drawing at first rising edge
|
/// use vintage looking scatter mode instead of line mode
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
triggering: bool,
|
scatter: bool,
|
||||||
|
|
||||||
/// Threshold value for triggering
|
/// don't draw reference line
|
||||||
#[arg(long, value_name = "VAL", default_value_t = 0.0)]
|
|
||||||
threshold: f64,
|
|
||||||
|
|
||||||
/// Length of trigger check in samples
|
|
||||||
#[arg(long, value_name = "SMPL", default_value_t = 1)]
|
|
||||||
check_depth: u32,
|
|
||||||
|
|
||||||
/// Trigger upon falling edge instead of rising
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
falling_edge: bool,
|
|
||||||
|
|
||||||
/// Don't draw reference line
|
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_reference: bool,
|
no_reference: bool,
|
||||||
|
|
||||||
/// Hide UI and only draw waveforms
|
/// hide UI and only draw waveforms
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_ui: bool,
|
no_ui: bool,
|
||||||
|
|
||||||
/// Don't use braille dots for drawing lines
|
/// don't use braille dots for drawing lines
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
no_braille: bool,
|
no_braille: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), std::io::Error> {
|
#[derive(Debug, Clone, Subcommand)]
|
||||||
let mut args = Args::parse();
|
pub enum ScopeSource {
|
||||||
|
|
||||||
|
#[cfg(feature = "pulseaudio")]
|
||||||
|
/// use PulseAudio Simple api to read data from an audio sink
|
||||||
|
Pulse {
|
||||||
|
/// source device to attach to
|
||||||
|
device: Option<String>,
|
||||||
|
|
||||||
|
/// PulseAudio server buffer size, in block number
|
||||||
|
#[arg(long, value_name = "N", default_value_t = 32)]
|
||||||
|
server_buffer: u32,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// use a file from filesystem and read its content
|
||||||
|
File {
|
||||||
|
/// path on filesystem of file or pipe
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut args = ScopeArgs::parse();
|
||||||
|
|
||||||
if let Some(txt) = &args.tune { // TODO make it less jank
|
if let Some(txt) = &args.tune { // TODO make it less jank
|
||||||
if let Ok(note) = txt.parse::<Note>() {
|
if let Ok(note) = txt.parse::<Note>() {
|
||||||
|
@ -115,13 +107,24 @@ fn main() -> Result<(), std::io::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = PulseAudioSimple::new(
|
let source = match &args.source {
|
||||||
args.device.as_deref(),
|
|
||||||
args.channels,
|
#[cfg(feature = "pulseaudio")]
|
||||||
args.sample_rate,
|
ScopeSource::Pulse { device, server_buffer } => {
|
||||||
args.buffer,
|
source::pulseaudio::PulseAudioSimpleDataSource::new(
|
||||||
args.server_buffer
|
device.as_deref(),
|
||||||
).unwrap();
|
args.channels,
|
||||||
|
args.sample_rate,
|
||||||
|
args.buffer,
|
||||||
|
*server_buffer,
|
||||||
|
)?
|
||||||
|
},
|
||||||
|
|
||||||
|
ScopeSource::File { path } => {
|
||||||
|
source::file::FileSource::new(path, args.buffer)?
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
let mut app = App::from(&args);
|
let mut app = App::from(&args);
|
||||||
|
|
||||||
|
@ -143,11 +146,9 @@ fn main() -> Result<(), std::io::Error> {
|
||||||
)?;
|
)?;
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
match res {
|
if let Err(e) = res {
|
||||||
Ok(()) => Ok(()),
|
eprintln!("[!] Error executing app: {:?}", e);
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[!] Error executing app: {:?}", e);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
119
src/source.rs
119
src/source.rs
|
@ -1,53 +1,88 @@
|
||||||
use libpulse_binding::{sample::{Spec, Format}, def::BufferAttr, error::PAErr, stream::Direction};
|
|
||||||
use libpulse_simple_binding::Simple;
|
|
||||||
|
|
||||||
pub trait DataSource {
|
pub trait DataSource {
|
||||||
fn recv(&mut self) -> Option<&[u8]>; // TODO convert in Result and make generic error
|
fn recv(&mut self) -> Option<&[u8]>; // TODO convert in Result and make generic error
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PulseAudioSimple {
|
pub mod file {
|
||||||
simple: Simple,
|
use std::{fs::File, io::Read};
|
||||||
buffer: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PulseAudioSimple {
|
pub struct FileSource {
|
||||||
pub fn new(
|
file: File,
|
||||||
device: Option<&str>, channels: u8, rate: u32, buffer: u32, server_buffer: u32
|
buffer: Vec<u8>,
|
||||||
) -> Result<Self, 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(Self { simple, buffer: vec![0; buffer as usize] })
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl DataSource for PulseAudioSimple {
|
impl FileSource {
|
||||||
fn recv(&mut self) -> Option<&[u8]> {
|
#[allow(clippy::new_ret_no_self)]
|
||||||
match self.simple.read(&mut self.buffer) {
|
pub fn new(path: &str, buffer: u32) -> Result<Box<dyn super::DataSource>, std::io::Error> {
|
||||||
Ok(()) => Some(&self.buffer),
|
Ok(Box::new(
|
||||||
Err(e) => {
|
FileSource {
|
||||||
eprintln!("[!] could not receive from pulseaudio: {}", e);
|
file: File::open(path)?,
|
||||||
None
|
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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PulseAudioSimpleDataSource {
|
||||||
|
#[allow(clippy::new_ret_no_self)]
|
||||||
|
pub fn new(
|
||||||
|
device: Option<&str>, channels: u8, rate: u32, buffer: u32, server_buffer: u32
|
||||||
|
) -> Result<Box<dyn super::DataSource>, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue