mirror of
https://github.com/alemidev/scope-tui.git
synced 2024-11-14 18:59:19 +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:
parent
1b67bfbf75
commit
6e1871d953
6 changed files with 258 additions and 159 deletions
|
@ -19,4 +19,4 @@ 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"
|
||||||
easyfft = "0.4.0"
|
rustfft = "6.1.0"
|
||||||
|
|
288
src/app.rs
288
src/app.rs
|
@ -27,143 +27,6 @@ pub struct App {
|
||||||
mode: CurrentDisplayMode,
|
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 {
|
impl From::<&crate::Args> for App {
|
||||||
fn from(args: &crate::Args) -> Self {
|
fn from(args: &crate::Args) -> Self {
|
||||||
let graph = GraphConfig {
|
let graph = GraphConfig {
|
||||||
|
@ -191,8 +54,13 @@ impl From::<&crate::Args> for App {
|
||||||
peaks: args.show_peaks,
|
peaks: args.show_peaks,
|
||||||
};
|
};
|
||||||
|
|
||||||
let vectorscope = Vectorscope {};
|
let vectorscope = Vectorscope::default();
|
||||||
let spectroscope = Spectroscope {};
|
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,
|
||||||
|
@ -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> {
|
fn make_header<'a>(cfg: &GraphConfig, module_header: &'a str, fps: usize, pause: bool) -> Table<'a> {
|
||||||
Table::new(
|
Table::new(
|
||||||
vec![
|
vec![
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub enum Dimension {
|
||||||
X, Y
|
X, Y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct GraphConfig {
|
pub struct GraphConfig {
|
||||||
pub samples: u32,
|
pub samples: u32,
|
||||||
pub scale: u32,
|
pub scale: u32,
|
||||||
|
@ -32,7 +33,7 @@ impl GraphConfig {
|
||||||
pub trait DisplayMode {
|
pub trait DisplayMode {
|
||||||
// MUST define
|
// MUST define
|
||||||
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis; // TODO simplify this
|
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
|
// SHOULD override
|
||||||
fn handle(&mut self, _event: Event) {}
|
fn handle(&mut self, _event: Event) {}
|
||||||
|
|
|
@ -5,6 +5,7 @@ use crate::app::update_value_f;
|
||||||
|
|
||||||
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Oscilloscope {
|
pub struct Oscilloscope {
|
||||||
pub triggering: bool,
|
pub triggering: bool,
|
||||||
pub falling_edge: bool,
|
pub falling_edge: bool,
|
||||||
|
@ -47,7 +48,7 @@ impl DisplayMode for Oscilloscope {
|
||||||
a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
|
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 out = Vec::new();
|
||||||
|
|
||||||
let mut trigger_offset = 0;
|
let mut trigger_offset = 0;
|
||||||
|
|
|
@ -1,24 +1,53 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use crossterm::event::{Event, KeyCode};
|
||||||
use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span};
|
use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span};
|
||||||
|
|
||||||
|
use crate::app::update_value_i;
|
||||||
|
|
||||||
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
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 {
|
impl DisplayMode for Spectroscope {
|
||||||
fn channel_name(&self, index: usize) -> String {
|
fn channel_name(&self, index: usize) -> String {
|
||||||
format!("{}", index)
|
match index {
|
||||||
|
0 => "L".into(),
|
||||||
|
1 => "R".into(),
|
||||||
|
_ => format!("{}", index),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header(&self, _: &GraphConfig) -> String {
|
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 {
|
fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
|
||||||
let (name, bounds) = match dimension {
|
let (name, bounds) = match dimension {
|
||||||
Dimension::X => ("frequency -", [-(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", [-(cfg.scale as f64), cfg.scale as f64]),
|
Dimension::Y => ("| level", [0.0, cfg.scale as f64 / 10.0]), // TODO super arbitraty! wtf
|
||||||
};
|
};
|
||||||
let mut a = Axis::default();
|
let mut a = Axis::default();
|
||||||
if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here
|
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)
|
a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
|
fn process(&mut self, cfg: &GraphConfig, data: &Vec<Vec<f64>>) -> Vec<DataSet> {
|
||||||
vec![
|
for (i, chan) in data.iter().enumerate() {
|
||||||
DataSet::new("".into(), vec![(0.0, 0.0), (20000.0, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
if self.buf.len() <= i {
|
||||||
DataSet::new("".into(), vec![(0.0, 0.0), (0.0, cfg.scale as f64)], cfg.marker_type, GraphType::Line, cfg.axis_color),
|
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 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() {
|
for (n, chan_queue) in self.buf.iter().enumerate().rev() {
|
||||||
let tmp = chunk.real_fft().iter().map(|x| (x.re, x.im)).collect();
|
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(
|
out.push(DataSet::new(
|
||||||
self.channel_name(n),
|
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,
|
cfg.marker_type,
|
||||||
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
|
if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
|
||||||
cfg.palette(n),
|
cfg.palette(n),
|
||||||
|
@ -50,4 +89,51 @@ impl DisplayMode for Spectroscope {
|
||||||
|
|
||||||
out
|
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),
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span};
|
||||||
|
|
||||||
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
use super::{DisplayMode, GraphConfig, DataSet, Dimension};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Vectorscope {}
|
pub struct Vectorscope {}
|
||||||
|
|
||||||
impl DisplayMode for 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();
|
let mut out = Vec::new();
|
||||||
|
|
||||||
for (n, chunk) in data.chunks(2).enumerate() {
|
for (n, chunk) in data.chunks(2).enumerate() {
|
||||||
|
|
Loading…
Reference in a new issue