summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorWavy Harp <wavyharp@gmail.com>2023-05-07 23:04:53 -0600
committerKyle McFarland <tfkyle@gmail.com>2023-05-07 23:04:53 -0600
commit991849b32acf83dd14a5096540bb053d2572502a (patch)
tree279b59d75d4ad6081f5242cf77d843ae6b37fc3d /src
downloadrustynotes-991849b32acf83dd14a5096540bb053d2572502a.zip
rustynotes-991849b32acf83dd14a5096540bb053d2572502a.tar.gz
rustynotes-991849b32acf83dd14a5096540bb053d2572502a.tar.bz2
initial importHEADmaster
currently everything is very tied to alsa and my system, for the moment you'll need to manually change the device names and maybe channels/period_size in src/main.rs, src/bin/smolguitar.rs and src/bin/loopbacker.rs, i'll fix that and add runtime period size/updater allocation soon, (and probably make a cpal backend as well so it can work on other platforms), but doing this initial commit to play around with stereo for now~
Diffstat (limited to 'src')
-rw-r--r--src/args.rs140
-rw-r--r--src/bin/loopbacker.rs48
-rw-r--r--src/bin/smolguitar.rs44
-rw-r--r--src/bin/test_serde.rs22
-rw-r--r--src/bin/testrfft.rs20
-rw-r--r--src/buf.rs244
-rw-r--r--src/lib.rs6
-rw-r--r--src/main.rs66
-rw-r--r--src/notes.rs168
-rw-r--r--src/outputers.rs266
-rw-r--r--src/proc.rs162
-rw-r--r--src/rfft.rs179
12 files changed, 1365 insertions, 0 deletions
diff --git a/src/args.rs b/src/args.rs
new file mode 100644
index 0000000..fe4737e
--- /dev/null
+++ b/src/args.rs
@@ -0,0 +1,140 @@
+use clap::{Parser, ValueEnum, Args};
+use crate::outputers::{ColourScheme, Outputers, SimpleOutputer, LineLayout, TuiOutputer};
+use derive_more::Display;
+use crate::notes::Note;
+use std::path::{PathBuf, Path};
+use std::error::Error;
+use std::fs::File;
+use std::io::BufReader;
+
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Display)]
+pub enum OutputerTypes {
+ #[display(fmt="simple")]
+ Simple,
+ #[display(fmt="line-layout")]
+ LineLayout,
+ #[display(fmt="tui")]
+ Tui,
+}
+
+// TODO: it might make sense to add a step and quanitize values (they're quanitized to 1/i16::MAX
+// anyway because it's 16bit signed ints from alsa), but i think just scaling by
+// max_threshold will work for now
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+pub struct SimpleArgs {
+ /// outputter to use
+ #[arg(short, long, default_value_t = OutputerTypes::Simple)]
+ pub outputer: OutputerTypes,
+
+ /// threshold for displaying the value, the values have a range of
+ /// 0 - 1 for the most part, a square wave at the max volume
+ /// of a single note will be ~1 (pure sine will be ~sqrt(0.5) or ~0.707ish)
+ /// but in practice most sounds won't get close to that, normally being in
+ /// the range of 0.005-0.10 will give you something usable without being too noisy,
+ /// highly depending on your input though
+ #[arg(short, long, default_value_t = 0.00526532149076897)]
+ pub threshold: f32,
+
+ /// max (saturating) threshold, for colour output in LineLayout and maybe with
+ /// certain other outputers this is used as a value for which any values higher
+ /// then this is considered a max value and displayed at full brightness.
+ ///
+ /// For LineLayout this contols the brightness of the colour output,
+ /// anything >= max_threshold will display at full brightness and anything below
+ /// will be scaled linearly, like brightness = min(1, note_val / max_threshold) and of course
+ /// r = r_max * brightness, g = g_max * brightness, b = b_max * brightness.
+ ///
+ /// it's recommended to set this somewhat close to what you
+ /// expect your highest value is going to be (you can play with that by experimenting
+ /// with the Simple output, in Simple output each value is scaled by 10,000 so 2000 for
+ /// example would be 0.2)
+ ///
+ /// because the max sine wave volume is sqrt(0.5) for now this defaults to sqrt(0.5), but
+ /// it's recommended to set it lower especially if the brightness of the bars seems too low for
+ /// your input.
+ ///
+ /// 0.2 - 0.3 seems like an actual good value in practice, but it's worth playing with
+ #[arg(short, long, default_value_t = 0.7071067811865476)]
+ pub max_threshold: f32,
+
+ /// don't use colour to output for outputers that support it
+ /// (currently LineLayout)
+ #[arg(long, default_value_t = false)]
+ pub plain: bool,
+
+ /*/// max brightness colour for LineLayout and maybe other outputers,
+ /// this is the r/g/b colour at >= max_threshold, values
+ /// threshold > val > max_threshold will scale linearly from
+ /// brightness = threshold to brightness = 1, currently
+ /// threshold just determines if the value is displayed and not
+ /// its colour, that's purely from the max threshold
+ /// (though that could be something to change, make the calculation something like
+ /// diff = note_val - threshold
+ /// max_diff = max_threshold - threshold
+ /// brightness = diff / max_diff)
+ ///
+ /// These should have a range of (0.0 - 255.0) because each value gets converted
+ /// to a u8 to determine the final rgb colour
+ #[arg(long, default_value_t = String::from("78ffdc"))]
+ pub colour: String,*/
+
+
+ /// File that contains the colourscheme as JSON, see the colourschemes/ directory for examples
+ /// we'll use adam neely's as the default for the moment, but this should be changed
+ /// TODO
+ /// NOTE (The default here is set at compile time, so you have to recompile if you change the
+ /// dir you're in)
+ #[arg(short, long)]
+ colours: Option<PathBuf>,
+
+ /// Output device
+ #[arg(long)]
+ pub outdev: Option<Option<String>>
+}
+
+impl SimpleArgs {
+ pub fn get_outputer(&self, notes: &[Note]) -> Outputers {
+ match self.outputer {
+ OutputerTypes::Simple => Outputers::Simple(SimpleOutputer),
+ // TODO: change this unwrap to a ? and a Result return value
+ OutputerTypes::LineLayout => Outputers::LineLayout(LineLayout::new(self.max_threshold, !self.plain, self.get_colours().unwrap(), &notes)),
+ OutputerTypes::Tui => Outputers::Tui(TuiOutputer::new().unwrap()),
+ }
+ }
+
+ pub fn get_outdev(&self) -> Option<String> {
+ match &self.outdev {
+ Some(outdev) => outdev.clone(),
+ None => Some(String::from("default")),
+ }
+ }
+
+ pub fn get_colours(&self) -> Result<ColourScheme, Box<dyn Error>> {
+ match (self.plain, &self.colours) {
+ (true, _) => Ok(ColourScheme::default()),
+ (false, None) => Ok(ColourScheme::default()),
+ (false, Some(cf)) => {
+ // first check if the file exists either as an absolute path or in the current
+ // directory, if it does try to load it
+ let mut fres = File::open(&cf);
+ // otherwise try to load the colourscheme from colourschemes/ in the project directory
+ if fres.is_err() {
+ //let pth: PathBuf = [env!("CARGO_MANIFEST_DIR"), "colourschemes", cf.as_path()].iter().collect();
+ let mut pth: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ pth.push("colourschemes");
+ pth.push(&cf);
+ fres = File::open(pth);
+ }
+ let file = fres?;
+ let reader = BufReader::new(file);
+ let colours: ColourScheme = serde_json::from_reader(reader)?;
+ // if either loaded successfully, make sure all of the colours are Rgb for later
+ Ok(colours)
+ }
+ }
+ }
+
+}
+
diff --git a/src/bin/loopbacker.rs b/src/bin/loopbacker.rs
new file mode 100644
index 0000000..ec3d136
--- /dev/null
+++ b/src/bin/loopbacker.rs
@@ -0,0 +1,48 @@
+use rustynotes::notes::{Notes, note_range, ProcerNotes};
+use rustynotes::proc::{Procer, ProcerData};
+use rustynotes::rfft::RFftProcer;
+use rustynotes::buf::{StaticBuffer, Procers};
+use rustynotes::outputers::{Outputers, SimpleOutputer};
+use rustynotes::args::SimpleArgs;
+use clap::{Parser, CommandFactory, FromArgMatches};
+
+fn main() {
+ const CHANNELS: u32 = 2;
+ const PERIOD_SIZE: usize = 800;
+ const PROC_SIZE: usize = 5600; //1 << 13; // 8k, 16k, 32k
+ //let disp_threshold: f32 = 0.07;
+ //let disp_t_str = "0.07";
+ let disp_t_str = "0.00526532149076897";
+ let sample_rate = 48000;
+ let notes = note_range((Notes::C, 2), (Notes::B, 10));
+ let rfproc: RFftProcer<f32, PERIOD_SIZE, PROC_SIZE> = RFftProcer::new(sample_rate);
+ let pnotes = rfproc.make_pnotes(&notes);
+ println!("notes len: {}, pnotes len: {}", notes.len(), pnotes.len());
+ for pnote in &pnotes {
+ println!("{}", pnote);
+ }
+ println!("{}", rfproc);
+
+ let mut cmd = SimpleArgs::command();
+ cmd = cmd.mut_arg("threshold", |ta| ta.default_value(disp_t_str));
+ let mut matches = cmd.get_matches();
+ let args = SimpleArgs::from_arg_matches_mut(&mut matches).unwrap();
+ let disp_threshold = args.threshold;
+ println!("Args: {:?}", args);
+
+ let rfpdata = ProcerData::new(&rfproc, ProcerNotes(pnotes, disp_threshold));
+ let outputer = args.get_outputer(&notes);
+ let mut buf: StaticBuffer<f32, PERIOD_SIZE, PROC_SIZE> = StaticBuffer::new(48000, CHANNELS,
+ vec![(Procers::Rfft(rfproc), rfpdata)], "mpd_c_snoop".to_string(), args.get_outdev(),
+ outputer);
+ println!("{}", buf);
+ let mut aout = alsa::Output::buffer_open().unwrap();
+ buf.adev.dump(&mut aout);
+ //buf.outdev.dump(&mut aout);
+ match &buf.outdev {
+ Some(outdev) => { outdev.dump(&mut aout); },
+ None => {},
+ }
+ println!("{}", aout);
+ buf.capture_loop();
+}
diff --git a/src/bin/smolguitar.rs b/src/bin/smolguitar.rs
new file mode 100644
index 0000000..bb36b64
--- /dev/null
+++ b/src/bin/smolguitar.rs
@@ -0,0 +1,44 @@
+use rustynotes::notes::{Notes, note_range, ProcerNotes};
+use rustynotes::proc::{Procer, ProcerData};
+use rustynotes::rfft::RFftProcer;
+use rustynotes::buf::{StaticBuffer, Procers};
+use rustynotes::outputers::{Outputers, SimpleOutputer, LineLayout};
+use rustynotes::args::SimpleArgs;
+use clap::Parser;
+
+fn main() {
+ const CHANNELS: u32 = 1;
+ const PERIOD_SIZE: usize = 480;
+ const PROC_SIZE: usize = 1 << 13; // 8k
+ //let disp_threshold = 0.00372314453125;
+ //let disp_threshold = 0.00526532149076897;
+ //let disp_threshold = 0.05;
+ let sample_rate = 48000;
+ let notes = note_range((Notes::C, 2), (Notes::E, 7));
+ let rfproc: RFftProcer<f32, PERIOD_SIZE, PROC_SIZE> = RFftProcer::new(sample_rate);
+ let pnotes = rfproc.make_pnotes(&notes);
+ for pnote in &pnotes {
+ println!("{}", pnote);
+ }
+ println!("{}", rfproc);
+ //let outputer = Outputers::Simple(SimpleOutputer);
+ //(2f32).sqrt()
+ //let outputer = Outputers::LineLayout(LineLayout::new(0.25, true, (60., 255., 220.), &notes));
+ let args = SimpleArgs::parse();
+ let disp_threshold = args.threshold;
+ let outputer = args.get_outputer(&notes);
+ let rfpdata = ProcerData::new(&rfproc, ProcerNotes(pnotes, disp_threshold));
+ let mut buf: StaticBuffer<f32, PERIOD_SIZE, PROC_SIZE> = StaticBuffer::new(48000, CHANNELS,
+ vec![(Procers::Rfft(rfproc), rfpdata)], "guitar_c".to_string(), args.get_outdev(),
+ outputer);
+ println!("{}", buf);
+ let mut aout = alsa::Output::buffer_open().unwrap();
+ buf.adev.dump(&mut aout);
+ //buf.outdev.dump(&mut aout);
+ match &buf.outdev {
+ Some(outdev) => { outdev.dump(&mut aout); },
+ None => {},
+ }
+ println!("{}", aout);
+ buf.capture_loop();
+}
diff --git a/src/bin/test_serde.rs b/src/bin/test_serde.rs
new file mode 100644
index 0000000..02b42e4
--- /dev/null
+++ b/src/bin/test_serde.rs
@@ -0,0 +1,22 @@
+use crossterm::style::Color;
+use serde::{Serialize, Deserialize};
+use serde_json::{to_string_pretty, from_str};
+
+#[derive(Serialize, Deserialize, Debug)]
+struct cscheme {
+ a: Color,
+ asharp: Color,
+ b: Color,
+}
+
+fn main() {
+ let val = Color::Rgb {r: 110, g: 255, b: 220};
+ let val2 = Color::AnsiValue(51);
+ let val3 = Color::DarkMagenta;
+ let scheme = cscheme {a: val, asharp: val2, b: val3};
+ let json = to_string_pretty(&scheme).unwrap();
+ println!("{}", json);
+ let result: cscheme = from_str(&json).unwrap();
+ println!("{:?}", result);
+ //println!("{} {}", to_string_pretty
+}
diff --git a/src/bin/testrfft.rs b/src/bin/testrfft.rs
new file mode 100644
index 0000000..ee8c72f
--- /dev/null
+++ b/src/bin/testrfft.rs
@@ -0,0 +1,20 @@
+use realfft::{RealFftPlanner, FftNum, RealToComplex};
+
+fn main() {
+ let mut planner: RealFftPlanner<f32> = RealFftPlanner::new();
+ //let rfft = planner.plan_fft_forward(4);
+ let rfft = planner.plan_fft_forward(40);
+ let mut scratch = rfft.make_scratch_vec();
+ let mut out_data = rfft.make_output_vec();
+ //let mut in_data = [1., 0.4, 0.3, -1.];
+ let mut in_data = [1.; 40];
+ //let mut in_data = [
+ rfft.process_with_scratch(&mut in_data, &mut out_data, &mut scratch).unwrap();
+ println!("{:?}", &out_data);
+ let scale = 1./40.;
+ for elem in out_data {
+ println!("{}", elem.norm_sqr().sqrt() * scale);
+ println!("{}", elem.l1_norm() * scale);
+ //println!("{}", elem * scale);
+ }
+}
diff --git a/src/buf.rs b/src/buf.rs
new file mode 100644
index 0000000..fb4a6dd
--- /dev/null
+++ b/src/buf.rs
@@ -0,0 +1,244 @@
+use crate::proc::{Procer, ProcerData, Update};
+use crate::notes::{Note, NoteValue, ProcerNote};
+use crate::outputers::{Outputer, Outputers, SimpleOutputer};
+use crate::rfft::RFftProcer;
+use realfft::FftNum;
+use rustfft::num_traits::float::Float;
+use alsa::{Direction, ValueOr};
+use alsa::pcm::{PCM, HwParams, Format, Access, State};
+use derive_more::Display;
+use std::fmt::Debug;
+use std::io::Write;
+//use std::collections::VecDeque;
+
+pub enum Procers<I: Default + Clone + FftNum + Float, const US: usize, const BS: usize> {
+ Rfft(RFftProcer<I, US, BS>),
+ // XXX: if i add a fir filter that doesn't need a buffer i think
+ // i can just spec it as Fir12Tet(Fir12TetProcer<I, US>) or similar
+ // here
+}
+
+#[derive(Display)]
+#[display(fmt="StaticBuffer(rate={}, psize={}, channels={}, opsize={}, adev=(name={}), outdev=(name={:?}), cperiods={}, operiods={})", rate, period_size, channels, out_period_size, adev_name, outdev_name, capture_periods, output_periods)]
+pub struct StaticBuffer<'nl, I: Default + Debug + Clone + FftNum + Float + NoteValue, const US: usize, const BS: usize> {
+ pub rate: u32,
+ pub period_size: usize,
+ pub channels: u32,
+ pub out_period_size: usize,
+ pub procers: Vec<(Procers<I, US, BS>, ProcerData<'nl, I>)>,
+ pub adev_name: String,
+ pub adev: PCM,
+ pub outdev_name: Option<String>,
+ pub outdev: Option<PCM>,
+ pub outputer: Outputers,
+ pub capture_periods: u32,
+ pub output_periods: u32,
+}
+
+impl<'nl, I: Default + Debug + Clone + FftNum + Float + From<i16> + NoteValue, const US: usize, const BS: usize> StaticBuffer<'nl, I, US, BS> {
+ pub fn new(rate: u32, channels: u32, procers: Vec<(Procers<I, US, BS>, ProcerData<'nl, I>)>, adev_name: String, outdev_name: Option<String>, outputer: Outputers) -> Self {
+ let mut adev = PCM::new(&adev_name, Direction::Capture, true).unwrap();
+ let mut capture_periods = 0;
+ {
+ let hwp = HwParams::any(&adev).unwrap();
+ hwp.set_channels(channels).unwrap();
+ //hwp.set_rate_resample(false);
+ hwp.set_rate(rate, ValueOr::Nearest).unwrap();
+ hwp.set_format(Format::s16()).unwrap();
+ hwp.set_access(Access::MMapInterleaved).unwrap();
+ hwp.set_period_size(US as i64, ValueOr::Nearest).unwrap();
+ hwp.set_periods(8, ValueOr::Nearest).unwrap();
+ // TODO: directly using the ALSA ring buffer and copying directly from it instead of
+ // going through the Update/VecDeque might be interesting in the future, i think it'd
+ // require changing the const sizes to runtime determined sizes though, so future
+ // changes to try~
+ adev.hw_params(&hwp).unwrap();
+ capture_periods = hwp.get_periods().unwrap();
+ println!("capture periods: {}", capture_periods);
+ }
+ let (outdev, out_period_size, output_periods) = match &outdev_name {
+ None => (None, 0, 0),
+ Some(od_name) => {
+ let mut outdev = PCM::new(&od_name, Direction::Playback, true).unwrap();
+ let mut output_periods = 0;
+ let mut out_period_size = 0;
+ {
+ let hwp = HwParams::any(&outdev).unwrap();
+ hwp.set_channels(channels).unwrap();
+ hwp.set_rate(rate, ValueOr::Nearest).unwrap();
+ hwp.set_format(Format::s16()).unwrap();
+ hwp.set_access(Access::RWInterleaved).unwrap();
+ //hwp.set_period_size(US as i64, ValueOr::Nearest).unwrap();
+ //hwp.set_periods(8, ValueOr::Nearest).unwrap();
+ outdev.hw_params(&hwp).unwrap();
+ output_periods = hwp.get_periods().unwrap();
+ out_period_size = hwp.get_period_size().unwrap() as usize;
+ println!("output periods: {}", output_periods);
+ }
+ (Some(outdev), out_period_size, output_periods)
+ }
+ };
+ return Self {
+ rate, procers, adev_name, adev, outdev_name, outdev,
+ period_size: US, out_period_size: out_period_size,
+ channels: channels,
+ outputer,
+ capture_periods, output_periods,
+ }
+ }
+
+ pub fn capture_loop(&mut self) {
+ let empty: Vec<i16> = vec![0i16; US * self.channels as usize];
+ //let mut line_str: String = format!("\r{:80}", "");
+ let in_io = self.adev.io_i16().unwrap();
+ let out_io_opt = {
+ match &self.outdev {
+ None => (None),
+ Some(outdev) => {
+ //self.adev.link(&outdev).unwrap();
+ let out_io = outdev.io_i16().unwrap();
+ // queue up an empty period in the outdev so we can sync between in and out devices
+ // with a 4 period latency
+ for _i in 0..std::cmp::min(4, self.output_periods/2) {
+ out_io.writei(&empty).unwrap();
+ }
+ outdev.start();//.unwrap();
+ println!("output available: {}", outdev.avail().unwrap());
+ Some(out_io)
+ }
+ }
+ };
+ self.adev.start();//.unwrap();
+ //let vt: I = I::from(112);
+ let conv_scale: I = <I as From<i16>>::from(i16::MAX);
+ println!("input available: {}", self.adev.avail_update().unwrap());
+ let mut latentframes = 0;
+ let mut unstarted = false;
+ loop {
+ /*println!("output available: {}", self.outdev.avail().unwrap());
+ println!("input available: {}", self.adev.avail_update().unwrap());*/
+ self.adev.wait(None).unwrap();
+ //println!("a");
+ // should be called right before mmap_begin according to:
+ // https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m___direct.html#ga6d4acf42de554d4d1177fb035d484ea4
+ let avail_frames = self.adev.avail_update().unwrap();
+ in_io.mmap(US, |in_slice| {
+ let size = in_slice.len();
+ //println!("slice length: {}", size);
+ if size == 0 {
+ println!("warning: passed a 0-sized buffer from mmap\n");
+ return 0;
+ }
+ let stride = self.channels as usize;
+ if size != (US * stride) {
+ println!("warning: passed a {} sized buffer from mmap\n\n", size);
+ return 0;
+ }
+ assert_eq!(size, US * stride);
+ let (written, oa) = match &out_io_opt {
+ Some(out_io) => {
+ let wri = out_io.writei(&in_slice).unwrap();
+ assert_eq!(wri, (size/stride));
+ let oaa = self.outdev.as_ref().unwrap().avail().unwrap();
+ (wri, oaa)
+ }
+ None => (size/stride, 0i64),
+ };
+ let ia = self.adev.avail_update().unwrap();
+ let total_outbuf: i64 = oa - ia;
+ /*print!("\rtotal: {}, out: {}, in: {}", total_outbuf, oa, ia);
+ std::io::stdout().flush().unwrap();*/
+ //return written;
+ //let constrained_slice = in_slice[0..US*stride];
+ let mut update: Update<I, US> = [I::default(); US];
+ // copy over the left channel data (even indexes)
+ // TODO/XXX: see if this uses SIMD or needs optimizing :o
+ // using iter and map/function that does strides would prob also work well :o
+ // gotta scale this from i16::MAX to float, alsa
+ // seems to divide by i16::MAX, not sure if there's a better way to do this
+ for i in 0..US {
+ update[i] = <I as From<i16>>::from(in_slice[i*stride]) / conv_scale;
+ };
+ // hmm, i only need to use 1 procer here for displaying, not sure what to do with
+ // the rest :o
+ //for (procer, pdata) in self.procers { }
+ let (ref mut procer, ref mut pdata) = self.procers.first_mut().unwrap();
+ let mut processed = false;
+ {
+ let mut pnotes = pdata.pnotes.0.as_mut_slice();
+ //let processed = true;
+ processed = procer.process_data(&update, pnotes);
+ }
+ if processed {
+ self.outputer.handle_pnotes(&mut pdata.pnotes);
+ //let mut position = 1;
+ // TODO: maybe copy, inplace probably works for now though
+ /*pnotes.sort_unstable_by(|pn1, pn2| (-pn1.2).partial_cmp(&-pn2.2).unwrap());
+ print!("\r{:-80}", pdata.pnotes);
+ std::io::stdout().flush();*/
+ }
+ //let mut position = 0;
+ //let mut remaining = in_slice.len();
+ //let mut remain_frames = remaining/(self.channels as usize);
+ //let mut handled = false;
+ //self.outdev.wait(None).unwrap();
+ //println!("wrote {} frames", written);
+ /*while handled == false {
+ self.outdev.wait(None).unwrap();
+ out_io.mmap(remain_frames, |out_slice| {
+ let outlen = out_slice.len();
+ println!("out slice len: {}", out_slice.len());
+ if outlen == 0 {
+ return 0;
+ }
+ out_slice.clone_from_slice(&in_slice);
+ handled = true;
+ return out_slice.len()/(self.channels as usize);
+ }).unwrap();
+ }*/
+ /*if latentframes > 0 {
+ latentframes -= 1;
+ } else if unstarted {
+ unstarted = false;
+ println!("starting outdev");
+ self.outdev.start().unwrap();
+ }*/
+ return in_slice.len()/(self.channels as usize);
+ }).unwrap();
+ }
+ }
+ //fn new(
+}
+
+/*impl StaticBuffer<'nl, B> {
+}*/
+
+//impl<'nl, B> StaticBuffer<'nl, A> {
+ //pub fn fill_initial(&self,
+ //pub fn new(rate: usize, period_size: usize, out_period_size: usize, procers: Vec<(Procers<f64>, ProcerData<'nl>)>,
+//}
+
+
+impl<I: Default + Clone + FftNum + Float + NoteValue, const US: usize, const BS: usize> Procer<I, US> for Procers<I, US, BS> {
+ fn get_size(&self) -> usize {
+ match self {
+ Procers::Rfft(rfp) => rfp.get_size(),
+ }
+ }
+ fn get_frequency(&self) -> usize {
+ match self {
+ Procers::Rfft(rfp) => rfp.get_frequency(),
+ }
+ }
+ fn make_pnotes<'nl>(&self, notes: &'nl [Note]) -> Vec<ProcerNote<'nl, I>> {
+ match self {
+ Procers::Rfft(rfp) => rfp.make_pnotes(notes),
+ }
+ }
+ fn process_data(&mut self, input: &Update<I, US>, notes: &mut [ProcerNote<I>]) -> bool {
+ match self {
+ Procers::Rfft(rfp) => rfp.process_data(input, notes),
+ }
+ }
+}
+
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..3a1eb7d
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,6 @@
+pub mod notes;
+pub mod proc;
+pub mod outputers;
+pub mod buf;
+pub mod rfft;
+pub mod args;
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..022b6ce
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,66 @@
+mod notes;
+mod proc;
+mod outputers;
+mod buf;
+mod rfft;
+mod args;
+
+use crate::notes::{Notes, note_range, ProcerNotes};
+use crate::proc::{Procer, ProcerData};
+use crate::rfft::RFftProcer;
+use crate::buf::{StaticBuffer, Procers};
+use crate::outputers::{Outputers, SimpleOutputer, LineLayout};
+use crate::args::SimpleArgs;
+use clap::{Parser, CommandFactory, FromArgMatches};
+
+//use std::collections::VecDeque;
+
+fn main() {
+ const CHANNELS: u32 = 2;
+ const PERIOD_SIZE: usize = 240; // 1024 frames
+ const PROC_SIZE: usize = 1 << 14; // 16k frames (so about .3413_ of a second)
+ //let disp_threshold = 0.00372314453125;
+ //let disp_threshold = 0.00526532149076897;
+ //let disp_threshold = 50. / (i16::MAX as f32);
+ //const PERIOD_SIZE: usize = 480;
+ //const PROC_SIZE: usize = 1 << 13; // 8k
+ //let period_size = 1024;
+ let sample_rate = 48000;
+ //let proc_size = 1 << 14; // 16k (so about .3413_ of a second)
+ // we update every period_size so it updates every 21.3_ms but we keep a running average of the
+ // last 341.3_ms so low frequencies can be interpreted with the fft (using fir or iir filters
+ // or wavelets or inner product spaces and frames is something to do in the future, but for now i'm just
+ // porting older python code over for performance that used ffts, and maybe messing with dct
+ // but it'll have the same limitations of ffts mostly probably)
+ let notes = note_range((Notes::C, 2), (Notes::E, 7));
+ // US and prob BS here should be *2 when dealing with stereo, but going with mono rn
+ let rfproc: RFftProcer<f32, PERIOD_SIZE, PROC_SIZE> = RFftProcer::new(sample_rate);
+ let pnotes = rfproc.make_pnotes(&notes);
+ //println!("{:#?}", pnotes);
+ for pnote in &pnotes {
+ println!("{}", pnote);
+ }
+ println!("{}", rfproc);
+ let args = SimpleArgs::parse();
+ println!("Args: {:?}", args);
+ // TODO: having an app-specific default threshold might be a good idea for smolguitar,
+ // not sure how to do that yet, maybe there's an API for changing the default of an arg in clap
+ let disp_threshold = args.threshold;
+ //let outputer = Outputers::Simple(SimpleOutputer);
+ //let outputer = Outputers::LineLayout(LineLayout::new(0.2, true, (0., 255., 220.), &notes));
+ let outputer = args.get_outputer(&notes);
+ let rfpdata = ProcerData::new(&rfproc, ProcerNotes(pnotes, disp_threshold));
+ let mut buf: StaticBuffer<f32, PERIOD_SIZE, PROC_SIZE> = StaticBuffer::new(48000, CHANNELS,
+ vec![(Procers::Rfft(rfproc), rfpdata)], "Microphone_c".to_string(), args.get_outdev(),
+ outputer);
+ println!("{}", buf);
+ let mut aout = alsa::Output::buffer_open().unwrap();
+ buf.adev.dump(&mut aout);
+ match &buf.outdev {
+ Some(outdev) => { outdev.dump(&mut aout); },
+ None => {},
+ }
+ println!("{}", aout);
+ buf.capture_loop();
+ //println!("{:#?}", pdata);
+}
diff --git a/src/notes.rs b/src/notes.rs
new file mode 100644
index 0000000..41eb45e
--- /dev/null
+++ b/src/notes.rs
@@ -0,0 +1,168 @@
+use derive_more::Display;
+use std::fmt::{Debug, Write};
+use num_traits::{NumOps, ToPrimitive};
+
+#[derive(Debug, Clone, Display, Copy, PartialEq)]
+pub enum Notes {
+ #[display(fmt=" c")]
+ C,
+ #[display(fmt="c#")]
+ Cs,
+ #[display(fmt=" d")]
+ D,
+ #[display(fmt="d#")]
+ Ds,
+ #[display(fmt=" e")]
+ E,
+ #[display(fmt=" f")]
+ F,
+ #[display(fmt="f#")]
+ Fs,
+ #[display(fmt=" g")]
+ G,
+ #[display(fmt="g#")]
+ Gs,
+ #[display(fmt=" a")]
+ A,
+ #[display(fmt="a#")]
+ As,
+ #[display(fmt=" b")]
+ B,
+}
+
+pub type BaseNote = (Notes, f64);
+
+const MIDDLE_A_VAL: f64 = 440.;
+//const MIDDLE_A_LOWER: f64 = MIDDLE_A_VAL * (2f64).powf(-0.5f64 / 12f64);
+//const MIDDLE_A_UPPER: f64 = MIDDLE_A_VAL * (2f64).powf(0.5f64 / 12f64);
+
+pub fn calc_freq_from_offset(offset: f64) -> f64 {
+ return MIDDLE_A_VAL * (2f64).powf(offset / 12f64);
+}
+
+pub const NOTE_MA_OFFSETS: [BaseNote; 12] = [
+ (Notes::C, -57.), (Notes::Cs, -56.),
+ (Notes::D, -55.), (Notes::Ds, -54.), (Notes::E, -53.),
+ (Notes::F, -52.), (Notes::Fs, -51.),
+ (Notes::G, -50.), (Notes::Gs, -49.),
+ (Notes::A, -48.), (Notes::As, -47.),
+ (Notes::B, -46.),
+];
+
+/*const NOTE_ORDER: [BaseNote; 12] = [
+ (Notes::C, 16.352), (Notes::Cs, 17.324),
+ (Notes::D, 18.354), (Notes::Ds, 19.445), (Notes::E, 20.602),
+ (Notes::F, 21.827), (Notes::Fs, 23.125),
+ (Notes::G, 24.5), (Notes::Gs, 25.957),
+ (Notes::A, 27.5), (Notes::As, 29.135),
+ (Notes::B, 30.868)
+];*/
+
+#[derive(Debug, Display)]
+#[display(fmt="{}{}", "note.0", octave)]
+pub struct Note {
+ pub note: BaseNote,
+ pub low_freq: f64,
+ pub freq: f64,
+ pub high_freq: f64,
+ pub octave: i32,
+ //value: f64,
+}
+
+pub trait NoteValue: Debug + PartialOrd + From<f32> + ToPrimitive + std::fmt::Display + Copy + NumOps {}
+//where <Self as std::ops::Mul>::Output: std::fmt::Display {}
+
+//impl NoteValue for f32 {}
+//impl NoteValue for f64 {}
+impl<T> NoteValue for T
+where T: Debug + PartialOrd + From<f32> + ToPrimitive + std::fmt::Display + Copy + NumOps {}
+
+// elements here are (Note, fft/proc data range for calculating the value, summed value, max value
+// of each element in the bin)
+#[derive(Debug, Display)]
+//#[display(fmt="{}({:?}) {}", _0, _1 ,_2)]
+//#[display(bound="I: From<f32> + std::ops::Mul + std::fmt::Display")]
+#[display(fmt="{} {:.0} ", _0, "*_3*I::from(10000.0)")]
+pub struct ProcerNote<'nl, I: NoteValue>(pub &'nl Note, pub core::ops::Range<usize>, pub I, pub I);
+// .0 here is the notes data, .1 is the threshold for printing~
+#[derive(Debug)]
+pub struct ProcerNotes<'nl, I: NoteValue>(pub Vec<ProcerNote<'nl, I>>, pub I);
+
+impl<'nl, I: NoteValue> std::fmt::Display for ProcerNotes<'nl, I> {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ //let threshold: I = I::from(0.00372314453125f32);
+ let threshold = self.1;
+ let mut tmp: String = String::with_capacity(1024);
+ for pn in self.0.iter().filter(|pn| pn.3 >= threshold) {
+ write!(tmp, "{pn}")?;
+ };
+ fmt.pad(&tmp);
+ Ok(())
+ }
+}
+
+
+/*impl<'nl, I: Debug + PartialOrd + From<f32>> Debug for ProcerNotes<'nl, I> {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ let threshold: I = I::from(122.0f32);
+ fmt.debug_list().entries(self.0.iter().filter(|pn| pn.2 > threshold)).finish()
+ }
+}*/
+
+pub trait Octavable {
+ fn get_octave(&self, octave: i32) -> Note;
+}
+
+impl Octavable for BaseNote {
+ fn get_octave(&self, octave: i32) -> Note {
+ let offset = self.1 + ((12i32 * octave) as f64);
+ let low_offset = offset - 0.5;
+ let high_offset = offset + 0.5;
+ match octave {
+ 0 => Note {
+ note: *self,
+ low_freq: calc_freq_from_offset(self.1 - 0.5),
+ freq: calc_freq_from_offset(self.1),
+ high_freq: calc_freq_from_offset(self.1 + 0.5),
+ octave: 0//, value: 0.
+ },
+ _ => Note {
+ note: *self,
+ low_freq: calc_freq_from_offset(low_offset),
+ freq: calc_freq_from_offset(offset),
+ high_freq: calc_freq_from_offset(high_offset),
+ octave: octave//, value: 0.
+ }
+ }
+ }
+}
+
+// start and end should be tuples of (Note, octave)
+pub fn note_range(start: (Notes, i32), end: (Notes, i32)) -> Vec<Note> {
+ let mut ret = Vec::with_capacity((end.1+3 - start.1) as usize *12);
+ let mut started: bool = false;
+ let start_note = start.0;
+ let (end_note, mut end_octave) = end;
+ end_octave += 2;
+ for octave in (start.1)..=end_octave {
+ for bnote in NOTE_MA_OFFSETS {
+ match (started, bnote.0, octave) {
+ (false, cur_note, _) if cur_note == start_note => { started = true; ret.push(bnote.get_octave(octave)) },
+ (true, cur_note, _) if cur_note == end_note && octave == end_octave => { ret.push(bnote.get_octave(octave)); return ret; },
+ (true, _, _) => { ret.push(bnote.get_octave(octave)) },
+ (_, _, _) => {},
+ }
+ }
+ }
+ return ret;
+}
+
+/*impl Octavable for Note {
+ fn get_octave(&self, octave: i32) -> Self {
+ match octave {
+ 0 => Note {freq: self.note.1, octave: 0, value: 0., ..*self},
+ _ => Note {freq: self.note.1 * ((1 << octave) as f64), octave: octave, value: 0., ..*self}
+ }
+ }
+}*/
+
diff --git a/src/outputers.rs b/src/outputers.rs
new file mode 100644
index 0000000..4c56e56
--- /dev/null
+++ b/src/outputers.rs
@@ -0,0 +1,266 @@
+use crate::notes::{Note, BaseNote, Notes, NoteValue, ProcerNote, ProcerNotes};
+use std::collections::VecDeque;
+use std::io::Write;
+use std::fmt::{Debug};
+use realfft::FftNum;
+//use clap::{Args,Subcommand};
+
+//use std::io;
+use crossterm::terminal;
+use crossterm::style::{Stylize, Color};
+use tui::{backend::CrosstermBackend, Terminal};
+use serde::{Serialize, Deserialize};
+use ansi_colours::rgb_from_ansi256;
+
+
+trait ToRgbVal {
+ fn to_rgb_value(&self) -> (u8, u8, u8);
+}
+
+impl ToRgbVal for Color {
+ fn to_rgb_value(&self) -> (u8, u8, u8) {
+ match self {
+ Color::Rgb {r, g, b} => (*r, *g, *b),
+ // I'll use the tango colours for the colour constants
+ // but the Ansi lookup table for the Ansi versions
+ Color::Reset => (0, 0, 0),
+ Color::Black => (0, 0, 0),
+ Color::DarkRed => (204, 0, 0),
+ Color::DarkGreen => (78, 154, 6),
+ Color::DarkYellow => (196, 160, 0),
+ Color::DarkBlue => (54, 101, 164),
+ Color::DarkMagenta => (117, 80, 123),
+ Color::DarkCyan => (6, 152, 154),
+ Color::Grey => (211, 215, 207),
+ Color::DarkGrey => (85, 87, 83),
+ Color::Red => (239, 41, 41),
+ Color::Green => (138, 226, 52),
+ Color::Yellow => (252, 233, 79),
+ Color::Blue => (114, 159, 207),
+ Color::Magenta => (173, 127, 168),
+ Color::Cyan => (52, 226, 226),
+ Color::White => (238, 238, 236),
+ Color::AnsiValue(ansi) => rgb_from_ansi256(*ansi),
+ }
+ }
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+pub struct ColourScheme {
+ pub a: Color, pub as_: Color,
+ pub b: Color,
+ pub c: Color, pub cs: Color,
+ pub d: Color, pub ds: Color,
+ pub e: Color,
+ pub f: Color, pub fs: Color,
+ pub g: Color, pub gs: Color,
+}
+
+impl ColourScheme {
+ fn get_colour(&self, note: Notes) -> Color {
+ match note {
+ Notes::C => self.c,
+ Notes::Cs => self.cs,
+ Notes::D => self.d,
+ Notes::Ds => self.ds,
+ Notes::E => self.e,
+ Notes::F => self.f,
+ Notes::Fs => self.fs,
+ Notes::G => self.g,
+ Notes::Gs => self.gs,
+ Notes::A => self.a,
+ Notes::As => self.as_,
+ Notes::B => self.b,
+ }
+ }
+}
+
+impl Default for ColourScheme {
+ fn default() -> Self {
+ let c = Color::Rgb {r: 140, g: 255, b: 210};
+ return Self {
+ a: c, as_: c,
+ b: c,
+ c: c, cs: c,
+ d: c, ds: c,
+ e: c,
+ f: c, fs: c,
+ g: c, gs: c,
+ }
+ }
+}
+
+pub trait Outputer<I: FftNum + NoteValue> {
+ fn setup(&mut self);
+ fn handle_pnotes(&mut self, notes: &mut ProcerNotes<I>);
+}
+
+pub enum Outputers {
+ Simple(SimpleOutputer),
+ LineLayout(LineLayout),
+ Tui(TuiOutputer),
+}
+
+pub struct SimpleOutputer;
+pub struct LineLayout {
+ term_width: usize,
+ notes_per_line: usize,
+ last_line: String,
+ empty: String,
+ last_was_empty: bool,
+ use_colour: bool,
+ //max_c: (f32, f32, f32),
+ colours: ColourScheme,
+ max_threshold: f32,
+ //threshold: f32,
+}
+pub struct TuiOutputer {
+ terminal: Terminal<tui::backend::CrosstermBackend<std::io::Stdout>>,
+}
+
+impl<I: FftNum + NoteValue> Outputer<I> for SimpleOutputer {
+ fn setup(&mut self) {}
+ fn handle_pnotes(&mut self, notes: &mut ProcerNotes<I>) {
+ notes.0.sort_unstable_by(|pn1, pn2| (-(pn1.3)).partial_cmp(&-(pn2.3)).unwrap());
+ print!("\r{:-80}", notes);
+ std::io::stdout().flush().unwrap();
+ }
+}
+
+pub fn line_note_char(note: &Note) -> char {
+ match note.note.0 {
+ Notes::C => 'c',
+ Notes::Cs => match note.octave % 10 {
+ 0 => '0',
+ 1 => '1',
+ 2 => '2',
+ 3 => '3',
+ 4 => '4',
+ 5 => '5',
+ 6 => '6',
+ 7 => '7',
+ 8 => '8',
+ 9 => '9',
+ _ => ' ',
+ },
+ Notes::D => 'd',
+ Notes::Ds => ' ',
+ Notes::E => 'e',
+ Notes::F => 'f',
+ Notes::Fs => ' ',
+ Notes::G => 'g',
+ Notes::Gs => ' ',
+ Notes::A => 'a',
+ Notes::As => ' ',
+ Notes::B => 'b'
+ }
+}
+
+impl LineLayout {
+ pub fn new(max_threshold: f32, use_colour: bool, colours: ColourScheme, notes: &[Note]) -> Self {
+ let term_width = terminal::size().unwrap().0 as usize;
+ let note_count = std::cmp::min(notes.len(), term_width);
+ return Self {
+ term_width, use_colour, colours, max_threshold, notes_per_line: note_count,
+ last_line: notes[0..note_count].iter().map(line_note_char).collect(),
+ empty: " ".repeat(term_width),
+ last_was_empty: true,
+ }
+ }
+
+ pub fn line_bw<I: FftNum + NoteValue>(&self, note_count: usize, notes: &ProcerNotes<I>) {
+ print!("\r");
+ let itx: String = notes.0[0..note_count].iter().map(|note| {
+ if note.3 >= notes.1 {
+ '▉'
+ } else {
+ ' '
+ }
+ }).collect();
+ print!("{}\n{}", itx, self.last_line);
+ std::io::stdout().flush().unwrap();
+ }
+
+ pub fn line_colour<I: FftNum + NoteValue>(&self, note_count: usize, notes: &ProcerNotes<I>) {
+ print!("\r");
+ // TODO: do grouping to avoid excessive control characters
+ // for now this just does the simplest thing though
+ for note in &notes.0[0..note_count] {
+ if note.3 >= notes.1 {
+ let mut bright: f32 = note.3.to_f32().unwrap() / self.max_threshold;
+ if bright > 1.0 {
+ bright = 1.0;
+ }
+ let (r, g, b) = self.colours.get_colour(note.0.note.0).to_rgb_value();
+ print!("{}", ' '.on(Color::Rgb {
+ r: (r as f32 * bright) as u8,
+ g: (g as f32 * bright) as u8,
+ b: (b as f32 * bright) as u8,
+ }));
+ } else {
+ print!(" ");
+ }
+ }
+ print!("\n{}", self.last_line);
+ std::io::stdout().flush().unwrap();
+ }
+}
+
+impl<I: FftNum + NoteValue> Outputer<I> for LineLayout {
+ fn setup(&mut self) {}
+ fn handle_pnotes(&mut self, notes: &mut ProcerNotes<I>) {
+ let notes_len = notes.0.len();
+ let note_count = std::cmp::min(notes_len, self.notes_per_line);
+ if notes.0[0..note_count].iter().all(|note| note.3 < notes.1) {
+ if self.last_was_empty {
+ return
+ } else {
+ self.last_was_empty = true;
+ }
+ } else {
+ self.last_was_empty = false;
+ }
+ match self.use_colour {
+ true => self.line_colour(note_count, notes),
+ false => self.line_bw(note_count, notes)
+ }
+ }
+}
+
+impl TuiOutputer {
+ pub fn new() -> Result<Self, std::io::Error> {
+ let stdout = std::io::stdout();
+ let backend = CrosstermBackend::new(stdout);
+ let mut terminal = Terminal::new(backend)?;
+ return Ok(Self {
+ terminal,
+ })
+ }
+}
+
+impl<I: FftNum + NoteValue> Outputer<I> for TuiOutputer {
+ fn setup(&mut self) {}
+ fn handle_pnotes(&mut self, notes: &mut ProcerNotes<I>) {
+ //self.terminal.draw(|frame| {})
+ }
+}
+
+
+impl<I: FftNum + NoteValue> Outputer<I> for Outputers {
+ fn setup(&mut self) {
+ match self {
+ Outputers::Simple(so) => <SimpleOutputer as Outputer<I>>::setup(so),
+ Outputers::LineLayout(llo) => <LineLayout as Outputer<I>>::setup(llo),
+ Outputers::Tui(to) => <TuiOutputer as Outputer<I>>::setup(to)
+ }
+ }
+
+ fn handle_pnotes(&mut self, notes: &mut ProcerNotes<I>) {
+ match self {
+ Outputers::Simple(so) => so.handle_pnotes(notes),
+ Outputers::LineLayout(llo) => llo.handle_pnotes(notes),
+ Outputers::Tui(to) => to.handle_pnotes(notes),
+ }
+ }
+}
+
diff --git a/src/proc.rs b/src/proc.rs
new file mode 100644
index 0000000..2b32747
--- /dev/null
+++ b/src/proc.rs
@@ -0,0 +1,162 @@
+//#![feature(split_array)]
+//#![feature(adt_const_params)]
+//#![feature(generic_const_exprs)]
+use crate::notes::{Note, NoteValue, ProcerNote, ProcerNotes};
+//use std::sync::Arc;
+use std::collections::VecDeque;
+
+// Arc does heap allocate, so it might make sense to just pass owned arrays around and copy/clone
+// when needed
+//pub type Update<const S: usize> = Arc<[f64; S]>;
+pub type Update<I, const S: usize> = [I; S];
+pub type UpDequer<I, const S: usize> = VecDeque<Update<I, S>>;
+
+pub trait Procer<I: NoteValue, const US: usize> {
+ fn get_size(&self) -> usize;
+ fn get_frequency(&self) -> usize;
+ fn make_pnotes<'nl>(&self, notes: &'nl [Note]) -> Vec<ProcerNote<'nl, I>>;
+ // this function only modifies ProcerNotes when you have enough data
+ // otherwise it leaves them alone or sets them to 0 (not sure which yet)
+ fn process_data(&mut self, input: &Update<I, US>, notes: &mut [ProcerNote<I>]) -> bool;
+}
+
+pub trait DequerUtils<I, const BS: usize> {
+ fn cur_max_buffer_size(&self) -> usize;
+ fn update_buffer_array(&self, buf: &mut [I; BS]) -> bool;
+ //fn make_buffer(&self) -> Option<[I; BS]>;
+}
+
+// the tuple this returns is (plan_size (ie the - index of the first element to copy from),
+// Option<copy_size>), if the option is None copy the entire update, otherwise copy
+// copy_size from the end of the first array
+const fn make_buf_plan(buf_size: usize, update_size: usize) -> (usize, Option<usize>) {
+ let remainder_size = buf_size % update_size;
+ let plan_size = buf_size / update_size;
+ if remainder_size > 0 {
+ return (plan_size+1, Some(remainder_size));
+ } else {
+ return (plan_size, None);
+ }
+}
+
+impl<I: Clone, const S: usize, const BS: usize> DequerUtils<I, BS> for UpDequer<I, S> {
+ #[inline(always)]
+ fn cur_max_buffer_size(&self) -> usize {
+ S * self.len()
+ }
+
+ fn update_buffer_array(&self, buf: &mut [I; BS]) -> bool {
+ let total_bufs = self.len();
+ let max_size = S * total_bufs;
+ if BS > max_size {
+ return false;
+ }
+ // XXX: calculate this at runtime for now, const generics (atleast currently) can't be used
+ // for calculating consts
+ // <https://github.com/rust-lang/rfcs/blob/master/text/2000-const-generics.md#when-a-const-variable-can-be-used>
+ // TODO: this could be made const itself by making a consts mod and putting the update and
+ // buffer sizes as consts in there instead of having them as const generic arguments
+ let plan = make_buf_plan(BS, S);
+ //let mut cur_start = 0;
+ let mut cur_index = total_bufs - plan.0;
+ //println!("{:?}, {}, {}", plan, cur_index, S);
+ let (copy_s, slicey) = match plan.1 {
+ None => (S, self.get(cur_index).unwrap().as_slice()),
+ Some(copy_size) => (copy_size, &self.get(cur_index).unwrap()[(S-copy_size)..])
+ };
+ //println!("{:?}", slicey);
+ // do the copy n stuff
+ let (mut left, mut right) = buf.split_at_mut(copy_s);
+ left.clone_from_slice(slicey);
+ //cur_start += copy_s;
+ cur_index += 1;
+ for up_i in cur_index..total_bufs {
+ //let (mut l2, r2) = right.split_at_mut(S);
+ //right = r2;
+ (left, right) = right.split_at_mut(S);
+ left.clone_from_slice(self.get(up_i).unwrap().as_slice());
+ }
+ return true;
+ }
+
+ /*fn make_buffer_vec(&self) -> Option<[I; BS]> {
+ let total_bufs = self.len();
+ let max_size = S * total_bufs;
+ if BS > max_size {
+ return None;
+ }
+ // convert this bit to a const fn, because it can be :o, could maybe make a const
+ // copy_bytes_from_iter function or something
+ //const remainder_size: usize = BS / S;
+ //const S_2: usize = S;
+ //const PLAN: (usize, Option<usize>) = make_buf_plan(BS_2, S_2);
+ /*const PARTIAL_FIRST: bool = PLAN.1.is_some();
+ let mut cur_start = 0;
+ let mut cur_index = total_bufs - PLAN.0;
+ let mut ret: [f64; BS] = [0.0; BS];
+ if PARTIAL_FIRST {
+ const Some(PVAL): usize = PARTIAL_FIRST;
+ let slicey = self.get(cur_index).rsplit_array_ref(copy_size);
+ let (left, _) = ret.split_array_mut::<PVAL>();
+ left.clone_from(slicey);
+ }*/
+ /*let slicey = match PLAN.1 {
+ None => &self.get(cur_index)
+ Some(copy_size) => &self.get(cur_index).rsplit_array_ref(copy_size)
+ }*/
+ let mut ret: [I; BS] = [I::default(); BS];
+ return Some(ret);
+ }*/
+}
+
+#[derive(Debug)]
+pub struct ProcerData<'nl, I: NoteValue> {
+ pub pnotes: ProcerNotes<'nl, I>,
+ size: usize,
+ frequency: usize,
+ pub current: bool,
+}
+
+impl<'nl, I: NoteValue> ProcerData<'nl, I> {
+ pub fn new<const US: usize>(procer: &impl Procer<I, US>, pnotes: ProcerNotes<'nl, I>) -> Self {
+ return Self {
+ pnotes: pnotes,
+ size: procer.get_size(),
+ frequency: procer.get_frequency(),
+ current: false,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::proc::{Update, UpDequer, DequerUtils};
+ use std::collections::VecDeque;
+ #[test]
+ fn test_update_buffer_array() {
+ let mut data_dq: UpDequer<f32, 5> = VecDeque::with_capacity(6);
+ let updates: [Update<f32, 5>; 5] = [[1.0, 2.0, 3.0, 4.0, 5.0], [6.0, 7.0, 8.0, 9.0, 10.0], [11.0, 12.0, 13.0, 14.0, 15.0], [16.0, 17.0, 18.0, 19.0, 20.0], [21.0, 22.0, 23.0, 24.0, 25.0]];
+ data_dq.push_back(updates[0]);
+ data_dq.push_back(updates[1]);
+ data_dq.push_back(updates[2]);
+ data_dq.push_back(updates[3]);
+ data_dq.push_back(updates[4]);
+ let mut ba: [f32; 24] = [0.0; 24];
+ let copied = data_dq.update_buffer_array(&mut ba);
+ assert_eq!(copied, true);
+ assert_eq!(ba, [2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0]);
+ let mut small_ba: [f32; 3] = [0.0; 3];
+ let copied2 = data_dq.update_buffer_array(&mut small_ba);
+ assert_eq!(copied2, true);
+ assert_eq!(small_ba, [23.0, 24.0, 25.0]);
+ let mut full_ba: [f32; 25] = [0.0; 25];
+ let copied3 = data_dq.update_buffer_array(&mut full_ba);
+ assert_eq!(copied2, true);
+ assert_eq!(full_ba, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0]);
+ let mut toobig: [f32; 26] = [0.0; 26];
+ let copied4 = data_dq.update_buffer_array(&mut toobig);
+ assert_eq!(copied4, false);
+ assert_eq!(toobig, [0.0; 26]);
+ }
+}
+
diff --git a/src/rfft.rs b/src/rfft.rs
new file mode 100644
index 0000000..8e5fc0f
--- /dev/null
+++ b/src/rfft.rs
@@ -0,0 +1,179 @@
+use crate::proc::{Procer, UpDequer, DequerUtils, Update};
+use crate::notes::{Note, NoteValue, ProcerNote};
+use core::ops::Range;
+//use std::collections::VecDeque;
+use std::sync::Arc;
+
+use realfft::{RealFftPlanner, FftNum, RealToComplex};
+use rustfft::num_complex::Complex;
+//use rustfft::num_traits::Zero;
+use rustfft::num_traits::One;
+use rustfft::num_traits::float::Float;
+use derive_more::Display;
+use std::fmt::Debug;
+
+
+trait DebugableRTC<I>: RealToComplex<I> + Debug {}
+
+
+// the Arc<dyn> here really increases compile times, but that might just be because it has to link
+// in realfft/rustfft where before it didn't get linked in
+
+#[derive(Display)]
+#[display(fmt="RfftProcer(size={}, outsize={}, freq={}, deque=(size={}, cap={}), in_buf_size={}, scratch=(size={}, cap={}) out_data=(size={}, cap={}))", size, outsize, frequency, "in_deque.len()", "in_deque.capacity()", "in_buf.len()", "scratch.len()", "scratch.capacity()", "out_data.len()", "out_data.capacity()")]
+pub struct RFftProcer<I: Clone + FftNum, const US: usize, const BS: usize> {
+ pub size: usize,
+ pub outsize: usize,
+ pub frequency: usize,
+ pub in_deque: UpDequer<I, US>,
+ pub in_buf: [I; BS],
+ pub rfft: Arc<dyn RealToComplex<I>>,
+ // TODO: could make this a fixed array instead of a vec too maybe
+ pub scratch: Vec<Complex<I>>,
+ pub out_data: Vec<Complex<I>>,
+ pub scale: I,
+ //pub in_deque: VecDeque<Update<A>>,
+}
+
+impl<I: Default + Copy + FftNum + Float, const US: usize, const BS: usize> RFftProcer<I, US, BS> {
+ pub fn new(frequency: usize) -> Self {
+ let retsize = BS/2+1;
+ // TODO: could do some cleverness with using double then you'd only need to pop elements
+ // off after you're double-full, but i think just having a buffer of 1 or 2 elements is
+ // easier here and popping before each add in process_data if we're full
+ let dq_capacity = (BS/US)+2;
+ assert!((dq_capacity * US) > BS);
+ let mut planner = RealFftPlanner::new();
+ let rfft = planner.plan_fft_forward(BS);
+ let scratch = rfft.make_scratch_vec();
+ let out_data = rfft.make_output_vec();
+ return Self {
+ size: BS,
+ outsize: retsize,
+ frequency: frequency,
+ in_deque: UpDequer::with_capacity(dq_capacity),
+ in_buf: [I::default(); BS],
+ rfft: rfft,
+ //out_data: Vec::with_capacity(retsize),
+ scratch: scratch,
+ out_data: out_data,
+ // the docs say to scale by 1/sqrt(len), but it seems like 1/len (where len is the real
+ // input size) is the correct scaling to get results with norm_sqrt results between 0-1
+ // I'm still not entirely sure about this though, l1_norm also seems to max out at len
+ // without scaling so 1/len i think is right for it too, but it also gives lower
+ // results when there's an imaginary part
+ //scale: /*I::from_usize(10).unwrap()*/I::one() / I::from_usize(retsize/*-1*/).unwrap().sqrt(),
+ scale: I::one() / I::from_usize(BS).unwrap(),
+ }
+ }
+}
+
+impl<I: Default + Clone + FftNum + Float + NoteValue, const US: usize, const BS: usize> Procer<I, US> for RFftProcer<I, US, BS> {
+ fn get_size(&self) -> usize {
+ self.size
+ }
+ fn get_frequency(&self) -> usize {
+ self.frequency
+ }
+
+ fn make_pnotes<'nl>(&self, notes: &'nl [Note]) -> Vec<ProcerNote<'nl, I>> {
+ // TODO: +1 or nah? (might have to handle it differently for even or odd, hopefully not
+ // though)
+ //let retsize = self.size/2+1;
+ let retsize = self.outsize;
+ let noteslen = notes.len();
+ let mut ret: Vec<ProcerNote<'nl, I>> = Vec::with_capacity(noteslen);
+ let mut cur_note_idx = 0;
+ let mut cutoff = notes[0].high_freq;
+ let scale: f64 = (self.frequency as f64)/(self.size as f64);
+ // TODO: since we do have .low_freq here as well you can start the low size of the range
+ // close to .low_freq (how close?) but for now we'll start it at 1
+ let mut cur_range: Range<usize> = 1..1;
+ for i in 1..retsize {
+ let freq: f64 = (i as f64) * scale;
+ if freq > cutoff {
+ while freq > cutoff {
+ let pnote = ProcerNote::<'nl, I>(&notes[cur_note_idx], cur_range.clone(), I::default(), I::default());
+ ret.push(pnote);
+ cur_range.start = cur_range.end;
+ // worth noting this is different than the python code,
+ // here frequencies above the high note aren't added in
+ // where the python code adds them all to the highest
+ // note, you change that by changing the return below
+ // here to a cutoff = <however you express infinite> i
+ // think
+ cur_note_idx += 1;
+ if cur_note_idx >= noteslen {
+ return ret;
+ }
+ cutoff = notes[cur_note_idx].high_freq;
+ }
+ }
+ //else {
+ cur_range.end += 1;
+ //}
+ }
+ // add the final pnote if we ran out of frequencies before running
+ // out of notes
+ ret.push(ProcerNote::<'nl, I>(&notes[cur_note_idx], cur_range, I::default(), I::default()));
+ return ret;
+ }
+
+ fn process_data(&mut self, input: &Update<I, US>, notes: &mut [ProcerNote<I>]) -> bool {
+ let dq_len = self.in_deque.len();
+ let dq_cap = self.in_deque.capacity();
+ // we should probably make sure buf_size can be made with the length of dq as well though
+ // so capacity can grow if new() messed up with the capacity
+ if dq_len == dq_cap {
+ self.in_deque.pop_front();
+ }
+ let scale = self.scale;
+ // XXX: mm this copies twice per update (once to add it to the deque and once to the buffer)
+ // which i kinda wanted to avoid (using an Arc would avoid it but it'd be extra heap
+ // allocations) atleast it doesn't realloc though so hopefully that's ok
+ // depending on how this works with Alsa i might wanna switch to passing in a slice and
+ // storing references in the UpDeque but that makes lifetime soup
+ // or something else maybes
+ // OHH though since i'm probably getting i16 from alsa and want to convert to f32 anyway it
+ // makes sense to copy (question is will that add a third copy or will the compiler be
+ // smart enough to convert inplace, i think ideally the conversion would happen here in
+ // this push_back actually, maybe the input Update should be AI and this push_back should
+ // be as I or Into or whatever)
+ self.in_deque.push_back(input.clone());
+ let updated = self.in_deque.update_buffer_array(&mut self.in_buf);
+ //println!("updated = {}", updated);
+ if updated {
+ //assert_eq!(self.in_buf, [I::default(); BS]);
+ let post_scale = I::from_u8(2).unwrap().sqrt();
+ self.rfft.process_with_scratch(&mut self.in_buf, &mut self.out_data, &mut self.scratch).unwrap();
+ for pnote in notes {
+ // TODO: figure out the best calculation for this
+ // TODO: map/sum or product might be quicker then fold here, since fold can't
+ // really use simd, also look at what the perftest code did to use simd
+ // XXX: python's abs uses the equiv of sqrt(norm_sqr) (https://docs.rs/num-complex/0.4.3/num_complex/struct.Complex.html#method.norm_sqr)
+ // ie. sqrt(|real|**2 + |imag|**2), gonna just use norm_sqrt here for now
+ // l1_norm might also be a good option (just |real| + |imag|), and gets rid of the
+ // pow too, but i dunno what's best here
+ // TODO: the python test2.py code actually currently uses max instead of folding here too
+ //let complexes = self.out_data[pnote.1.clone()].iter().fold((I::default(), Complex::<I>::default()), |(sum, mx), c| {let c_sqrt = c.scale(scale).norm_sqr().sqrt(); (sum + c_sqrt, std::cmp::max_by(mx, *c, |a: &Complex<I>, b: &Complex<I>| { a.norm_sqr().partial_cmp(&b.norm_sqr()).unwrap() }))});
+ let complexes = self.out_data[pnote.1.clone()].iter().fold((I::default(), Complex::<I>::default(), I::default()), |(sum, mx_c, mx_sqrt), c| {
+ let c_sqr = c.scale(scale).norm_sqr();
+ let c_sqrt = c_sqr.sqrt();
+ if c_sqrt > mx_sqrt {
+ (sum + c_sqr, *c, c_sqrt)
+ } else {
+ (sum + c_sqr, mx_c, mx_sqrt)
+ }
+ });
+ pnote.2 = complexes.0.sqrt() * post_scale; // / I::from_usize(pnote.1.len()).unwrap();
+ //pnote.3 = complexes.1.scale(scale).norm_sqr() * I::from_f32(2.0).unwrap();
+ //pnote.2 = complexes.0.scale(scale).norm_sqr().sqrt();
+ //pnote.3 = complexes.1.scale(scale).norm_sqr().sqrt();
+ pnote.3 = complexes.2 * post_scale;
+ //pnote.2 = self.out_data[pnote.1.clone()].iter().fold(I::default(), |acc, c| acc + (c.norm_sqr().sqrt()*scale))/*.sqrt()*/;
+ //pnote.2 = self.out_data[pnote.1.clone()].iter().map(|c| c.norm_sqr()).max_by(|a, b| { a.partial_cmp(b).unwrap() } );
+ }
+ }
+ return updated;
+ }
+}