1
0
Fork 0
mirror of https://github.com/alemidev/scope-tui.git synced 2024-11-14 10:49:20 +01:00

feat: implemented spectroscope

it's not perfect and lower frequency tend to have way higher amplitudes,
which is suspicious, but it works!
This commit is contained in:
əlemi 2023-09-18 00:57:16 +02:00
parent 1b67bfbf75
commit 6e1871d953
Signed by: alemi
GPG key ID: A4895B84D311642C
6 changed files with 258 additions and 159 deletions

View file

@ -19,4 +19,4 @@ libpulse-simple-binding = "2.25"
clap = { version = "4.0.32", features = ["derive"] }
derive_more = "0.99.17"
thiserror = "1.0.48"
easyfft = "0.4.0"
rustfft = "6.1.0"

View file

@ -27,143 +27,6 @@ pub struct App {
mode: CurrentDisplayMode,
}
impl App {
pub fn run<T : Backend>(&mut self, mut source: impl DataSource, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
// prepare globals
let fmt = Signed16PCM{}; // TODO some way to choose this?
let mut fps = 0;
let mut framerate = 0;
let mut last_poll = Instant::now();
let mut channels = vec![];
loop {
let data = source.recv().unwrap();
if !self.pause {
channels = fmt.oscilloscope(data, self.channels);
}
fps += 1;
if last_poll.elapsed().as_secs() >= 1 {
framerate = fps;
fps = 0;
last_poll = Instant::now();
}
{
let display = match self.mode {
CurrentDisplayMode::Oscilloscope => &self.oscilloscope as &dyn DisplayMode,
CurrentDisplayMode::Vectorscope => &self.vectorscope as &dyn DisplayMode,
CurrentDisplayMode::Spectroscope => &self.spectroscope as &dyn DisplayMode,
};
let mut datasets = Vec::new();
if self.graph.references {
datasets.append(&mut display.references(&self.graph));
}
datasets.append(&mut display.process(&self.graph, &channels));
terminal.draw(|f| {
let mut size = f.size();
if self.graph.show_ui {
f.render_widget(
make_header(&self.graph, &display.header(&self.graph), framerate, self.pause),
Rect { x: size.x, y: size.y, width: size.width, height:1 } // a 1px line at the top
);
size.height -= 1;
size.y += 1;
}
let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
.x_axis(display.axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes?
.y_axis(display.axis(&self.graph, Dimension::Y));
f.render_widget(chart, size)
}).unwrap();
}
while event::poll(Duration::from_millis(0))? { // process all enqueued events
let event = event::read()?;
if self.process_events(event.clone())? { return Ok(()); }
self.current_display().handle(event);
}
}
}
fn current_display(&mut self) -> &mut dyn DisplayMode {
match self.mode {
CurrentDisplayMode::Oscilloscope => &mut self.oscilloscope as &mut dyn DisplayMode,
CurrentDisplayMode::Vectorscope => &mut self.vectorscope as &mut dyn DisplayMode,
CurrentDisplayMode::Spectroscope => &mut self.spectroscope as &mut dyn DisplayMode,
}
}
fn process_events(&mut self, event: Event) -> Result<bool, io::Error> {
let mut quit = false;
if let Event::Key(key) = event {
if let KeyModifiers::CONTROL = key.modifiers {
match key.code { // mimic other programs shortcuts to quit, for user friendlyness
KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true,
_ => {},
}
}
let magnitude = match key.modifiers {
KeyModifiers::SHIFT => 10.0,
KeyModifiers::CONTROL => 5.0,
KeyModifiers::ALT => 0.2,
_ => 1.0,
};
match key.code {
KeyCode::Up => update_value_i(&mut self.graph.scale, true, 250, magnitude, 0..32768), // inverted to act as zoom
KeyCode::Down => update_value_i(&mut self.graph.scale, false, 250, magnitude, 0..32768), // inverted to act as zoom
KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width),
KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width),
KeyCode::Char('q') => quit = true,
KeyCode::Char(' ') => self.pause = !self.pause,
KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter,
KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui,
KeyCode::Char('r') => self.graph.references = !self.graph.references,
KeyCode::Tab => { // switch modes
match self.mode {
CurrentDisplayMode::Oscilloscope => self.mode = CurrentDisplayMode::Vectorscope,
CurrentDisplayMode::Vectorscope => self.mode = CurrentDisplayMode::Spectroscope,
CurrentDisplayMode::Spectroscope => self.mode = CurrentDisplayMode::Oscilloscope,
}
},
_ => {},
}
};
Ok(quit)
}
}
pub fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: Range<f64>) {
let delta = base * magnitude;
if *val + delta > range.end {
*val = range.end
} else if *val + delta < range.start {
*val = range.start
} else {
*val += delta;
}
}
pub fn update_value_i(val: &mut u32, inc: bool, base: u32, magnitude: f64, range: Range<u32>) {
let delta = (base as f64 * magnitude) as u32;
if inc {
if range.end - delta < *val {
*val = range.end
} else {
*val += delta
}
} else if range.start + delta > *val {
*val = range.start
} else {
*val -= delta
}
}
impl From::<&crate::Args> for App {
fn from(args: &crate::Args) -> Self {
let graph = GraphConfig {
@ -191,8 +54,13 @@ impl From::<&crate::Args> for App {
peaks: args.show_peaks,
};
let vectorscope = Vectorscope {};
let spectroscope = Spectroscope {};
let vectorscope = Vectorscope::default();
let spectroscope = Spectroscope {
sampling_rate: args.sample_rate,
buffer_size: graph.width,
average: 1,
buf: Vec::new(),
};
App {
graph, oscilloscope, vectorscope, spectroscope,
@ -203,6 +71,148 @@ impl From::<&crate::Args> for App {
}
}
impl App {
pub fn run<T : Backend>(&mut self, mut source: impl DataSource, terminal: &mut Terminal<T>) -> Result<(), io::Error> {
// prepare globals
let fmt = Signed16PCM{}; // TODO some way to choose this?
let mut fps = 0;
let mut framerate = 0;
let mut last_poll = Instant::now();
let mut channels = vec![];
loop {
let data = source.recv().unwrap();
if !self.pause {
channels = fmt.oscilloscope(data, self.channels);
}
fps += 1;
if last_poll.elapsed().as_secs() >= 1 {
framerate = fps;
fps = 0;
last_poll = Instant::now();
}
{
let mut datasets = Vec::new();
let graph = self.graph.clone(); // TODO cheap fix...
if self.graph.references {
datasets.append(&mut self.current_display_mut().references(&graph));
}
datasets.append(&mut self.current_display_mut().process(&graph, &channels));
terminal.draw(|f| {
let mut size = f.size();
if self.graph.show_ui {
f.render_widget(
make_header(&self.graph, &self.current_display().header(&self.graph), framerate, self.pause),
Rect { x: size.x, y: size.y, width: size.width, height:1 } // a 1px line at the top
);
size.height -= 1;
size.y += 1;
}
let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
.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
let event = event::read()?;
if self.process_events(event.clone())? { return Ok(()); }
self.current_display_mut().handle(event);
}
}
}
fn current_display_mut(&mut self) -> &mut dyn DisplayMode {
match self.mode {
CurrentDisplayMode::Oscilloscope => &mut self.oscilloscope as &mut dyn DisplayMode,
CurrentDisplayMode::Vectorscope => &mut self.vectorscope as &mut dyn DisplayMode,
CurrentDisplayMode::Spectroscope => &mut self.spectroscope as &mut dyn DisplayMode,
}
}
fn current_display(&self) -> &dyn DisplayMode {
match self.mode {
CurrentDisplayMode::Oscilloscope => &self.oscilloscope as &dyn DisplayMode,
CurrentDisplayMode::Vectorscope => &self.vectorscope as &dyn DisplayMode,
CurrentDisplayMode::Spectroscope => &self.spectroscope as &dyn DisplayMode,
}
}
fn process_events(&mut self, event: Event) -> Result<bool, io::Error> {
let mut quit = false;
if let Event::Key(key) = event {
if let KeyModifiers::CONTROL = key.modifiers {
match key.code { // mimic other programs shortcuts to quit, for user friendlyness
KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true,
_ => {},
}
}
let magnitude = match key.modifiers {
KeyModifiers::SHIFT => 10.0,
KeyModifiers::CONTROL => 5.0,
KeyModifiers::ALT => 0.2,
_ => 1.0,
};
match key.code {
KeyCode::Up => update_value_i(&mut self.graph.scale, true, 250, magnitude, 0..32768), // inverted to act as zoom
KeyCode::Down => update_value_i(&mut self.graph.scale, false, 250, magnitude, 0..32768), // inverted to act as zoom
KeyCode::Right => update_value_i(&mut self.graph.samples, true, 25, magnitude, 0..self.graph.width*10),
KeyCode::Left => update_value_i(&mut self.graph.samples, false, 25, magnitude, 0..self.graph.width*10),
KeyCode::Char('q') => quit = true,
KeyCode::Char(' ') => self.pause = !self.pause,
KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter,
KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui,
KeyCode::Char('r') => self.graph.references = !self.graph.references,
KeyCode::Tab => { // switch modes
match self.mode {
CurrentDisplayMode::Oscilloscope => self.mode = CurrentDisplayMode::Vectorscope,
CurrentDisplayMode::Vectorscope => self.mode = CurrentDisplayMode::Spectroscope,
CurrentDisplayMode::Spectroscope => self.mode = CurrentDisplayMode::Oscilloscope,
}
},
_ => {},
}
};
Ok(quit)
}
}
// TODO can these be removed or merged somewhere else?
pub fn update_value_f(val: &mut f64, base: f64, magnitude: f64, range: Range<f64>) {
let delta = base * magnitude;
if *val + delta > range.end {
*val = range.end
} else if *val + delta < range.start {
*val = range.start
} else {
*val += delta;
}
}
pub fn update_value_i(val: &mut u32, inc: bool, base: u32, magnitude: f64, range: Range<u32>) {
let delta = (base as f64 * magnitude) as u32;
if inc {
if range.end - delta < *val {
*val = range.end
} else {
*val += delta
}
} else if range.start + delta > *val {
*val = range.start
} else {
*val -= delta
}
}
fn make_header<'a>(cfg: &GraphConfig, module_header: &'a str, fps: usize, pause: bool) -> Table<'a> {
Table::new(
vec![

View file

@ -9,6 +9,7 @@ pub enum Dimension {
X, Y
}
#[derive(Debug, Clone)]
pub struct GraphConfig {
pub samples: u32,
pub scale: u32,
@ -32,7 +33,7 @@ impl GraphConfig {
pub trait DisplayMode {
// MUST define
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet>;
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet>;
// SHOULD override
fn handle(&mut self, _event: Event) {}

View file

@ -5,6 +5,7 @@ use crate::app::update_value_f;
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
#[derive(Default)]
pub struct Oscilloscope {
pub triggering: bool,
pub falling_edge: bool,
@ -47,7 +48,7 @@ impl DisplayMode for Oscilloscope {
a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
}
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
let mut out = Vec::new();
let mut trigger_offset = 0;

View file

@ -1,24 +1,53 @@
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 super::{DisplayMode, GraphConfig, DataSet, Dimension};
use easyfft::prelude::*;
use rustfft::{FftPlanner, num_complex::Complex};
pub struct Spectroscope {}
#[derive(Default)]
pub struct Spectroscope {
pub sampling_rate: u32,
pub buffer_size: u32,
pub average: u32,
pub buf: Vec<VecDeque<Vec<f64>>>,
}
fn complex_to_magnitude(c: Complex<f64>) -> f64 {
let squared = (c.re * c.re) + (c.im * c.im);
squared.sqrt()
}
impl DisplayMode for Spectroscope {
fn channel_name(&self, index: usize) -> String {
format!("{}", index)
match index {
0 => "L".into(),
1 => "R".into(),
_ => format!("{}", index),
}
}
fn header(&self, _: &GraphConfig) -> String {
"live".into()
if self.average <= 1 {
format!("live -- {:.3}Hz buckets", self.sampling_rate as f64 / self.buffer_size as f64)
} else {
format!(
"{}x average ({:.1}s) -- {:.3}Hz buckets",
self.average,
(self.average * self.buffer_size) as f64 / self.sampling_rate as f64,
self.sampling_rate as f64 / (self.buffer_size * self.average) as f64
)
}
}
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
let (name, bounds) = match dimension {
Dimension::X => ("frequency -", [-(cfg.scale as f64), cfg.scale as f64]),
Dimension::Y => ("| level", [-(cfg.scale as f64), cfg.scale as f64]),
Dimension::X => ("frequency -", [20.0f64.ln(), ((cfg.samples as f64 / cfg.width as f64) * 20000.0).ln()]),
Dimension::Y => ("| level", [0.0, cfg.scale as f64 / 10.0]), // TODO super arbitraty! wtf
};
let mut a = Axis::default();
if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here
@ -27,21 +56,31 @@ impl DisplayMode for Spectroscope {
a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
}
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
vec![
DataSet::new("".into(), vec![(0.0, 0.0), (20000.0, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(0.0, 0.0), (0.0, cfg.scale as f64)], cfg.marker_type, GraphType::Line, cfg.axis_color),
]
}
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
for (i, chan) in data.iter().enumerate() {
if self.buf.len() <= i {
self.buf.push(VecDeque::new());
}
self.buf[i].push_back(chan.clone());
while self.buf[i].len() > self.average as usize {
self.buf[i].pop_front();
}
}
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
let mut out = Vec::new();
let mut planner: FftPlanner<f64> = FftPlanner::new();
let sample_len = self.buffer_size * self.average;
let resolution = self.sampling_rate as f64 / sample_len as f64;
let fft = planner.plan_fft_forward(sample_len as usize);
for (n, chunk) in data.iter().enumerate() {
let tmp = chunk.real_fft().iter().map(|x| (x.re, x.im)).collect();
for (n, chan_queue) in self.buf.iter().enumerate().rev() {
let chunk = chan_queue.iter().flatten().collect::<Vec<&f64>>();
let max_val = chunk.iter().max_by(|a, b| a.total_cmp(b)).expect("empty dataset?");
let mut tmp : Vec<Complex<f64>> = chunk.iter().map(|x| Complex { re: *x / *max_val, im: 0.0 }).collect();
fft.process(tmp.as_mut_slice());
out.push(DataSet::new(
self.channel_name(n),
tmp,
tmp[..=tmp.len() / 2].iter().enumerate().map(|(i,x)| ((i as f64 * resolution).ln(), complex_to_magnitude(*x))).collect(),
cfg.marker_type,
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
cfg.palette(n),
@ -50,4 +89,51 @@ impl DisplayMode for Spectroscope {
out
}
fn handle(&mut self, event: Event) {
if let Event::Key(key) = event {
match key.code {
KeyCode::PageUp => update_value_i(&mut self.average, true, 1, 1., 1..65535),
KeyCode::PageDown => update_value_i(&mut self.average, false, 1, 1., 1..65535),
_ => {}
}
}
}
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
let s = cfg.scale as f64 / 10.0;
vec![
DataSet::new("".into(), vec![(0.0, 0.0), ((cfg.samples as f64).ln(), 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
// TODO can we auto generate these? lol...
DataSet::new("".into(), vec![(20.0f64.ln(), 0.0), (20.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(30.0f64.ln(), 0.0), (30.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(40.0f64.ln(), 0.0), (40.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(50.0f64.ln(), 0.0), (50.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(60.0f64.ln(), 0.0), (60.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(70.0f64.ln(), 0.0), (70.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(80.0f64.ln(), 0.0), (80.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(90.0f64.ln(), 0.0), (90.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(100.0f64.ln(), 0.0), (100.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(200.0f64.ln(), 0.0), (200.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(300.0f64.ln(), 0.0), (300.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(400.0f64.ln(), 0.0), (400.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(500.0f64.ln(), 0.0), (500.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(600.0f64.ln(), 0.0), (600.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(700.0f64.ln(), 0.0), (700.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(800.0f64.ln(), 0.0), (800.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(900.0f64.ln(), 0.0), (900.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(1000.0f64.ln(), 0.0), (1000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(2000.0f64.ln(), 0.0), (2000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(3000.0f64.ln(), 0.0), (3000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(4000.0f64.ln(), 0.0), (4000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(5000.0f64.ln(), 0.0), (5000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(6000.0f64.ln(), 0.0), (6000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(7000.0f64.ln(), 0.0), (7000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(8000.0f64.ln(), 0.0), (8000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(9000.0f64.ln(), 0.0), (9000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(10000.0f64.ln(), 0.0), (10000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
DataSet::new("".into(), vec![(20000.0f64.ln(), 0.0), (20000.0f64.ln(), s)], cfg.marker_type, GraphType::Line, cfg.axis_color),
]
}
}

View file

@ -2,6 +2,7 @@ use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span};
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
#[derive(Default)]
pub struct Vectorscope {}
impl DisplayMode for Vectorscope {
@ -32,7 +33,7 @@ impl DisplayMode for Vectorscope {
]
}
fn process(&self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
let mut out = Vec::new();
for (n, chunk) in data.chunks(2).enumerate() {