mirror of
https://github.com/alemidev/scope-tui.git
synced 2024-11-14 02:39:20 +01:00
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
This commit is contained in:
parent
299efd7012
commit
771987063d
15 changed files with 283 additions and 162 deletions
|
@ -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"]
|
||||
|
|
36
src/app.rs
36
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<T : Backend>(&mut self, mut source: Box<dyn DataSource>, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
|
||||
// prepare globals
|
||||
let fmt = Signed16PCM{}; // TODO some way to choose this?
|
||||
|
||||
pub fn run<T : Backend>(&mut self, mut source: Box<dyn DataSource<f64>>, terminal: &mut Terminal<T>) -> 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 { "---" }),
|
||||
|
|
|
@ -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<f64>>) -> Vec<DataSet>;
|
||||
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet>;
|
||||
fn mode_str(&self) -> &'static str;
|
||||
|
||||
// SHOULD override
|
||||
|
|
|
@ -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<f64>>) -> Vec<DataSet> {
|
||||
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<f64>>) -> Vec<DataSet> {
|
||||
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
|
||||
if self.average == 0 { self.average = 1 } // otherwise fft breaks
|
||||
if !cfg.pause {
|
||||
for (i, chan) in data.iter().enumerate() {
|
||||
|
|
|
@ -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<f64>>) -> Vec<DataSet> {
|
||||
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
for (n, chunk) in data.chunks(2).enumerate() {
|
||||
|
|
67
src/input/cpal.rs
Normal file
67
src/input/cpal.rs
Normal file
|
@ -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<Matrix<f64>>,
|
||||
#[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<Box<impl super::DataSource<f64>>, 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<f64> for DefaultAudioDeviceWithCPAL {
|
||||
fn recv(&mut self) -> Option<super::Matrix<f64>> {
|
||||
match self.rx.recv() {
|
||||
Ok(x) => Some(x),
|
||||
Err(e) => {
|
||||
println!("error receiving from source? {e}");
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
42
src/input/file.rs
Normal file
42
src/input/file.rs
Normal file
|
@ -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<u8>,
|
||||
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<Box<dyn super::DataSource<f64>>, std::io::Error> {
|
||||
Ok(Box::new(
|
||||
FileSource {
|
||||
channels, sample_rate, limit_rate,
|
||||
file: File::open(path)?,
|
||||
buffer: vec![0u8; buffer * channels],
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::DataSource<f64> for FileSource {
|
||||
fn recv(&mut self) -> Option<Matrix<f64>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
11
src/input/format/mod.rs
Normal file
11
src/input/format/mod.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
pub trait SampleParser<T> {
|
||||
fn parse(data: &[u8]) -> T;
|
||||
}
|
||||
|
||||
pub struct Signed16PCM;
|
||||
impl SampleParser<f64> for Signed16PCM {
|
||||
fn parse(chunk: &[u8]) -> f64 {
|
||||
(chunk[0] as i16 | (chunk[1] as i16) << 8) as f64
|
||||
}
|
||||
}
|
31
src/input/mod.rs
Normal file
31
src/input/mod.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
pub mod format;
|
||||
|
||||
#[cfg(feature = "pulseaudio")]
|
||||
pub mod pulse;
|
||||
|
||||
pub mod file;
|
||||
|
||||
pub mod cpal;
|
||||
|
||||
pub type Matrix<T> = Vec<Vec<T>>;
|
||||
|
||||
pub trait DataSource<T> {
|
||||
fn recv(&mut self) -> Option<Matrix<T>>; // 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<I, O>(stream: impl Iterator<Item = I>, channels: usize, norm: O) -> Matrix<O>
|
||||
where I : Copy + Into<O>, O : Copy + std::ops::Div<Output = O>
|
||||
{
|
||||
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
|
||||
}
|
61
src/input/pulse.rs
Normal file
61
src/input/pulse.rs
Normal file
|
@ -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<u8>,
|
||||
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<Box<dyn super::DataSource<f64>>, 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<f64> for PulseAudioSimpleDataSource {
|
||||
fn recv(&mut self) -> Option<super::Matrix<f64>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
src/main.rs
52
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<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// timeout (in seconds) waiting for audio stream
|
||||
#[arg(long, default_value_t = 60)]
|
||||
timeout: u64,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
@ -111,19 +124,34 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
#[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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Vec<f64>>;
|
||||
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<Vec<f64>> {
|
||||
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
|
||||
}
|
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
impl FileSource {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(path: &str, buffer: u32) -> Result<Box<dyn super::DataSource>, 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<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