diff options
author | Wavy Harp <wavyharp@gmail.com> | 2023-05-07 23:04:53 -0600 |
---|---|---|
committer | Kyle McFarland <tfkyle@gmail.com> | 2023-05-07 23:04:53 -0600 |
commit | 991849b32acf83dd14a5096540bb053d2572502a (patch) | |
tree | 279b59d75d4ad6081f5242cf77d843ae6b37fc3d /src | |
download | rustynotes-991849b32acf83dd14a5096540bb053d2572502a.zip rustynotes-991849b32acf83dd14a5096540bb053d2572502a.tar.gz rustynotes-991849b32acf83dd14a5096540bb053d2572502a.tar.bz2 |
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.rs | 140 | ||||
-rw-r--r-- | src/bin/loopbacker.rs | 48 | ||||
-rw-r--r-- | src/bin/smolguitar.rs | 44 | ||||
-rw-r--r-- | src/bin/test_serde.rs | 22 | ||||
-rw-r--r-- | src/bin/testrfft.rs | 20 | ||||
-rw-r--r-- | src/buf.rs | 244 | ||||
-rw-r--r-- | src/lib.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 66 | ||||
-rw-r--r-- | src/notes.rs | 168 | ||||
-rw-r--r-- | src/outputers.rs | 266 | ||||
-rw-r--r-- | src/proc.rs | 162 | ||||
-rw-r--r-- | src/rfft.rs | 179 |
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(), ¬es)), + 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(¬es); + 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(¬es); + 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(¬es); + 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.), ¬es)); + let args = SimpleArgs::parse(); + let disp_threshold = args.threshold; + let outputer = args.get_outputer(¬es); + 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(¬es); + //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.), ¬es)); + let outputer = args.get_outputer(¬es); + 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 ¬es.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>(¬es[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>(¬es[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; + } +} |