use clap::Parser;
use desperado::{IqFormat, IqSource};
use num_complex::Complex;
use rustfft::FftPlanner;
use plotters::prelude::*;
#[derive(Parser, Debug)]
#[command(author, version, about = "A waterfall plot generator from IQ files", long_about = None)]
struct Args {
#[arg(short, long)]
bin_file: String,
#[arg(short, long)]
sample_rate: u32,
#[arg(short, long)]
center_freq: u32,
#[arg(short, long, default_value = "cu8")]
iq_format: IqFormat,
#[arg(short, long, default_value_t = 1024)]
fft_size: usize,
}
fn fftshift(data: &mut [Complex<f32>]) {
let n = data.len();
let half = n / 2;
data.rotate_left(half);
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let iq = IqSource::from_file(
args.bin_file,
args.center_freq,
args.sample_rate,
args.fft_size,
args.iq_format,
)?;
let mut planner = FftPlanner::<f32>::new();
let fft = planner.plan_fft_forward(args.fft_size);
let mut waterfall: Vec<Vec<f32>> = Vec::new();
let mut line = vec![0f32; args.fft_size];
for chunk in iq {
let samples = chunk?;
let mut buffer = samples.clone();
fft.process(&mut buffer);
fftshift(&mut buffer);
let scale = 1.0 / args.fft_size as f32;
for v in buffer.iter_mut() {
*v *= scale;
}
for (j, c) in buffer.iter().enumerate() {
line[j] = 10.0 * c.norm_sqr().log10(); }
waterfall.push(line.clone());
}
let max_db = 0.0; let min_db = -100.0;
let root = BitMapBackend::new("waterfall.png", (args.fft_size as u32, 512)).into_drawing_area();
root.fill(&BLACK)?;
let height = waterfall.len();
for (row, spectrum) in waterfall.iter().enumerate() {
let y = height - 1 - row; for (col, &db) in spectrum.iter().enumerate() {
let norm = ((db - min_db) / (max_db - min_db + 1e-6)).clamp(0.0, 1.0);
let color = if norm < 0.25 {
RGBColor(0, (norm * 4.0 * 64.0) as u8, 128)
} else if norm < 0.5 {
RGBColor(
((norm - 0.25) * 4.0 * 255.0) as u8,
255,
(128.0 - (norm - 0.25) * 4.0 * 128.0) as u8,
)
} else if norm < 0.75 {
RGBColor(255, (255.0 - (norm - 0.5) * 4.0 * 128.0) as u8, 0)
} else {
RGBColor(255, 255, ((norm - 0.75) * 4.0 * 255.0) as u8)
};
root.draw_pixel((col as i32, y as i32), &color)?;
}
}
let freq_start = args.center_freq as f32 - (args.sample_rate as f32) / 2.0;
let freq_end = args.center_freq as f32 + (args.sample_rate as f32) / 2.0;
let mut chart = ChartBuilder::on(&root)
.margin(10)
.set_label_area_size(LabelAreaPosition::Bottom, 40)
.build_cartesian_2d(freq_start * 1e-6..freq_end * 1e-6, 0..height)?;
chart
.configure_mesh()
.x_desc("Frequency (MHz)")
.axis_desc_style(("Roboto Condensed", 18).into_font().color(&BLACK))
.label_style(("Roboto Condensaed", 18).into_font().color(&BLACK))
.draw()?;
root.present()?;
println!("Waterfall plot saved to waterfall.png");
Ok(())
}