Skip to content

Commit

Permalink
feat: add cpal source, refactor sources
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alemidev committed Mar 18, 2024
1 parent 299efd7 commit 7719870
Show file tree
Hide file tree
Showing 15 changed files with 283 additions and 162 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
36 changes: 16 additions & 20 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -18,7 +17,7 @@ pub enum CurrentDisplayMode {
}

pub struct App {
channels: u8,
#[allow(unused)] channels: u8,
graph: GraphConfig,
oscilloscope: Oscilloscope,
vectorscope: Vectorscope,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -169,7 +165,7 @@ impl App {
},
KeyCode::Esc => {
self.graph.samples = self.graph.width;
self.graph.scale = 20000;
self.graph.scale = 1.;
},
_ => {},
}
Expand Down Expand Up @@ -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 { "---" }),
Expand Down
6 changes: 4 additions & 2 deletions src/display/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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,
Expand All @@ -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
Expand Down
9 changes: 4 additions & 5 deletions src/display/oscilloscope.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/display/spectroscope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 3 additions & 1 deletion src/display/vectorscope.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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() {
Expand Down
67 changes: 67 additions & 0 deletions src/input/cpal.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 42 additions & 0 deletions src/input/file.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions src/input/format/mod.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 7719870

Please sign in to comment.