fix a slew of bugs and latency
This commit is contained in:
@@ -3,6 +3,10 @@ name = "spectrust"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
clap = { version = "4.5.21", features = ["derive"] }
|
clap = { version = "4.5.21", features = ["derive"] }
|
||||||
|
|||||||
+134
-290
@@ -2,46 +2,59 @@ use clap::Parser;
|
|||||||
use ncurses::*;
|
use ncurses::*;
|
||||||
use ncurses::{LcCategory, setlocale};
|
use ncurses::{LcCategory, setlocale};
|
||||||
use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
|
use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
|
||||||
use std::collections::{VecDeque, HashMap};
|
use std::collections::VecDeque;
|
||||||
use std::io::{stdin, Read};
|
use std::io::{stdin, Read};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
// Create screen buffer struct to track cell states
|
const EMPTY_CELL: Cell = Cell { character: ' ', filled: false };
|
||||||
// Each cell stores a character and its state (whether it's filled or not)
|
const BLOCK_CELL: Cell = Cell { character: '█', filled: true };
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
const SUB_BLOCKS: [char; 7] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇'];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||||
struct Cell {
|
struct Cell {
|
||||||
character: String,
|
character: char,
|
||||||
filled: bool,
|
filled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewDiff struct for efficient terminal updates
|
|
||||||
struct ViewDiff {
|
struct ViewDiff {
|
||||||
changes: HashMap<(usize, usize), String>,
|
changes: Vec<(i32, i32, char)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViewDiff {
|
impl ViewDiff {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
ViewDiff {
|
ViewDiff { changes: Vec::with_capacity(512) }
|
||||||
changes: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_change(&mut self, y: usize, x: usize, character: String) {
|
fn clear(&mut self) {
|
||||||
self.changes.insert((y, x), character);
|
self.changes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_change(&mut self, y: usize, x: usize, character: char) {
|
||||||
|
self.changes.push((y as i32, x as i32, character));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply(&self) {
|
fn apply(&self) {
|
||||||
for ((y, x), character) in &self.changes {
|
let mut buf = [0u8; 4];
|
||||||
let _ = mvprintw(*y as i32, *x as i32, character);
|
for &(y, x, ch) in &self.changes {
|
||||||
|
let _ = mvprintw(y, x, ch.encode_utf8(&mut buf));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_bar(buf: &mut Vec<Vec<Cell>>, col: usize, sr: usize, bar_height_f: f32) {
|
||||||
|
let full = (bar_height_f as usize).min(sr);
|
||||||
|
for j in 0..full {
|
||||||
|
buf[sr - 1 - j][col] = BLOCK_CELL;
|
||||||
|
}
|
||||||
|
let sub_idx = (bar_height_f.fract() * 8.0) as usize;
|
||||||
|
if sub_idx > 0 && full < sr {
|
||||||
|
buf[sr - 1 - full][col] = Cell { character: SUB_BLOCKS[sub_idx - 1], filled: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to handle terminal resizing
|
|
||||||
fn handle_terminal_resize(
|
fn handle_terminal_resize(
|
||||||
rows: &mut i32,
|
rows: &mut i32,
|
||||||
cols: &mut i32,
|
cols: &mut i32,
|
||||||
@@ -51,43 +64,38 @@ fn handle_terminal_resize(
|
|||||||
max_height: i32,
|
max_height: i32,
|
||||||
safe_rows: &mut i32,
|
safe_rows: &mut i32,
|
||||||
safe_cols: &mut i32,
|
safe_cols: &mut i32,
|
||||||
empty_cell: &Cell,
|
|
||||||
current_buffer: &mut Vec<Vec<Cell>>,
|
current_buffer: &mut Vec<Vec<Cell>>,
|
||||||
next_buffer: &mut Vec<Vec<Cell>>,
|
next_buffer: &mut Vec<Vec<Cell>>,
|
||||||
last_bars: &mut Vec<f32>,
|
last_bars: &mut Vec<f32>,
|
||||||
|
freq_bands: &mut Vec<f32>,
|
||||||
freq_mapping: &mut Vec<usize>,
|
freq_mapping: &mut Vec<usize>,
|
||||||
log_power: f32,
|
log_power: f32,
|
||||||
fft_size: usize
|
fft_size: usize,
|
||||||
) {
|
) {
|
||||||
// Update terminal dimensions
|
|
||||||
*rows = new_rows;
|
*rows = new_rows;
|
||||||
*cols = new_cols;
|
*cols = new_cols;
|
||||||
|
|
||||||
// Update safe dimensions to account for new terminal size
|
|
||||||
*safe_cols = new_cols.min(max_width);
|
*safe_cols = new_cols.min(max_width);
|
||||||
*safe_rows = new_rows.min(max_height);
|
*safe_rows = new_rows.min(max_height);
|
||||||
|
|
||||||
// Resize screen buffers
|
let sc = *safe_cols as usize;
|
||||||
*current_buffer = vec![vec![empty_cell.clone(); *safe_cols as usize]; *safe_rows as usize];
|
let sr = *safe_rows as usize;
|
||||||
*next_buffer = vec![vec![empty_cell.clone(); *safe_cols as usize]; *safe_rows as usize];
|
|
||||||
|
|
||||||
// Resize last_bars to fit new screen width
|
*current_buffer = vec![vec![EMPTY_CELL; sc]; sr];
|
||||||
*last_bars = vec![0.0; *safe_cols as usize];
|
*next_buffer = vec![vec![EMPTY_CELL; sc]; sr];
|
||||||
|
|
||||||
|
last_bars.clear();
|
||||||
|
last_bars.resize(sc, 0.0);
|
||||||
|
freq_bands.clear();
|
||||||
|
freq_bands.resize(sc, 0.0);
|
||||||
|
|
||||||
// Recalculate frequency mapping for new terminal width
|
|
||||||
freq_mapping.clear();
|
freq_mapping.clear();
|
||||||
freq_mapping.reserve(*safe_cols as usize);
|
for i in 0..sc {
|
||||||
for i in 0..*safe_cols as usize {
|
let log_pos = (i as f32 / sc as f32).powf(log_power * 0.8);
|
||||||
// Using a more balanced logarithmic scale to better distribute frequencies
|
|
||||||
let log_pos = (i as f32 / *safe_cols as f32).powf(log_power * 0.8);
|
|
||||||
let index = (log_pos * (fft_size / 2) as f32) as usize;
|
let index = (log_pos * (fft_size / 2) as f32) as usize;
|
||||||
freq_mapping.push(index.min(fft_size / 2 - 1));
|
freq_mapping.push(index.min(fft_size / 2 - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completely clear the screen
|
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
// Apply changes
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,164 +122,141 @@ struct Cli {
|
|||||||
#[arg(short, long, help = "Logarithmic scaling power (higher values emphasize lower frequencies)", default_value_t = 1.4)]
|
#[arg(short, long, help = "Logarithmic scaling power (higher values emphasize lower frequencies)", default_value_t = 1.4)]
|
||||||
log_power: f32,
|
log_power: f32,
|
||||||
|
|
||||||
#[arg(short, long, help = "Drop-off factor for bar animation (0.0-1.0)", default_value_t = 0.85)]
|
#[arg(short, long, help = "Drop-off factor for bar animation (0.0-1.0)", default_value_t = 0.75)]
|
||||||
drop_off: f32,
|
drop_off: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// Initialize ncurses with proper locale for UTF-8 support
|
let _ = setlocale(LcCategory::all, "");
|
||||||
let _ = setlocale(LcCategory::all, ""); // Set locale before ncurses init for UTF-8 support
|
|
||||||
initscr();
|
initscr();
|
||||||
noecho();
|
noecho();
|
||||||
curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
|
curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
|
||||||
keypad(stdscr(), true);
|
keypad(stdscr(), true);
|
||||||
// Enable handling of window resize signals
|
timeout(0);
|
||||||
timeout(0); // Non-blocking input for getch()
|
|
||||||
|
|
||||||
// Get terminal dimensions
|
|
||||||
let mut rows: i32 = 0;
|
let mut rows: i32 = 0;
|
||||||
let mut cols: i32 = 0;
|
let mut cols: i32 = 0;
|
||||||
getmaxyx(stdscr(), &mut rows, &mut cols);
|
getmaxyx(stdscr(), &mut rows, &mut cols);
|
||||||
|
|
||||||
let sample_rate: usize = 48000;
|
let sample_rate: usize = 48000;
|
||||||
let bit_depth: usize = 16;
|
let bit_depth: usize = 16;
|
||||||
let num_channels: usize = 2; // Default to assume stereo for better handling
|
let num_channels: usize = 2;
|
||||||
let samples_per_frame: usize = sample_rate / usize::from(cli.fps);
|
let samples_per_frame: usize = sample_rate / usize::from(cli.fps);
|
||||||
let fft_size: usize = 1024;
|
let fft_size: usize = 1024;
|
||||||
|
|
||||||
// Set up FFT processor
|
|
||||||
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
||||||
|
|
||||||
// Set up audio input source - account for num_channels in buffer size
|
let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)];
|
||||||
let mut input_buffer = vec![0; samples_per_frame * num_channels * (bit_depth / 8)];
|
let mut complex_buffer = vec![Complex { re: 0.0f32, im: 0.0f32 }; fft_size];
|
||||||
let mut complex_buffer = vec![Complex { re: 0.0, im: 0.0 }; fft_size];
|
|
||||||
|
let hann_window: Vec<f32> = (0..fft_size)
|
||||||
|
.map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Set up input reader based on command line arguments
|
|
||||||
enum AudioSource {
|
enum AudioSource {
|
||||||
StdIn(std::io::StdinLock<'static>),
|
StdIn(std::io::StdinLock<'static>),
|
||||||
File(File),
|
File(File),
|
||||||
PipeWire(std::process::ChildStdout),
|
PipeWire(std::process::ChildStdout),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize display without a title bar
|
|
||||||
refresh();
|
refresh();
|
||||||
|
|
||||||
// Set up audio source
|
|
||||||
let mut audio_source = if cli.pipewire {
|
let mut audio_source = if cli.pipewire {
|
||||||
// Launch PipeWire capture process with raw output format
|
|
||||||
// Using stereo (2 channels) for consistent behavior with other input sources
|
|
||||||
let process = Command::new("pw-record")
|
let process = Command::new("pw-record")
|
||||||
.args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"])
|
.args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to start pw-record");
|
.expect("Failed to start pw-record");
|
||||||
|
|
||||||
let stdout = process.stdout.expect("Failed to capture pw-record stdout");
|
let stdout = process.stdout.expect("Failed to capture pw-record stdout");
|
||||||
refresh();
|
refresh();
|
||||||
AudioSource::PipeWire(stdout)
|
AudioSource::PipeWire(stdout)
|
||||||
} else if let Some(filename) = &cli.input {
|
} else if let Some(filename) = &cli.input {
|
||||||
// Read from file
|
|
||||||
let file = File::open(filename).expect("Failed to open input file");
|
let file = File::open(filename).expect("Failed to open input file");
|
||||||
AudioSource::File(file)
|
AudioSource::File(file)
|
||||||
} else {
|
} else {
|
||||||
// Read from stdin
|
|
||||||
AudioSource::StdIn(stdin().lock())
|
AudioSource::StdIn(stdin().lock())
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define maximum sizes to avoid overflow
|
|
||||||
let max_width = 1000;
|
let max_width = 1000;
|
||||||
let max_height = 1000;
|
let max_height = 1000;
|
||||||
|
|
||||||
// Calculate safe dimensions that will be updated when terminal resizes
|
|
||||||
let mut safe_cols = cols.min(max_width);
|
let mut safe_cols = cols.min(max_width);
|
||||||
let mut safe_rows = rows.min(max_height);
|
let mut safe_rows = rows.min(max_height);
|
||||||
|
|
||||||
// Initialize screen buffers with safe dimensions
|
let mut sc = safe_cols as usize;
|
||||||
let empty_cell = Cell { character: " ".to_string(), filled: false };
|
let mut sr = safe_rows as usize;
|
||||||
let mut current_buffer: Vec<Vec<Cell>> = vec![vec![empty_cell.clone(); safe_cols as usize]; safe_rows as usize];
|
|
||||||
let mut next_buffer: Vec<Vec<Cell>> = vec![vec![empty_cell.clone(); safe_cols as usize]; safe_rows as usize];
|
|
||||||
|
|
||||||
// For smooth drop-off
|
let mut current_buffer: Vec<Vec<Cell>> = vec![vec![EMPTY_CELL; sc]; sr];
|
||||||
let mut last_bars: Vec<f32> = vec![0.0; safe_cols as usize];
|
let mut next_buffer: Vec<Vec<Cell>> = vec![vec![EMPTY_CELL; sc]; sr];
|
||||||
let drop_factor: f32 = cli.drop_off.max(0.0).min(1.0); // Clamp between 0 and 1
|
|
||||||
|
let mut last_bars: Vec<f32> = vec![0.0; sc];
|
||||||
|
let mut freq_bands: Vec<f32> = vec![0.0; sc];
|
||||||
|
let drop_factor: f32 = cli.drop_off.clamp(0.0, 1.0);
|
||||||
|
|
||||||
let interval = Duration::from_secs_f32(1.0 / f32::from(cli.fps));
|
let interval = Duration::from_secs_f32(1.0 / f32::from(cli.fps));
|
||||||
|
|
||||||
let mut last_frame_time = Instant::now();
|
let mut last_frame_time = Instant::now();
|
||||||
|
|
||||||
// Store max amplitudes for brief stability (but not smoothing)
|
|
||||||
let mut max_magnitudes: VecDeque<f32> = VecDeque::with_capacity(usize::from(cli.fps / 2));
|
let mut max_magnitudes: VecDeque<f32> = VecDeque::with_capacity(usize::from(cli.fps / 2));
|
||||||
|
|
||||||
// Frequency scaling with safe dimensions - balanced logarithmic mapping for better audio visualization
|
let mut freq_mapping: Vec<usize> = (0..sc)
|
||||||
let mut freq_mapping: Vec<usize> = Vec::with_capacity(safe_cols as usize);
|
.map(|i| {
|
||||||
for i in 0..safe_cols as usize {
|
let log_pos = (i as f32 / sc as f32).powf(cli.log_power * 0.8);
|
||||||
// Using a more balanced logarithmic scale to better distribute frequencies
|
((log_pos * (fft_size / 2) as f32) as usize).min(fft_size / 2 - 1)
|
||||||
// Lower log_power to give more space to high frequencies
|
})
|
||||||
let log_pos = (i as f32 / safe_cols as f32).powf(cli.log_power * 0.8); // Reduced power for better balance
|
.collect();
|
||||||
let index = (log_pos * (fft_size / 2) as f32) as usize;
|
|
||||||
freq_mapping.push(index.min(fft_size / 2 - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track if terminal has been resized
|
|
||||||
let mut last_terminal_resize = Instant::now();
|
let mut last_terminal_resize = Instant::now();
|
||||||
let resize_check_interval = Duration::from_millis(500); // Check resize every 500ms
|
let resize_check_interval = Duration::from_millis(500);
|
||||||
|
|
||||||
|
let mut all_samples: Vec<f32> = Vec::with_capacity(fft_size * 4);
|
||||||
|
let mut view_diff = ViewDiff::new();
|
||||||
|
let mut max_magnitude: f32 = 1.0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Check for terminal resize - polling approach for terminals that don't send KEY_RESIZE events
|
|
||||||
if last_terminal_resize.elapsed() >= resize_check_interval {
|
if last_terminal_resize.elapsed() >= resize_check_interval {
|
||||||
let mut new_rows = 0;
|
let mut new_rows = 0;
|
||||||
let mut new_cols = 0;
|
let mut new_cols = 0;
|
||||||
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
|
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
|
||||||
|
|
||||||
// Handle resize operation
|
|
||||||
if new_rows != rows || new_cols != cols {
|
if new_rows != rows || new_cols != cols {
|
||||||
handle_terminal_resize(&mut rows, &mut cols, new_rows, new_cols,
|
handle_terminal_resize(
|
||||||
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
&mut rows, &mut cols, new_rows, new_cols,
|
||||||
&empty_cell, &mut current_buffer, &mut next_buffer,
|
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
||||||
&mut last_bars, &mut freq_mapping, cli.log_power, fft_size);
|
&mut current_buffer, &mut next_buffer,
|
||||||
|
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
||||||
|
cli.log_power, fft_size,
|
||||||
|
);
|
||||||
|
sc = safe_cols as usize;
|
||||||
|
sr = safe_rows as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_terminal_resize = Instant::now();
|
last_terminal_resize = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for character input - direct resize event
|
|
||||||
let ch = getch();
|
let ch = getch();
|
||||||
if ch == KEY_RESIZE {
|
if ch == KEY_RESIZE {
|
||||||
// Terminal was resized, update dimensions
|
|
||||||
let mut new_rows = 0;
|
let mut new_rows = 0;
|
||||||
let mut new_cols = 0;
|
let mut new_cols = 0;
|
||||||
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
|
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
|
||||||
|
handle_terminal_resize(
|
||||||
// Handle resize operation
|
&mut rows, &mut cols, new_rows, new_cols,
|
||||||
handle_terminal_resize(&mut rows, &mut cols, new_rows, new_cols,
|
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
||||||
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
&mut current_buffer, &mut next_buffer,
|
||||||
&empty_cell, &mut current_buffer, &mut next_buffer,
|
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
||||||
&mut last_bars, &mut freq_mapping, cli.log_power, fft_size);
|
cli.log_power, fft_size,
|
||||||
|
);
|
||||||
|
sc = safe_cols as usize;
|
||||||
|
sr = safe_rows as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process audio frames at regular intervals
|
|
||||||
if last_frame_time.elapsed() >= interval {
|
if last_frame_time.elapsed() >= interval {
|
||||||
// Clear the entire screen before rendering the new frame (prevents lingering characters)
|
for row in next_buffer.iter_mut() {
|
||||||
erase();
|
row.fill(EMPTY_CELL);
|
||||||
|
|
||||||
// No welcome message - keeping the display clean as requested
|
|
||||||
|
|
||||||
// Reset next buffer for this frame - using safe dimensions
|
|
||||||
for y in 0..safe_rows as usize {
|
|
||||||
for x in 0..safe_cols as usize {
|
|
||||||
next_buffer[y][x] = empty_cell.clone();
|
|
||||||
// Also reset the current buffer to ensure clean state
|
|
||||||
current_buffer[y][x] = empty_cell.clone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read and process all available data for this frame to prevent latency
|
all_samples.clear();
|
||||||
let mut all_samples = Vec::new();
|
|
||||||
let mut buffer_is_empty = false;
|
let mut buffer_is_empty = false;
|
||||||
|
|
||||||
// Process all available data until buffer is empty or we have an error
|
|
||||||
loop {
|
loop {
|
||||||
let read_result = match &mut audio_source {
|
let read_result = match &mut audio_source {
|
||||||
AudioSource::StdIn(input) => input.read(&mut input_buffer),
|
AudioSource::StdIn(input) => input.read(&mut input_buffer),
|
||||||
@@ -281,84 +266,53 @@ fn main() {
|
|||||||
|
|
||||||
match read_result {
|
match read_result {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
// No data read but not EOF - buffer is empty
|
|
||||||
buffer_is_empty = true;
|
buffer_is_empty = true;
|
||||||
break;
|
break;
|
||||||
},
|
}
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
// Resize input buffer to match actual bytes read
|
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
||||||
let actual_input = &input_buffer[0..n];
|
|
||||||
|
|
||||||
// Convert raw bytes to audio samples, handling multichannel input
|
|
||||||
let mut cursor = std::io::Cursor::new(actual_input);
|
|
||||||
|
|
||||||
// Read as many complete sample frames as possible
|
|
||||||
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
||||||
let mut frame_sum = 0.0;
|
let mut frame_sum = 0.0f32;
|
||||||
let mut valid_channels = 0;
|
let mut valid_channels = 0u32;
|
||||||
|
|
||||||
// Read all channels and average them
|
|
||||||
for _ in 0..num_channels {
|
for _ in 0..num_channels {
|
||||||
match cursor.read_i16::<LittleEndian>() {
|
match cursor.read_i16::<LittleEndian>() {
|
||||||
Ok(sample) => {
|
Ok(sample) => {
|
||||||
frame_sum += sample as f32;
|
frame_sum += sample as f32;
|
||||||
valid_channels += 1;
|
valid_channels += 1;
|
||||||
},
|
}
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Average the channels (if any valid channels were read)
|
|
||||||
if valid_channels > 0 {
|
if valid_channels > 0 {
|
||||||
all_samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
all_samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've collected enough samples, we can stop to avoid too much processing
|
|
||||||
if all_samples.len() >= fft_size * 2 {
|
if all_samples.len() >= fft_size * 2 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
|
||||||
break;
|
|
||||||
},
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
// Buffer is currently empty, but might have data later
|
|
||||||
buffer_is_empty = true;
|
buffer_is_empty = true;
|
||||||
break;
|
break;
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Error occurred
|
|
||||||
// Show error message in the buffer - using safe dimensions
|
|
||||||
let error_msg = format!("Error: {}", e);
|
let error_msg = format!("Error: {}", e);
|
||||||
for (i, c) in error_msg.chars().enumerate() {
|
for (i, c) in error_msg.chars().enumerate() {
|
||||||
if i < safe_cols as usize {
|
if i < sc {
|
||||||
next_buffer[0][i] = Cell {
|
next_buffer[0][i] = Cell { character: c, filled: true };
|
||||||
character: c.to_string(),
|
|
||||||
filled: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
view_diff.clear();
|
||||||
// Create a view diff for error display
|
for y in 0..sr {
|
||||||
let mut view_diff = ViewDiff::new();
|
for x in 0..sc {
|
||||||
|
|
||||||
// Record only the changed cells - using safe dimensions
|
|
||||||
for y in 0..safe_rows as usize {
|
|
||||||
for x in 0..safe_cols as usize {
|
|
||||||
if current_buffer[y][x] != next_buffer[y][x] {
|
if current_buffer[y][x] != next_buffer[y][x] {
|
||||||
view_diff.add_change(y, x, next_buffer[y][x].character.clone());
|
view_diff.add_change(y, x, next_buffer[y][x].character);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all changes in one batch operation
|
|
||||||
view_diff.apply();
|
view_diff.apply();
|
||||||
|
|
||||||
// Swap buffers
|
|
||||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -366,193 +320,83 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !all_samples.is_empty() {
|
if !all_samples.is_empty() {
|
||||||
// Process all samples by averaging if we have more than fft_size
|
|
||||||
if all_samples.len() > fft_size {
|
if all_samples.len() > fft_size {
|
||||||
// Calculate how many samples to average per FFT bin
|
let spb = all_samples.len() / fft_size;
|
||||||
let samples_per_bin = all_samples.len() / fft_size;
|
let rem = all_samples.len() % fft_size;
|
||||||
let remainder = all_samples.len() % fft_size;
|
|
||||||
|
|
||||||
// Average samples to fill complex buffer (ensures we don't lose data)
|
|
||||||
for i in 0..fft_size {
|
for i in 0..fft_size {
|
||||||
let start = i * samples_per_bin;
|
let start = i * spb;
|
||||||
let end = if i < remainder {
|
let end = start + spb + if i < rem { 1 } else { 0 };
|
||||||
start + samples_per_bin + 1 // Distribute remainder
|
let bin = &all_samples[start..end];
|
||||||
} else {
|
complex_buffer[i].re = bin.iter().sum::<f32>() / bin.len() as f32;
|
||||||
start + samples_per_bin
|
|
||||||
};
|
|
||||||
|
|
||||||
// Average the samples in this bin
|
|
||||||
let bin_samples = &all_samples[start..end];
|
|
||||||
let avg = bin_samples.iter().sum::<f32>() / bin_samples.len() as f32;
|
|
||||||
|
|
||||||
complex_buffer[i].re = avg;
|
|
||||||
complex_buffer[i].im = 0.0;
|
complex_buffer[i].im = 0.0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we have fewer samples than fft_size, use them directly
|
|
||||||
for i in 0..fft_size {
|
for i in 0..fft_size {
|
||||||
if i < all_samples.len() {
|
complex_buffer[i].re = if i < all_samples.len() { all_samples[i] } else { 0.0 };
|
||||||
complex_buffer[i].re = all_samples[i];
|
complex_buffer[i].im = 0.0;
|
||||||
complex_buffer[i].im = 0.0;
|
|
||||||
} else {
|
|
||||||
complex_buffer[i].re = 0.0;
|
|
||||||
complex_buffer[i].im = 0.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply window function (Hann window) to reduce spectral leakage
|
|
||||||
for i in 0..fft_size {
|
for i in 0..fft_size {
|
||||||
let window = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos());
|
complex_buffer[i].re *= hann_window[i];
|
||||||
complex_buffer[i].re *= window;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process FFT using standard rustfft implementation (already optimized)
|
|
||||||
fft.process(&mut complex_buffer);
|
fft.process(&mut complex_buffer);
|
||||||
|
|
||||||
// Calculate frequency bands using our mapping - with safe dimensions
|
|
||||||
let mut freq_bands: Vec<f32> = vec![0.0; safe_cols as usize];
|
|
||||||
|
|
||||||
// We only use the first half of FFT results (Nyquist theorem)
|
|
||||||
for (i, &mapping_idx) in freq_mapping.iter().enumerate() {
|
for (i, &mapping_idx) in freq_mapping.iter().enumerate() {
|
||||||
if i < freq_bands.len() && mapping_idx < fft_size / 2 {
|
if mapping_idx < fft_size / 2 {
|
||||||
// Get magnitude and apply frequency-dependent scaling for better visualization
|
|
||||||
let magnitude = complex_buffer[mapping_idx].norm();
|
let magnitude = complex_buffer[mapping_idx].norm();
|
||||||
|
|
||||||
// Boost higher frequencies to make them more visible
|
|
||||||
// The higher the frequency, the more we boost it
|
|
||||||
let freq_boost = 1.0 + (mapping_idx as f32 / (fft_size / 2) as f32) * 4.0;
|
let freq_boost = 1.0 + (mapping_idx as f32 / (fft_size / 2) as f32) * 4.0;
|
||||||
|
let scaled = (magnitude * 80.0 * freq_boost).powf(0.5);
|
||||||
// Amplify the signal with the frequency boost
|
freq_bands[i] = if scaled > last_bars[i] { scaled } else { last_bars[i] * drop_factor };
|
||||||
let scaled_magnitude = (magnitude * 80.0 * freq_boost).powf(0.5);
|
|
||||||
|
|
||||||
// Direct response for punchier visualization
|
|
||||||
freq_bands[i] = if scaled_magnitude > last_bars[i] {
|
|
||||||
// Immediate rise for maximum punch
|
|
||||||
scaled_magnitude
|
|
||||||
} else {
|
|
||||||
// Quick fall with drop factor
|
|
||||||
last_bars[i] * drop_factor
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last bars for next frame
|
std::mem::swap(&mut last_bars, &mut freq_bands);
|
||||||
last_bars = freq_bands.clone();
|
|
||||||
|
|
||||||
// Calculate max magnitude over all frequencies for normalization
|
let current_max = last_bars.iter().copied().fold(0.0f32, f32::max);
|
||||||
let current_max = freq_bands.iter().cloned().fold(0.0, f32::max);
|
|
||||||
|
|
||||||
// Make sure we have a reasonable minimum value to avoid empty display
|
|
||||||
let current_max = if current_max < 0.001 { 1.0 } else { current_max };
|
let current_max = if current_max < 0.001 { 1.0 } else { current_max };
|
||||||
|
|
||||||
// Store this frame's maximum for very minimal smoothing
|
|
||||||
max_magnitudes.push_front(current_max);
|
max_magnitudes.push_front(current_max);
|
||||||
|
|
||||||
// Keep just half a second of history - enough to prevent extreme flickering
|
|
||||||
// but not enough to create significant smoothing
|
|
||||||
if max_magnitudes.len() > usize::from(cli.fps) / 2 {
|
if max_magnitudes.len() > usize::from(cli.fps) / 2 {
|
||||||
max_magnitudes.pop_back();
|
max_magnitudes.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct maximum for most punchy visualization
|
max_magnitude = max_magnitudes.iter().copied().fold(0.0f32, f32::max).max(0.1);
|
||||||
// Just take the current frame's maximum with minimal safety buffer
|
|
||||||
let max_magnitude = if !max_magnitudes.is_empty() {
|
|
||||||
max_magnitudes.iter().cloned().fold(0.0, f32::max).max(0.1)
|
|
||||||
} else {
|
|
||||||
current_max.max(0.1)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update next buffer with the new bar states - using safe dimensions
|
for (i, &magnitude) in last_bars[..sc].iter().enumerate() {
|
||||||
for (i, &magnitude) in freq_bands.iter().enumerate() {
|
render_bar(&mut next_buffer, i, sr, (magnitude / max_magnitude) * sr as f32);
|
||||||
if i < safe_cols as usize {
|
|
||||||
let normalized_magnitude = magnitude / max_magnitude;
|
|
||||||
let bar_height = (normalized_magnitude * safe_rows as f32) as i32;
|
|
||||||
|
|
||||||
for j in 0..safe_rows as i32 {
|
|
||||||
let y = safe_rows - 1 - j;
|
|
||||||
if j < bar_height {
|
|
||||||
// Simple solid bar - one character wide
|
|
||||||
let char_to_use = "▒"; // Using medium shade block (U+2592) for better compatibility
|
|
||||||
|
|
||||||
if y >= 0 && y < safe_rows && i < safe_cols as usize {
|
|
||||||
next_buffer[y as usize][i] = Cell {
|
|
||||||
character: char_to_use.to_string(),
|
|
||||||
filled: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a view diff for efficient terminal updates
|
view_diff.clear();
|
||||||
let mut view_diff = ViewDiff::new();
|
for y in 0..sr {
|
||||||
|
for x in 0..sc {
|
||||||
// Record only the changed cells (diff and patch) - using safe dimensions
|
|
||||||
for y in 0..safe_rows as usize {
|
|
||||||
for x in 0..safe_cols as usize {
|
|
||||||
if current_buffer[y][x] != next_buffer[y][x] {
|
if current_buffer[y][x] != next_buffer[y][x] {
|
||||||
view_diff.add_change(y, x, next_buffer[y][x].character.clone());
|
view_diff.add_change(y, x, next_buffer[y][x].character);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply changes efficiently in one batch
|
|
||||||
view_diff.apply();
|
view_diff.apply();
|
||||||
|
|
||||||
// Swap buffers for next frame
|
|
||||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||||
|
|
||||||
last_frame_time = Instant::now();
|
last_frame_time = Instant::now();
|
||||||
refresh();
|
refresh();
|
||||||
} else if buffer_is_empty {
|
} else if buffer_is_empty {
|
||||||
// Reset next buffer - using safe dimensions
|
for i in 0..sc {
|
||||||
for y in 0..safe_rows as usize {
|
last_bars[i] *= drop_factor;
|
||||||
for x in 0..safe_cols as usize {
|
render_bar(&mut next_buffer, i, sr, (last_bars[i] / max_magnitude) * sr as f32);
|
||||||
next_buffer[y][x] = empty_cell.clone();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update buffer with reduced heights - using safe dimensions
|
view_diff.clear();
|
||||||
for i in 0..last_bars.len() {
|
for y in 0..sr {
|
||||||
if i < safe_cols as usize {
|
for x in 0..sc {
|
||||||
last_bars[i] *= drop_factor;
|
|
||||||
|
|
||||||
let bar_height = (last_bars[i] * safe_rows as f32) as i32;
|
|
||||||
for j in 0..bar_height {
|
|
||||||
let y = safe_rows - 1 - j;
|
|
||||||
if y >= 0 && y < safe_rows && i < safe_cols as usize {
|
|
||||||
// Simple solid bar - one character wide
|
|
||||||
let char_to_use = "▒"; // Using medium shade block (U+2592) for better compatibility
|
|
||||||
|
|
||||||
next_buffer[y as usize][i] = Cell {
|
|
||||||
character: char_to_use.to_string(),
|
|
||||||
filled: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a view diff for efficient terminal updates
|
|
||||||
let mut view_diff = ViewDiff::new();
|
|
||||||
|
|
||||||
// Record only the changed cells - using safe dimensions
|
|
||||||
for y in 0..safe_rows as usize {
|
|
||||||
for x in 0..safe_cols as usize {
|
|
||||||
if current_buffer[y][x] != next_buffer[y][x] {
|
if current_buffer[y][x] != next_buffer[y][x] {
|
||||||
view_diff.add_change(y, x, next_buffer[y][x].character.clone());
|
view_diff.add_change(y, x, next_buffer[y][x].character);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all changes in one batch operation
|
|
||||||
view_diff.apply();
|
view_diff.apply();
|
||||||
|
|
||||||
// Swap buffers
|
|
||||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||||
|
last_frame_time = Instant::now();
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user