diff --git a/.gitignore b/.gitignore index 196e176..c2b1127 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,73 @@ Cargo.lock # Added by cargo /target +# Created by https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer,macos,osx +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,rust-analyzer,macos,osx + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### OSX ### +# General + +# Icon must end with two \r + +# Thumbnails + +# Files that might appear in the root of a volume + +# Directories potentially created on remote AFP share + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### rust-analyzer ### +# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules) +rust-project.json + + +# End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer,macos,osx + diff --git a/Cargo.toml b/Cargo.toml index ac3fa35..73415b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fftviz" -version = "0.1.1" +version = "0.1.2" edition = "2021" authors = ["Gursimar Singh "] license = "MIT" @@ -11,9 +11,6 @@ repository = "https://github.com/gursi26/fftviz" keywords = ["cli"] categories = ["command-line-utilities"] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] bincode = "1.3.3" clap = { version = "4.5.0", features = ["derive"] } diff --git a/README.md b/README.md index da113e8..4f9ac53 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# fft-visualizer -A lightweight FFT visualizer for audio files. +# fftviz +A lightweight FFT visualizer for audio files. Built with Rust + Raylib. + +![demo image](assets/demo.png) diff --git a/assets/demo.png b/assets/demo.png new file mode 100644 index 0000000..e8a6145 Binary files /dev/null and b/assets/demo.png differ diff --git a/src/fft.rs b/src/fft.rs index b173f6a..479ab7a 100644 --- a/src/fft.rs +++ b/src/fft.rs @@ -61,6 +61,7 @@ pub fn normalize_fft(mut fft: FFT, bounds: &[f32], scaling_factor: &[f32]) -> FF fft } +#[allow(dead_code)] pub fn write_fft_to_binary_file(filepath: &PathBuf, fft: &FFT) -> io::Result<()> { let mut file = File::create(filepath)?; let encoded_data = serialize(fft).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; @@ -68,6 +69,7 @@ pub fn write_fft_to_binary_file(filepath: &PathBuf, fft: &FFT) -> io::Result<()> Ok(()) } +#[allow(dead_code)] pub fn read_fft_from_binary_file(filepath: &PathBuf) -> io::Result { let mut file = File::open(filepath)?; let mut buffer = Vec::new(); @@ -141,16 +143,3 @@ pub fn compute_fft(audio_path: &PathBuf) -> FFT { } } -pub fn compute_and_cache_fft(audio_path: &PathBuf) -> FFT { - let file_name = audio_path.file_stem().unwrap().to_str().unwrap(); - let mut cache_path = audio_path.parent().unwrap().to_path_buf(); - cache_path.push(format!(".{}.fft", file_name)); - - if cache_path.is_file() && !FORCE_CACHE_REFRESH { - let fft = read_fft_from_binary_file(&cache_path).unwrap(); - return fft; - } - let fft = compute_fft(audio_path); - write_fft_to_binary_file(&cache_path, &fft).unwrap(); - fft -} diff --git a/src/main.rs b/src/main.rs index 0dddcf2..89c3a0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,8 @@ use std::path::PathBuf; // Constants const RENDERING_FPS: u32 = 60; -const SCREEN_WIDTH: i32 = 1780; -const SCREEN_HEIGHT: i32 = 1000; +const SCREEN_WIDTH: i32 = 1000; +const SCREEN_HEIGHT: i32 = 700; const FREQUENCY_RESOLUTION: u32 = 100; const FFT_FPS: u32 = 12; @@ -27,8 +27,6 @@ const FFT_WINDOW: i32 = const BAR_INTERPOLATION_FACTOR: u32 = 2; const RESCALING_THRESHOLDS: &[f32] = &[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]; const RESCALING_FACTOR: &[f32] = &[2.0, 1.7, 1.3, 1.2, 1.1, 1.0, 0.9, 0.8, 0.7]; -const CACHE_FFT: bool = false; -const FORCE_CACHE_REFRESH: bool = false; #[derive(Debug, Parser)] @@ -42,25 +40,29 @@ struct CLIArgs { #[arg(long = "border-size", default_value_t = 1)] border_size: u32, - /// Border color for each bar + /// Border color for each bar (in hex) #[arg(long = "border-color", default_value_t = String::from("000000"))] border_color: String, - /// Color for each bar + /// Color for each bar (in hex) #[arg(long = "bar-color", default_value_t = String::from("FF0000"))] bar_color: String, - /// Set to false to disable printing currently playing song title - #[arg(long = "print-text", action = ArgAction::SetFalse)] - print_text: bool, + /// Whether to disable printing + #[arg(long = "disable-title", action = ArgAction::SetTrue)] + disable_title: bool, - /// Text of currently playing label + /// Color for currently playing text (in hex) #[arg(long = "text-color", default_value_t = String::from("FFFFFF"))] text_color: String, /// Font size of currently playing label #[arg(long = "font-size", default_value_t = 25)] font_size: u32, + + // Background color (in hex) + #[arg(long = "background-color", default_value_t = String::from("000000"))] + background_color: String, } struct FFTArgs { @@ -68,9 +70,10 @@ struct FFTArgs { border_size: i32, border_color: Color, bar_color: Color, - print_text: bool, + disable_title: bool, text_color: Color, font_size: i32, + background_color: Color } fn main() { @@ -83,14 +86,7 @@ fn main() { cache_path.push(format!(".{}.fft", file_name)); println!("Computing FFT..."); - let mut fft; - if CACHE_FFT { - fft = compute_and_cache_fft(&p); - } else if cache_path.is_file() { - fft = read_fft_from_binary_file(&cache_path).unwrap(); - } else { - fft = compute_fft(&p); - } + let mut fft = compute_fft(&p); fft = normalize_fft(fft, RESCALING_THRESHOLDS, RESCALING_FACTOR); let mut fft_vec = fft.fft; @@ -105,7 +101,7 @@ fn main() { let mut fft = fft_vec.into_iter().peekable(); let mut i = 0; - let (mut rl, thread) = raylib::init().title("fft-visualizer").build(); + let (mut rl, thread) = raylib::init().title("fftviz").build(); rl.set_target_fps(RENDERING_FPS); rl.set_window_size(SCREEN_WIDTH, SCREEN_HEIGHT); @@ -119,7 +115,7 @@ fn main() { while !rl.window_should_close() && fft.peek().is_some() && !rl.is_key_down(KeyboardKey::KEY_Q) { let mut d = rl.begin_drawing(&thread); - d.clear_background(Color::BLACK); + d.clear_background(args.background_color); if i as u32 % num_frame_gen == 0 { fft_chunk = fft.next().unwrap(); @@ -157,7 +153,7 @@ fn main() { ); } - if args.print_text { + if !args.disable_title { d.draw_text( &format!("Playing: {:?}", p.file_stem().unwrap().to_str().unwrap())[..], 10, diff --git a/src/utils.rs b/src/utils.rs index f5ffbe9..dffd218 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,8 +11,9 @@ pub fn cli_args_to_fft_args(cli_args: CLIArgs) -> FFTArgs { border_size: cli_args.border_size as i32, border_color: Color::from_hex(&cli_args.border_color[..]).unwrap(), bar_color: Color::from_hex(&cli_args.bar_color[..]).unwrap(), - print_text: cli_args.print_text, + disable_title: cli_args.disable_title, text_color: Color::from_hex(&cli_args.text_color[..]).unwrap(), font_size: cli_args.font_size as i32, + background_color: Color::from_hex(&cli_args.background_color[..]).unwrap() } }