Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite renderer in bevy #1

Merged
merged 2 commits into from Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fftviz"
version = "0.1.3"
version = "0.1.5"
edition = "2021"
authors = ["Gursimar Singh <gursi26.dev@gmail.com>"]
license = "MIT"
Expand All @@ -12,9 +12,9 @@ keywords = ["cli"]
categories = ["command-line-utilities"]

[dependencies]
bevy = "0.13.0"
bincode = "1.3.3"
clap = { version = "4.5.0", features = ["derive"] }
raylib = "3.7.0"
rayon = "1.9.0"
rodio = "0.17.3"
serde = { version = "1.0.197", features = ["derive"] }
Expand Down
Binary file removed assets/demo.png
Binary file not shown.
Binary file added assets/fonts/Roboto-Regular.ttf
Binary file not shown.
4 changes: 2 additions & 2 deletions src/fft.rs
Expand Up @@ -21,10 +21,10 @@ pub struct FFT {
}


pub fn time_interpolate(v1: &Vec<f32>, v2: &Vec<f32>) -> Vec<f32> {
pub fn time_interpolate(v1: &Vec<f32>, v2: &Vec<f32>, alpha: f32) -> Vec<f32> {
v1.iter()
.zip(v2.iter())
.map(|(x, y)| (x.clone() + y.clone()) / 2.0)
.map(|(x, y)| x.clone() * (1.0 - alpha) + y.clone() * alpha)
.collect::<Vec<f32>>()
}

Expand Down
269 changes: 199 additions & 70 deletions src/main.rs
@@ -1,16 +1,23 @@
mod utils;
mod fft;
mod utils;

use utils::*;
use fft::*;
use utils::*;

use bevy::{
prelude::*,
sprite::{MaterialMesh2dBundle, Mesh2dHandle},
};
use clap::{ArgAction, Parser};
use raylib::prelude::*;
use rodio::{source::Source, Decoder, OutputStream};
use std::ffi::OsString;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::time::Duration;
use std::ffi::OsString;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use bevy::render::mesh::VertexAttributeValues;
use bevy::sprite::Anchor;


// Constants
Expand All @@ -24,7 +31,7 @@ const FREQ_WINDOW_LOW: f32 = 0.0;
const FREQ_WINDOW_HIGH: f32 = 5000.0;
const FFT_WINDOW: i32 =
((256 as u64 / 107 as u64) * FREQUENCY_RESOLUTION as u64).next_power_of_two() as i32;
const BAR_INTERPOLATION_FACTOR: u32 = 2;
const BAR_INTERPOLATION_FACTOR: u32 = 1;
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];

Expand Down Expand Up @@ -65,15 +72,159 @@ struct CLIArgs {
background_color: String,
}

#[derive(Resource)]
struct FFTArgs {
file_path: String,
file_path: PathBuf,
border_size: i32,
border_color: Color,
bar_color: Color,
border_color: String,
bar_color: String,
disable_title: bool,
text_color: Color,
text_color: String,
font_size: i32,
background_color: Color
background_color: String,
}

#[derive(Resource)]
struct FFTQueue {
fft: Vec<Vec<f32>>,
curr_bars: Vec<Handle<Mesh>>,
c: usize,
i: usize,
}

fn update_bars(
mut window: Query<&mut Window>,
mut meshes: ResMut<Assets<Mesh>>,
mut fft_queue: ResMut<FFTQueue>,
args: Res<FFTArgs>,
) {
let h = window.single_mut().physical_height();
let mut update_i = false;
let interval = RENDERING_FPS / FFT_FPS;

let curr_fft = match fft_queue.c as u32 % interval {
0 => {
if fft_queue.i > fft_queue.fft.len() {
std::process::exit(0);
}
update_i = true;
fft_queue.fft[fft_queue.i].clone()
}
rem => time_interpolate(
&(fft_queue.fft[fft_queue.i - 1]),
&(fft_queue.fft[fft_queue.i]),
rem as f32 / interval as f32,
),
};

for (handle, new_value) in fft_queue.curr_bars.chunks(2).zip(curr_fft.iter()) {
let (handle1, handle2) = (handle[0].clone(), handle[1].clone());

let rect = meshes.get_mut(handle1).unwrap();
let dims = rect.attribute_mut(Mesh::ATTRIBUTE_POSITION).unwrap();
match dims {
VertexAttributeValues::Float32x3(x) => {
x[0][1] = new_value.clone() * (h / 2) as f32;
x[1][1] = new_value.clone() * (h / 2) as f32;
x[2][1] = -new_value.clone() * (h / 2) as f32;
x[3][1] = -new_value.clone() * (h / 2) as f32;
}
_ => {}
}

let rect = meshes.get_mut(handle2).unwrap();
let dims = rect.attribute_mut(Mesh::ATTRIBUTE_POSITION).unwrap();
match dims {
VertexAttributeValues::Float32x3(x) => {
x[0][1] = new_value.clone() * (h / 2) as f32 - args.border_size as f32;
x[1][1] = new_value.clone() * (h / 2) as f32 - args.border_size as f32;
x[2][1] = -new_value.clone() * (h / 2) as f32 + args.border_size as f32;
x[3][1] = -new_value.clone() * (h / 2) as f32 + args.border_size as f32;
}
_ => {}
}
}

if update_i {
fft_queue.i += 1;
}
fft_queue.c += 1;
}

fn startup(
mut commands: Commands,
mut window: Query<&mut Window>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut fft_queue: ResMut<FFTQueue>,
args: Res<FFTArgs>,
asset_server: Res<AssetServer>,
) {
commands.spawn(Camera2dBundle {
camera: Camera {
clear_color: ClearColorConfig::Custom(
Color::hex(args.background_color.clone()).unwrap(),
),
..Default::default()
},
..Default::default()
});

let w = window.single_mut().physical_width();
let h = window.single_mut().physical_height();

if !args.disable_title {
let font = asset_server.load("fonts/Roboto-Regular.ttf");
let text_style = TextStyle {
font: font.clone(),
font_size: args.font_size as f32,
color: Color::hex(args.text_color.clone()).unwrap(),
};

commands.spawn(
Text2dBundle {
text: Text::from_section(format!("Playing: \"{}\"", args.file_path.file_name().unwrap().to_str().unwrap()), text_style.clone()),
transform: Transform::from_xyz(-(w as f32 / 2.0) + 10.0, (h as f32 / 2.0) - 10.0, 0.0),
text_anchor: Anchor::TopLeft,
..default()
},
);
};

let num_bars = fft_queue.fft[0].len();
let bar_size = w as f32 / num_bars as f32;
let mut handle_vec = Vec::new();

for i in 0..num_bars {
let handle1 = meshes.add(Rectangle::new(bar_size, 0.0));
handle_vec.push(handle1.clone());

commands.spawn(MaterialMesh2dBundle {
mesh: Mesh2dHandle(handle1),
material: materials.add(Color::hex(args.border_color.clone()).unwrap()),
transform: Transform::from_xyz(
bar_size * i as f32 + (bar_size / 2.0) - (w / 2) as f32,
0.0,
0.0,
),
..default()
});

let handle2 = meshes.add(Rectangle::new(bar_size - args.border_size as f32, 0.0));
handle_vec.push(handle2.clone());

commands.spawn(MaterialMesh2dBundle {
mesh: Mesh2dHandle(handle2),
material: materials.add(Color::hex(args.bar_color.clone()).unwrap()),
transform: Transform::from_xyz(
bar_size * i as f32 + (bar_size / 2.0) - (w / 2) as f32,
0.0,
0.0,
),
..default()
});
}
fft_queue.curr_bars = handle_vec;
}

fn main() {
Expand All @@ -98,70 +249,48 @@ fn main() {
*c = reversed;
}

let mut fft = fft_vec.into_iter().peekable();
let mut i = 0;

let (mut rl, thread) = raylib::init().title("fftviz").build();
rl.set_target_fps(RENDERING_FPS);
rl.set_window_size(SCREEN_WIDTH, SCREEN_HEIGHT);
fft_vec
.par_iter_mut()
.for_each(|x| space_interpolate(x, BAR_INTERPOLATION_FACTOR));

let mut binding = App::new();
let app = binding
.add_plugins((DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "fftviz".into(),
name: Some("fftviz".into()),
resolution: (SCREEN_WIDTH as f32, SCREEN_HEIGHT as f32).into(),
resizable: false,
position: WindowPosition::Centered(MonitorSelection::Current),
prevent_default_event_handling: false,
enabled_buttons: bevy::window::EnabledButtons {
maximize: false,
..Default::default()
},
visible: true,
..default()
}),
..default()
}),))
.insert_resource(FFTQueue {
fft: fft_vec,
curr_bars: Vec::new(),
i: 0,
c: 0,
})
.insert_resource(args)
.add_systems(Startup, startup)
.add_systems(
Update,
(update_bars.run_if(bevy::time::common_conditions::on_timer(
Duration::from_secs_f64(1.0 / RENDERING_FPS as f64),
)),),
);

let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let file = BufReader::new(File::open(&p).unwrap());
let source = Decoder::new(file).unwrap();
stream_handle.play_raw(source.convert_samples()).unwrap();

let num_frame_gen = RENDERING_FPS / FFT_FPS;
let mut fft_chunk: Vec<f32> = Vec::new();

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(args.background_color);

if i as u32 % num_frame_gen == 0 {
fft_chunk = fft.next().unwrap();
} else {
let next_chunk = fft.peek().unwrap();
fft_chunk = time_interpolate(&fft_chunk, next_chunk);
}

let mut new_chunk = fft_chunk.clone();
space_interpolate(&mut new_chunk, BAR_INTERPOLATION_FACTOR);

let (h, w) = (d.get_screen_height(), d.get_screen_width());
let bar_start_idxs = (0..((w as i32) + 1))
.step_by(w as usize / new_chunk.len() as usize)
.collect::<Vec<i32>>();

for j in 0..(bar_start_idxs.len() - 1 as usize) {
let curr_fft_value =
(new_chunk[j.clamp(0, new_chunk.len() - 1)] * h as f32 * 0.5) as i32;
let (start_x_id, end_x_id) = (bar_start_idxs[j], bar_start_idxs[j + 1]);
d.draw_rectangle(
start_x_id,
(h / 2) - curr_fft_value,
end_x_id - start_x_id,
curr_fft_value * 2,
args.border_color,
);

d.draw_rectangle(
start_x_id + args.border_size,
(h / 2) - curr_fft_value + args.border_size,
end_x_id - start_x_id - (args.border_size * 2),
curr_fft_value * 2 - (args.border_size * 2),
args.bar_color,
);
}

if !args.disable_title {
d.draw_text(
&format!("Playing: {:?}", p.file_stem().unwrap().to_str().unwrap())[..],
10,
10,
args.font_size,
args.text_color,
);
}
i += 1;
}
app.run();
}
10 changes: 5 additions & 5 deletions src/utils.rs
Expand Up @@ -7,13 +7,13 @@ pub fn cli_args_to_fft_args(cli_args: CLIArgs) -> FFTArgs {
std::process::exit(1);
}
FFTArgs {
file_path: cli_args.file_path,
file_path: Path::new(&cli_args.file_path).to_path_buf(),
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(),
border_color: cli_args.border_color,
bar_color: cli_args.bar_color,
disable_title: cli_args.disable_title,
text_color: Color::from_hex(&cli_args.text_color[..]).unwrap(),
text_color: cli_args.text_color,
font_size: cli_args.font_size as i32,
background_color: Color::from_hex(&cli_args.background_color[..]).unwrap()
background_color: cli_args.background_color
}
}