initial commit

This commit is contained in:
2024-12-30 21:27:37 +01:00
commit 68683498d7
4 changed files with 1718 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/target
Generated
+1448
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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
View File
@@ -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(())
}