From: Axy Date: Fri, 5 Jun 2026 22:35:15 +0000 (+0200) Subject: Partial implementation that seems to kind work yippee X-Git-Url: https://git.uwuaxy.net/sitemap.xml?a=commitdiff_plain;ds=inline;p=axy%2Fminisheet.git Partial implementation that seems to kind work yippee --- diff --git a/Cargo.lock b/Cargo.lock index b6cc4fa..d1c3337 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -338,6 +344,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -548,6 +563,15 @@ dependencies = [ "regex", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "filedescriptor" version = "0.8.3" @@ -571,6 +595,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -948,11 +982,23 @@ dependencies = [ "clap", "cpal", "eyre", + "png", "ratatui", + "realfft", "rustfft", "symphonia", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -1332,6 +1378,19 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.12.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1502,6 +1561,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "realfft" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1700,6 +1768,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "siphasher" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index 5e0ee9d..34b42ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,15 @@ name = "minisheet" version = "0.1.0" edition = "2024" +[profile.dev] +opt-level=3 + [dependencies] clap = { version = "4.6.1", features = ["derive"] } cpal = "0.17.3" eyre = "0.6.12" +png = "0.18.1" ratatui = "0.30.0" +realfft = { version = "3.5.0", features = ["avx"] } rustfft = "6.4.1" symphonia = { version = "0.6.0", features = ["all"] } diff --git a/flake.nix b/flake.nix index 400b105..d2bc1c5 100644 --- a/flake.nix +++ b/flake.nix @@ -41,7 +41,6 @@ buildInputs = with pkgs; [ alsa-lib-with-plugins ]; - }; cargoArtifacts = craneLib.buildDepsOnly commonArgs; diff --git a/src/main.rs b/src/main.rs index 6d01505..3b58275 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,23 @@ use std::ffi::OsStr; use std::fs::File; +use std::iter::{repeat, repeat_n}; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread::sleep; +use std::time::Duration; use clap::Parser; -use rustfft::FftPlanner; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use eyre::{OptionExt, bail}; +use realfft::RealToComplex; use rustfft::num_complex::Complex; -use symphonia::core::formats::FormatOptions; +use symphonia::core::audio::conv::IntoSample; +use symphonia::core::audio::{Audio, AudioBuffer}; +use symphonia::core::codecs::CodecParameters; +use symphonia::core::codecs::audio::AudioDecoderOptions; use symphonia::core::formats::probe::Hint; +use symphonia::core::formats::{FormatOptions, TrackType}; use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; @@ -15,14 +26,16 @@ use symphonia::core::meta::MetadataOptions; struct Args { #[arg(help = "The file you want to make a sheetmusic of")] file: PathBuf, + #[arg( + short, + long, + help = "Only read the file audio", + default_value_t = false + )] + file_audio_only: bool, } -const FFT_BUF_SIZE: usize = 16; - -trait AudioTransformer { - const MINIMUM_BUF_DURATION: f32; - fn apply(&mut self, buf: &mut [Complex], buf_duration: f32); -} +const FFT_BUF_SIZE: usize = 4096; fn main() -> eyre::Result<()> { let args = Args::parse(); @@ -40,20 +53,183 @@ fn main() -> eyre::Result<()> { let mut probe = symphonia::default::get_probe().probe(&hint, mss, fmt_opts, meta_opts)?; - /* - let mut buf: [Complex; FFT_BUF_SIZE] = std::array::from_fn(|i| { - if i % 2 == 0 { - Complex { re: 1.0, im: 0.0 } - } else { - Complex { re: 0.0, im: 0.0 } + let track = probe + .default_track(TrackType::Audio) + .ok_or_eyre("No track found in file")?; + + let dec_opts = AudioDecoderOptions::default(); + + let CodecParameters::Audio(codec_params) = track + .codec_params + .as_ref() + .ok_or_eyre("No codec params for track")? + else { + bail!("Codec params for track aren't audio"); + }; + + let mut decoder = + symphonia::default::get_codecs().make_audio_decoder(&codec_params, &dec_opts)?; + + let track_id = track.id; + + let mut sample_rate = None; + + let mut samples = Vec::::new(); + let mut f32_buf = None; + + while let Some(packet) = probe.next_packet()? { + if packet.track_id != track_id { + continue; + } + + let decoded = decoder.decode(&packet)?; + + sample_rate = Some(decoded.spec().rate()); + let f32_buf = + f32_buf.get_or_insert_with(|| AudioBuffer::::new(decoded.spec().clone(), 0)); + f32_buf.grow_capacity(decoded.samples_planar()); + f32_buf.clear(); + f32_buf.resize_uninit(decoded.samples_planar()); + decoded.copy_to(f32_buf); + let start = samples.len(); + samples.extend((0..decoded.samples_planar()).map(|_| 0.0)); + samples[start..].clone_from_slice(f32_buf.plane(0).ok_or_eyre("No audio plane in file")?); + } + + let rate = sample_rate.ok_or_eyre("Empty audio file")?; + + let host = cpal::default_host(); + let device = host + .default_input_device() + .ok_or_eyre("No output device found")?; + let config = device + .supported_output_configs()? + .next() + .ok_or_eyre("No supported output config")?; + let supported_config = config + .try_with_sample_rate(rate) + .ok_or_eyre("Invalid supported sample rates")?; + + let mut notes_map: Vec<(f32, Vec)> = Vec::new(); + // We start at A1 + let mut curr_note = 440.0 / 8.0; + let mul = 2.0f32.powf(1.0 / 12.0); + for _ in 0..76 { + notes_map.push((curr_note, Vec::new())); + curr_note *= mul; + } + + let fft_ratio = 512; + let mut fft = FFT::new(FFT_BUF_SIZE); + for window in fft.all_windows(&samples, FFT_BUF_SIZE / fft_ratio) { + let sample = fft.process(window, rate)?; + for (frequency, buf) in &mut notes_map { + buf.push(sample(*frequency)) } - }); - dbg!(buf); - let mut fft_planner = FftPlanner::new(); - let fft = fft_planner.plan_fft_forward(FFT_BUF_SIZE); - fft.process(&mut buf); - dbg!(buf); - */ - - Ok(()) + } + + if !args.file_audio_only { + for dst in samples.iter_mut() { + *dst = 0.0; + } + for (frequency, dat) in ¬es_map { + let mul = frequency / (rate as f32) * 2.0 * std::f32::consts::PI; + for (i, dst) in samples.iter_mut().enumerate() { + let intensity = dat[i * fft_ratio / FFT_BUF_SIZE]; + let pos = (i as f32) * mul; + *dst += pos.sin() * intensity / 16.0; + } + } + } + + let state = Arc::new(SharedState::default()); + let state2 = Arc::clone(&state); + let stream = device.build_output_stream( + &supported_config.into(), + move |data: &mut [i16], _: &cpal::OutputCallbackInfo| { + let state = &state2; + for (dst, src) in data.iter_mut().zip( + samples + .get(state.timestamp.load(Ordering::Relaxed)..) + .unwrap_or(&[]) + .iter() + .chain(repeat(&0.0)), + ) { + *dst = >::into_sample(*src); + } + state.timestamp.fetch_add(data.len(), Ordering::Relaxed); + }, + move |_err| {}, + None, + )?; + stream.play()?; + loop { + sleep(Duration::from_secs(1000)); + } +} + +#[derive(Default)] +struct SharedState { + timestamp: AtomicUsize, +} + +struct FFT { + window: Vec, + input: Vec, + fft: Arc>, + output: Vec>, + scratch: Vec>, +} + +impl FFT { + fn all_windows<'t>( + &'_ self, + slice: &'t [f32], + sample_spacing: usize, + ) -> impl Iterator + 't> + 't { + let len = self.len(); + (0..(slice.len())) + .filter(move |i| i % sample_spacing == 0) + .map(move |window| { + (window..(window + len)).map(move |i: usize| { + i.checked_sub(len) + .and_then(|i| slice.get(i).copied()) + .unwrap_or(0.0) + }) + }) + } + fn new(len: usize) -> Self { + let fft = realfft::RealFftPlanner::new().plan_fft_forward(len); + Self { + window: (0..len) + .map(|i| { + let pos = (i as f32) / (len - 1) as f32; + 0.5 * (1.0 - f32::cos(std::f32::consts::PI * 2.0 * pos)) + }) + .collect(), + input: fft.make_input_vec(), + output: fft.make_output_vec(), + scratch: fft.make_scratch_vec(), + fft, + } + } + fn process<'t>( + &'t mut self, + samples: impl IntoIterator, + hz: u32, + ) -> eyre::Result f32 + 't> { + self.input.clear(); + self.input + .extend(samples.into_iter().zip(&self.window).map(|(a, b)| a * *b)); + self.fft + .process_with_scratch(&mut self.input, &mut self.output, &mut self.scratch)?; + let mul = 1.0 / (self.len() as f32).sqrt(); + let self2 = &*self; + let discrete_query = move |i: usize| (self2.output[i] * mul).norm(); + let hz_to_idx = move |query_hz: f32| (self2.len() as f32 * query_hz / (hz as f32)) as usize; + Ok(move |query_hz| discrete_query(hz_to_idx(query_hz))) + } + fn len(&self) -> usize { + self.window.len() + } }