diff --git a/Cargo.lock b/Cargo.lock index f53281e..f277de5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "Actuate" -version = "1.3.91" +version = "1.4.0" dependencies = [ "anyhow", "dirs", @@ -203,7 +203,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "baseview" version = "0.1.0" -source = "git+https://github.com/RustAudio/baseview.git?rev=9a0b42c09d712777b2edb4c5e0cb6baf21e988f0#9a0b42c09d712777b2edb4c5e0cb6baf21e988f0" +source = "git+https://github.com/RustAudio/baseview.git?rev=3724a00c970a9cd10a3494fa3c37bca66d631bb7#3724a00c970a9cd10a3494fa3c37bca66d631bb7" dependencies = [ "cocoa", "core-foundation", @@ -771,9 +771,9 @@ checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "ecolor" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4feb366740ded31a004a0e4452fbf84e80ef432ecf8314c485210229672fd1" +checksum = "b6a7fc3172c2ef56966b2ce4f84177e159804c40b9a84de8861558ce4a59f422" dependencies = [ "bytemuck", "emath", @@ -781,9 +781,9 @@ dependencies = [ [[package]] name = "egui" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd34cec49ab55d85ebf70139cb1ccd29c977ef6b6ba4fe85489d6877ee9ef3" +checksum = "49e2be082f77715496b4a39fdc6f5dc7491fefe2833111781b8697ea6ee919a7" dependencies = [ "ahash", "bitflags 2.9.1", @@ -791,12 +791,14 @@ dependencies = [ "epaint", "nohash-hasher", "profiling", + "smallvec", + "unicode-segmentation", ] [[package]] name = "egui-baseview" -version = "0.5.0" -source = "git+https://github.com/BillyDM/egui-baseview.git?rev=ec70c3fe6b2f070dcacbc22924431edbe24bd1c0#ec70c3fe6b2f070dcacbc22924431edbe24bd1c0" +version = "0.6.0" +source = "git+https://github.com/BillyDM/egui-baseview.git?rev=b6bcb51312bf9b2b710753475f7efb4cfe39ddef#b6bcb51312bf9b2b710753475f7efb4cfe39ddef" dependencies = [ "baseview", "copypasta", @@ -811,19 +813,18 @@ dependencies = [ [[package]] name = "egui_file" -version = "0.22.0" -source = "git+https://github.com/Ardura/egui_file.git?rev=409258b8858ac1fb881ae2b5e40ff6d4a5cd474a#409258b8858ac1fb881ae2b5e40ff6d4a5cd474a" +version = "0.23.0" +source = "git+https://github.com/Barugon/egui_file.git?rev=28a242a7e921144e9ac442cc00fad6eca01b59e6#28a242a7e921144e9ac442cc00fad6eca01b59e6" dependencies = [ "dyn-clone", - "nih_plug", - "nih_plug_egui", + "egui", ] [[package]] name = "egui_glow" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910906e3f042ea6d2378ec12a6fd07698e14ddae68aed2d819ffe944a73aab9e" +checksum = "d44f3fd4fdc5f960c9e9ef7327c26647edc3141abf96102980647129d49358e6" dependencies = [ "ahash", "bytemuck", @@ -845,18 +846,18 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "emath" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e4cadcff7a5353ba72b7fea76bf2122b5ebdbc68e8155aa56dfdea90083fe1b" +checksum = "935df67dc48fdeef132f2f7ada156ddc79e021344dd42c17f066b956bb88dde3" dependencies = [ "bytemuck", ] [[package]] name = "epaint" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fcc0f5a7c613afd2dee5e4b30c3e6acafb8ad6f0edb06068811f708a67c562" +checksum = "b66fc0a5a9d322917de9bd3ac7d426ca8aa3127fbf1e76fae5b6b25e051e06a3" dependencies = [ "ab_glyph", "ahash", @@ -871,9 +872,9 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7e7a64c02cf7a5b51e745a9e45f60660a286f151c238b9d397b3e923f5082f" +checksum = "4f6cf8ce0fb817000aa24f5e630bda904a353536bd430b83ebc1dceee95b4a3a" [[package]] name = "equivalent" @@ -1486,7 +1487,7 @@ dependencies = [ [[package]] name = "nih_plug" version = "0.0.0" -source = "git+https://github.com/Ardura/nih-plug.git?rev=32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51#32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51" +source = "git+https://github.com/Ardura/nih-plug.git?rev=ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3#ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3" dependencies = [ "anyhow", "anymap3", @@ -1517,7 +1518,7 @@ dependencies = [ [[package]] name = "nih_plug_derive" version = "0.1.0" -source = "git+https://github.com/Ardura/nih-plug.git?rev=32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51#32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51" +source = "git+https://github.com/Ardura/nih-plug.git?rev=ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3#ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3" dependencies = [ "proc-macro2", "quote", @@ -1527,7 +1528,7 @@ dependencies = [ [[package]] name = "nih_plug_egui" version = "0.0.0" -source = "git+https://github.com/Ardura/nih-plug.git?rev=32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51#32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51" +source = "git+https://github.com/Ardura/nih-plug.git?rev=ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3#ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3" dependencies = [ "baseview", "crossbeam", diff --git a/Cargo.toml b/Cargo.toml index b73d3fb..eabd487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Actuate" -version = "1.3.91" +version = "1.4.0" edition = "2021" authors = ["Ardura "] license = "GPL-3.0-or-later" @@ -18,11 +18,11 @@ hound = "3.5.0" lazy_static = "1.4.0" # Nih plug fork for actuate -nih_plug = { git = "https://github.com/Ardura/nih-plug.git", rev = "32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51", features = ["assert_process_allocs"] } -nih_plug_egui = { git = "https://github.com/Ardura/nih-plug.git", rev = "32e4e96d0d9820f28bcfd22a6cde4c05b5c71a51" } +nih_plug = { git = "https://github.com/Ardura/nih-plug.git", rev = "ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3", features = ["assert_process_allocs"] } +nih_plug_egui = { git = "https://github.com/Ardura/nih-plug.git", rev = "ce77d8c6daeebcdcc15e84a178ddce617ee6a3d3" } # egui_file fork for nih-plug/Actuate -egui_file = { git = "https://github.com/Ardura/egui_file.git", rev = "409258b8858ac1fb881ae2b5e40ff6d4a5cd474a" } +egui_file = { git = "https://github.com/Barugon/egui_file.git", rev = "28a242a7e921144e9ac442cc00fad6eca01b59e6" } num-complex = "0.4.4" num-traits = "0.2.17" diff --git a/src/actuate_gui.rs b/src/actuate_gui.rs index 868a929..ebd7824 100644 --- a/src/actuate_gui.rs +++ b/src/actuate_gui.rs @@ -104,10 +104,13 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe let browse_preset_active: Arc = Arc::clone(&instance.browsing_presets); let import_preset_active: Arc = Arc::clone(&instance.importing_presets); let export_preset_active: Arc = Arc::clone(&instance.exporting_presets); + let export_last_sound: Arc = Arc::clone(&instance.export_last_sound); let safety_clip_output: Arc> = Arc::clone(&instance.safety_clip_output); + let hq_mode: Arc = Arc::clone(&instance.hq_mode); let AM1: Arc> = Arc::clone(&instance.audio_module_1); let AM2: Arc> = Arc::clone(&instance.audio_module_2); let AM3: Arc> = Arc::clone(&instance.audio_module_3); + let recorder: Arc> = Arc::clone(&instance.recorder); let update_current_preset: Arc = Arc::clone(&instance.update_current_preset); let filter_select_outside: Arc> = @@ -175,6 +178,10 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe let ext = Some(OsStr::new("actuate")); move |path: &Path| -> bool { path.extension() == ext } }); + let export_filter = Box::new({ + let ext = Some(OsStr::new("wav")); + move |path: &Path| -> bool { path.extension() == ext } + }); let sample_filter = Box::new({ let ext = Some(OsStr::new("wav")); move |path: &Path| -> bool { path.extension() == ext } @@ -218,6 +225,25 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe } ) ); + let export_wav_dialog_main: Arc> = Arc::new( + Mutex::new( + if PathBuf::from((*default_dir.lock().unwrap().clone()).to_string()).exists() { + FileDialog::save_file(Some(PathBuf::from((*default_dir.lock().unwrap().clone()).to_string()))) + //.current_pos([(WIDTH/4) as f32, 10.0]) + .show_files_filter(export_filter) + .keep_on_top(true) + .show_new_folder(false) + .show_rename(false) + } else { + FileDialog::save_file(Some(home_dir.clone())) + //.current_pos([(WIDTH/4) as f32, 10.0]) + .show_files_filter(export_filter) + .keep_on_top(true) + .show_new_folder(false) + .show_rename(false) + } + ) + ); let load_sample_dialog: Arc> = Arc::new( Mutex::new( @@ -348,6 +374,8 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe } if params.filter_cutoff_link.value() { setter.set_parameter(¶ms.filter_cutoff_2, params.filter_cutoff.value()); + // This is set again here so it doesn't mess up FL Automation when linked after modifying + setter.set_parameter(¶ms.filter_cutoff, params.filter_cutoff.value()); } // Assign default colors @@ -437,7 +465,7 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe ui.label(RichText::new("Actuate") .font(FONT) .color(FONT_COLOR)) - .on_hover_text("v1.3.9 by Ardura!"); + .on_hover_text("v1.4.0 by Ardura!"); ui.add_space(2.0); ui.separator(); @@ -992,8 +1020,8 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe export_preset_active.store(true, Ordering::SeqCst); } if export_preset_active.load(Ordering::SeqCst) { - let save_dialock = save_dialog_main.clone(); - let mut save_dialog = save_dialock.lock().unwrap(); + let export_dialock = export_wav_dialog_main.clone(); + let mut save_dialog = export_dialock.lock().unwrap(); save_dialog.open(); let mut dvar = Some(save_dialog); if let Some(s_dialog) = &mut dvar { @@ -1016,6 +1044,42 @@ pub(crate) fn make_actuate_gui(instance: &mut Actuate, _async_executor: AsyncExe } } ui.checkbox(&mut safety_clip_output.lock().unwrap(), "Safety Clip").on_hover_text("Clip the output at 0dB to save your ears/speakers"); + let hq_check = slim_checkbox::AtomicSlimCheckbox::new(&hq_mode, "HQ Rendering"); + ui.add(hq_check); + let export_last_note_button = ui.button(RichText::new("Export Last Note") + .font(SMALLER_FONT) + .background_color(DARK_GREY_UI_COLOR) + .color(TEAL_GREEN) + ); + if export_last_note_button.clicked() { + export_last_sound.store(true, Ordering::SeqCst); + } + if export_last_sound.load(Ordering::SeqCst) { + let save_dialock = save_dialog_main.clone(); + let mut save_dialog = save_dialock.lock().unwrap(); + save_dialog.open(); + let mut dvar = Some(save_dialog); + if let Some(s_dialog) = &mut dvar { + if s_dialog.show(egui_ctx).selected() { + if let Some(file) = s_dialog.path() { + let saved_file = Some(file.to_path_buf()); + let mut str_path = saved_file.unwrap_or_default().as_path().to_str().unwrap().to_string(); + if !str_path.contains(".wav") { + str_path = str_path + ".wav" + } + let _ = recorder.lock().unwrap().export(&str_path); + export_last_sound.store(false, Ordering::SeqCst); + } + } + + match s_dialog.state() { + State::Cancelled | State::Closed => { + export_last_sound.store(false, Ordering::SeqCst); + }, + _ => {} + } + } + } }); const KNOB_SIZE: f32 = 28.0; const TEXT_SIZE: f32 = 11.0; diff --git a/src/audio_module.rs b/src/audio_module.rs index 4ec30a9..7dff668 100644 --- a/src/audio_module.rs +++ b/src/audio_module.rs @@ -25,7 +25,7 @@ use egui_file::{FileDialog, State}; use nih_plug::{ prelude::{Enum, NoteEvent, ParamSetter, Smoother, SmoothingStyle}, util::{self, db_to_gain} }; -use nih_plug_egui::egui::{self, Pos2, Rect, RichText, CornerRadius, ScrollArea, Ui}; +use nih_plug_egui::egui::{self, scroll_area::ScrollSource, CornerRadius, Pos2, Rect, RichText, ScrollArea, Ui}; use pitch_shift::PitchShifter; use rand::Rng; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; @@ -1635,8 +1635,7 @@ Random: Sample uses a new random position every note".to_string()); ui.add_space(1.0); ui.horizontal(|ui| { ScrollArea::horizontal() - .drag_to_scroll(true) - .enable_scrolling(true) + .scroll_source(ScrollSource::ALL) .hscroll(true) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) .max_width(WIDTH as f32 - 238.0) @@ -2505,7 +2504,10 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); cutoff_mod: f32, resonance_mod_2: f32, cutoff_mod_2: f32, - ) -> (f32, f32, bool, bool) { + hq_mode: bool, + raw_cutoff_1: f32, + raw_cutoff_2: f32, + ) -> (f32, f32, bool, bool, usize) { // If the process is in here the file dialog is not open per lib.rs // Midi events are processed here @@ -2948,11 +2950,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); }; } else { // Nothing is in our sample library, skip attempting audio output - return (0.0, 0.0, false, false); + return (0.0, 0.0, false, false, 0); } } else { // Nothing is in our sample library, skip attempting audio output - return (0.0, 0.0, false, false); + return (0.0, 0.0, false, false, 0); } } _ => { @@ -3046,12 +3048,12 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); // TILT Filters tilt_filter_l_1: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff, + raw_cutoff_1, TiltFilter::ResponseType::Lowpass ), tilt_filter_r_1: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff, + raw_cutoff_1, TiltFilter::ResponseType::Lowpass ), // VCF Filters @@ -3107,12 +3109,12 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); // TILT Filters tilt_filter_l_2: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff_2, + raw_cutoff_2, TiltFilter::ResponseType::Lowpass ), tilt_filter_r_2: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff_2, + raw_cutoff_2, TiltFilter::ResponseType::Lowpass ), // VCF Filters @@ -3168,20 +3170,20 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); V4F_r_1: V4FilterStruct::default(), V4F_r_2: V4FilterStruct::default(), // A4I Filter - A4I_l_1: A4iFilter::new(self.filter_cutoff, self.filter_cutoff, self.filter_resonance), - A4I_l_2: A4iFilter::new(self.filter_cutoff_2, self.filter_cutoff_2, self.filter_resonance_2), - A4I_r_1: A4iFilter::new(self.filter_cutoff, self.filter_cutoff, self.filter_resonance), - A4I_r_2: A4iFilter::new(self.filter_cutoff_2, self.filter_cutoff_2, self.filter_resonance_2), + A4I_l_1: A4iFilter::new(raw_cutoff_1, raw_cutoff_1, self.filter_resonance), + A4I_l_2: A4iFilter::new(raw_cutoff_2, raw_cutoff_2, self.filter_resonance_2), + A4I_r_1: A4iFilter::new(raw_cutoff_1, raw_cutoff_1, self.filter_resonance), + A4I_r_2: A4iFilter::new(raw_cutoff_2, raw_cutoff_2, self.filter_resonance_2), // A4II Filter - A4II_l_1: A4iiFilter::new(self.filter_cutoff, self.sample_rate, self.filter_resonance), - A4II_l_2: A4iiFilter::new(self.filter_cutoff_2, self.sample_rate, self.filter_resonance_2), - A4II_r_1: A4iiFilter::new(self.filter_cutoff, self.sample_rate, self.filter_resonance), - A4II_r_2: A4iiFilter::new(self.filter_cutoff_2, self.sample_rate, self.filter_resonance_2), + A4II_l_1: A4iiFilter::new(raw_cutoff_1, self.sample_rate, self.filter_resonance), + A4II_l_2: A4iiFilter::new(raw_cutoff_2, self.sample_rate, self.filter_resonance_2), + A4II_r_1: A4iiFilter::new(raw_cutoff_1, self.sample_rate, self.filter_resonance), + A4II_r_2: A4iiFilter::new(raw_cutoff_2, self.sample_rate, self.filter_resonance_2), // A4III Filter - A4III_l_1: A4iiiFilter::new(self.filter_cutoff, self.sample_rate, self.filter_resonance), - A4III_l_2: A4iiiFilter::new(self.filter_cutoff_2, self.sample_rate, self.filter_resonance_2), - A4III_r_1: A4iiiFilter::new(self.filter_cutoff, self.sample_rate, self.filter_resonance), - A4III_r_2: A4iiiFilter::new(self.filter_cutoff_2, self.sample_rate, self.filter_resonance_2), + A4III_l_1: A4iiiFilter::new(raw_cutoff_1, self.sample_rate, self.filter_resonance), + A4III_l_2: A4iiiFilter::new(raw_cutoff_2, self.sample_rate, self.filter_resonance_2), + A4III_r_1: A4iiiFilter::new(raw_cutoff_1, self.sample_rate, self.filter_resonance), + A4III_r_2: A4iiiFilter::new(raw_cutoff_2, self.sample_rate, self.filter_resonance_2), cutoff_modulation: cutoff_mod, cutoff_modulation_2: cutoff_mod_2, @@ -3193,11 +3195,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); // POLYFILTER FILTER ATTACK UPDATES // Reset our attack to start from the filter cutoff - new_voice.filter_atk_smoother_1.reset(self.filter_cutoff); + new_voice.filter_atk_smoother_1.reset(raw_cutoff_1); // Since we're in attack state at the start of our note we need to setup the attack going to the env peak new_voice.filter_atk_smoother_1.set_target( self.sample_rate, - (self.filter_cutoff + (raw_cutoff_1 + ( // This scales the peak env to be much gentler for the TILT filter match self.filter_alg_type { @@ -3215,11 +3217,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); ); // Reset our attack to start from the filter cutoff 2 - new_voice.filter_atk_smoother_2.reset(self.filter_cutoff_2); + new_voice.filter_atk_smoother_2.reset(raw_cutoff_2); // Since we're in attack state at the start of our note we need to setup the attack going to the env peak new_voice.filter_atk_smoother_2.set_target( self.sample_rate, - (self.filter_cutoff_2 + (raw_cutoff_2 + ( // This scales the peak env to be much gentler for the TILT filter match self.filter_alg_type_2 { @@ -4190,12 +4192,12 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); // TILT Filters tilt_filter_l_1: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff, + raw_cutoff_1, TiltFilter::ResponseType::Lowpass ), tilt_filter_r_1: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff, + raw_cutoff_1, TiltFilter::ResponseType::Lowpass ), // VCF Filters @@ -4212,12 +4214,12 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); // TILT Filters tilt_filter_l_2: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff_2, + raw_cutoff_2, TiltFilter::ResponseType::Lowpass ), tilt_filter_r_2: TiltFilterStruct::new( self.sample_rate, - self.filter_cutoff_2, + raw_cutoff_2, TiltFilter::ResponseType::Lowpass ), // VCF Filters @@ -4234,20 +4236,20 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); V4F_r_1: V4FilterStruct::default(), V4F_r_2: V4FilterStruct::default(), // A4I Filter - A4I_l_1: A4iFilter::new(self.sample_rate, self.filter_cutoff, 0.0), - A4I_l_2: A4iFilter::new(self.sample_rate, self.filter_cutoff_2, 0.0), - A4I_r_1: A4iFilter::new(self.sample_rate, self.filter_cutoff, 0.0), - A4I_r_2: A4iFilter::new(self.sample_rate, self.filter_cutoff_2, 0.0), + A4I_l_1: A4iFilter::new(self.sample_rate, raw_cutoff_1, 0.0), + A4I_l_2: A4iFilter::new(self.sample_rate, raw_cutoff_2, 0.0), + A4I_r_1: A4iFilter::new(self.sample_rate, raw_cutoff_1, 0.0), + A4I_r_2: A4iFilter::new(self.sample_rate, raw_cutoff_2, 0.0), // A4II Filter - A4II_l_1: A4iiFilter::new(self.filter_cutoff, self.sample_rate, 0.0), - A4II_l_2: A4iiFilter::new(self.filter_cutoff_2, self.sample_rate, 0.0), - A4II_r_1: A4iiFilter::new(self.filter_cutoff, self.sample_rate, 0.0), - A4II_r_2: A4iiFilter::new(self.filter_cutoff_2, self.sample_rate, 0.0), + A4II_l_1: A4iiFilter::new(raw_cutoff_1, self.sample_rate, 0.0), + A4II_l_2: A4iiFilter::new(raw_cutoff_2, self.sample_rate, 0.0), + A4II_r_1: A4iiFilter::new(raw_cutoff_1, self.sample_rate, 0.0), + A4II_r_2: A4iiFilter::new(raw_cutoff_2, self.sample_rate, 0.0), // A4III Filter - A4III_l_1: A4iiiFilter::new(self.filter_cutoff, self.sample_rate, 0.0), - A4III_l_2: A4iiiFilter::new(self.filter_cutoff_2, self.sample_rate, 0.0), - A4III_r_1: A4iiiFilter::new(self.filter_cutoff, self.sample_rate, 0.0), - A4III_r_2: A4iiiFilter::new(self.filter_cutoff_2, self.sample_rate, 0.0), + A4III_l_1: A4iiiFilter::new(raw_cutoff_1, self.sample_rate, 0.0), + A4III_l_2: A4iiiFilter::new(raw_cutoff_2, self.sample_rate, 0.0), + A4III_r_1: A4iiiFilter::new(raw_cutoff_1, self.sample_rate, 0.0), + A4III_r_2: A4iiiFilter::new(raw_cutoff_2, self.sample_rate, 0.0), cutoff_modulation: cutoff_mod, cutoff_modulation_2: cutoff_mod_2, resonance_modulation: 0.0, @@ -4384,55 +4386,55 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); let temp_center_voices = match self.audio_module_type { AudioModuleType::Sine => { - Oscillator::get_sine(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_sine(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Tri => { - Oscillator::get_tri(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_tri(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Saw => { - Oscillator::get_saw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_saw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::RSaw => { - Oscillator::get_rsaw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_rsaw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::WSaw => { - Oscillator::get_wsaw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_wsaw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::RASaw => { - Oscillator::get_rasaw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_rasaw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::SSaw => { - Oscillator::get_ssaw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_ssaw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Ramp => { - Oscillator::get_ramp(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_ramp(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Square => { - Oscillator::get_square(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_square(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::RSquare => { - Oscillator::get_rsquare(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_rsquare(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Pulse => { - Oscillator::get_pulse(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_pulse(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Noise => { self.noise_obj.generate_sample() * temp_osc_gain_multiplier }, AudioModuleType::BentSaw => { - Oscillator::get_bent_Saw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_bent_Saw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::ScSaw => { - Oscillator::get_s_cubic_saw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_s_cubic_saw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::AsymSaw => { - Oscillator::get_asym_saw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_asym_saw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::SkewSaw => { - Oscillator::get_skew_saw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_skew_saw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::StepSaw => { - Oscillator::get_step_saw(voice.phase) * temp_osc_gain_multiplier + Oscillator::get_step_saw(voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Additive | AudioModuleType::Granulizer | AudioModuleType::Off | AudioModuleType::UnsetAm | AudioModuleType::Sampler => 0.0, }; @@ -4513,55 +4515,55 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); let temp_unison_voice_out = match self.audio_module_type { AudioModuleType::Sine => { - Oscillator::get_sine(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_sine(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Tri => { - Oscillator::get_tri(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_tri(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Saw => { - Oscillator::get_saw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_saw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::RSaw => { - Oscillator::get_rsaw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_rsaw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::WSaw => { - Oscillator::get_wsaw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_wsaw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::RASaw => { - Oscillator::get_rasaw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_rasaw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::SSaw => { - Oscillator::get_ssaw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_ssaw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Ramp => { - Oscillator::get_ramp(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_ramp(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Square => { - Oscillator::get_square(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_square(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::RSquare => { - Oscillator::get_rsquare(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_rsquare(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Pulse => { - Oscillator::get_pulse(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_pulse(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Noise => { self.noise_obj.generate_sample() * temp_osc_gain_multiplier }, AudioModuleType::BentSaw => { - Oscillator::get_bent_Saw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_bent_Saw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::ScSaw => { - Oscillator::get_s_cubic_saw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_s_cubic_saw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::AsymSaw => { - Oscillator::get_asym_saw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_asym_saw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::SkewSaw => { - Oscillator::get_skew_saw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_skew_saw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::StepSaw => { - Oscillator::get_step_saw(internal_unison_voice.phase) * temp_osc_gain_multiplier + Oscillator::get_step_saw(internal_unison_voice.phase, hq_mode) * temp_osc_gain_multiplier }, AudioModuleType::Additive | AudioModuleType::Granulizer | AudioModuleType::Off | AudioModuleType::UnsetAm | AudioModuleType::Sampler => 0.0, }; @@ -4606,11 +4608,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_1.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_1.next(), OscState::Sustaining => voice.filter_dec_smoother_1.next(), - OscState::Off => self.filter_cutoff, + OscState::Off => raw_cutoff_1, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_1.set_target(self.sample_rate, self.filter_cutoff); + voice.filter_rel_smoother_1.set_target(self.sample_rate, raw_cutoff_1); } // If our attack has finished @@ -4625,7 +4627,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_1.set_target( self.sample_rate, ( - self.filter_cutoff * (self.filter_env_sustain / 1999.9) + raw_cutoff_1 * (self.filter_env_sustain / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -4656,7 +4658,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), + _ => (raw_cutoff_1 + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), }; } @@ -4673,11 +4675,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_2.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_2.next(), OscState::Sustaining => voice.filter_dec_smoother_2.next(), - OscState::Off => self.filter_cutoff_2, + OscState::Off => raw_cutoff_2, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_2.set_target(self.sample_rate, self.filter_cutoff_2); + voice.filter_rel_smoother_2.set_target(self.sample_rate, raw_cutoff_2); } // If our attack has finished @@ -4692,7 +4694,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_2.set_target( self.sample_rate, ( - self.filter_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) + raw_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -4722,7 +4724,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), + _ => (raw_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), }; } @@ -5101,11 +5103,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_1.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_1.next(), OscState::Sustaining => voice.filter_dec_smoother_1.next(), - OscState::Off => self.filter_cutoff, + OscState::Off => raw_cutoff_1, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_1.set_target(self.sample_rate, self.filter_cutoff); + voice.filter_rel_smoother_1.set_target(self.sample_rate, raw_cutoff_1); } // If our attack has finished @@ -5120,7 +5122,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_1.set_target( self.sample_rate, ( - self.filter_cutoff * (self.filter_env_sustain / 1999.9) + raw_cutoff_1 * (self.filter_env_sustain / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -5151,7 +5153,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), + _ => (raw_cutoff_1 + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), }; } @@ -5168,11 +5170,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_2.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_2.next(), OscState::Sustaining => voice.filter_dec_smoother_2.next(), - OscState::Off => self.filter_cutoff_2, + OscState::Off => raw_cutoff_2, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_2.set_target(self.sample_rate, self.filter_cutoff_2); + voice.filter_rel_smoother_2.set_target(self.sample_rate, raw_cutoff_2); } // If our attack has finished @@ -5187,7 +5189,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_2.set_target( self.sample_rate, ( - self.filter_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) + raw_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -5217,7 +5219,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), + _ => (raw_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), }; } @@ -5566,11 +5568,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_1.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_1.next(), OscState::Sustaining => voice.filter_dec_smoother_1.next(), - OscState::Off => self.filter_cutoff, + OscState::Off => raw_cutoff_1, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_1.set_target(self.sample_rate, self.filter_cutoff); + voice.filter_rel_smoother_1.set_target(self.sample_rate, raw_cutoff_1); } // If our attack has finished @@ -5585,7 +5587,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_1.set_target( self.sample_rate, ( - self.filter_cutoff * (self.filter_env_sustain / 1999.9) + raw_cutoff_1 * (self.filter_env_sustain / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -5616,7 +5618,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), + _ => (raw_cutoff_1 + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), }; } @@ -5633,11 +5635,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_2.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_2.next(), OscState::Sustaining => voice.filter_dec_smoother_2.next(), - OscState::Off => self.filter_cutoff_2, + OscState::Off => raw_cutoff_2, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_2.set_target(self.sample_rate, self.filter_cutoff_2); + voice.filter_rel_smoother_2.set_target(self.sample_rate, raw_cutoff_2); } // If our attack has finished @@ -5652,7 +5654,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_2.set_target( self.sample_rate, ( - self.filter_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) + raw_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -5682,7 +5684,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), + _ => (raw_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), }; } @@ -5959,11 +5961,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_1.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_1.next(), OscState::Sustaining => voice.filter_dec_smoother_1.next(), - OscState::Off => self.filter_cutoff, + OscState::Off => raw_cutoff_1, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_1.set_target(self.sample_rate, self.filter_cutoff); + voice.filter_rel_smoother_1.set_target(self.sample_rate, raw_cutoff_1); } // If our attack has finished @@ -5978,7 +5980,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_1.set_target( self.sample_rate, ( - self.filter_cutoff * (self.filter_env_sustain / 1999.9) + raw_cutoff_1 * (self.filter_env_sustain / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -6009,7 +6011,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), + _ => (raw_cutoff_1 + voice.cutoff_modulation + cutoff_mod).clamp(20.0, 20000.0), }; } @@ -6026,11 +6028,11 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); OscState::Attacking => voice.filter_atk_smoother_2.next(), OscState::Decaying | OscState::Releasing => voice.filter_dec_smoother_2.next(), OscState::Sustaining => voice.filter_dec_smoother_2.next(), - OscState::Off => self.filter_cutoff_2, + OscState::Off => raw_cutoff_2, }, ); // Move release to the cutoff to end - voice.filter_rel_smoother_2.set_target(self.sample_rate, self.filter_cutoff_2); + voice.filter_rel_smoother_2.set_target(self.sample_rate, raw_cutoff_2); } // If our attack has finished @@ -6045,7 +6047,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); voice.filter_dec_smoother_2.set_target( self.sample_rate, ( - self.filter_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) + raw_cutoff_2 * (self.filter_env_sustain_2 / 1999.9) ).clamp(20.0, 20000.0), ); } @@ -6075,7 +6077,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); } } // I don't expect this to be used - _ => (self.filter_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), + _ => (raw_cutoff_2 + voice.cutoff_modulation_2 + cutoff_mod_2).clamp(20.0, 20000.0), }; } @@ -6229,7 +6231,7 @@ MRandom: Every voice uses its own unique random phase every note".to_string()); }; // Send it back - (output_signal_l, output_signal_r, note_on, note_off) + (output_signal_l, output_signal_r, note_on, note_off, self.playing_voices.len()) } pub fn set_playing(&mut self, new_bool: bool) { diff --git a/src/audio_module/Oscillator.rs b/src/audio_module/Oscillator.rs index 5413ced..b031dc0 100644 --- a/src/audio_module/Oscillator.rs +++ b/src/audio_module/Oscillator.rs @@ -1,3 +1,5 @@ +use std::f32::consts::{self, FRAC_2_PI, PI}; + use nih_plug::params::enums::Enum; //use rand::{rngs::StdRng, Rng, SeedableRng}; use rand::Rng; @@ -382,130 +384,237 @@ pub enum RetriggerStyle { } // Sine wave oscillator with lerp smoothing -pub fn get_sine(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - let frac = phase * (TABLE_SIZE - 1) as f32 - index as f32; - let next_index = index + 1; - - let sine = if next_index < TABLE_SIZE - 1 { - SIN_TABLE[index] * (1.0 - frac) + SIN_TABLE[next_index] * frac +pub fn get_sine(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + let phase_angle = phase * 2.0 * PI; + phase_angle.sin() } else { - SIN_TABLE[index] // If next_index is out of bounds, use the current index - }; - sine + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + let frac = phase * (TABLE_SIZE - 1) as f32 - index as f32; + let next_index = index + 1; + + let sine = if next_index < TABLE_SIZE - 1 { + SIN_TABLE[index] * (1.0 - frac) + SIN_TABLE[next_index] * frac + } else { + SIN_TABLE[index] // If next_index is out of bounds, use the current index + }; + sine + } } // Rounded Saw Wave with rounding amount -pub fn get_rsaw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return RSAW_TABLE[index]; +pub fn get_rsaw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + let scaled_phase = -1.0 + 2.0 * phase; + // Calculate the rounded sawtooth waveform directly + scaled_phase * (1.0 - scaled_phase.powi(30)) + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return RSAW_TABLE[index]; + } } // Rounded Saw Wave with analog-ey modification -pub fn get_rasaw(phase: f32) -> f32 { +pub fn get_rasaw(phase: f32, hq_mode: bool) -> f32 { let index = (phase * (TABLE_SIZE - 1) as f32) as usize; let mut rng = rand::thread_rng(); - let random_int: u32 = rng.gen_range(0..=2); - // Based on our int, use the three seed-noise tables - match random_int { - 0 => { - return ASAW_TABLE_1[index]; - } - 1 => { - return ASAW_TABLE_2[index]; - } - 2 => { - return ASAW_TABLE_3[index]; - } - _ => { - return 0.0; + if hq_mode { + let randomness = rng.gen_range(-0.009..0.009); // Adjust the range of randomness + + let scaled_phase = -1.0 + 2.0 * (phase + randomness); + + // Calculate the rounded sawtooth waveform directly + scaled_phase * (1.0 - scaled_phase.powi(60)) + } else { + let random_int: u32 = rng.gen_range(0..=2); + // Based on our int, use the three seed-noise tables + match random_int { + 0 => { + return ASAW_TABLE_1[index]; + } + 1 => { + return ASAW_TABLE_2[index]; + } + 2 => { + return ASAW_TABLE_3[index]; + } + _ => { + return 0.0; + } } } } // Saw Wave -pub fn get_saw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return SAW_TABLE[index]; +pub fn get_saw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + -1.0 + 2.0 * phase + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return SAW_TABLE[index]; + } } // "Analog" inspired "whiter" Saw wave -pub fn get_wsaw(phase: f32) -> f32 { +pub fn get_wsaw(phase: f32, hq_mode: bool) -> f32 { let index = (phase * (TABLE_SIZE - 1) as f32) as usize; let mut rng = rand::thread_rng(); - let random_bool: bool = rng.gen(); - // Based on our random bool, obtain the Saw waveforms with seed-introduced randomness waveform tables - if random_bool { - return WSAW_TABLE_1[index]; + if hq_mode { + let randomness = rng.gen_range(-0.1..0.1); + -1.0 + 2.0 * (phase + randomness) } else { - return WSAW_TABLE_2[index]; + let random_bool: bool = rng.gen(); + // Based on our random bool, obtain the Saw waveforms with seed-introduced randomness waveform tables + if random_bool { + return WSAW_TABLE_1[index]; + } else { + return WSAW_TABLE_2[index]; + } } } // "Analog" inspired "subtle warm" Saw wave -pub fn get_ssaw(phase: f32) -> f32 { +pub fn get_ssaw(phase: f32, hq_mode: bool) -> f32 { let index = (phase * (TABLE_SIZE - 1) as f32) as usize; let mut rng = rand::thread_rng(); - let random_bool: bool = rng.gen(); - // Based on our random bool, obtain the Saw waveforms with seed-introduced randomness waveform tables - if random_bool { - return SSAW_TABLE_1[index]; + if hq_mode { + let randomness = rng.gen_range(-0.01..0.01); // Adjust the range of randomness + -1.0 + 2.0 * (phase + randomness) } else { - return SSAW_TABLE_2[index]; + let random_bool: bool = rng.gen(); + // Based on our random bool, obtain the Saw waveforms with seed-introduced randomness waveform tables + if random_bool { + return SSAW_TABLE_1[index]; + } else { + return SSAW_TABLE_2[index]; + } } } // Ramp Wave -pub fn get_ramp(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return RAMP_TABLE[index]; +pub fn get_ramp(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + let scaled_phase = -1.0 + 2.0 * phase; + // Calculate the ramp wave directly + -scaled_phase % consts::TAU + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return RAMP_TABLE[index]; + } } // Square Wave -pub fn get_square(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return SQUARE_TABLE[index]; +pub fn get_square(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + // Calculate the square wave directly + if phase < 0.5 { + 1.0 // Positive phase half + } else { + -1.0 // Negative phase half + } + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return SQUARE_TABLE[index]; + } } // 1/4 Pulse Wave -pub fn get_pulse(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return PULSE_TABLE[index]; +pub fn get_pulse(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + // Calculate the pulse wave directly + if phase < 0.25 { + 1.0 // Positive phase quarter + } else { + -1.0 // Negative phase three-quarters + } + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return PULSE_TABLE[index]; + } } -pub fn get_rsquare(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return RSQUARE_TABLE[index]; +pub fn get_rsquare(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + let mod_scaled: i32 = scale_range(0.15, 2.0, 8.0).floor() as i32 * 2; + + let scaled_phase = -1.0 + 2.0 * phase; + + // Calculate the rounded square wave directly + if scaled_phase < 0.0 { + (2.0 * scaled_phase + 1.0).powi(mod_scaled) - 1.0 + } else { + -(2.0 * scaled_phase - 1.0).powi(mod_scaled) + 1.0 + } + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return RSQUARE_TABLE[index]; + } } -pub fn get_tri(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return TRI_TABLE[index]; +pub fn get_tri(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + return (FRAC_2_PI) * (((2.0 * PI) * phase).sin()).asin(); + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return TRI_TABLE[index]; + } } -pub fn get_skew_saw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return SKEW_SAW_TABLE[index]; +pub fn get_skew_saw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + // Use an exponential function to skew the phase + 2.0 * phase.powf(0.3) - 1.0 + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return SKEW_SAW_TABLE[index]; + } } -pub fn get_bent_Saw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return BENT_SAW_TABLE[index]; +pub fn get_bent_Saw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + if phase < 0.4 { + -1.0 + (2.5) * phase / 0.4 + } else { + let out = -1.0 + (1.5) * (phase - 0.4) / (0.6) + (2.5); + out - 2.0 // Normalize to -1 to 1 + } + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return BENT_SAW_TABLE[index]; + } } -pub fn get_step_saw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return STEP_SAW_TABLE[index]; +pub fn get_step_saw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + let step_size = TABLE_SIZE / 9_usize; + let step_index = phase / step_size as f32; + 2.0 * (step_index / 8.0) - 1.0 + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return STEP_SAW_TABLE[index]; + } } -pub fn get_s_cubic_saw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return S_CUBIC_SAW_TABLE[index]; +pub fn get_s_cubic_saw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + -4.0 * phase.powi(3) + 6.0 * phase.powi(2) - 1.0 + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return S_CUBIC_SAW_TABLE[index]; + } } -pub fn get_asym_saw(phase: f32) -> f32 { - let index = (phase * (TABLE_SIZE - 1) as f32) as usize; - return ASYM_SAW_TABLE[index]; +pub fn get_asym_saw(phase: f32, hq_mode: bool) -> f32 { + if hq_mode { + if phase < 0.5 { + -1.0 + 2.0 * phase.powf(1.5) + } else { + 1.0 - 2.0 * (1.0 - phase).powf(1.5) + } + } else { + let index = (phase * (TABLE_SIZE - 1) as f32) as usize; + return ASYM_SAW_TABLE[index]; + } } // Bard helped me out on this one @@ -536,3 +645,7 @@ impl DeterministicWhiteNoiseGenerator { x } } + +pub fn scale_range(value: f32, out_min: f32, out_max: f32) -> f32 { + out_min + value * (out_max - out_min) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 142a544..77e7bdf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,7 @@ mod audio_module; mod fx; mod old_preset_structs; mod release_downloader; +mod recorder; // Plugin sizing const WIDTH: u32 = 920; @@ -101,8 +102,10 @@ pub struct Actuate { browsing_presets: Arc, importing_presets: Arc, exporting_presets: Arc, + export_last_sound: Arc, update_current_preset: Arc, safety_clip_output: Arc>, + hq_mode: Arc, current_note_on_velocity: Arc, @@ -211,6 +214,9 @@ pub struct Actuate { // Download controller download_in_progress: Arc, download_status: Arc>, + + // Recorder + recorder: Arc>, } impl Default for Actuate { @@ -225,6 +231,7 @@ impl Default for Actuate { // Studio One fix for internal windows let importing_presets = Arc::new(AtomicBool::new(false)); let exporting_presets = Arc::new(AtomicBool::new(false)); + let export_last_sound = Arc::new(AtomicBool::new(false)); // End Studio One fix for internal windows // Safety Clipper @@ -255,7 +262,9 @@ impl Default for Actuate { safety_clip_output: safety_clip_output, importing_presets: importing_presets, exporting_presets: exporting_presets, + export_last_sound: export_last_sound, update_current_preset: update_current_preset, + hq_mode: Arc::new(AtomicBool::new(false)), current_note_on_velocity: Arc::new(AtomicF32::new(0.0)), @@ -367,6 +376,7 @@ impl Default for Actuate { preset_browser_lite_db: Arc::new(RwLock::new(HashMap::new())), download_in_progress: Arc::new(AtomicBool::new(false)), download_status: Arc::new(Mutex::new(String::new())), + recorder: Arc::new(Mutex::new(recorder::Recorder::new(44100.0, 10))), } } } @@ -3552,6 +3562,10 @@ impl Plugin for Actuate { _aux: &mut AuxiliaryBuffers, context: &mut impl ProcessContext, ) -> ProcessStatus { + if context.transport().sample_rate != self.sample_rate { + self.sample_rate = context.transport().sample_rate; + } + // Clear any voices on change of module type (especially during play) // This fixes panics and other broken things attempting to play during preset change/load if self.clear_voices.clone().load(Ordering::SeqCst) { @@ -4765,6 +4779,11 @@ impl Actuate { let mut fm_wave_1: f32 = 0.0; let mut fm_wave_2: f32 = 0.0; + + let mut voices_1: usize = 0; + let mut voices_2: usize = 0; + let mut voices_3: usize = 0; + // Since File Dialog can be set by any of these we need to check each time if !self.file_dialog.load(Ordering::SeqCst) //&& self.params.audio_module_1_type.value() != AudioModuleType::Off @@ -4776,6 +4795,7 @@ impl Actuate { wave1_r, reset_filter_controller1, note_off_filter_controller1, + voices_1 ) = am1_lock.process( sample_id, midi_event.clone(), @@ -4809,6 +4829,9 @@ impl Actuate { + modulations_2.temp_mod_cutoff_2 + modulations_3.temp_mod_cutoff_2 + modulations_4.temp_mod_cutoff_2, + self.hq_mode.load(Ordering::Relaxed), + self.params.filter_cutoff.value(), + self.params.filter_cutoff_2.value() ); // Sum to MONO fm_wave_1 = (wave1_l + wave1_r)/2.0; @@ -4829,6 +4852,7 @@ impl Actuate { wave2_r, reset_filter_controller2, note_off_filter_controller2, + voices_2, ) = am2_lock.process( sample_id, midi_event.clone(), @@ -4862,6 +4886,9 @@ impl Actuate { + modulations_2.temp_mod_cutoff_2 + modulations_3.temp_mod_cutoff_2 + modulations_4.temp_mod_cutoff_2, + self.hq_mode.load(Ordering::Relaxed), + self.params.filter_cutoff.value(), + self.params.filter_cutoff_2.value() ); // Sum to MONO fm_wave_2 = (wave2_l + wave2_r)/2.0; @@ -4882,6 +4909,7 @@ impl Actuate { wave3_r, reset_filter_controller3, note_off_filter_controller3, + voices_3 ) = am3_lock.process( sample_id, midi_event.clone(), @@ -4915,6 +4943,9 @@ impl Actuate { + modulations_2.temp_mod_cutoff_2 + modulations_3.temp_mod_cutoff_2 + modulations_4.temp_mod_cutoff_2, + self.hq_mode.load(Ordering::Relaxed), + self.params.filter_cutoff.value(), + self.params.filter_cutoff_2.value() ); // I know this isn't a perfect 3rd, but 0.01 is acceptable headroom let levelAmp3 = self.params.audio_module_3_level.value(); @@ -4969,6 +5000,8 @@ impl Actuate { } // Try to trigger our filter mods on note on! This is sequential/single because we just need a trigger at a point in time if reset_filter_controller1 || reset_filter_controller2 || reset_filter_controller3 { + self.recorder.lock().unwrap().reset(); + // Set our filter in attack state self.fm_state = OscState::Attacking; // Consume our params for smoothing @@ -5177,6 +5210,10 @@ impl Actuate { left_output = (wave1_l + wave2_l + wave3_l)*0.33; right_output = (wave1_r + wave2_r + wave3_r)*0.33; + if (voices_1 + voices_2 + voices_3) > 0 { + self.recorder.lock().unwrap().push(left_output, right_output); + } + // FX //////////////////////////////////////////////////////////////////////////////////////// if self.params.use_fx.value() { diff --git a/src/recorder.rs b/src/recorder.rs new file mode 100644 index 0000000..95ee2e7 --- /dev/null +++ b/src/recorder.rs @@ -0,0 +1,54 @@ +use hound; + +pub struct Recorder { + buffer: Vec, + sample_rate: u32, + max_samples: usize, +} + +impl Recorder { + pub fn new(sample_rate: f32, max_seconds: u32) -> Self { + let max_samples = sample_rate as usize * max_seconds as usize * 2_usize; + let sr_usize = sample_rate as u32; + Self { + buffer: Vec::with_capacity(max_samples), + sample_rate: sr_usize, + max_samples, + } + } + + pub fn reset(&mut self) { + self.buffer.clear(); + } + + pub fn push(&mut self, left: f32, right: f32) { + if self.buffer.len() + 2 <= self.max_samples { + self.buffer.push(left); + self.buffer.push(right); + } + } + + // Export WAV file + pub fn export(&self, path: &str) -> hound::Result<()> { + let spec = hound::WavSpec { + channels: 2, + sample_rate: self.sample_rate, + bits_per_sample: 32, + sample_format: hound::SampleFormat::Float, + }; + + let mut writer = hound::WavWriter::create(path, spec)?; + let mut silence_tracker: Vec = Vec::new(); + for &sample in &self.buffer { + if sample == 0.0 { + silence_tracker.push(0.0); + } + if silence_tracker.len() > 100 { + break; + } + writer.write_sample(sample)?; + } + writer.finalize()?; + Ok(()) + } +}