Compare commits
4 Commits
v0.2.4
..
cd8c4a8ff3
| Author | SHA1 | Date | |
|---|---|---|---|
|
cd8c4a8ff3
|
|||
|
b7e861093e
|
|||
|
5201b503e6
|
|||
|
79544f64c3
|
Generated
+1
-1
@@ -429,7 +429,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spectrust"
|
name = "spectrust"
|
||||||
version = "0.2.4"
|
version = "0.2.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "spectrust"
|
name = "spectrust"
|
||||||
version = "0.2.4"
|
version = "0.2.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# spectrust
|
||||||
|
|
||||||
|
A terminal audio spectrum analyzer written in Rust. Visualizes PCM audio in real time using block characters — reads from PipeWire, a file, or stdin.
|
||||||
|
|
||||||
|
```
|
||||||
|
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▁▂▃▄▅▆▇█
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
**Arch Linux (AUR)**
|
||||||
|
```
|
||||||
|
yay -S spectrust
|
||||||
|
```
|
||||||
|
|
||||||
|
**From source**
|
||||||
|
```
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Rust 1.70+. PipeWire (`pw-record`) is only needed for the `-p` capture mode.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
spectrust [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `-f, --fps <N>` | `60` | Target frames per second |
|
||||||
|
| `-p, --pipewire` | — | Capture audio directly from PipeWire |
|
||||||
|
| `-i, --input <FILE>` | — | Read PCM from a file (stdin if omitted) |
|
||||||
|
| `-l, --log-power <N>` | `1.4` | Frequency scaling power (0.5–3.0); higher values give more space to bass |
|
||||||
|
| `-d, --drop-off <N>` | `0.75` | Bar drop-off factor per frame (0.0 = instant, 1.0 = no drop) |
|
||||||
|
| `--rate <HZ>` | `48000` | Sample rate — must match the audio source |
|
||||||
|
| `--channels <N>` | `2` | Channel count — must match the audio source |
|
||||||
|
|
||||||
|
### Input modes
|
||||||
|
|
||||||
|
**PipeWire** — capture system audio directly:
|
||||||
|
```
|
||||||
|
spectrust -p
|
||||||
|
```
|
||||||
|
|
||||||
|
**stdin** — pipe from any recorder:
|
||||||
|
```
|
||||||
|
pw-record --format=s16 --rate=48000 --channels=2 --raw - | spectrust
|
||||||
|
arecord -f S16_LE -r 48000 -c 2 | spectrust
|
||||||
|
```
|
||||||
|
|
||||||
|
**File** — replay a raw PCM file:
|
||||||
|
```
|
||||||
|
spectrust -i recording.pcm
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime controls
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `q` / `Ctrl+C` | Quit |
|
||||||
|
| `[` / `]` | Decrease / increase drop-off speed |
|
||||||
|
| `,` / `.` | Decrease / increase log frequency scaling |
|
||||||
|
| `-` / `+` | Decrease / increase sensitivity |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Input is expected as signed 16-bit little-endian PCM (s16le). This matches `pw-record --format=s16 --raw` and `arecord -f S16_LE`.
|
||||||
|
- If `--rate` or `--channels` don't match the actual source the visualization will be distorted.
|
||||||
|
- The sensitivity control scales bar heights at render time; it has the most noticeable effect on quiet signals.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||||
pkgname=spectrust
|
pkgname=spectrust
|
||||||
pkgver=0.2.4
|
pkgver=0.2.6
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Terminal audio spectrum analyzer"
|
pkgdesc="Terminal audio spectrum analyzer"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
+139
-105
@@ -13,6 +13,8 @@ use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::{stdout, Read, Write};
|
use std::io::{stdout, Read, Write};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
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};
|
||||||
@@ -63,6 +65,15 @@ fn render_bar(buf: &mut Vec<Vec<Cell>>, col: usize, sr: usize, bar_height_f: f32
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_freq_mapping(sc: usize, log_power: f32, fft_size: usize) -> Vec<usize> {
|
||||||
|
(0..sc)
|
||||||
|
.map(|i| {
|
||||||
|
let log_pos = (i as f32 / sc as f32).powf(log_power * 0.8);
|
||||||
|
((log_pos * (fft_size / 2) as f32) as usize).min(fft_size / 2 - 1)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_terminal_resize(
|
fn handle_terminal_resize(
|
||||||
rows: &mut u16,
|
rows: &mut u16,
|
||||||
cols: &mut u16,
|
cols: &mut u16,
|
||||||
@@ -96,12 +107,7 @@ fn handle_terminal_resize(
|
|||||||
freq_bands.clear();
|
freq_bands.clear();
|
||||||
freq_bands.resize(sc, 0.0);
|
freq_bands.resize(sc, 0.0);
|
||||||
|
|
||||||
freq_mapping.clear();
|
*freq_mapping = build_freq_mapping(sc, log_power, fft_size);
|
||||||
for i in 0..sc {
|
|
||||||
let log_pos = (i as f32 / sc 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = stdout();
|
let mut out = stdout();
|
||||||
let _ = queue!(out, Clear(ClearType::All));
|
let _ = queue!(out, Clear(ClearType::All));
|
||||||
@@ -110,16 +116,21 @@ fn handle_terminal_resize(
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "Spectrust")]
|
#[command(name = "Spectrust")]
|
||||||
#[command(version = "0.1.0")]
|
#[command(version)]
|
||||||
#[command(about = "A PCM spectrum analyzer for audio visualization",
|
#[command(about = "A PCM spectrum analyzer for audio visualization",
|
||||||
long_about = "Spectrust is a PCM spectrum analyzer that visualizes audio in your terminal.\n\
|
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\
|
It can read from stdin, a file, or capture directly from PipeWire.\n\n\
|
||||||
Examples:\n\
|
Examples:\n\
|
||||||
- Direct PipeWire capture: spectrust -p\n\
|
- Direct PipeWire capture: spectrust -p\n\
|
||||||
- From audio file: spectrust -i audio.pcm\n\
|
- From audio file: spectrust -i audio.pcm\n\
|
||||||
- From stdin: pw-record --raw - | spectrust")]
|
- From stdin: pw-record --raw - | spectrust\n\n\
|
||||||
|
Runtime controls:\n\
|
||||||
|
- q / Ctrl+C quit\n\
|
||||||
|
- [ / ] decrease / increase drop-off\n\
|
||||||
|
- , / . decrease / increase log scaling\n\
|
||||||
|
- - / + decrease / increase sensitivity")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(value_name = "FPS", default_value_t = 60)]
|
#[arg(short, long, help = "Target frames per second", default_value_t = 60)]
|
||||||
fps: u16,
|
fps: u16,
|
||||||
|
|
||||||
#[arg(short, long, help = "Use PipeWire to capture audio directly")]
|
#[arg(short, long, help = "Use PipeWire to capture audio directly")]
|
||||||
@@ -128,11 +139,17 @@ struct Cli {
|
|||||||
#[arg(short, long, help = "Input file (reads from stdin if not provided)")]
|
#[arg(short, long, help = "Input file (reads from stdin if not provided)")]
|
||||||
input: Option<String>,
|
input: Option<String>,
|
||||||
|
|
||||||
#[arg(short, long, help = "Logarithmic scaling power (higher values emphasize lower frequencies)", default_value_t = 1.4)]
|
#[arg(short, long, help = "Logarithmic frequency scaling power (0.5–3.0; higher values give more space to bass)", 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.75)]
|
#[arg(short, long, help = "Bar drop-off factor per frame (0.0 = instant drop, 1.0 = no drop)", default_value_t = 0.75)]
|
||||||
drop_off: f32,
|
drop_off: f32,
|
||||||
|
|
||||||
|
#[arg(long, help = "Sample rate in Hz — must match the audio source", default_value_t = 48000)]
|
||||||
|
rate: u32,
|
||||||
|
|
||||||
|
#[arg(long, help = "Number of audio channels — must match the audio source", default_value_t = 2)]
|
||||||
|
channels: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -151,45 +168,94 @@ fn main() {
|
|||||||
|
|
||||||
let (mut cols, mut rows) = terminal::size().expect("failed to get terminal size");
|
let (mut cols, mut rows) = terminal::size().expect("failed to get terminal size");
|
||||||
|
|
||||||
let sample_rate: usize = 48000;
|
let sample_rate: usize = cli.rate as usize;
|
||||||
let bit_depth: usize = 16;
|
let bit_depth: usize = 16;
|
||||||
let num_channels: usize = 2;
|
let num_channels: usize = cli.channels as usize;
|
||||||
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;
|
||||||
|
|
||||||
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
let fft = Radix4::new(fft_size, FftDirection::Forward);
|
||||||
|
|
||||||
let mut input_buffer = vec![0u8; 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.0f32, im: 0.0f32 }; fft_size];
|
||||||
|
|
||||||
let hann_window: Vec<f32> = (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()))
|
.map(|i| 0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / fft_size as f32).cos()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
enum AudioSource {
|
|
||||||
StdIn(std::io::StdinLock<'static>),
|
|
||||||
File(File),
|
|
||||||
PipeWire(std::process::ChildStdout),
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = out.flush();
|
let _ = out.flush();
|
||||||
|
|
||||||
let mut audio_source = if cli.pipewire {
|
let reader: Box<dyn Read + Send + 'static> = if cli.pipewire {
|
||||||
let process = Command::new("pw-record")
|
let spawn_result = Command::new("pw-record")
|
||||||
.args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"])
|
.args([
|
||||||
|
"--format=s16",
|
||||||
|
&format!("--rate={}", cli.rate),
|
||||||
|
&format!("--channels={}", cli.channels),
|
||||||
|
"--raw",
|
||||||
|
"-",
|
||||||
|
])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.spawn()
|
.spawn();
|
||||||
.expect("failed to start pw-record");
|
match spawn_result {
|
||||||
let pipe = process.stdout.expect("failed to capture pw-record stdout");
|
Ok(mut process) => {
|
||||||
let _ = out.flush();
|
Box::new(process.stdout.take().expect("failed to capture pw-record stdout"))
|
||||||
AudioSource::PipeWire(pipe)
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
let _ = execute!(out, LeaveAlternateScreen);
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
eprintln!("error: pw-record not found — install PipeWire or pipe audio via stdin.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ = execute!(out, LeaveAlternateScreen);
|
||||||
|
let _ = disable_raw_mode();
|
||||||
|
eprintln!("error: failed to start pw-record: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Some(filename) = &cli.input {
|
} else if let Some(filename) = &cli.input {
|
||||||
let file = File::open(filename).expect("failed to open input file");
|
Box::new(File::open(filename).expect("failed to open input file"))
|
||||||
AudioSource::File(file)
|
|
||||||
} else {
|
} else {
|
||||||
AudioSource::StdIn(std::io::stdin().lock())
|
Box::new(std::io::stdin())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel::<Vec<f32>>();
|
||||||
|
let _audio_thread = thread::spawn(move || {
|
||||||
|
let mut reader = reader;
|
||||||
|
let mut input_buffer = vec![0u8; samples_per_frame * num_channels * (bit_depth / 8)];
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut input_buffer) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(n) => {
|
||||||
|
let mut samples = Vec::with_capacity(n / (num_channels * (bit_depth / 8)));
|
||||||
|
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
||||||
|
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
||||||
|
let mut frame_sum = 0.0f32;
|
||||||
|
let mut valid_channels = 0u32;
|
||||||
|
for _ in 0..num_channels {
|
||||||
|
match cursor.read_i16::<LittleEndian>() {
|
||||||
|
Ok(sample) => {
|
||||||
|
frame_sum += sample as f32;
|
||||||
|
valid_channels += 1;
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if valid_channels > 0 {
|
||||||
|
samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !samples.is_empty() && tx.send(samples).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let max_width: u16 = 1000;
|
let max_width: u16 = 1000;
|
||||||
let max_height: u16 = 1000;
|
let max_height: u16 = 1000;
|
||||||
|
|
||||||
@@ -204,19 +270,16 @@ fn main() {
|
|||||||
|
|
||||||
let mut last_bars: Vec<f32> = vec![0.0; sc];
|
let mut last_bars: Vec<f32> = vec![0.0; sc];
|
||||||
let mut freq_bands: 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 mut drop_factor: f32 = cli.drop_off.clamp(0.0, 1.0);
|
||||||
|
let mut log_power: f32 = cli.log_power;
|
||||||
|
let mut sensitivity: f32 = 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();
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
let mut freq_mapping: Vec<usize> = (0..sc)
|
let mut freq_mapping: Vec<usize> = build_freq_mapping(sc, log_power, fft_size);
|
||||||
.map(|i| {
|
|
||||||
let log_pos = (i as f32 / sc as f32).powf(cli.log_power * 0.8);
|
|
||||||
((log_pos * (fft_size / 2) as f32) as usize).min(fft_size / 2 - 1)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut last_terminal_resize = Instant::now();
|
let mut last_terminal_resize = Instant::now();
|
||||||
let resize_check_interval = Duration::from_millis(500);
|
let resize_check_interval = Duration::from_millis(500);
|
||||||
@@ -226,7 +289,8 @@ fn main() {
|
|||||||
let mut max_magnitude: f32 = 1.0;
|
let mut max_magnitude: f32 = 1.0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if event::poll(Duration::ZERO).unwrap_or(false) {
|
let time_to_next_frame = interval.saturating_sub(last_frame_time.elapsed());
|
||||||
|
if event::poll(time_to_next_frame).unwrap_or(false) {
|
||||||
match event::read() {
|
match event::read() {
|
||||||
Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break,
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break,
|
||||||
Ok(Event::Key(KeyEvent {
|
Ok(Event::Key(KeyEvent {
|
||||||
@@ -234,13 +298,33 @@ fn main() {
|
|||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
..
|
..
|
||||||
})) => break,
|
})) => break,
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('['), .. })) => {
|
||||||
|
drop_factor = (drop_factor - 0.05).max(0.0);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char(']'), .. })) => {
|
||||||
|
drop_factor = (drop_factor + 0.05).min(1.0);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char(','), .. })) => {
|
||||||
|
log_power = (log_power - 0.1).max(0.5);
|
||||||
|
freq_mapping = build_freq_mapping(sc, log_power, fft_size);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('.'), .. })) => {
|
||||||
|
log_power = (log_power + 0.1).min(3.0);
|
||||||
|
freq_mapping = build_freq_mapping(sc, log_power, fft_size);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('-'), .. })) => {
|
||||||
|
sensitivity = (sensitivity / 1.2).max(0.1);
|
||||||
|
}
|
||||||
|
Ok(Event::Key(KeyEvent { code: KeyCode::Char('+' | '='), .. })) => {
|
||||||
|
sensitivity = (sensitivity * 1.2).min(10.0);
|
||||||
|
}
|
||||||
Ok(Event::Resize(new_cols, new_rows)) => {
|
Ok(Event::Resize(new_cols, new_rows)) => {
|
||||||
handle_terminal_resize(
|
handle_terminal_resize(
|
||||||
&mut rows, &mut cols, new_rows, new_cols,
|
&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,
|
&mut current_buffer, &mut next_buffer,
|
||||||
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
||||||
cli.log_power, fft_size,
|
log_power, fft_size,
|
||||||
);
|
);
|
||||||
sc = safe_cols as usize;
|
sc = safe_cols as usize;
|
||||||
sr = safe_rows as usize;
|
sr = safe_rows as usize;
|
||||||
@@ -257,7 +341,7 @@ fn main() {
|
|||||||
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,
|
&mut current_buffer, &mut next_buffer,
|
||||||
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
&mut last_bars, &mut freq_bands, &mut freq_mapping,
|
||||||
cli.log_power, fft_size,
|
log_power, fft_size,
|
||||||
);
|
);
|
||||||
sc = safe_cols as usize;
|
sc = safe_cols as usize;
|
||||||
sr = safe_rows as usize;
|
sr = safe_rows as usize;
|
||||||
@@ -272,69 +356,17 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
all_samples.clear();
|
all_samples.clear();
|
||||||
let mut buffer_is_empty = false;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let read_result = match &mut audio_source {
|
match rx.try_recv() {
|
||||||
AudioSource::StdIn(input) => input.read(&mut input_buffer),
|
Ok(batch) => all_samples.extend_from_slice(&batch),
|
||||||
AudioSource::File(file) => file.read(&mut input_buffer),
|
Err(_) => break,
|
||||||
AudioSource::PipeWire(pipe) => pipe.read(&mut input_buffer),
|
|
||||||
};
|
|
||||||
|
|
||||||
match read_result {
|
|
||||||
Ok(0) => {
|
|
||||||
buffer_is_empty = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(n) => {
|
|
||||||
let mut cursor = std::io::Cursor::new(&input_buffer[..n]);
|
|
||||||
while cursor.position() < (n as u64 - (num_channels * (bit_depth / 8)) as u64 + 1) {
|
|
||||||
let mut frame_sum = 0.0f32;
|
|
||||||
let mut valid_channels = 0u32;
|
|
||||||
for _ in 0..num_channels {
|
|
||||||
match cursor.read_i16::<LittleEndian>() {
|
|
||||||
Ok(sample) => {
|
|
||||||
frame_sum += sample as f32;
|
|
||||||
valid_channels += 1;
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if valid_channels > 0 {
|
|
||||||
all_samples.push((frame_sum / valid_channels as f32) / 32768.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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_empty = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("Error: {}", e);
|
|
||||||
for (i, c) in error_msg.chars().enumerate() {
|
|
||||||
if i < sc {
|
|
||||||
next_buffer[0][i] = Cell { character: c, filled: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
view_diff.clear();
|
|
||||||
for y in 0..sr {
|
|
||||||
for x in 0..sc {
|
|
||||||
if current_buffer[y][x] != next_buffer[y][x] {
|
|
||||||
view_diff.add_change(y, x, next_buffer[y][x].character);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
view_diff.apply();
|
|
||||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
|
||||||
let _ = out.flush();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Latency guard: if the audio thread got ahead, drop old samples
|
||||||
|
if all_samples.len() > fft_size * 2 {
|
||||||
|
let excess = all_samples.len() - fft_size * 2;
|
||||||
|
all_samples.drain(..excess);
|
||||||
|
}
|
||||||
|
|
||||||
if !all_samples.is_empty() {
|
if !all_samples.is_empty() {
|
||||||
if all_samples.len() > fft_size {
|
if all_samples.len() > fft_size {
|
||||||
@@ -364,7 +396,7 @@ fn main() {
|
|||||||
if mapping_idx < fft_size / 2 {
|
if mapping_idx < fft_size / 2 {
|
||||||
let magnitude = complex_buffer[mapping_idx].norm();
|
let magnitude = complex_buffer[mapping_idx].norm();
|
||||||
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);
|
let scaled = (magnitude * 80.0 * freq_boost).sqrt();
|
||||||
freq_bands[i] = if scaled > last_bars[i] { scaled } else { last_bars[i] * drop_factor };
|
freq_bands[i] = if scaled > last_bars[i] { scaled } else { last_bars[i] * drop_factor };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,7 +414,8 @@ fn main() {
|
|||||||
max_magnitude = max_magnitudes.iter().copied().fold(0.0f32, f32::max).max(0.1);
|
max_magnitude = max_magnitudes.iter().copied().fold(0.0f32, f32::max).max(0.1);
|
||||||
|
|
||||||
for (i, &magnitude) in last_bars[..sc].iter().enumerate() {
|
for (i, &magnitude) in last_bars[..sc].iter().enumerate() {
|
||||||
render_bar(&mut next_buffer, i, sr, (magnitude / max_magnitude) * sr as f32);
|
let height = ((magnitude / max_magnitude) * sr as f32 * sensitivity).min(sr as f32);
|
||||||
|
render_bar(&mut next_buffer, i, sr, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
view_diff.clear();
|
view_diff.clear();
|
||||||
@@ -397,10 +430,11 @@ fn main() {
|
|||||||
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();
|
||||||
let _ = out.flush();
|
let _ = out.flush();
|
||||||
} else if buffer_is_empty {
|
} else {
|
||||||
for i in 0..sc {
|
for i in 0..sc {
|
||||||
last_bars[i] *= drop_factor;
|
last_bars[i] *= drop_factor;
|
||||||
render_bar(&mut next_buffer, i, sr, (last_bars[i] / max_magnitude) * sr as f32);
|
let height = ((last_bars[i] / max_magnitude) * sr as f32 * sensitivity).min(sr as f32);
|
||||||
|
render_bar(&mut next_buffer, i, sr, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
view_diff.clear();
|
view_diff.clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user