looks good enough for a -p
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+363
@@ -0,0 +1,363 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.164"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
||||
|
||||
[[package]]
|
||||
name = "ncurses"
|
||||
version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd71afa95710e841d173521e8483e764004eb332bdf47bd01d00f568688027f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "primal-check"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustfft"
|
||||
version = "6.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86"
|
||||
dependencies = [
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"primal-check",
|
||||
"strength_reduce",
|
||||
"transpose",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "spectrust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"clap",
|
||||
"ncurses",
|
||||
"rustfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strength_reduce"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "transpose"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"strength_reduce",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "spectrust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.5.0"
|
||||
clap = { version = "4.5.21", features = ["derive"] }
|
||||
ncurses = "6.0.1"
|
||||
rustfft = "6.2.0"
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
use clap::Parser;
|
||||
use ncurses::*;
|
||||
use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{stdin, Read};
|
||||
use std::fs::File;
|
||||
use std::time::{Duration, Instant};
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread::sleep;
|
||||
|
||||
// Create screen buffer struct to track cell states
|
||||
// Each cell stores a character and its state (whether it's filled or not)
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Cell {
|
||||
character: String,
|
||||
filled: bool,
|
||||
}
|
||||
|
||||
// Function to handle terminal resizing
|
||||
fn handle_terminal_resize(
|
||||
rows: &mut i32,
|
||||
cols: &mut i32,
|
||||
new_rows: i32,
|
||||
new_cols: i32,
|
||||
max_width: i32,
|
||||
max_height: i32,
|
||||
safe_rows: &mut i32,
|
||||
safe_cols: &mut i32,
|
||||
empty_cell: &Cell,
|
||||
current_buffer: &mut Vec<Vec<Cell>>,
|
||||
next_buffer: &mut Vec<Vec<Cell>>,
|
||||
last_bars: &mut Vec<f32>,
|
||||
freq_mapping: &mut Vec<usize>,
|
||||
log_power: f32,
|
||||
fft_size: usize
|
||||
) {
|
||||
// Update terminal dimensions
|
||||
*rows = new_rows;
|
||||
*cols = new_cols;
|
||||
|
||||
// Update safe dimensions to account for new terminal size
|
||||
*safe_cols = new_cols.min(max_width);
|
||||
*safe_rows = new_rows.min(max_height);
|
||||
|
||||
// Resize screen buffers
|
||||
*current_buffer = vec![vec![empty_cell.clone(); *safe_cols as usize]; *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
|
||||
*last_bars = vec![0.0; *safe_cols as usize];
|
||||
|
||||
// Recalculate frequency mapping for new terminal width
|
||||
freq_mapping.clear();
|
||||
freq_mapping.reserve(*safe_cols as usize);
|
||||
for i in 0..*safe_cols as usize {
|
||||
// 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;
|
||||
freq_mapping.push(index.min(fft_size / 2 - 1));
|
||||
}
|
||||
|
||||
// Completely clear the screen
|
||||
clear();
|
||||
|
||||
// Apply changes
|
||||
refresh();
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "Spectrust")]
|
||||
#[command(version = "0.1.0")]
|
||||
#[command(about = "A PCM spectrum analyzer for audio visualization",
|
||||
long_about = "Spectrust is a PCM spectrum analyzer that visualizes audio in your terminal.\n\
|
||||
It can read from stdin, a file, or capture directly from PipeWire.\n\n\
|
||||
Examples:\n\
|
||||
- Direct PipeWire capture: spectrust -p\n\
|
||||
- From audio file: spectrust -i audio.pcm\n\
|
||||
- From stdin: pw-record --raw - | spectrust")]
|
||||
struct Cli {
|
||||
#[arg(value_name = "FPS", default_value_t = 60)]
|
||||
fps: u16,
|
||||
|
||||
#[arg(short, long, help = "Use PipeWire to capture audio directly")]
|
||||
pipewire: bool,
|
||||
|
||||
#[arg(short, long, help = "Input file (reads from stdin if not provided)")]
|
||||
input: Option<String>,
|
||||
|
||||
#[arg(short, long, help = "Logarithmic scaling power (higher values emphasize lower frequencies)", default_value_t = 1.4)]
|
||||
log_power: f32,
|
||||
|
||||
#[arg(short, long, help = "Drop-off factor for bar animation (0.0-1.0)", default_value_t = 0.85)]
|
||||
drop_off: f32,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize ncurses with proper locale for UTF-8 support
|
||||
setlocale(LcCategory::all, ""); // Set locale before ncurses init for UTF-8 support
|
||||
initscr();
|
||||
noecho();
|
||||
curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);
|
||||
keypad(stdscr(), true);
|
||||
// Enable handling of window resize signals
|
||||
timeout(0); // Non-blocking input for getch()
|
||||
|
||||
// Get terminal dimensions
|
||||
let mut rows: i32 = 0;
|
||||
let mut cols: i32 = 0;
|
||||
getmaxyx(stdscr(), &mut rows, &mut cols);
|
||||
|
||||
let sample_rate: usize = 48000;
|
||||
let bit_depth: usize = 16;
|
||||
let samples_per_frame: usize = sample_rate / usize::from(cli.fps);
|
||||
let fft_size: usize = 1024;
|
||||
|
||||
// Set up FFT processor
|
||||
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
||||
|
||||
// Set up audio input source
|
||||
let mut input_buffer = vec![0; samples_per_frame * (bit_depth / 8)];
|
||||
let mut complex_buffer = vec![Complex { re: 0.0, im: 0.0 }; fft_size];
|
||||
|
||||
// Set up input reader based on command line arguments
|
||||
enum AudioSource {
|
||||
StdIn(std::io::StdinLock<'static>),
|
||||
File(File),
|
||||
PipeWire(std::process::ChildStdout),
|
||||
}
|
||||
|
||||
// Initialize display without a title bar
|
||||
refresh();
|
||||
|
||||
// Set up audio source
|
||||
let mut audio_source = if cli.pipewire {
|
||||
// Launch PipeWire capture process with raw output format
|
||||
let process = Command::new("pw-record")
|
||||
.args(["--format=s16", "--rate=48000", "--channels=1", "--raw", "-"])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start pw-record");
|
||||
|
||||
let stdout = process.stdout.expect("Failed to capture pw-record stdout");
|
||||
refresh();
|
||||
AudioSource::PipeWire(stdout)
|
||||
} else if let Some(filename) = &cli.input {
|
||||
// Read from file
|
||||
let file = File::open(filename).expect("Failed to open input file");
|
||||
AudioSource::File(file)
|
||||
} else {
|
||||
// Read from stdin
|
||||
AudioSource::StdIn(stdin().lock())
|
||||
};
|
||||
|
||||
// Define maximum sizes to avoid overflow
|
||||
let max_width = 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_rows = rows.min(max_height);
|
||||
|
||||
// Initialize screen buffers with safe dimensions
|
||||
let empty_cell = Cell { character: " ".to_string(), filled: false };
|
||||
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 last_bars: Vec<f32> = vec![0.0; safe_cols as usize];
|
||||
let drop_factor: f32 = cli.drop_off.max(0.0).min(1.0); // Clamp between 0 and 1
|
||||
|
||||
let interval = Duration::from_secs_f32(1.0 / f32::from(cli.fps));
|
||||
|
||||
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));
|
||||
|
||||
// Frequency scaling with safe dimensions - balanced logarithmic mapping for better audio visualization
|
||||
let mut freq_mapping: Vec<usize> = Vec::with_capacity(safe_cols as usize);
|
||||
for i in 0..safe_cols as usize {
|
||||
// Using a more balanced logarithmic scale to better distribute frequencies
|
||||
// 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
|
||||
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 resize_check_interval = Duration::from_millis(500); // Check resize every 500ms
|
||||
|
||||
loop {
|
||||
// Check for terminal resize - polling approach for terminals that don't send KEY_RESIZE events
|
||||
if last_terminal_resize.elapsed() >= resize_check_interval {
|
||||
let mut new_rows = 0;
|
||||
let mut new_cols = 0;
|
||||
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
|
||||
|
||||
// Handle resize operation
|
||||
if new_rows != rows || new_cols != cols {
|
||||
handle_terminal_resize(&mut rows, &mut cols, new_rows, new_cols,
|
||||
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
||||
&empty_cell, &mut current_buffer, &mut next_buffer,
|
||||
&mut last_bars, &mut freq_mapping, cli.log_power, fft_size);
|
||||
}
|
||||
|
||||
last_terminal_resize = Instant::now();
|
||||
}
|
||||
|
||||
// Check for character input - direct resize event
|
||||
let ch = getch();
|
||||
if ch == KEY_RESIZE {
|
||||
// Terminal was resized, update dimensions
|
||||
let mut new_rows = 0;
|
||||
let mut new_cols = 0;
|
||||
getmaxyx(stdscr(), &mut new_rows, &mut new_cols);
|
||||
|
||||
// Handle resize operation
|
||||
handle_terminal_resize(&mut rows, &mut cols, new_rows, new_cols,
|
||||
max_width, max_height, &mut safe_rows, &mut safe_cols,
|
||||
&empty_cell, &mut current_buffer, &mut next_buffer,
|
||||
&mut last_bars, &mut freq_mapping, cli.log_power, fft_size);
|
||||
}
|
||||
|
||||
// Process audio frames at regular intervals
|
||||
if last_frame_time.elapsed() >= interval {
|
||||
// Clear the entire screen before rendering the new frame (prevents lingering characters)
|
||||
erase();
|
||||
|
||||
// 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
|
||||
let mut all_samples = Vec::new();
|
||||
let mut buffer_is_empty = false;
|
||||
|
||||
// Process all available data until buffer is empty or we have an error
|
||||
loop {
|
||||
let read_result = match &mut audio_source {
|
||||
AudioSource::StdIn(input) => input.read(&mut input_buffer),
|
||||
AudioSource::File(file) => file.read(&mut input_buffer),
|
||||
AudioSource::PipeWire(stdout) => stdout.read(&mut input_buffer),
|
||||
};
|
||||
|
||||
match read_result {
|
||||
Ok(0) => {
|
||||
// No data read but not EOF - buffer is empty
|
||||
buffer_is_empty = true;
|
||||
break;
|
||||
},
|
||||
Ok(n) => {
|
||||
// Resize input buffer to match actual bytes read
|
||||
let actual_input = &input_buffer[0..n];
|
||||
|
||||
// Convert raw bytes to audio samples
|
||||
let mut cursor = std::io::Cursor::new(actual_input);
|
||||
|
||||
// Read as many complete samples as possible
|
||||
while cursor.position() < (n as u64 - 1) {
|
||||
match cursor.read_i16::<LittleEndian>() {
|
||||
Ok(sample) => all_samples.push(sample as f32 / 32768.0),
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// If we've collected enough samples, we can stop to avoid too much processing
|
||||
if all_samples.len() >= fft_size * 2 {
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
break;
|
||||
},
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
// Buffer is currently empty, but might have data later
|
||||
buffer_is_empty = true;
|
||||
break;
|
||||
},
|
||||
Err(e) => {
|
||||
// Error occurred
|
||||
// Show error message in the buffer - using safe dimensions
|
||||
let error_msg = format!("Error: {}", e);
|
||||
for (i, c) in error_msg.chars().enumerate() {
|
||||
if i < safe_cols as usize {
|
||||
next_buffer[0][i] = Cell {
|
||||
character: c.to_string(),
|
||||
filled: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Render 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] {
|
||||
let _ = mvprintw(y as i32, x as i32, &next_buffer[y][x].character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swap buffers
|
||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||
|
||||
refresh();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !all_samples.is_empty() {
|
||||
// Process all samples by averaging if we have more than fft_size
|
||||
if all_samples.len() > fft_size {
|
||||
// Calculate how many samples to average per FFT bin
|
||||
let samples_per_bin = 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 {
|
||||
let start = i * samples_per_bin;
|
||||
let end = if i < remainder {
|
||||
start + samples_per_bin + 1 // Distribute remainder
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
// If we have fewer samples than fft_size, use them directly
|
||||
for i in 0..fft_size {
|
||||
if i < all_samples.len() {
|
||||
complex_buffer[i].re = all_samples[i];
|
||||
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 {
|
||||
let window = 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos());
|
||||
complex_buffer[i].re *= window;
|
||||
}
|
||||
|
||||
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() {
|
||||
if i < freq_bands.len() && mapping_idx < fft_size / 2 {
|
||||
// Get magnitude and apply frequency-dependent scaling for better visualization
|
||||
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;
|
||||
|
||||
// Amplify the signal with the frequency boost
|
||||
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
|
||||
last_bars = freq_bands.clone();
|
||||
|
||||
// Calculate max magnitude over all frequencies for normalization
|
||||
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 };
|
||||
|
||||
// Store this frame's maximum for very minimal smoothing
|
||||
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 {
|
||||
max_magnitudes.pop_back();
|
||||
}
|
||||
|
||||
// Direct maximum for most punchy visualization
|
||||
// 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 freq_bands.iter().enumerate() {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render only the cells that changed (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] {
|
||||
let _ = mvprintw(y as i32, x as i32, &next_buffer[y][x].character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swap buffers for next frame
|
||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||
|
||||
last_frame_time = Instant::now();
|
||||
refresh();
|
||||
} else if buffer_is_empty {
|
||||
// Reset next buffer - 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();
|
||||
}
|
||||
}
|
||||
|
||||
// Update buffer with reduced heights - using safe dimensions
|
||||
for i in 0..last_bars.len() {
|
||||
if i < safe_cols as usize {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render only the cells that changed - 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] {
|
||||
let _ = mvprintw(y as i32, x as i32, &next_buffer[y][x].character);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Swap buffers
|
||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user