Compare commits
6 Commits
v0.2.2
...
5201b503e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
5201b503e6
|
|||
|
79544f64c3
|
|||
|
df06d7fa36
|
|||
|
af5a345458
|
|||
|
cf05f6a796
|
|||
|
cce05dbfcc
|
Generated
+1
-1
@@ -429,7 +429,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "spectrust"
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "spectrust"
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Bendik Aagaard Lynghaug
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||
pkgname=spectrust
|
||||
pkgver=0.2.2
|
||||
pkgver=0.2.5
|
||||
pkgrel=1
|
||||
pkgdesc="Terminal audio spectrum analyzer using ncurses"
|
||||
pkgdesc="Terminal audio spectrum analyzer"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://prosjekt.klingenbergbygg.no/bl/spectrust"
|
||||
license=('MIT')
|
||||
depends=('ncurses')
|
||||
depends=()
|
||||
provides=('spectrust')
|
||||
conflicts=('spectrust-git' 'spectrust-bin')
|
||||
source_x86_64=("spectrust-v${pkgver}-x86_64.tar.gz::https://prosjekt.klingenbergbygg.no/bl/spectrust/releases/download/v${pkgver}/spectrust-v${pkgver}-x86_64.tar.gz")
|
||||
|
||||
+54
-73
@@ -13,6 +13,8 @@ use rustfft::{algorithm::Radix4, num_complex::Complex, Fft, FftDirection};
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{stdout, Read, Write};
|
||||
use std::fs::File;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::process::{Command, Stdio};
|
||||
@@ -159,22 +161,15 @@ fn main() {
|
||||
|
||||
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 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();
|
||||
|
||||
enum AudioSource {
|
||||
StdIn(std::io::StdinLock<'static>),
|
||||
File(File),
|
||||
PipeWire(std::process::ChildStdout),
|
||||
}
|
||||
|
||||
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")
|
||||
.args(["--format=s16", "--rate=48000", "--channels=2", "--raw", "-"])
|
||||
.stdout(Stdio::piped())
|
||||
@@ -182,14 +177,51 @@ fn main() {
|
||||
.expect("failed to start pw-record");
|
||||
let pipe = process.stdout.expect("failed to capture pw-record stdout");
|
||||
let _ = out.flush();
|
||||
AudioSource::PipeWire(pipe)
|
||||
Box::new(pipe)
|
||||
} else if let Some(filename) = &cli.input {
|
||||
let file = File::open(filename).expect("failed to open input file");
|
||||
AudioSource::File(file)
|
||||
Box::new(File::open(filename).expect("failed to open input file"))
|
||||
} 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_height: u16 = 1000;
|
||||
|
||||
@@ -226,7 +258,8 @@ fn main() {
|
||||
let mut max_magnitude: f32 = 1.0;
|
||||
|
||||
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() {
|
||||
Ok(Event::Key(KeyEvent { code: KeyCode::Char('q'), .. })) => break,
|
||||
Ok(Event::Key(KeyEvent {
|
||||
@@ -272,68 +305,16 @@ fn main() {
|
||||
}
|
||||
|
||||
all_samples.clear();
|
||||
let mut buffer_is_empty = false;
|
||||
|
||||
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(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;
|
||||
}
|
||||
match rx.try_recv() {
|
||||
Ok(batch) => all_samples.extend_from_slice(&batch),
|
||||
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() {
|
||||
@@ -364,7 +345,7 @@ fn main() {
|
||||
if mapping_idx < fft_size / 2 {
|
||||
let magnitude = complex_buffer[mapping_idx].norm();
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -397,7 +378,7 @@ fn main() {
|
||||
std::mem::swap(&mut current_buffer, &mut next_buffer);
|
||||
last_frame_time = Instant::now();
|
||||
let _ = out.flush();
|
||||
} else if buffer_is_empty {
|
||||
} else {
|
||||
for i in 0..sc {
|
||||
last_bars[i] *= drop_factor;
|
||||
render_bar(&mut next_buffer, i, sr, (last_bars[i] / max_magnitude) * sr as f32);
|
||||
|
||||
Reference in New Issue
Block a user