initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+1448
File diff suppressed because it is too large
Load Diff
+16
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "chirptune"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
native-dialog = "0.7.0"
|
||||||
|
rustfft = "6.2.0"
|
||||||
|
|
||||||
|
[dependencies.sdl2]
|
||||||
|
version = "0.37.0"
|
||||||
|
|
||||||
|
[dependencies.rodio]
|
||||||
|
version = "0.20.1"
|
||||||
|
default-features = false
|
||||||
|
features = ["symphonia-all"]
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
extern crate native_dialog;
|
||||||
|
extern crate rodio;
|
||||||
|
extern crate rustfft;
|
||||||
|
extern crate sdl2;
|
||||||
|
|
||||||
|
use native_dialog::FileDialog;
|
||||||
|
use rodio::{Decoder, OutputStream, Sink, Source};
|
||||||
|
use sdl2::event::Event;
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn calculate_spectogram(
|
||||||
|
buffer: &[i16],
|
||||||
|
start_time: Duration,
|
||||||
|
duration: Duration,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
window_size: usize,
|
||||||
|
hop_size: usize,
|
||||||
|
sample_rate: u32,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let start_sample = (start_time.as_secs_f32() * sample_rate as f32) as usize;
|
||||||
|
let num_samples = (duration.as_secs_f32() * sample_rate as f32) as usize;
|
||||||
|
let end_sample = (start_sample + num_samples).min(buffer.len());
|
||||||
|
|
||||||
|
let num_windows = (end_sample - start_sample) / hop_size;
|
||||||
|
let mut pixels = vec![0; (width * height * 4) as usize];
|
||||||
|
|
||||||
|
for window_idx in 0..num_windows {
|
||||||
|
let start = start_sample + (window_idx * hop_size);
|
||||||
|
let end = (start + window_size).min(buffer.len());
|
||||||
|
|
||||||
|
let mut windowed = vec![0.0f32; window_size];
|
||||||
|
for i in 0..end - start {
|
||||||
|
// Hamming window
|
||||||
|
let window_val =
|
||||||
|
0.54 - 0.46 * (2.0 * std::f32::consts::PI * i as f32 / window_size as f32).cos();
|
||||||
|
windowed[i] = window_val * buffer[start + i] as f32 / i16::MAX as f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fft_input = vec![rustfft::num_complex::Complex32::new(0.0, 0.0); window_size];
|
||||||
|
for i in 0..window_size {
|
||||||
|
fft_input[i].re = if i < windowed.len() { windowed[i] } else { 0.0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let fft = rustfft::FftPlanner::new().plan_fft_forward(window_size);
|
||||||
|
fft.process(&mut fft_input);
|
||||||
|
|
||||||
|
let magnitudes: Vec<f32> = fft_input
|
||||||
|
.iter()
|
||||||
|
.take(window_size / 2)
|
||||||
|
.map(|c| (c.norm() + 1e-6).log10() * 20.0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let max_mag = magnitudes.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
|
||||||
|
let min_mag = magnitudes.iter().fold(f32::INFINITY, |a, &b| a.min(b));
|
||||||
|
|
||||||
|
let x = ((window_idx * width as usize) / num_windows) as usize;
|
||||||
|
for (y, mag) in magnitudes.iter().enumerate() {
|
||||||
|
let normalized = ((mag - min_mag) / (max_mag - min_mag) * 255.0) as u8;
|
||||||
|
let y_coord = ((y * height as usize) / (window_size / 2)) as usize;
|
||||||
|
|
||||||
|
let offset = (y_coord * width as usize + x) * 4;
|
||||||
|
if offset + 3 < pixels.len() {
|
||||||
|
pixels[offset] = normalized;
|
||||||
|
pixels[offset + 1] = normalized;
|
||||||
|
pixels[offset + 2] = normalized;
|
||||||
|
pixels[offset + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
const ALLOWED_EXTENSIONS: [&str; 5] = ["mp3", "wav", "ogg", "flac", "opus"];
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let filename = match args.get(1) {
|
||||||
|
Some(f) => {
|
||||||
|
if !ALLOWED_EXTENSIONS
|
||||||
|
.iter()
|
||||||
|
.any(|ext| f.to_lowercase().ends_with(&format!(".{}", ext)))
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
"Please select an audio file (.{})",
|
||||||
|
ALLOWED_EXTENSIONS.join(", .")
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
f.to_string()
|
||||||
|
}
|
||||||
|
None => FileDialog::new()
|
||||||
|
.add_filter("Audio Files", &ALLOWED_EXTENSIONS)
|
||||||
|
.show_open_single_file()
|
||||||
|
.unwrap()
|
||||||
|
.map(|path| path.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
println!("No audio file selected");
|
||||||
|
std::process::exit(1);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||||
|
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||||
|
|
||||||
|
let file = BufReader::new(File::open(&filename).unwrap());
|
||||||
|
let source = Decoder::new(file).unwrap();
|
||||||
|
let sample_rate = source.sample_rate();
|
||||||
|
let buffer: Vec<i16> = source.collect();
|
||||||
|
|
||||||
|
let sdl_context = sdl2::init().unwrap();
|
||||||
|
let video_subsystem = sdl_context.video().unwrap();
|
||||||
|
|
||||||
|
let window = video_subsystem
|
||||||
|
.window("Chirptune", 1024, 1024)
|
||||||
|
.position_centered()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let mut canvas = window.into_canvas().build().unwrap();
|
||||||
|
let texture_creator = canvas.texture_creator();
|
||||||
|
let mut spectogram = texture_creator
|
||||||
|
.create_texture_streaming(None, 1024, 1024)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let window_size = 1024;
|
||||||
|
let hop_size = 128;
|
||||||
|
|
||||||
|
let mut window_start = Duration::from_secs_f32(0.0);
|
||||||
|
let initial_duration = buffer.len() as f32 / sample_rate as f32;
|
||||||
|
let mut window_duration = Duration::from_secs_f32(3.0f32.min(initial_duration));
|
||||||
|
|
||||||
|
let mut pixels = calculate_spectogram(
|
||||||
|
&buffer,
|
||||||
|
window_start,
|
||||||
|
window_duration,
|
||||||
|
1024,
|
||||||
|
1024,
|
||||||
|
window_size,
|
||||||
|
hop_size,
|
||||||
|
sample_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
spectogram.update(None, &pixels, 1024 * 4).unwrap();
|
||||||
|
|
||||||
|
let file = BufReader::new(File::open(&filename).unwrap());
|
||||||
|
let source = Decoder::new_looped(file).unwrap();
|
||||||
|
let duration = buffer.len() as f32 / source.sample_rate() as f32 / source.channels() as f32;
|
||||||
|
sink.append(source);
|
||||||
|
sink.pause();
|
||||||
|
|
||||||
|
let mut event_pump = sdl_context.event_pump().unwrap();
|
||||||
|
'running: loop {
|
||||||
|
canvas.clear();
|
||||||
|
canvas.copy(&spectogram, None, None).unwrap();
|
||||||
|
|
||||||
|
let position = sink.get_pos().as_secs_f32();
|
||||||
|
let window_position =
|
||||||
|
(position - window_start.as_secs_f32()) / window_duration.as_secs_f32();
|
||||||
|
let x_pos = (window_position * 1024.0) as i32;
|
||||||
|
|
||||||
|
if position > window_start.as_secs_f32() + window_duration.as_secs_f32() {
|
||||||
|
sink.try_seek(window_start).unwrap();
|
||||||
|
sink.pause();
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Position: {:.2}s, Window Start: {:.2}s, Window Duration: {:.2}s, X: {}",
|
||||||
|
position,
|
||||||
|
window_start.as_secs_f32(),
|
||||||
|
window_duration.as_secs_f32(),
|
||||||
|
x_pos
|
||||||
|
);
|
||||||
|
canvas.set_draw_color(Color::RGB(255, 0, 0));
|
||||||
|
canvas.draw_line((x_pos, 0), (x_pos, 1024)).unwrap();
|
||||||
|
|
||||||
|
for event in event_pump.poll_iter() {
|
||||||
|
match event {
|
||||||
|
Event::Quit { .. }
|
||||||
|
| Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Escape),
|
||||||
|
..
|
||||||
|
} => break 'running,
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Space),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if sink.is_paused() {
|
||||||
|
sink.play();
|
||||||
|
} else {
|
||||||
|
sink.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(key), ..
|
||||||
|
} => {
|
||||||
|
sink.pause();
|
||||||
|
match key {
|
||||||
|
Keycode::Left | Keycode::A => {
|
||||||
|
window_start = Duration::from_secs_f32(
|
||||||
|
(window_start.as_secs_f32() - 0.5).max(0.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Keycode::Right | Keycode::D => {
|
||||||
|
window_start = Duration::from_secs_f32(
|
||||||
|
(window_start.as_secs_f32() + 0.5)
|
||||||
|
.min(duration - window_duration.as_secs_f32()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Keycode::Up | Keycode::W => {
|
||||||
|
window_duration = Duration::from_secs_f32(
|
||||||
|
(window_duration.as_secs_f32() * 0.8).max(1.0),
|
||||||
|
);
|
||||||
|
if window_start.as_secs_f32() + window_duration.as_secs_f32() > duration
|
||||||
|
{
|
||||||
|
window_start = Duration::from_secs_f32(
|
||||||
|
duration - window_duration.as_secs_f32(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Keycode::Down | Keycode::S => {
|
||||||
|
let new_duration = (window_duration.as_secs_f32() * 1.25)
|
||||||
|
.min(10.0)
|
||||||
|
.min(duration - window_start.as_secs_f32());
|
||||||
|
window_duration = Duration::from_secs_f32(new_duration);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
sink.try_seek(window_start).unwrap();
|
||||||
|
pixels = calculate_spectogram(
|
||||||
|
&buffer,
|
||||||
|
window_start,
|
||||||
|
window_duration,
|
||||||
|
1024,
|
||||||
|
1024,
|
||||||
|
window_size,
|
||||||
|
hop_size,
|
||||||
|
sample_rate,
|
||||||
|
);
|
||||||
|
spectogram.update(None, &pixels, 1024 * 4).unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.present();
|
||||||
|
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user