]> Untitled Git - axy/minisheet.git/commitdiff
Partial implementation that seems to kind work yippee master
authorAxy <gilliardmarthey.axel@gmail.com>
Fri, 5 Jun 2026 22:35:15 +0000 (00:35 +0200)
committerAxy <gilliardmarthey.axel@gmail.com>
Fri, 5 Jun 2026 22:35:15 +0000 (00:35 +0200)
Cargo.lock
Cargo.toml
flake.nix
src/main.rs

index b6cc4fa8c22472d6409eee55c0fa05d1b861f793..d1c3337cfb2452f843cca83c5d71d83dcfcf8058 100644 (file)
@@ -2,6 +2,12 @@
 # It is not intended for manual editing.
 version = 4
 
 # 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"
 [[package]]
 name = "aho-corasick"
 version = "1.1.4"
@@ -338,6 +344,15 @@ dependencies = [
  "libc",
 ]
 
  "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"
 [[package]]
 name = "crossterm"
 version = "0.29.0"
@@ -548,6 +563,15 @@ dependencies = [
  "regex",
 ]
 
  "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"
 [[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"
 
 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"
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -948,11 +982,23 @@ dependencies = [
  "clap",
  "cpal",
  "eyre",
  "clap",
  "cpal",
  "eyre",
+ "png",
  "ratatui",
  "ratatui",
+ "realfft",
  "rustfft",
  "symphonia",
 ]
 
  "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"
 [[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"
 
 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"
 [[package]]
 name = "portable-atomic"
 version = "1.13.1"
@@ -1502,6 +1561,15 @@ dependencies = [
  "unicode-width",
 ]
 
  "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"
 [[package]]
 name = "redox_syscall"
 version = "0.5.18"
@@ -1700,6 +1768,12 @@ dependencies = [
  "libc",
 ]
 
  "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"
 [[package]]
 name = "siphasher"
 version = "1.0.3"
index 5e0ee9d0d5c342de949ad1a5d466efc4a6bf7f46..34b42ad43374c8db996079d60a19e367c5cbb12d 100644 (file)
@@ -3,10 +3,15 @@ name = "minisheet"
 version = "0.1.0"
 edition = "2024"
 
 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"
 [dependencies]
 clap = { version = "4.6.1", features = ["derive"] }
 cpal = "0.17.3"
 eyre = "0.6.12"
+png = "0.18.1"
 ratatui = "0.30.0"
 ratatui = "0.30.0"
+realfft = { version = "3.5.0", features = ["avx"] }
 rustfft = "6.4.1"
 symphonia = { version = "0.6.0", features = ["all"] }
 rustfft = "6.4.1"
 symphonia = { version = "0.6.0", features = ["all"] }
index 400b1057a7eedcd0ccf45fd55490082d83e6c022..d2bc1c52db531f926d13372bdfb428dc1e15b2fe 100644 (file)
--- a/flake.nix
+++ b/flake.nix
@@ -41,7 +41,6 @@
           buildInputs = with pkgs; [
             alsa-lib-with-plugins
           ];
           buildInputs = with pkgs; [
             alsa-lib-with-plugins
           ];
-
         };
 
         cargoArtifacts = craneLib.buildDepsOnly commonArgs;
         };
 
         cargoArtifacts = craneLib.buildDepsOnly commonArgs;
index 6d01505e9a530730a03dbd6688d6e8399e190c9b..3b5827501109dde40a43f9fe9dac73c1e4dd5da7 100644 (file)
@@ -1,12 +1,23 @@
 use std::ffi::OsStr;
 use std::fs::File;
 use std::ffi::OsStr;
 use std::fs::File;
+use std::iter::{repeat, repeat_n};
 use std::path::PathBuf;
 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 clap::Parser;
-use rustfft::FftPlanner;
+use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
+use eyre::{OptionExt, bail};
+use realfft::RealToComplex;
 use rustfft::num_complex::Complex;
 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::probe::Hint;
+use symphonia::core::formats::{FormatOptions, TrackType};
 use symphonia::core::io::MediaSourceStream;
 use symphonia::core::meta::MetadataOptions;
 
 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,
 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<f32>], buf_duration: f32);
-}
+const FFT_BUF_SIZE: usize = 4096;
 
 fn main() -> eyre::Result<()> {
     let args = Args::parse();
 
 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 probe = symphonia::default::get_probe().probe(&hint, mss, fmt_opts, meta_opts)?;
 
-    /*
-    let mut buf: [Complex<f32>; 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::<f32>::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::<f32>::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<f32>)> = 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 &notes_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 = <f32 as IntoSample<i16>>::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<f32>,
+    input: Vec<f32>,
+    fft: Arc<dyn RealToComplex<f32>>,
+    output: Vec<Complex<f32>>,
+    scratch: Vec<Complex<f32>>,
+}
+
+impl FFT {
+    fn all_windows<'t>(
+        &'_ self,
+        slice: &'t [f32],
+        sample_spacing: usize,
+    ) -> impl Iterator<Item = impl Iterator<Item = f32> + '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<Item = f32>,
+        hz: u32,
+    ) -> eyre::Result<impl Fn(f32) -> 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()
+    }
 }
 }