/*
Copyright (C) 2023 Ardura
This program is free software:
you can redistribute it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation,either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program.
If not, see https://www.gnu.org/licenses/.
#####################################
Audio Module by Ardura
This is intended to be a generic implementation that can be extended for other audio code as generators
#####################################
*/
use nih_plug::{
params::enums::Enum,
prelude::{NoteEvent, ParamSetter, Smoother, SmoothingStyle},
util,
};
use nih_plug_egui::egui::{RichText, Ui};
use pitch_shift::PitchShifter;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, f32::consts::SQRT_2, path::PathBuf, sync::Arc};
// Audio module files
pub(crate) mod Oscillator;
use self::Oscillator::{DeterministicWhiteNoiseGenerator, OscState, RetriggerStyle, SmoothStyle};
#[allow(unused_imports)]
use crate::{
toggle_switch,
ui_knob,
ActuateParams,
CustomWidgets::{CustomParamSlider, CustomVerticalSlider},
A_BACKGROUND_COLOR_TOP,
// UI Colors
A_KNOB_OUTSIDE_COLOR,
DARK_GREY_UI_COLOR,
FONT_COLOR,
LIGHTER_GREY_UI_COLOR,
LIGHTER_PURPLE,
LIGHT_GREY_UI_COLOR,
SMALLER_FONT,
SYNTH_BARS_PURPLE,
SYNTH_MIDDLE_BLUE,
SYNTH_SOFT_BLUE,
SYNTH_SOFT_BLUE2,
PitchRouting
};
use CustomParamSlider::ParamSlider as HorizontalParamSlider;
use CustomVerticalSlider::ParamSlider as VerticalParamSlider;
use Oscillator::VoiceType;
// When you create a new audio module, you should add it here
#[derive(Enum, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum AudioModuleType {
Off,
Osc,
Sampler,
Granulizer,
}
#[derive(Clone)]
struct VoiceVec {
/// The identifier for this voice
voices: VecDeque<SingleVoice>,
}
// This is the information used to track voices and midi inputs
// I made single voice a struct so that we can add and remove via struct instead of running
// into any threading issues trying to modify different vecs in the same function rather than 1 struct
// Underscores are to get rid of the compiler warning thinking it's not used but it's stored for debugging or passed between structs
// and still functional.
#[derive(Clone)]
struct SingleVoice {
/// The note's key/note, in `0..128`. Only used for the voice terminated event.
note: u8,
/// Velocity of our note
_velocity: f32,
/// Mod amount for velocity inputted to this AM
vel_mod_amount: f32,
/// The voice's current phase.
phase: f32,
/// The phase increment. This is based on the voice's frequency, derived from the note index.
phase_delta: f32,
/// Oscillator state for amplitude controlling
state: Oscillator::OscState,
// These are the attack and release smoothers
amp_current: f32,
osc_attack: Smoother<f32>,
osc_decay: Smoother<f32>,
osc_release: Smoother<f32>,
// Pitch modulation info
pitch_enabled: bool,
pitch_current: f32,
pitch_env_peak: f32,
pitch_state: Oscillator::OscState,
pitch_attack: Smoother<f32>,
pitch_decay: Smoother<f32>,
pitch_release: Smoother<f32>,
// Pitch modulation info 2
pitch_enabled_2: bool,
pitch_current_2: f32,
pitch_env_peak_2: f32,
pitch_state_2: Oscillator::OscState,
pitch_attack_2: Smoother<f32>,
pitch_decay_2: Smoother<f32>,
pitch_release_2: Smoother<f32>,
// Final info for a note to work
_detune: f32,
_unison_detune_value: f32,
frequency: f32,
_attack_time: f32,
_decay_time: f32,
_release_time: f32,
_retrigger: RetriggerStyle,
_voice_type: Oscillator::VoiceType,
// This is only used for unison detunes
_angle: f32,
// Sampler/Granulizer Pos
sample_pos: usize,
loop_it: bool,
grain_start_pos: usize,
_granular_hold: i32,
_granular_gap: i32,
granular_hold_end: usize,
next_grain_pos: usize,
_end_position: usize,
_granular_crossfade: i32,
grain_attack: Smoother<f32>,
grain_release: Smoother<f32>,
grain_state: GrainState,
}
#[derive(Enum, PartialEq, Clone, Copy, Serialize, Deserialize)]
enum GrainState {
Attacking,
Releasing,
}
#[derive(Clone)]
pub struct AudioModule {
// Stored sample rate in case the audio module needs it
sample_rate: f32,
pub audio_module_type: AudioModuleType,
// This flipflops stereo with 2 voices to make it make some sense to our ears
two_voice_stereo_flipper: bool,
///////////////////////////////////////////////////////////
// Audio modules should go here as a struct
//
// Osc doesn't have one though
//
///////////////////////////////////////////////////////////
// Granulizer/Sampler
pub loaded_sample: Vec<Vec<f32>>,
// Hold calculated notes
pub sample_lib: Vec<Vec<Vec<f32>>>,
// Treat this like a wavetable synth would
pub loop_wavetable: bool,
// Shift notes like a single cycle - aligned wth 3xosc
pub single_cycle: bool,
// Restretch length with tracking bool
pub restretch: bool,
pub prev_restretch: bool,
// Granulizer other options
pub start_position: f32,
pub _end_position: f32,
pub grain_hold: i32,
pub grain_gap: i32,
pub grain_crossfade: i32,
///////////////////////////////////////////////////////////
// Stored params from main lib here on a per-module basis
// Osc module knob storage
pub osc_type: VoiceType,
pub osc_octave: i32,
pub osc_semitones: i32,
pub osc_detune: f32,
pub osc_attack: f32,
pub osc_decay: f32,
pub osc_sustain: f32,
pub osc_release: f32,
pub osc_mod_amount: f32,
pub osc_retrigger: RetriggerStyle,
pub osc_atk_curve: SmoothStyle,
pub osc_dec_curve: SmoothStyle,
pub osc_rel_curve: SmoothStyle,
pub osc_unison: i32,
pub osc_unison_detune: f32,
pub osc_stereo: f32,
// Voice storage
playing_voices: VoiceVec,
unison_voices: VoiceVec,
// Tracking stopping voices too
is_playing: bool,
// Noise variables
noise_obj: Oscillator::DeterministicWhiteNoiseGenerator,
// Pitch mod storage
pitch_enable: bool,
pitch_env_peak: f32,
pitch_env_attack: f32,
pitch_env_decay: f32,
pitch_env_sustain: f32,
pitch_env_release: f32,
pitch_env_atk_curve: SmoothStyle,
pitch_env_dec_curve: SmoothStyle,
pitch_env_rel_curve: SmoothStyle,
pitch_enable_2: bool,
pitch_env_peak_2: f32,
pitch_env_attack_2: f32,
pitch_env_decay_2: f32,
pitch_env_sustain_2: f32,
pitch_env_release_2: f32,
pitch_env_atk_curve_2: SmoothStyle,
pitch_env_dec_curve_2: SmoothStyle,
pitch_env_rel_curve_2: SmoothStyle,
}
// When you create a new audio module you need to add its default creation here as well
#[allow(overflowing_literals)]
impl Default for AudioModule {
fn default() -> Self {
Self {
// Audio modules will use these
sample_rate: 44100.0,
audio_module_type: AudioModuleType::Osc,
two_voice_stereo_flipper: true,
// Granulizer/Sampler
loaded_sample: vec![vec![0.0, 0.0]],
sample_lib: vec![vec![vec![0.0, 0.0]]], //Vec<Vec<Vec<f32>>>
loop_wavetable: false,
single_cycle: false,
restretch: true,
prev_restretch: false,
start_position: 0.0,
_end_position: 1.0,
grain_hold: 200,
grain_gap: 200,
grain_crossfade: 50,
// Osc module knob storage
osc_type: VoiceType::Sine,
osc_octave: 0,
osc_semitones: 0,
osc_detune: 0.0,
osc_attack: 0.0001,
osc_decay: 0.0001,
osc_sustain: 999.9,
osc_release: 0.07,
osc_mod_amount: 0.0,
osc_retrigger: RetriggerStyle::Free,
osc_atk_curve: SmoothStyle::Linear,
osc_rel_curve: SmoothStyle::Linear,
osc_dec_curve: SmoothStyle::Linear,
osc_unison: 1,
osc_unison_detune: 0.0,
osc_stereo: 1.0,
// Voice storage
playing_voices: VoiceVec {
voices: VecDeque::new(),
},
unison_voices: VoiceVec {
voices: VecDeque::new(),
},
// Tracking stopping voices
is_playing: false,
// Noise variables
noise_obj: DeterministicWhiteNoiseGenerator::new(371722539),
// Pitch mod storage
pitch_enable: false,
pitch_env_peak: 0.0,
pitch_env_attack: 0.0,
pitch_env_decay: 300.0,
pitch_env_sustain: 0.0,
pitch_env_release: 0.0,
pitch_env_atk_curve: SmoothStyle::Linear,
pitch_env_dec_curve: SmoothStyle::Linear,
pitch_env_rel_curve: SmoothStyle::Linear,
pitch_enable_2: false,
pitch_env_peak_2: 0.0,
pitch_env_attack_2: 0.0,
pitch_env_decay_2: 300.0,
pitch_env_sustain_2: 0.0,
pitch_env_release_2: 0.0,
pitch_env_atk_curve_2: SmoothStyle::Linear,
pitch_env_dec_curve_2: SmoothStyle::Linear,
pitch_env_rel_curve_2: SmoothStyle::Linear,
}
}
}
impl AudioModule {
// Draw functions for each module type. This works at CRATE level on the params to draw all 3!!!
// Passing the params here is not the nicest thing but we have to move things around to get past the threading stuff + egui's gui separation
pub fn draw_modules(ui: &mut Ui, params: Arc<ActuateParams>, setter: &ParamSetter<'_>) {
// Resetting these from the draw thread since setter is valid here - I recognize this is ugly/bad practice
if params.load_sample_1.value() {
setter.set_parameter(¶ms.load_sample_1, false);
}
if params.load_sample_2.value() {
setter.set_parameter(¶ms.load_sample_2, false);
}
if params.load_sample_3.value() {
setter.set_parameter(¶ms.load_sample_3, false);
}
const VERT_BAR_HEIGHT: f32 = 106.0;
let VERT_BAR_HEIGHT_SHORTENED: f32 = VERT_BAR_HEIGHT - ui.spacing().interact_size.y - 3.0;
const VERT_BAR_WIDTH: f32 = 14.0;
const HCURVE_WIDTH: f32 = 120.0;
const HCURVE_BWIDTH: f32 = 28.0;
const DISABLED_SPACE: f32 = 116.0;
// This is ugly but I couldn't figure out a better architechture for egui and separating audio modules
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Spot One (1)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
match params._audio_module_1_type.value() {
AudioModuleType::Off => {
// Blank space
ui.label("Disabled");
ui.add_space(DISABLED_SPACE);
}
AudioModuleType::Osc => {
const KNOB_SIZE: f32 = 30.0;
const TEXT_SIZE: f32 = 11.0;
// Oscillator
ui.vertical(|ui| {
ui.add_space(1.0);
ui.horizontal(|ui| {
ui.vertical(|ui| {
let osc_1_type_knob =
ui_knob::ArcKnob::for_param(¶ms.osc_1_type, setter, KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_type_knob);
let osc_1_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_retrigger,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_retrigger_knob);
});
ui.vertical(|ui| {
let osc_1_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_octave,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_octave_knob);
let osc_1_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_semitones,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_semitones_knob);
});
ui.vertical(|ui| {
let osc_1_stereo_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_stereo,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_stereo_knob);
let osc_1_unison_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_unison,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_unison_knob);
});
ui.vertical(|ui| {
let osc_1_detune_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_detune,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_detune_knob);
let osc_1_unison_detune_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_unison_detune,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_unison_detune_knob);
});
// ADSR
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_attack, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_decay, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_sustain, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_release, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
// Curve sliders
ui.vertical(|ui| {
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_1_atk_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_1_dec_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_1_rel_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
});
});
});
}
AudioModuleType::Sampler => {
const KNOB_SIZE: f32 = 25.0;
const TEXT_SIZE: f32 = 11.0;
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(RichText::new("Load Sample")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("The longer the sample the longer the process!");
let switch_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.load_sample_1, setter);
ui.add(switch_toggle);
ui.label(RichText::new("Resample")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("Reload your sample after changing this! On: Resample, Off: Repitch");
let stretch_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.restretch_1, setter);
ui.add(stretch_toggle);
ui.label(RichText::new("Looping")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("To repeat your sample if MIDI key is held");
let loop_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.loop_sample_1, setter);
ui.add(loop_toggle);
ui.label(RichText::new("Single Cycle")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("Use this with Looping ON and Resample ON if you loaded a single cycle waveform");
let sc_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.single_cycle_1, setter);
ui.add(sc_toggle);
});
ui.horizontal(|ui|{
ui.vertical(|ui| {
let osc_1_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_octave,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_octave_knob);
let osc_1_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_retrigger,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_retrigger_knob);
});
ui.vertical(|ui| {
let osc_1_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_semitones,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_semitones_knob);
});
ui.vertical(|ui|{
let start_position_1_knob = ui_knob::ArcKnob::for_param(
¶ms.start_position_1,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(start_position_1_knob);
let end_position_1_knob = ui_knob::ArcKnob::for_param(
¶ms.end_position_1,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(end_position_1_knob);
});
// ADSR
ui.add(VerticalParamSlider::for_param(¶ms.osc_1_attack, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_1_decay, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_1_sustain, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_1_release, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
// Curve sliders
ui.vertical(|ui| {
ui.add(HorizontalParamSlider::for_param(¶ms.osc_1_atk_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
ui.add(HorizontalParamSlider::for_param(¶ms.osc_1_dec_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
ui.add(HorizontalParamSlider::for_param(¶ms.osc_1_rel_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
});
});
});
}
AudioModuleType::Granulizer => {
const KNOB_SIZE: f32 = 25.0;
const TEXT_SIZE: f32 = 11.0;
// This fixes the granulizer release being longer than a grain itself
if params.grain_hold_1.value() < params.grain_crossfade_1.value() {
setter.set_parameter(¶ms.grain_crossfade_1, params.grain_hold_1.value());
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new("Load Sample")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("The longer the sample the longer the process!");
let switch_toggle =
toggle_switch::ToggleSwitch::for_param(¶ms.load_sample_1, setter);
ui.add(switch_toggle);
ui.label(
RichText::new("Looping")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("To repeat your sample if MIDI key is held");
let loop_toggle =
toggle_switch::ToggleSwitch::for_param(¶ms.loop_sample_1, setter);
ui.add(loop_toggle);
ui.add_space(30.0);
ui.label(
RichText::new("Note: ADSR is per note, Shape is AR per grain")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("ADSR is per note, Shape is AR per grain");
});
ui.horizontal(|ui| {
ui.vertical(|ui| {
let osc_1_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_octave,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_octave_knob);
let osc_1_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_semitones,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_semitones_knob);
});
ui.vertical(|ui| {
let osc_1_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_1_retrigger,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_1_retrigger_knob);
let grain_crossfade_1_knob = ui_knob::ArcKnob::for_param(
¶ms.grain_crossfade_1,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_crossfade_1_knob);
});
ui.vertical(|ui| {
let grain_hold_1_knob = ui_knob::ArcKnob::for_param(
¶ms.grain_hold_1,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_hold_1_knob);
let grain_gap_1_knob =
ui_knob::ArcKnob::for_param(¶ms.grain_gap_1, setter, KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_gap_1_knob);
});
ui.vertical(|ui| {
let start_position_1_knob = ui_knob::ArcKnob::for_param(
¶ms.start_position_1,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(start_position_1_knob);
let end_position_1_knob = ui_knob::ArcKnob::for_param(
¶ms.end_position_1,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(end_position_1_knob);
});
// ADSR
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_attack, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_decay, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_sustain, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_1_release, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
// Curve sliders
ui.vertical(|ui| {
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_1_atk_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_1_dec_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_1_rel_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
});
});
});
}
}
ui.separator();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Spot Two (2)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
match params._audio_module_2_type.value() {
AudioModuleType::Off => {
// Blank space
ui.label("Disabled");
ui.add_space(DISABLED_SPACE);
}
AudioModuleType::Osc => {
const KNOB_SIZE: f32 = 30.0;
const TEXT_SIZE: f32 = 12.0;
// Oscillator
ui.vertical(|ui| {
ui.add_space(1.0);
ui.horizontal(|ui| {
ui.vertical(|ui| {
let osc_2_type_knob =
ui_knob::ArcKnob::for_param(¶ms.osc_2_type, setter, KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_type_knob);
let osc_2_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_retrigger,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_retrigger_knob);
});
ui.vertical(|ui| {
let osc_2_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_octave,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_octave_knob);
let osc_2_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_semitones,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_semitones_knob);
});
ui.vertical(|ui| {
let osc_2_stereo_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_stereo,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_stereo_knob);
let osc_2_unison_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_unison,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_unison_knob);
});
ui.vertical(|ui| {
let osc_2_detune_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_detune,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_detune_knob);
let osc_2_unison_detune_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_unison_detune,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_unison_detune_knob);
});
// ADSR
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_attack, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_decay, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_sustain, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_release, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
// Curve sliders
ui.vertical(|ui| {
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_2_atk_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_2_dec_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_2_rel_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
});
});
});
}
AudioModuleType::Sampler => {
const KNOB_SIZE: f32 = 25.0;
const TEXT_SIZE: f32 = 11.0;
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(RichText::new("Load Sample")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("The longer the sample the longer the process!");
let switch_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.load_sample_2, setter);
ui.add(switch_toggle);
ui.label(RichText::new("Resample")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("Reload your sample after changing this! On: Resample, Off: Repitch");
let stretch_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.restretch_2, setter);
ui.add(stretch_toggle);
ui.label(RichText::new("Looping")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("To repeat your sample if MIDI key is held");
let loop_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.loop_sample_2, setter);
ui.add(loop_toggle);
ui.label(RichText::new("Single Cycle")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("Use this with Looping ON and Resample ON if you loaded a single cycle waveform");
let sc_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.single_cycle_2, setter);
ui.add(sc_toggle);
});
ui.horizontal(|ui|{
ui.vertical(|ui| {
let osc_2_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_octave,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_octave_knob);
let osc_2_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_retrigger,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_retrigger_knob);
});
ui.vertical(|ui| {
let osc_2_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_semitones,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_semitones_knob);
});
ui.vertical(|ui|{
let start_position_2_knob = ui_knob::ArcKnob::for_param(
¶ms.start_position_2,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(start_position_2_knob);
let end_position_2_knob = ui_knob::ArcKnob::for_param(
¶ms.end_position_2,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(end_position_2_knob);
});
// ADSR
ui.add(VerticalParamSlider::for_param(¶ms.osc_2_attack, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_2_decay, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_2_sustain, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_2_release, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
// Curve sliders
ui.vertical(|ui| {
ui.add(HorizontalParamSlider::for_param(¶ms.osc_2_atk_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
ui.add(HorizontalParamSlider::for_param(¶ms.osc_2_dec_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
ui.add(HorizontalParamSlider::for_param(¶ms.osc_2_rel_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
});
});
});
}
AudioModuleType::Granulizer => {
const KNOB_SIZE: f32 = 25.0;
const TEXT_SIZE: f32 = 11.0;
// This fixes the granulizer release being longer than a grain itself
if params.grain_hold_2.value() < params.grain_crossfade_2.value() {
setter.set_parameter(¶ms.grain_crossfade_2, params.grain_hold_2.value());
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new("Load Sample")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("The longer the sample the longer the process!");
let switch_toggle =
toggle_switch::ToggleSwitch::for_param(¶ms.load_sample_2, setter);
ui.add(switch_toggle);
ui.label(
RichText::new("Looping")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("To repeat your sample if MIDI key is held");
let loop_toggle =
toggle_switch::ToggleSwitch::for_param(¶ms.loop_sample_2, setter);
ui.add(loop_toggle);
ui.add_space(30.0);
ui.label(
RichText::new("Note: ADSR is per note, Shape is AR per grain")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("ADSR is per note, Shape is AR per grain");
});
ui.horizontal(|ui| {
ui.vertical(|ui| {
let osc_2_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_octave,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_octave_knob);
let osc_2_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_semitones,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_semitones_knob);
});
ui.vertical(|ui| {
let osc_2_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_2_retrigger,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_2_retrigger_knob);
let grain_crossfade_2_knob = ui_knob::ArcKnob::for_param(
¶ms.grain_crossfade_2,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_crossfade_2_knob);
});
ui.vertical(|ui| {
let grain_hold_2_knob = ui_knob::ArcKnob::for_param(
¶ms.grain_hold_2,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_hold_2_knob);
let grain_gap_2_knob =
ui_knob::ArcKnob::for_param(¶ms.grain_gap_2, setter, KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_gap_2_knob);
});
ui.vertical(|ui| {
let start_position_2_knob = ui_knob::ArcKnob::for_param(
¶ms.start_position_2,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(start_position_2_knob);
let end_position_2_knob = ui_knob::ArcKnob::for_param(
¶ms.end_position_2,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(end_position_2_knob);
});
// ADSR
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_attack, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_decay, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_sustain, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_2_release, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
// Curve sliders
ui.vertical(|ui| {
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_2_atk_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_2_dec_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_2_rel_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
});
});
});
}
}
ui.separator();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Spot Three (3)
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
match params._audio_module_3_type.value() {
AudioModuleType::Off => {
// Blank space
ui.label("Disabled");
ui.add_space(DISABLED_SPACE);
}
AudioModuleType::Osc => {
const KNOB_SIZE: f32 = 30.0;
const TEXT_SIZE: f32 = 12.0;
// Oscillator
ui.vertical(|ui| {
// For some reason, 2.0 fixes the gui moving instead of 1.0
ui.add_space(2.0);
ui.horizontal(|ui| {
ui.vertical(|ui| {
let osc_3_type_knob =
ui_knob::ArcKnob::for_param(¶ms.osc_3_type, setter, KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_type_knob);
let osc_3_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_retrigger,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_retrigger_knob);
});
ui.vertical(|ui| {
let osc_3_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_octave,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_octave_knob);
let osc_3_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_semitones,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_semitones_knob);
});
ui.vertical(|ui| {
let osc_3_stereo_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_stereo,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_stereo_knob);
let osc_3_unison_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_unison,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_unison_knob);
});
ui.vertical(|ui| {
let osc_3_detune_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_detune,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_detune_knob);
let osc_3_unison_detune_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_unison_detune,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_unison_detune_knob);
});
// ADSR
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_attack, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_decay, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_sustain, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_release, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
// Curve sliders
ui.vertical(|ui| {
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_3_atk_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_3_dec_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_3_rel_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
});
});
});
}
AudioModuleType::Sampler => {
const KNOB_SIZE: f32 = 25.0;
const TEXT_SIZE: f32 = 11.0;
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(RichText::new("Load Sample")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("The longer the sample the longer the process!");
let switch_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.load_sample_3, setter);
ui.add(switch_toggle);
ui.label(RichText::new("Resample")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("Reload your sample after changing this! On: Resample, Off: Repitch");
let stretch_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.restretch_3, setter);
ui.add(stretch_toggle);
ui.label(RichText::new("Looping")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("To repeat your sample if MIDI key is held");
let loop_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.loop_sample_3, setter);
ui.add(loop_toggle);
ui.label(RichText::new("Single Cycle")
.font(SMALLER_FONT)
.color(FONT_COLOR))
.on_hover_text("Use this with Looping ON and Resample ON if you loaded a single cycle waveform");
let sc_toggle = toggle_switch::ToggleSwitch::for_param(¶ms.single_cycle_3, setter);
ui.add(sc_toggle);
});
ui.horizontal(|ui|{
ui.vertical(|ui| {
let osc_3_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_octave,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_octave_knob);
let osc_3_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_retrigger,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_retrigger_knob);
});
ui.vertical(|ui| {
let osc_3_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_semitones,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_semitones_knob);
});
ui.vertical(|ui|{
let start_position_3_knob = ui_knob::ArcKnob::for_param(
¶ms.start_position_3,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(start_position_3_knob);
let end_position_3_knob = ui_knob::ArcKnob::for_param(
¶ms.end_position_3,
setter,
KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(end_position_3_knob);
});
// ADSR
ui.add(VerticalParamSlider::for_param(¶ms.osc_3_attack, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_3_decay, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_3_sustain, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
ui.add(VerticalParamSlider::for_param(¶ms.osc_3_release, setter).with_width(VERT_BAR_WIDTH).with_height(VERT_BAR_HEIGHT_SHORTENED).set_reversed(true).override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR));
// Curve sliders
ui.vertical(|ui| {
ui.add(HorizontalParamSlider::for_param(¶ms.osc_3_atk_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
ui.add(HorizontalParamSlider::for_param(¶ms.osc_3_dec_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
ui.add(HorizontalParamSlider::for_param(¶ms.osc_3_rel_curve, setter).with_width(HCURVE_BWIDTH).set_left_sided_label(true).set_label_width(HCURVE_WIDTH).override_colors(
DARK_GREY_UI_COLOR,
A_KNOB_OUTSIDE_COLOR));
});
});
});
}
AudioModuleType::Granulizer => {
const KNOB_SIZE: f32 = 25.0;
const TEXT_SIZE: f32 = 11.0;
// This fixes the granulizer release being longer than a grain itself
if params.grain_hold_3.value() < params.grain_crossfade_3.value() {
setter.set_parameter(¶ms.grain_crossfade_3, params.grain_hold_3.value());
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new("Load Sample")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("The longer the sample the longer the process!");
let switch_toggle =
toggle_switch::ToggleSwitch::for_param(¶ms.load_sample_3, setter);
ui.add(switch_toggle);
ui.label(
RichText::new("Looping")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("To repeat your sample if MIDI key is held");
let loop_toggle =
toggle_switch::ToggleSwitch::for_param(¶ms.loop_sample_3, setter);
ui.add(loop_toggle);
ui.add_space(30.0);
ui.label(
RichText::new("Note: ADSR is per note, Shape is AR per grain")
.font(SMALLER_FONT)
.color(FONT_COLOR),
)
.on_hover_text("ADSR is per note, Shape is AR per grain");
});
ui.horizontal(|ui| {
ui.vertical(|ui| {
let osc_3_octave_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_octave,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_octave_knob);
let osc_3_semitones_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_semitones,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_semitones_knob);
});
ui.vertical(|ui| {
let osc_3_retrigger_knob = ui_knob::ArcKnob::for_param(
¶ms.osc_3_retrigger,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(A_KNOB_OUTSIDE_COLOR)
.use_outline(true)
.set_text_size(TEXT_SIZE);
ui.add(osc_3_retrigger_knob);
let grain_crossfade_3_knob = ui_knob::ArcKnob::for_param(
¶ms.grain_crossfade_3,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_crossfade_3_knob);
});
ui.vertical(|ui| {
let grain_hold_3_knob = ui_knob::ArcKnob::for_param(
¶ms.grain_hold_3,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_hold_3_knob);
let grain_gap_3_knob =
ui_knob::ArcKnob::for_param(¶ms.grain_gap_3, setter, KNOB_SIZE)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(grain_gap_3_knob);
});
ui.vertical(|ui| {
let start_position_3_knob = ui_knob::ArcKnob::for_param(
¶ms.start_position_3,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(start_position_3_knob);
let end_position_3_knob = ui_knob::ArcKnob::for_param(
¶ms.end_position_3,
setter,
KNOB_SIZE,
)
.preset_style(ui_knob::KnobStyle::NewPresets1)
.set_fill_color(DARK_GREY_UI_COLOR)
.set_line_color(SYNTH_MIDDLE_BLUE)
.set_text_size(TEXT_SIZE);
ui.add(end_position_3_knob);
});
// ADSR
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_attack, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_decay, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_sustain, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
VerticalParamSlider::for_param(¶ms.osc_3_release, setter)
.with_width(VERT_BAR_WIDTH)
.with_height(VERT_BAR_HEIGHT_SHORTENED)
.set_reversed(true)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
// Curve sliders
ui.vertical(|ui| {
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_3_atk_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_3_dec_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
ui.add(
HorizontalParamSlider::for_param(¶ms.osc_3_rel_curve, setter)
.with_width(HCURVE_BWIDTH)
.set_left_sided_label(true)
.set_label_width(HCURVE_WIDTH)
.override_colors(DARK_GREY_UI_COLOR, A_KNOB_OUTSIDE_COLOR),
);
});
});
});
}
}
}
// Index proper params from knobs
// This lets us have a copy for voices, and also track changes like restretch changing or ADR slopes
pub fn consume_params(&mut self, params: Arc<ActuateParams>, voice_index: usize) {
match voice_index {
1 => {
self.audio_module_type = params._audio_module_1_type.value();
self.osc_type = params.osc_1_type.value();
if self.osc_octave != params.osc_1_octave.value() {
let oct_shift = self.osc_octave - params.osc_1_octave.value();
for voice in self.playing_voices.voices.iter_mut() {
voice.note -= (oct_shift * 12) as u8;
}
for uni_voice in self.unison_voices.voices.iter_mut() {
uni_voice.note -= (oct_shift * 12) as u8;
}
}
self.osc_octave = params.osc_1_octave.value();
if self.osc_semitones != params.osc_1_semitones.value() {
let semi_shift = self.osc_semitones - params.osc_1_semitones.value();
for voice in self.playing_voices.voices.iter_mut() {
voice.note -= semi_shift as u8;
}
for uni_voice in self.unison_voices.voices.iter_mut() {
uni_voice.note -= semi_shift as u8;
}
}
match params.pitch_routing.value() {
PitchRouting::Osc1 | PitchRouting::Osc1_Osc2 | PitchRouting::Osc1_Osc3 | PitchRouting::All => {
self.pitch_enable = params.pitch_enable.value();
self.pitch_env_peak = params.pitch_env_peak.value();
self.pitch_env_attack = params.pitch_env_attack.value();
self.pitch_env_decay = params.pitch_env_decay.value();
self.pitch_env_sustain = params.pitch_env_sustain.value();
self.pitch_env_release = params.pitch_env_release.value();
self.pitch_env_atk_curve = params.pitch_env_atk_curve.value();
self.pitch_env_dec_curve = params.pitch_env_dec_curve.value();
self.pitch_env_rel_curve = params.pitch_env_rel_curve.value();
}
_ => {
self.pitch_enable = false;
}
}
match params.pitch_routing_2.value() {
PitchRouting::Osc1 | PitchRouting::Osc1_Osc2 | PitchRouting::Osc1_Osc3 | PitchRouting::All => {
self.pitch_enable_2 = params.pitch_enable_2.value();
self.pitch_env_peak_2 = params.pitch_env_peak_2.value();
self.pitch_env_attack_2 = params.pitch_env_attack_2.value();
self.pitch_env_decay_2 = params.pitch_env_decay_2.value();
self.pitch_env_sustain_2 = params.pitch_env_sustain_2.value();
self.pitch_env_release_2 = params.pitch_env_release_2.value();
self.pitch_env_atk_curve_2 = params.pitch_env_atk_curve_2.value();
self.pitch_env_dec_curve_2 = params.pitch_env_dec_curve_2.value();
self.pitch_env_rel_curve_2 = params.pitch_env_rel_curve_2.value();
}
_ => {
self.pitch_enable_2 = false;
}
}
self.osc_semitones = params.osc_1_semitones.value();
self.osc_detune = params.osc_1_detune.value();
self.osc_attack = params.osc_1_attack.value();
self.osc_decay = params.osc_1_decay.value();
self.osc_sustain = params.osc_1_sustain.value();
self.osc_release = params.osc_1_release.value();
self.osc_retrigger = params.osc_1_retrigger.value();
self.osc_atk_curve = params.osc_1_atk_curve.value();
self.osc_dec_curve = params.osc_1_dec_curve.value();
self.osc_rel_curve = params.osc_1_rel_curve.value();
self.osc_unison = params.osc_1_unison.value();
self.osc_unison_detune = params.osc_1_unison_detune.value();
self.osc_stereo = params.osc_1_stereo.value();
self.loop_wavetable = params.loop_sample_1.value();
self.single_cycle = params.single_cycle_1.value();
self.restretch = params.restretch_1.value();
self.start_position = params.start_position_1.value();
self._end_position = params.end_position_1.value();
self.grain_hold = params.grain_hold_1.value();
self.grain_gap = params.grain_gap_1.value();
self.grain_crossfade = params.grain_crossfade_1.value();
},
2 => {
self.audio_module_type = params._audio_module_2_type.value();
self.osc_type = params.osc_2_type.value();
if self.osc_octave != params.osc_2_octave.value() {
let oct_shift = self.osc_octave - params.osc_2_octave.value();
for voice in self.playing_voices.voices.iter_mut() {
voice.note -= (oct_shift * 12) as u8;
}
for uni_voice in self.unison_voices.voices.iter_mut() {
uni_voice.note -= (oct_shift * 12) as u8;
}
}
self.osc_octave = params.osc_2_octave.value();
if self.osc_semitones != params.osc_2_semitones.value() {
let semi_shift = self.osc_semitones - params.osc_2_semitones.value();
for voice in self.playing_voices.voices.iter_mut() {
voice.note -= semi_shift as u8;
}
for uni_voice in self.unison_voices.voices.iter_mut() {
uni_voice.note -= semi_shift as u8;
}
}
match params.pitch_routing.value() {
PitchRouting::Osc2 | PitchRouting::Osc1_Osc2 | PitchRouting::Osc2_Osc3 | PitchRouting::All => {
self.pitch_enable = params.pitch_enable.value();
self.pitch_env_peak = params.pitch_env_peak.value();
self.pitch_env_attack = params.pitch_env_attack.value();
self.pitch_env_decay = params.pitch_env_decay.value();
self.pitch_env_sustain = params.pitch_env_sustain.value();
self.pitch_env_release = params.pitch_env_release.value();
self.pitch_env_atk_curve = params.pitch_env_atk_curve.value();
self.pitch_env_dec_curve = params.pitch_env_dec_curve.value();
self.pitch_env_rel_curve = params.pitch_env_rel_curve.value();
}
_ => {
self.pitch_enable = false;
}
}
match params.pitch_routing_2.value() {
PitchRouting::Osc2 | PitchRouting::Osc1_Osc2 | PitchRouting::Osc2_Osc3 | PitchRouting::All => {
self.pitch_enable_2 = params.pitch_enable_2.value();
self.pitch_env_peak_2 = params.pitch_env_peak_2.value();
self.pitch_env_attack_2 = params.pitch_env_attack_2.value();
self.pitch_env_decay_2 = params.pitch_env_decay_2.value();
self.pitch_env_sustain_2 = params.pitch_env_sustain_2.value();
self.pitch_env_release_2 = params.pitch_env_release_2.value();
self.pitch_env_atk_curve_2 = params.pitch_env_atk_curve_2.value();
self.pitch_env_dec_curve_2 = params.pitch_env_dec_curve_2.value();
self.pitch_env_rel_curve_2 = params.pitch_env_rel_curve_2.value();
}
_ => {
self.pitch_enable_2 = false;
}
}
self.osc_semitones = params.osc_2_semitones.value();
self.osc_detune = params.osc_2_detune.value();
self.osc_attack = params.osc_2_attack.value();
self.osc_decay = params.osc_2_decay.value();
self.osc_sustain = params.osc_2_sustain.value();
self.osc_release = params.osc_2_release.value();
self.osc_retrigger = params.osc_2_retrigger.value();
self.osc_atk_curve = params.osc_2_atk_curve.value();
self.osc_dec_curve = params.osc_2_dec_curve.value();
self.osc_rel_curve = params.osc_2_rel_curve.value();
self.osc_unison = params.osc_2_unison.value();
self.osc_unison_detune = params.osc_2_unison_detune.value();
self.osc_stereo = params.osc_2_stereo.value();
self.loop_wavetable = params.loop_sample_2.value();
self.single_cycle = params.single_cycle_2.value();
self.restretch = params.restretch_2.value();
self.start_position = params.start_position_2.value();
self._end_position = params.end_position_2.value();
self.grain_hold = params.grain_hold_2.value();
self.grain_gap = params.grain_gap_2.value();
self.grain_crossfade = params.grain_crossfade_2.value();
},
3 => {
self.audio_module_type = params._audio_module_3_type.value();
self.osc_type = params.osc_3_type.value();
if self.osc_octave != params.osc_3_octave.value() {
let oct_shift = self.osc_octave - params.osc_3_octave.value();
for voice in self.playing_voices.voices.iter_mut() {
voice.note -= (oct_shift * 12) as u8;
}
for uni_voice in self.unison_voices.voices.iter_mut() {
uni_voice.note -= (oct_shift * 12) as u8;
}
}
self.osc_octave = params.osc_3_octave.value();
if self.osc_semitones != params.osc_3_semitones.value() {
let semi_shift = self.osc_semitones - params.osc_3_semitones.value();
for voice in self.playing_voices.voices.iter_mut() {
voice.note -= semi_shift as u8;
}
for uni_voice in self.unison_voices.voices.iter_mut() {
uni_voice.note -= semi_shift as u8;
}
}
match params.pitch_routing.value() {
PitchRouting::Osc3 | PitchRouting::Osc2_Osc3 | PitchRouting::Osc1_Osc3 | PitchRouting::All => {
self.pitch_enable = params.pitch_enable.value();
self.pitch_env_peak = params.pitch_env_peak.value();
self.pitch_env_attack = params.pitch_env_attack.value();
self.pitch_env_decay = params.pitch_env_decay.value();
self.pitch_env_sustain = params.pitch_env_sustain.value();
self.pitch_env_release = params.pitch_env_release.value();
self.pitch_env_atk_curve = params.pitch_env_atk_curve.value();
self.pitch_env_dec_curve = params.pitch_env_dec_curve.value();
self.pitch_env_rel_curve = params.pitch_env_rel_curve.value();
}
_ => {
self.pitch_enable = false;
}
}
match params.pitch_routing_2.value() {
PitchRouting::Osc3 | PitchRouting::Osc2_Osc3 | PitchRouting::Osc1_Osc3 | PitchRouting::All => {
self.pitch_enable_2 = params.pitch_enable_2.value();
self.pitch_env_peak_2 = params.pitch_env_peak_2.value();
self.pitch_env_attack_2 = params.pitch_env_attack_2.value();
self.pitch_env_decay_2 = params.pitch_env_decay_2.value();
self.pitch_env_sustain_2 = params.pitch_env_sustain_2.value();
self.pitch_env_release_2 = params.pitch_env_release_2.value();
self.pitch_env_atk_curve_2 = params.pitch_env_atk_curve_2.value();
self.pitch_env_dec_curve_2 = params.pitch_env_dec_curve_2.value();
self.pitch_env_rel_curve_2 = params.pitch_env_rel_curve_2.value();
}
_ => {
self.pitch_enable_2 = false;
}
}
self.osc_semitones = params.osc_3_semitones.value();
self.osc_detune = params.osc_3_detune.value();
self.osc_attack = params.osc_3_attack.value();
self.osc_decay = params.osc_3_decay.value();
self.osc_sustain = params.osc_3_sustain.value();
self.osc_release = params.osc_3_release.value();
self.osc_retrigger = params.osc_3_retrigger.value();
self.osc_atk_curve = params.osc_3_atk_curve.value();
self.osc_dec_curve = params.osc_3_dec_curve.value();
self.osc_rel_curve = params.osc_3_rel_curve.value();
self.osc_unison = params.osc_3_unison.value();
self.osc_unison_detune = params.osc_3_unison_detune.value();
self.osc_stereo = params.osc_3_stereo.value();
self.loop_wavetable = params.loop_sample_3.value();
self.single_cycle = params.single_cycle_3.value();
self.restretch = params.restretch_3.value();
self.start_position = params.start_position_3.value();
self._end_position = params.end_position_3.value();
self.grain_hold = params.grain_hold_3.value();
self.grain_gap = params.grain_gap_3.value();
self.grain_crossfade = params.grain_crossfade_3.value();
},
_ => {},
}
}
// I was looking at the PolyModSynth Example and decided on this
// Handle the audio module midi events and regular pricessing
// This is an INDIVIDUAL instance process unlike the GUI function
// This sends back the OSC output + note on for filter to reset
pub fn process(
&mut self,
_sample_id: usize,
event_passed: Option<NoteEvent<()>>,
voice_max: usize,
detune_mod: f32,
uni_detune_mod: f32,
velocity_mod: f32,
uni_velocity_mod: f32,
vel_gain_mod: f32,
vel_lfo_gain_mod: f32,
) -> (f32, f32, bool, bool) {
// If the process is in here the file dialog is not open per lib.rs
// Midi events are processed here
let mut note_on: bool = false;
let mut note_off: bool = false;
match event_passed {
// The event was valid
Some(mut event) => {
event = event_passed.unwrap();
match event {
////////////////////////////////////////////////////////////
// MIDI EVENT NOTE ON
////////////////////////////////////////////////////////////
NoteEvent::NoteOn {
mut note, velocity, ..
} => {
// Osc + generic stuff
note_on = true;
let mut new_phase: f32 = 0.0;
// Calculate our pitch mod stuff if applicable
let pitch_attack_smoother: Smoother<f32>;
let pitch_decay_smoother: Smoother<f32>;
let pitch_release_smoother: Smoother<f32>;
let pitch_mod_current: f32;
let pitch_attack_smoother_2: Smoother<f32>;
let pitch_decay_smoother_2: Smoother<f32>;
let pitch_release_smoother_2: Smoother<f32>;
let pitch_mod_current_2: f32;
if self.pitch_enable {
pitch_attack_smoother = match self.pitch_env_atk_curve {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.pitch_env_attack))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.pitch_env_attack.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.pitch_env_attack))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.pitch_env_attack))
}
};
pitch_decay_smoother = match self.pitch_env_dec_curve {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.pitch_env_decay))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.pitch_env_decay.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.pitch_env_decay))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.pitch_env_decay))
}
};
pitch_release_smoother = match self.pitch_env_rel_curve {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.pitch_env_release))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.pitch_env_release.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.pitch_env_release))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.pitch_env_release))
}
};
match pitch_attack_smoother.style {
SmoothingStyle::Logarithmic(_) => {
pitch_attack_smoother.reset(0.0001);
pitch_attack_smoother
.set_target(self.sample_rate, self.pitch_env_peak.max(0.0001));
}
_ => {
pitch_attack_smoother.reset(0.0);
pitch_attack_smoother.set_target(self.sample_rate, self.pitch_env_peak);
}
}
pitch_mod_current = pitch_attack_smoother.next();
} else {
pitch_attack_smoother = Smoother::new(SmoothingStyle::None);
pitch_decay_smoother = Smoother::new(SmoothingStyle::None);
pitch_release_smoother = Smoother::new(SmoothingStyle::None);
pitch_mod_current = 0.0;
}
// Pitch mod 2
if self.pitch_enable_2 {
pitch_attack_smoother_2 = match self.pitch_env_atk_curve_2 {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.pitch_env_attack_2))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.pitch_env_attack_2.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.pitch_env_attack_2))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.pitch_env_attack_2))
}
};
pitch_decay_smoother_2 = match self.pitch_env_dec_curve_2 {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.pitch_env_decay_2))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.pitch_env_decay_2.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.pitch_env_decay_2))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.pitch_env_decay_2))
}
};
pitch_release_smoother_2 = match self.pitch_env_rel_curve_2 {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.pitch_env_release_2))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.pitch_env_release_2.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.pitch_env_release_2))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.pitch_env_release_2))
}
};
match pitch_attack_smoother_2.style {
SmoothingStyle::Logarithmic(_) => {
pitch_attack_smoother_2.reset(0.0001);
pitch_attack_smoother_2
.set_target(self.sample_rate, self.pitch_env_peak_2.max(0.0001));
}
_ => {
pitch_attack_smoother_2.reset(0.0);
pitch_attack_smoother_2.set_target(self.sample_rate, self.pitch_env_peak_2);
}
}
pitch_mod_current_2 = pitch_attack_smoother_2.next();
} else {
pitch_attack_smoother_2 = Smoother::new(SmoothingStyle::None);
pitch_decay_smoother_2 = Smoother::new(SmoothingStyle::None);
pitch_release_smoother_2 = Smoother::new(SmoothingStyle::None);
pitch_mod_current_2 = 0.0;
}
// Sampler when single cycle needs this!!!
if self.single_cycle {
// 31 comes from comparing with 3xOsc position in MIDI notes
note += 31;
}
// Shift our note per octave
match self.osc_octave {
-2 => {
note -= 24;
}
-1 => {
note -= 12;
}
0 => {
note -= 0;
}
1 => {
note += 12;
}
2 => {
note += 24;
}
_ => {}
}
// Shift our note per semitones
note += self.osc_semitones as u8;
// Shift our note per detune
// I'm so glad nih-plug has this helper for f32 conversions!
let base_note = if velocity_mod <= 0.0 {
note as f32 + self.osc_detune + detune_mod + pitch_mod_current + pitch_mod_current_2
} else {
note as f32
+ self.osc_detune
+ detune_mod
+ velocity_mod.clamp(0.0, 1.0) * velocity
+ pitch_mod_current
+ pitch_mod_current_2
};
// Reset the retrigger on Oscs
match self.osc_retrigger {
RetriggerStyle::Retrigger => {
// Start our phase back at 0
new_phase = 0.0;
}
RetriggerStyle::Random | RetriggerStyle::UniRandom => {
match self.audio_module_type {
AudioModuleType::Osc => {
// Get a random phase to use
// Poly solution is to pass the phase to the struct
// instead of the osc alone
let mut rng = rand::thread_rng();
new_phase = rng.gen_range(0.0..1.0);
}
AudioModuleType::Sampler => {
let mut rng = rand::thread_rng();
// Prevent panic when no sample loaded yet
if self.sample_lib.len() > 1 {
new_phase = rng.gen_range(
0.0..self.sample_lib[note as usize][0].len() as f32,
);
}
}
AudioModuleType::Granulizer => {
let mut rng = rand::thread_rng();
// Prevent panic when no sample loaded yet
if self.sample_lib.len() > 1 {
new_phase = rng.gen_range(
0.0..self.sample_lib[note as usize][0].len() as f32,
);
}
}
_ => {}
}
}
RetriggerStyle::Free => {
// Do nothing for osc
match self.audio_module_type {
AudioModuleType::Sampler | AudioModuleType::Granulizer => {
new_phase = 0.0;
}
_ => {}
}
}
}
// Create an array of unison notes based off the param for how many unison voices we need
let mut unison_notes: Vec<f32> = vec![0.0; self.osc_unison as usize];
// If we have any unison voices
if self.osc_unison > 1 {
// Calculate the detune step amount per amount of voices
let detune_step = self.osc_unison_detune / self.osc_unison as f32;
for unison_voice in 0..(self.osc_unison as usize - 1) {
// Create the detuned notes around the base note
if unison_voice % 2 == 1 {
unison_notes[unison_voice] = util::f32_midi_note_to_freq(
base_note
+ uni_detune_mod
+ (uni_velocity_mod.clamp(0.0, 1.0) * velocity)
+ detune_step * (unison_voice + 1) as f32
+ pitch_mod_current
+ pitch_mod_current_2,
);
} else {
unison_notes[unison_voice] = util::f32_midi_note_to_freq(
base_note
- uni_detune_mod
- (uni_velocity_mod.clamp(0.0, 1.0) * velocity)
- detune_step * (unison_voice) as f32
- pitch_mod_current
- pitch_mod_current_2,
);
}
}
}
let attack_smoother: Smoother<f32> = match self.osc_atk_curve {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.osc_attack))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.osc_attack.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.osc_attack))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.osc_attack))
}
};
let decay_smoother: Smoother<f32> = match self.osc_dec_curve {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.osc_decay))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.osc_decay.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.osc_decay))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.osc_decay))
}
};
let release_smoother: Smoother<f32> = match self.osc_rel_curve {
SmoothStyle::Linear => {
Smoother::new(SmoothingStyle::Linear(self.osc_release))
}
SmoothStyle::Logarithmic => Smoother::new(SmoothingStyle::Logarithmic(
self.osc_release.clamp(0.0001, 999.9),
)),
SmoothStyle::Exponential => {
Smoother::new(SmoothingStyle::Exponential(self.osc_release))
}
SmoothStyle::LogSteep => {
Smoother::new(SmoothingStyle::LogSteep(self.osc_release))
}
};
match attack_smoother.style {
SmoothingStyle::Logarithmic(_) => {
attack_smoother.reset(0.0001);
attack_smoother
.set_target(self.sample_rate, velocity.clamp(0.0001, 999.9));
}
_ => {
attack_smoother.reset(0.0);
attack_smoother.set_target(self.sample_rate, velocity);
}
}
let scaled_sample_pos;
let scaled_end_pos;
match self.audio_module_type {
AudioModuleType::Sampler | AudioModuleType::Granulizer => {
// If ANY Sample content
if self.loaded_sample.len() > 0 && self.sample_lib.len() > 0 {
// If our loaded sample variable or generated sample library has any content
if self.loaded_sample[0].len() > 1
&& self.sample_lib[0][0].len() > 1
&& self.sample_lib.len() > 1
{
// Create our granulizer/sampler starting position from our knob scale
scaled_sample_pos = if self.start_position > 0.0
&& self.osc_retrigger != RetriggerStyle::Random
&& self.osc_retrigger != RetriggerStyle::UniRandom
{
(self.sample_lib[note as usize][0].len() as f32
* self.start_position)
.floor()
as usize
}
// Retrigger and use 0
else if self.osc_retrigger != RetriggerStyle::Random
&& self.osc_retrigger != RetriggerStyle::UniRandom
{
0_usize
}
// Retrigger with random
else {
new_phase.floor() as usize
};
scaled_end_pos = if self._end_position < 1.0 {
(self.sample_lib[note as usize][0].len() as f32
* self._end_position)
.ceil()
as usize
}
// use end positions
else {
self.sample_lib[note as usize][0].len()
};
} else {
// Nothing is in our sample library, skip attempting audio output
return (0.0, 0.0, false, false);
}
} else {
// Nothing is in our sample library, skip attempting audio output
return (0.0, 0.0, false, false);
}
}
_ => {
// These fields aren't used by Osc, Off
scaled_sample_pos = 0;
scaled_end_pos = 0;
}
}
// Osc Updates
let new_voice: SingleVoice = SingleVoice {
note: note,
_velocity: velocity,
vel_mod_amount: velocity_mod,
phase: new_phase,
//phase_delta: detuned_note / self.sample_rate,
phase_delta: 0.0,
state: OscState::Attacking,
// These get cloned since smoother cannot be copied
amp_current: 0.0,
osc_attack: attack_smoother.clone(),
osc_decay: decay_smoother.clone(),
osc_release: release_smoother.clone(),
pitch_enabled: self.pitch_enable,
pitch_env_peak: self.pitch_env_peak,
pitch_current: pitch_mod_current,
pitch_state: OscState::Attacking,
pitch_attack: pitch_attack_smoother.clone(),
pitch_decay: pitch_decay_smoother.clone(),
pitch_release: pitch_release_smoother.clone(),
pitch_enabled_2: self.pitch_enable_2,
pitch_env_peak_2: self.pitch_env_peak_2,
pitch_current_2: pitch_mod_current_2,
pitch_state_2: OscState::Attacking,
pitch_attack_2: pitch_attack_smoother_2.clone(),
pitch_decay_2: pitch_decay_smoother_2.clone(),
pitch_release_2: pitch_release_smoother_2.clone(),
_detune: self.osc_detune,
_unison_detune_value: self.osc_unison_detune,
//frequency: detuned_note,
frequency: 0.0,
_attack_time: self.osc_attack,
_decay_time: self.osc_decay,
_release_time: self.osc_release,
_retrigger: self.osc_retrigger,
_voice_type: self.osc_type,
_angle: 0.0,
sample_pos: scaled_sample_pos,
loop_it: self.loop_wavetable,
grain_start_pos: scaled_sample_pos,
_granular_gap: self.grain_gap,
_granular_hold: self.grain_hold,
granular_hold_end: scaled_sample_pos + self.grain_hold as usize,
next_grain_pos: scaled_sample_pos
+ self.grain_hold as usize
+ self.grain_gap as usize,
_end_position: scaled_end_pos,
_granular_crossfade: self.grain_crossfade,
grain_attack: Smoother::new(SmoothingStyle::Linear(
self.grain_crossfade as f32,
)),
grain_release: Smoother::new(SmoothingStyle::Linear(
self.grain_crossfade as f32,
)),
grain_state: GrainState::Attacking,
};
// Add our voice struct to our voice tracking deque
self.playing_voices.voices.push_back(new_voice);
// Add unison voices to our voice tracking deque
if self.osc_unison > 1 && self.audio_module_type == AudioModuleType::Osc {
let unison_even_voices = if self.osc_unison % 2 == 0 {
self.osc_unison
} else {
self.osc_unison - 1
};
let mut unison_angles = vec![0.0; unison_even_voices as usize];
for i in 1..(unison_even_voices + 1) {
let voice_angle = self.calculate_panning(i - 1, self.osc_unison);
unison_angles[(i - 1) as usize] = voice_angle;
}
for unison_voice in 0..(self.osc_unison as usize - 1) {
let uni_phase = match self.osc_retrigger {
RetriggerStyle::UniRandom => {
let mut rng = rand::thread_rng();
rng.gen_range(0.0..1.0)
}
_ => new_phase,
};
let new_unison_voice: SingleVoice = SingleVoice {
note: note,
_velocity: velocity,
vel_mod_amount: uni_velocity_mod,
phase: uni_phase,
phase_delta: unison_notes[unison_voice] / self.sample_rate,
state: OscState::Attacking,
// These get cloned since smoother cannot be copied
amp_current: 0.0,
osc_attack: attack_smoother.clone(),
osc_decay: decay_smoother.clone(),
osc_release: release_smoother.clone(),
pitch_enabled: self.pitch_enable,
pitch_env_peak: self.pitch_env_peak,
pitch_current: pitch_mod_current,
pitch_state: OscState::Attacking,
pitch_attack: pitch_attack_smoother.clone(),
pitch_decay: pitch_decay_smoother.clone(),
pitch_release: pitch_release_smoother.clone(),
pitch_enabled_2: self.pitch_enable_2,
pitch_env_peak_2: self.pitch_env_peak_2,
pitch_current_2: pitch_mod_current_2,
pitch_state_2: OscState::Attacking,
pitch_attack_2: pitch_attack_smoother_2.clone(),
pitch_decay_2: pitch_decay_smoother_2.clone(),
pitch_release_2: pitch_release_smoother_2.clone(),
_detune: self.osc_detune,
_unison_detune_value: self.osc_unison_detune,
//frequency: unison_notes[unison_voice],
frequency: 0.0,
//frequency: detuned_note,
_attack_time: self.osc_attack,
_decay_time: self.osc_decay,
_release_time: self.osc_release,
_retrigger: self.osc_retrigger,
_voice_type: self.osc_type,
_angle: unison_angles[unison_voice],
sample_pos: 0,
grain_start_pos: 0,
loop_it: self.loop_wavetable,
_granular_gap: 200,
_granular_hold: 200,
granular_hold_end: 200,
next_grain_pos: 400,
_end_position: scaled_end_pos,
_granular_crossfade: 50,
grain_attack: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_release: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_state: GrainState::Attacking,
};
self.unison_voices.voices.push_back(new_unison_voice);
}
}
// Remove the last voice when > voice_max
if self.playing_voices.voices.len() > voice_max {
self.playing_voices.voices.resize(
voice_max,
// Insert a dummy "Off" entry when resizing UP
SingleVoice {
note: 0,
_velocity: 0.0,
vel_mod_amount: 0.0,
phase: 0.0,
phase_delta: 0.0,
state: OscState::Off,
// These get cloned since smoother cannot be copied
amp_current: 0.0,
osc_attack: attack_smoother.clone(),
osc_decay: decay_smoother.clone(),
osc_release: release_smoother.clone(),
pitch_enabled: self.pitch_enable,
pitch_env_peak: self.pitch_env_peak,
pitch_current: 0.0,
pitch_state: OscState::Attacking,
pitch_attack: pitch_attack_smoother.clone(),
pitch_decay: pitch_decay_smoother.clone(),
pitch_release: pitch_release_smoother.clone(),
pitch_enabled_2: self.pitch_enable_2,
pitch_env_peak_2: self.pitch_env_peak_2,
pitch_current_2: 0.0,
pitch_state_2: OscState::Attacking,
pitch_attack_2: pitch_attack_smoother_2.clone(),
pitch_decay_2: pitch_decay_smoother_2.clone(),
pitch_release_2: pitch_release_smoother_2.clone(),
_detune: 0.0,
_unison_detune_value: 0.0,
frequency: 0.0,
_attack_time: self.osc_attack,
_decay_time: self.osc_decay,
_release_time: self.osc_release,
_retrigger: self.osc_retrigger,
_voice_type: self.osc_type,
_angle: 0.0,
sample_pos: 0,
loop_it: self.loop_wavetable,
grain_start_pos: 0,
_granular_gap: 200,
_granular_hold: 200,
granular_hold_end: 200,
next_grain_pos: 400,
_end_position: scaled_end_pos,
_granular_crossfade: 50,
grain_attack: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_release: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_state: GrainState::Attacking,
},
);
if self.osc_unison > 1 && self.audio_module_type == AudioModuleType::Osc
{
self.unison_voices.voices.resize(
voice_max as usize,
// Insert a dummy "Off" entry when resizing UP
SingleVoice {
note: 0,
_velocity: 0.0,
vel_mod_amount: 0.0,
phase: new_phase,
phase_delta: 0.0,
state: OscState::Off,
// These get cloned since smoother cannot be copied
amp_current: 0.0,
osc_attack: attack_smoother.clone(),
osc_decay: decay_smoother.clone(),
osc_release: release_smoother.clone(),
pitch_enabled: self.pitch_enable,
pitch_env_peak: 0.0,
pitch_current: 0.0,
pitch_state: OscState::Off,
pitch_attack: pitch_attack_smoother.clone(),
pitch_decay: pitch_decay_smoother.clone(),
pitch_release: pitch_release_smoother.clone(),
pitch_enabled_2: self.pitch_enable_2,
pitch_env_peak_2: 0.0,
pitch_current_2: 0.0,
pitch_state_2: OscState::Off,
pitch_attack_2: pitch_attack_smoother_2.clone(),
pitch_decay_2: pitch_decay_smoother_2.clone(),
pitch_release_2: pitch_release_smoother_2.clone(),
_detune: 0.0,
_unison_detune_value: 0.0,
frequency: 0.0,
_attack_time: self.osc_attack,
_decay_time: self.osc_decay,
_release_time: self.osc_release,
_retrigger: self.osc_retrigger,
_voice_type: self.osc_type,
_angle: 0.0,
sample_pos: 0,
grain_start_pos: 0,
loop_it: self.loop_wavetable,
_granular_gap: 200,
_granular_hold: 200,
granular_hold_end: 200,
next_grain_pos: 400,
_end_position: scaled_end_pos,
_granular_crossfade: 50,
grain_attack: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_release: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_state: GrainState::Attacking,
},
);
}
}
// Remove any off notes
for (i, voice) in self.playing_voices.voices.clone().iter().enumerate() {
if voice.state == OscState::Off {
self.playing_voices.voices.remove(i);
} else if voice.grain_state == GrainState::Releasing
&& voice.grain_release.steps_left() == 0
{
self.playing_voices.voices.remove(i);
}
}
if self.audio_module_type == AudioModuleType::Osc {
for (i, unison_voice) in
self.unison_voices.voices.clone().iter().enumerate()
{
if unison_voice.state == OscState::Off {
self.unison_voices.voices.remove(i);
}
}
}
}
////////////////////////////////////////////////////////////
// MIDI EVENT NOTE OFF
////////////////////////////////////////////////////////////
NoteEvent::NoteOff { note, .. } => {
// Set note off variable to pass back to filter
note_off = true;
// Get voices on our note and not already releasing
// When a voice reaches 0.0 target on releasing
let mut shifted_note: u8 = note;
// Sampler when single cycle needs this!!!
if self.single_cycle {
// 31 comes from comparing with 3xOsc position in MIDI notes
shifted_note += 31;
}
// Calculate note shifting to match note on shifts
let semi_shift: u8 = self.osc_semitones as u8;
shifted_note = match self.osc_octave {
-2 => shifted_note - 24 + semi_shift,
-1 => shifted_note - 12 + semi_shift,
0 => shifted_note + semi_shift,
1 => shifted_note + 12 + semi_shift,
2 => shifted_note + 24 + semi_shift,
_ => shifted_note + semi_shift,
};
if self.audio_module_type == AudioModuleType::Osc {
// Update the matching unison voices
for unison_voice in self.unison_voices.voices.iter_mut() {
if unison_voice.note == shifted_note
&& unison_voice.state != OscState::Releasing
{
// Start our release level from our current gain on the voice
unison_voice.osc_release.reset(unison_voice.amp_current);
// Set our new release target to 0.0 so the note fades
match unison_voice.osc_release.style {
SmoothingStyle::Logarithmic(_) | SmoothingStyle::LogSteep(_)=> {
unison_voice
.osc_release
.set_target(self.sample_rate, 0.0001);
}
_ => {
unison_voice
.osc_release
.set_target(self.sample_rate, 0.0);
}
}
// Update our current amp
unison_voice.amp_current = unison_voice.osc_release.next();
// Update our voice state
unison_voice.state = OscState::Releasing;
}
}
}
// Iterate through our voice vecdeque to find the one to update
for voice in self.playing_voices.voices.iter_mut() {
// Update current voices to releasing state if they're valid
if voice.note == shifted_note && voice.state != OscState::Releasing {
// Start our release level from our current gain on the voice
voice.osc_release.reset(voice.amp_current);
// Set our new release target to 0.0 so the note fades
match voice.osc_release.style {
SmoothingStyle::Logarithmic(_) | SmoothingStyle::LogSteep(_) => {
voice.osc_release.set_target(self.sample_rate, 0.0001);
}
_ => {
voice.osc_release.set_target(self.sample_rate, 0.0);
}
}
// Update our current amp
voice.amp_current = voice.osc_release.next();
// Update our base voice state to releasing
voice.state = OscState::Releasing;
}
}
}
// Stop event - doesn't seem to work from FL Studio but left in here
NoteEvent::Choke { .. } => {
self.playing_voices.voices.clear();
self.unison_voices.voices.clear();
}
_ => (),
}
}
// The event was invalid - do nothing
None => (),
}
// This is a dummy entry
let mut next_grain: SingleVoice = SingleVoice {
note: 0,
_velocity: 0.0,
vel_mod_amount: 0.0,
phase: 0.0,
phase_delta: 0.0,
state: OscState::Off,
// These get cloned since smoother cannot be copied
amp_current: 0.0,
osc_attack: Smoother::new(SmoothingStyle::None),
osc_decay: Smoother::new(SmoothingStyle::None),
osc_release: Smoother::new(SmoothingStyle::None),
pitch_enabled: false,
pitch_env_peak: 0.0,
pitch_current: 0.0,
pitch_state: OscState::Off,
pitch_attack: Smoother::new(SmoothingStyle::None),
pitch_decay: Smoother::new(SmoothingStyle::None),
pitch_release: Smoother::new(SmoothingStyle::None),
pitch_enabled_2: false,
pitch_env_peak_2: 0.0,
pitch_current_2: 0.0,
pitch_state_2: OscState::Off,
pitch_attack_2: Smoother::new(SmoothingStyle::None),
pitch_decay_2: Smoother::new(SmoothingStyle::None),
pitch_release_2: Smoother::new(SmoothingStyle::None),
_detune: 0.0,
_unison_detune_value: 0.0,
frequency: 0.0,
_attack_time: self.osc_attack,
_decay_time: self.osc_decay,
_release_time: self.osc_release,
_retrigger: self.osc_retrigger,
_voice_type: self.osc_type,
_angle: 0.0,
sample_pos: 0,
loop_it: self.loop_wavetable,
grain_start_pos: 0,
_granular_gap: 200,
_granular_hold: 200,
granular_hold_end: 200,
next_grain_pos: 400,
_end_position: 800,
_granular_crossfade: 50,
grain_attack: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_release: Smoother::new(SmoothingStyle::Linear(5.0)),
grain_state: GrainState::Attacking,
};
let mut new_grain: bool = false;
// Second check for off notes before output to cut down on iterating...with iterating
for (i, voice) in self.playing_voices.voices.clone().iter().enumerate() {
if voice.state == OscState::Off {
self.playing_voices.voices.remove(i);
} else if voice.grain_state == GrainState::Releasing
&& voice.grain_release.steps_left() == 0
{
self.playing_voices.voices.remove(i);
}
}
if self.audio_module_type == AudioModuleType::Osc {
for (i, unison_voice) in self.unison_voices.voices.clone().iter().enumerate() {
if unison_voice.state == OscState::Off {
self.unison_voices.voices.remove(i);
}
}
}
////////////////////////////////////////////////////////////
// Update our voices before output
////////////////////////////////////////////////////////////
for voice in self.playing_voices.voices.iter_mut() {
if self.audio_module_type == AudioModuleType::Osc
|| self.audio_module_type == AudioModuleType::Sampler
{
// Move our phase outside of the midi events
// I couldn't find much on how to model this so I based it off previous note phase
voice.phase += voice.phase_delta;
if voice.phase > 1.0 {
voice.phase -= 1.0;
}
// This happens on extreme pitch envelope values only and catches wild increments
// or pitches above nyquist that would alias into other pitches
if voice.phase > 1.0 {
voice.phase = voice.phase % 1.0;
}
// Move our pitch envelopes if this is an Osc
if self.audio_module_type == AudioModuleType::Osc && voice.pitch_enabled {
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if voice.pitch_attack.steps_left() == 0 && voice.pitch_state == OscState::Attacking {
voice.pitch_state = OscState::Decaying;
voice.pitch_current = voice.pitch_attack.next();
// Now we will use decay smoother from here
voice.pitch_decay.reset(voice.pitch_current);
let sustain_scaled = self.pitch_env_sustain / 999.9;
voice.pitch_decay.set_target(self.sample_rate, sustain_scaled.clamp(0.0001, 999.9));
}
// Move from Decaying to Sustain hold
if voice.pitch_decay.steps_left() == 0 && voice.pitch_state == OscState::Decaying {
let sustain_scaled = self.pitch_env_sustain / 999.9;
voice.pitch_current = sustain_scaled;
voice.pitch_decay.set_target(self.sample_rate, sustain_scaled.clamp(0.0001, 999.9));
voice.pitch_state = OscState::Sustaining;
}
// End of release
if voice.pitch_state == OscState::Releasing && voice.pitch_release.steps_left() == 0 {
voice.pitch_state = OscState::Off;
}
} else {
// Reassign here for safety
voice.pitch_current = 0.0;
voice.pitch_state = OscState::Off;
}
if self.audio_module_type == AudioModuleType::Osc && voice.pitch_enabled_2 {
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if voice.pitch_attack_2.steps_left() == 0 && voice.pitch_state_2 == OscState::Attacking {
voice.pitch_state_2 = OscState::Decaying;
voice.pitch_current_2 = voice.pitch_attack_2.next();
// Now we will use decay smoother from here
voice.pitch_decay_2.reset(voice.pitch_current_2);
let sustain_scaled_2 = self.pitch_env_sustain_2 / 999.9;
voice.pitch_decay_2.set_target(self.sample_rate, sustain_scaled_2.clamp(0.0001, 999.9));
}
// Move from Decaying to Sustain hold
if voice.pitch_decay_2.steps_left() == 0 && voice.pitch_state_2 == OscState::Decaying {
let sustain_scaled_2 = self.pitch_env_sustain_2 / 999.9;
voice.pitch_current_2 = sustain_scaled_2;
voice.pitch_decay_2.set_target(self.sample_rate, sustain_scaled_2.clamp(0.0001, 999.9));
voice.pitch_state_2 = OscState::Sustaining;
}
// End of release
if voice.pitch_state_2 == OscState::Releasing && voice.pitch_release_2.steps_left() == 0 {
voice.pitch_state_2 = OscState::Off;
}
} else {
// Reassign here for safety
voice.pitch_current_2 = 0.0;
voice.pitch_state_2 = OscState::Off;
}
// Move from attack to decay if needed
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if voice.osc_attack.steps_left() == 0 && voice.state == OscState::Attacking {
voice.state = OscState::Decaying;
voice.amp_current = voice.osc_attack.next();
// Now we will use decay smoother from here
voice.osc_decay.reset(voice.amp_current);
let sustain_scaled = self.osc_sustain / 999.9;
voice.osc_decay.set_target(self.sample_rate, sustain_scaled);
}
// Move from Decaying to Sustain hold
if voice.osc_decay.steps_left() == 0 && voice.state == OscState::Decaying {
let sustain_scaled = self.osc_sustain / 999.9;
voice.amp_current = sustain_scaled;
voice.osc_decay.set_target(self.sample_rate, sustain_scaled);
voice.state = OscState::Sustaining;
}
// End of release
if voice.state == OscState::Releasing && voice.osc_release.steps_left() == 0 {
voice.state = OscState::Off;
}
} else if self.audio_module_type == AudioModuleType::Granulizer {
// Move from attack to decay if needed
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if voice.osc_attack.steps_left() == 0 && voice.state == OscState::Attacking {
voice.state = OscState::Decaying;
voice.amp_current = voice.osc_attack.next();
// Now we will use decay smoother from here
voice.osc_decay.reset(voice.amp_current);
let sustain_scaled = self.osc_sustain / 999.9;
voice.osc_decay.set_target(self.sample_rate, sustain_scaled);
}
// Move from Decaying to Sustain hold
if voice.osc_decay.steps_left() == 0 && voice.state == OscState::Decaying {
let sustain_scaled = self.osc_sustain / 999.9;
voice.amp_current = sustain_scaled;
voice.osc_decay.set_target(self.sample_rate, sustain_scaled);
voice.state = OscState::Sustaining;
}
let scaled_start_position =
(self.loaded_sample[0].len() as f32 * self.start_position).floor() as usize;
let scaled_end_position =
(self.loaded_sample[0].len() as f32 * self._end_position).floor() as usize;
// If our end grain marker goes outside of our sample length it should wrap around if looping
if voice.granular_hold_end > self.loaded_sample[0].len()
|| voice.granular_hold_end > voice._end_position
{
if voice.loop_it {
voice.granular_hold_end = voice.granular_hold_end
- self.loaded_sample[0].len()
+ scaled_start_position;
} else {
voice.granular_hold_end = scaled_end_position;
}
}
// If our next grain goes outside of our sample length it should also wrap on loop
if voice.next_grain_pos > self.loaded_sample[0].len()
|| voice.next_grain_pos > voice._end_position
{
if voice.loop_it {
voice.next_grain_pos -= self.loaded_sample[0].len() - scaled_start_position;
} else {
voice.next_grain_pos = scaled_end_position;
}
}
// If we are in the start grain crossfade
if voice.sample_pos == voice.grain_start_pos {
voice.grain_state = GrainState::Attacking;
voice.grain_attack.reset(0.0);
voice.grain_attack.set_target(self.sample_rate, 1.0);
}
// If we are in the end grain crossfade
else if voice.sample_pos > voice.granular_hold_end
&& voice.grain_state == GrainState::Attacking
{
voice.grain_state = GrainState::Releasing;
voice.grain_release.reset(1.0);
voice.grain_release.set_target(self.sample_rate, 0.0);
// If we are at the end of our grain and need to create a new one
new_grain = true;
let new_end = voice.next_grain_pos + self.grain_hold as usize;
next_grain = SingleVoice {
note: voice.note,
_velocity: voice._velocity,
vel_mod_amount: 0.0,
phase: voice.phase,
phase_delta: voice.phase_delta,
state: voice.state,
// These get cloned since smoother cannot be copied
amp_current: voice.amp_current,
osc_attack: voice.osc_attack.clone(),
osc_decay: voice.osc_decay.clone(),
osc_release: voice.osc_release.clone(),
pitch_enabled: voice.pitch_enabled,
pitch_env_peak: voice.pitch_env_peak,
pitch_current: voice.pitch_current,
pitch_state: voice.pitch_state,
pitch_attack: voice.pitch_attack.clone(),
pitch_decay: voice.pitch_decay.clone(),
pitch_release: voice.pitch_release.clone(),
pitch_enabled_2: voice.pitch_enabled_2,
pitch_env_peak_2: voice.pitch_env_peak_2,
pitch_current_2: voice.pitch_current_2,
pitch_state_2: voice.pitch_state_2,
pitch_attack_2: voice.pitch_attack_2.clone(),
pitch_decay_2: voice.pitch_decay_2.clone(),
pitch_release_2: voice.pitch_release_2.clone(),
_detune: voice._detune,
_unison_detune_value: voice._unison_detune_value,
frequency: voice.frequency,
_attack_time: voice._attack_time,
_decay_time: voice._decay_time,
_release_time: voice._release_time,
_retrigger: voice._retrigger,
_voice_type: voice._voice_type,
_angle: voice._angle,
sample_pos: voice.next_grain_pos,
loop_it: voice.loop_it,
grain_start_pos: voice.next_grain_pos,
_granular_gap: self.grain_gap,
_granular_hold: self.grain_hold,
granular_hold_end: new_end,
next_grain_pos: new_end + self.grain_gap as usize,
_end_position: voice._end_position,
_granular_crossfade: self.grain_crossfade,
grain_attack: Smoother::new(SmoothingStyle::Linear(
self.grain_crossfade as f32,
)),
grain_release: Smoother::new(SmoothingStyle::Linear(
self.grain_crossfade as f32,
)),
grain_state: GrainState::Attacking,
};
}
// End of release
if (voice.state == OscState::Releasing && voice.osc_release.steps_left() == 0)
|| (voice.grain_state == GrainState::Releasing
&& voice.grain_release.steps_left() == 0)
{
voice.state = OscState::Off;
}
}
}
match self.audio_module_type {
AudioModuleType::Osc | AudioModuleType::Sampler => {
// Update our matching unison voices
for unison_voice in self.unison_voices.voices.iter_mut() {
// Move our phase outside of the midi events
// I couldn't find much on how to model this so I based it off previous note phase
unison_voice.phase += unison_voice.phase_delta;
if unison_voice.phase > 1.0 {
unison_voice.phase -= 1.0;
}
// This happens on extreme pitch envelope values only and catches wild increments
// or pitches above nyquist that would alias into other pitches
if unison_voice.phase > 1.0 {
unison_voice.phase = unison_voice.phase % 1.0;
}
// Move our pitch envelopes if this is an Osc
if self.audio_module_type == AudioModuleType::Osc && unison_voice.pitch_enabled {
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if unison_voice.pitch_attack.steps_left() == 0 && unison_voice.pitch_state == OscState::Attacking {
unison_voice.pitch_state = OscState::Decaying;
unison_voice.pitch_current = unison_voice.pitch_attack.next();
// Now we will use decay smoother from here
unison_voice.pitch_decay.reset(unison_voice.pitch_current);
let sustain_scaled = self.pitch_env_sustain / 999.9;
unison_voice.pitch_decay.set_target(self.sample_rate, sustain_scaled.clamp(0.0001, 999.9));
}
// Move from Decaying to Sustain hold
if unison_voice.pitch_decay.steps_left() == 0 && unison_voice.pitch_state == OscState::Decaying {
let sustain_scaled = self.pitch_env_sustain / 999.9;
unison_voice.pitch_current = sustain_scaled;
unison_voice.pitch_decay.set_target(self.sample_rate, sustain_scaled.clamp(0.0001, 999.9));
unison_voice.pitch_state = OscState::Sustaining;
}
// End of release
if unison_voice.pitch_state == OscState::Releasing && unison_voice.pitch_release.steps_left() == 0 {
unison_voice.pitch_state = OscState::Off;
}
} else {
// Reassign here for safety
unison_voice.pitch_current = 0.0;
unison_voice.pitch_state = OscState::Off;
}
if self.audio_module_type == AudioModuleType::Osc && unison_voice.pitch_enabled_2 {
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if unison_voice.pitch_attack_2.steps_left() == 0 && unison_voice.pitch_state_2 == OscState::Attacking {
unison_voice.pitch_state_2 = OscState::Decaying;
unison_voice.pitch_current_2 = unison_voice.pitch_attack_2.next();
// Now we will use decay smoother from here
unison_voice.pitch_decay_2.reset(unison_voice.pitch_current_2);
let sustain_scaled_2 = self.pitch_env_sustain_2 / 999.9;
unison_voice.pitch_decay_2.set_target(self.sample_rate, sustain_scaled_2.clamp(0.0001, 999.9));
}
// Move from Decaying to Sustain hold
if unison_voice.pitch_decay_2.steps_left() == 0 && unison_voice.pitch_state_2 == OscState::Decaying {
let sustain_scaled_2 = self.pitch_env_sustain_2 / 999.9;
unison_voice.pitch_current_2 = sustain_scaled_2;
unison_voice.pitch_decay_2.set_target(self.sample_rate, sustain_scaled_2.clamp(0.0001, 999.9));
unison_voice.pitch_state_2 = OscState::Sustaining;
}
// End of release
if unison_voice.pitch_state_2 == OscState::Releasing && unison_voice.pitch_release_2.steps_left() == 0 {
unison_voice.pitch_state_2 = OscState::Off;
}
} else {
// Reassign here for safety
unison_voice.pitch_current_2 = 0.0;
unison_voice.pitch_state_2 = OscState::Off;
}
// Move from attack to decay if needed
// Attack is over so use decay amount to reach sustain level - reusing current smoother
if unison_voice.osc_attack.steps_left() == 0
&& unison_voice.state == OscState::Attacking
{
unison_voice.state = OscState::Decaying;
unison_voice.amp_current = unison_voice.osc_attack.next();
// Now we will use decay smoother from here
unison_voice.osc_decay.reset(unison_voice.amp_current);
let sustain_scaled = self.osc_sustain / 999.9;
unison_voice
.osc_decay
.set_target(self.sample_rate, sustain_scaled);
}
// Move from Decaying to Sustain hold
if unison_voice.osc_decay.steps_left() == 0
&& unison_voice.state == OscState::Decaying
{
unison_voice.state = OscState::Sustaining;
let sustain_scaled = self.osc_sustain / 999.9;
unison_voice.amp_current = sustain_scaled;
unison_voice
.osc_decay
.set_target(self.sample_rate, sustain_scaled);
}
// End of release
if unison_voice.state == OscState::Releasing
&& unison_voice.osc_release.steps_left() == 0
{
unison_voice.state = OscState::Off;
}
}
}
_ => {}
}
// Add our new grain to our voices
if new_grain {
self.playing_voices.voices.push_back(next_grain);
}
////////////////////////////////////////////////////////////
// Create output
////////////////////////////////////////////////////////////
let output_signal_l: f32;
let output_signal_r: f32;
(output_signal_l, output_signal_r) = match self.audio_module_type {
AudioModuleType::Osc => {
let mut summed_voices_l: f32 = 0.0;
let mut summed_voices_r: f32 = 0.0;
let mut stereo_voices_l: f32 = 0.0;
let mut stereo_voices_r: f32 = 0.0;
let mut center_voices: f32 = 0.0;
for voice in self.playing_voices.voices.iter_mut() {
// Move the pitch envelope stuff independently of the MIDI info
if voice.pitch_enabled {
voice.pitch_current = match voice.pitch_state {
OscState::Attacking => {
voice.pitch_attack.next()
},
OscState::Decaying => {
voice.pitch_decay.next()
},
OscState::Sustaining => {
self.pitch_env_sustain / 999.9
},
OscState::Releasing => {
voice.pitch_release.next()
},
OscState::Off => 0.0,
}
}
if voice.pitch_enabled_2 {
voice.pitch_current_2 = match voice.pitch_state_2 {
OscState::Attacking => {
voice.pitch_attack_2.next()
},
OscState::Decaying => {
voice.pitch_decay_2.next()
},
OscState::Sustaining => {
self.pitch_env_sustain_2 / 999.9
},
OscState::Releasing => {
voice.pitch_release_2.next()
},
OscState::Off => 0.0,
}
}
let temp_osc_gain_multiplier: f32;
// Get our current gain amount for use in match below
// Include gain scaling if mod is there
if vel_gain_mod != -2.0 {
temp_osc_gain_multiplier = match voice.state {
OscState::Attacking => {
voice.osc_attack.next() * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Decaying => {
voice.osc_decay.next() * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Sustaining => {
(self.osc_sustain / 999.9) * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Releasing => {
voice.osc_release.next() * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Off => 0.0,
};
} else {
temp_osc_gain_multiplier = match voice.state {
OscState::Attacking => voice.osc_attack.next() * vel_lfo_gain_mod,
OscState::Decaying => voice.osc_decay.next() * vel_lfo_gain_mod,
OscState::Sustaining => (self.osc_sustain / 999.9) * vel_lfo_gain_mod,
OscState::Releasing => voice.osc_release.next() * vel_lfo_gain_mod,
OscState::Off => 0.0,
};
}
voice.amp_current = temp_osc_gain_multiplier;
let nyquist = self.sample_rate/2.0;
if voice.vel_mod_amount == 0.0 {
let base_note = voice.note as f32 + voice._detune + detune_mod + voice.pitch_current + voice.pitch_current_2;
voice.phase_delta =
util::f32_midi_note_to_freq(base_note).min(nyquist) / self.sample_rate;
} else {
let base_note = voice.note as f32
+ voice._detune
+ detune_mod
+ (voice.vel_mod_amount * voice._velocity)
+ voice.pitch_current
+ voice.pitch_current_2;
voice.phase_delta =
util::f32_midi_note_to_freq(base_note).min(nyquist) / self.sample_rate;
}
if self.audio_module_type == AudioModuleType::Osc {
center_voices += match self.osc_type {
VoiceType::Sine => {
Oscillator::get_sine(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::Tri => {
Oscillator::get_tri(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::Saw => {
Oscillator::get_saw(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::RSaw => {
Oscillator::get_rsaw(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::WSaw => {
Oscillator::get_wsaw(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::RASaw => {
Oscillator::get_rasaw(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::SSaw => {
Oscillator::get_ssaw(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::Ramp => {
Oscillator::get_ramp(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::Square => {
Oscillator::get_square(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::RSquare => {
Oscillator::get_rsquare(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::Pulse => {
Oscillator::get_pulse(voice.phase) * temp_osc_gain_multiplier
}
VoiceType::Noise => {
self.noise_obj.generate_sample() * temp_osc_gain_multiplier
}
};
}
}
// Stereo applies to unison voices
for unison_voice in self.unison_voices.voices.iter_mut() {
let temp_osc_gain_multiplier: f32;
// Get our current gain amount for use in match below
// Include gain scaling if mod is there
if vel_gain_mod != -2.0 {
temp_osc_gain_multiplier = match unison_voice.state {
OscState::Attacking => {
unison_voice.osc_attack.next() * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Decaying => {
unison_voice.osc_decay.next() * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Sustaining => {
(self.osc_sustain / 999.9) * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Releasing => {
unison_voice.osc_release.next() * vel_gain_mod * vel_lfo_gain_mod
}
OscState::Off => 0.0,
};
} else {
temp_osc_gain_multiplier = match unison_voice.state {
OscState::Attacking => {
unison_voice.osc_attack.next() * vel_lfo_gain_mod
}
OscState::Decaying => unison_voice.osc_decay.next() * vel_lfo_gain_mod,
OscState::Sustaining => (self.osc_sustain / 999.9) * vel_lfo_gain_mod,
OscState::Releasing => {
unison_voice.osc_release.next() * vel_lfo_gain_mod
}
OscState::Off => 0.0,
};
}
unison_voice.amp_current = temp_osc_gain_multiplier;
if unison_voice.vel_mod_amount == 0.0 {
let base_note = unison_voice.note as f32
+ unison_voice._unison_detune_value
+ uni_detune_mod
+ unison_voice.pitch_current
+ unison_voice.pitch_current_2;
unison_voice.phase_delta =
util::f32_midi_note_to_freq(base_note) / self.sample_rate;
} else {
let base_note = unison_voice.note as f32
+ unison_voice._unison_detune_value
+ uni_detune_mod
+ (unison_voice.vel_mod_amount * unison_voice._velocity)
+ unison_voice.pitch_current
+ unison_voice.pitch_current_2;
unison_voice.phase_delta =
util::f32_midi_note_to_freq(base_note) / self.sample_rate;
}
if self.osc_unison > 1 {
let mut temp_unison_voice: f32 = 0.0;
if self.audio_module_type == AudioModuleType::Osc {
temp_unison_voice = match self.osc_type {
VoiceType::Sine => {
Oscillator::get_sine(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::Tri => {
Oscillator::get_tri(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::Saw => {
Oscillator::get_saw(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::RSaw => {
Oscillator::get_rsaw(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::WSaw => {
Oscillator::get_wsaw(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::SSaw => {
Oscillator::get_ssaw(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::RASaw => {
Oscillator::get_rasaw(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::Ramp => {
Oscillator::get_ramp(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::Square => {
Oscillator::get_square(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::RSquare => {
Oscillator::get_rsquare(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::Pulse => {
Oscillator::get_pulse(unison_voice.phase)
* temp_osc_gain_multiplier
}
VoiceType::Noise => {
self.noise_obj.generate_sample() * temp_osc_gain_multiplier
}
};
}
// Create our stereo pan for unison
// Our angle comes back as radians
let pan = unison_voice._angle;
// Calculate the amplitudes for the panned voice using vector operations
let scale = SQRT_2 / 2.0;
let left_amp = scale * (pan.cos() + pan.sin()) * temp_unison_voice;
let right_amp = scale * (pan.cos() - pan.sin()) * temp_unison_voice;
// Add the voice to the sum of stereo voices
stereo_voices_l += left_amp;
stereo_voices_r += right_amp;
}
}
// Sum our voices for output
summed_voices_l += center_voices;
summed_voices_r += center_voices;
// Scaling of output based on stereo voices and unison
summed_voices_l += stereo_voices_l / (self.osc_unison - 1).clamp(1, 9) as f32;
summed_voices_r += stereo_voices_r / (self.osc_unison - 1).clamp(1, 9) as f32;
// Stereo Spreading code
let width_coeff = self.osc_stereo * 0.5;
let mid = (summed_voices_l + summed_voices_r) * 0.5;
let stereo = (summed_voices_r - summed_voices_l) * width_coeff;
summed_voices_l = mid - stereo;
summed_voices_r = mid + stereo;
// Return output
(summed_voices_l, summed_voices_r)
}
AudioModuleType::Sampler => {
let mut summed_voices_l: f32 = 0.0;
let mut summed_voices_r: f32 = 0.0;
for voice in self.playing_voices.voices.iter_mut() {
// Get our current gain amount for use in match below
let temp_osc_gain_multiplier: f32 = match voice.state {
OscState::Attacking => voice.osc_attack.next(),
OscState::Decaying => voice.osc_decay.next(),
OscState::Sustaining => self.osc_sustain / 999.9,
OscState::Releasing => voice.osc_release.next(),
OscState::Off => 0.0,
};
voice.amp_current = temp_osc_gain_multiplier;
let usize_note = voice.note as usize;
// If we even have valid samples loaded
if self.sample_lib[0][0].len() > 1
&& self.loaded_sample[0].len() > 1
&& self.sample_lib.len() > 1
{
// Use our Vec<midi note value<VectorOfChannels<VectorOfSamples>>>
// If our note is valid 0-127
if usize_note < self.sample_lib.len() {
// If our sample position is valid for our note
if voice.sample_pos < self.sample_lib[usize_note][0].len() {
// Get our channels of sample vectors
let NoteVector = &self.sample_lib[usize_note];
// We don't need to worry about mono/stereo here because it's been setup in load_new_sample()
summed_voices_l +=
NoteVector[0][voice.sample_pos] * temp_osc_gain_multiplier;
summed_voices_r +=
NoteVector[1][voice.sample_pos] * temp_osc_gain_multiplier;
}
}
let scaled_start_position = (self.sample_lib[usize_note][0].len() as f32
* self.start_position)
.floor() as usize;
let scaled_end_position = (self.sample_lib[usize_note][0].len() as f32
* self._end_position)
.floor() as usize;
// Sampler moves position
voice.sample_pos += 1;
if voice.loop_it
&& (voice.sample_pos > self.sample_lib[usize_note][0].len()
|| voice.sample_pos > scaled_end_position)
{
voice.sample_pos = scaled_start_position;
} else if voice.sample_pos > scaled_end_position {
voice.sample_pos = self.sample_lib[usize_note][0].len();
voice.state = OscState::Off;
}
}
}
(summed_voices_l, summed_voices_r)
}
AudioModuleType::Off => {
// Do nothing, return 0.0
(0.0, 0.0)
}
AudioModuleType::Granulizer => {
let mut summed_voices_l: f32 = 0.0;
let mut summed_voices_r: f32 = 0.0;
for voice in self.playing_voices.voices.iter_mut() {
// Get our current gain amount for use in match below
let temp_osc_gain_multiplier: f32 = match voice.state {
OscState::Attacking => voice.osc_attack.next(),
OscState::Decaying => voice.osc_decay.next(),
OscState::Sustaining => self.osc_sustain / 999.9,
OscState::Releasing => voice.osc_release.next(),
OscState::Off => 0.0,
};
voice.amp_current = temp_osc_gain_multiplier;
let usize_note = voice.note as usize;
// If we even have valid samples loaded
if self.sample_lib[0][0].len() > 1
&& self.loaded_sample[0].len() > 1
&& self.sample_lib.len() > 1
{
// Use our Vec<midi note value<VectorOfChannels<VectorOfSamples>>>
// If our note is valid 0-127
if usize_note < self.sample_lib.len() {
// If our sample position is valid for our note
if voice.sample_pos < self.sample_lib[usize_note][0].len() {
// Get our channels of sample vectors
let NoteVector = &self.sample_lib[usize_note];
// If we are in crossfade or in middle of grain after atttack ends
if voice.grain_state == GrainState::Attacking {
// Add our current grain
if voice.grain_attack.steps_left() != 0 {
// This format is: Output = CurrentSample * Voice ADSR * GrainRelease
summed_voices_l += NoteVector[0][voice.sample_pos]
* temp_osc_gain_multiplier
* voice.grain_attack.next();
summed_voices_r += NoteVector[1][voice.sample_pos]
* temp_osc_gain_multiplier
* voice.grain_attack.next();
} else {
// This format is: Output = CurrentSample * Voice ADSR * GrainRelease
summed_voices_l += NoteVector[0][voice.sample_pos]
* temp_osc_gain_multiplier;
summed_voices_r += NoteVector[1][voice.sample_pos]
* temp_osc_gain_multiplier;
}
}
// If we are in crossfade
else if voice.grain_state == GrainState::Releasing {
summed_voices_l += NoteVector[0][voice.sample_pos]
* temp_osc_gain_multiplier
* voice.grain_release.next();
summed_voices_r += NoteVector[1][voice.sample_pos]
* temp_osc_gain_multiplier
* voice.grain_release.next();
}
}
}
let scaled_start_position = (self.loaded_sample[0].len() as f32
* self.start_position)
.floor() as usize;
let scaled_end_position = (self.loaded_sample[0].len() as f32
* self._end_position)
.floor() as usize;
// Granulizer moves position
voice.sample_pos += 1;
if voice.loop_it
&& (voice.sample_pos > self.loaded_sample[0].len()
|| voice.sample_pos > scaled_end_position)
{
voice.sample_pos = scaled_start_position;
} else if voice.sample_pos > scaled_end_position {
voice.sample_pos = self.sample_lib[usize_note][0].len();
voice.state = OscState::Off;
}
}
}
(summed_voices_l, summed_voices_r)
}
};
// Send it back
(output_signal_l, output_signal_r, note_on, note_off)
}
pub fn set_playing(&mut self, new_bool: bool) {
self.is_playing = new_bool;
}
pub fn get_playing(&mut self) -> bool {
self.is_playing
}
pub fn clear_voices(&mut self) {
self.playing_voices.voices.clear();
self.unison_voices.voices.clear();
}
pub fn load_new_sample(&mut self, path: PathBuf) {
let reader = hound::WavReader::open(&path);
if let Ok(mut reader) = reader {
let spec = reader.spec();
//let inner_sample_rate = spec.sample_rate as f32;
let channels = spec.channels as usize;
let samples;
if spec.bits_per_sample == 8 {
// Since 16 bit is loud I'm scaling this one too for safety
samples = match spec.sample_format {
hound::SampleFormat::Int => reader
.samples::<i8>()
.map(|s| {
util::db_to_gain(-36.0)
* ((s.unwrap_or_default() as f32 * 256.0) / i8::MAX as f32)
})
.collect::<Vec<f32>>(),
hound::SampleFormat::Float => reader
.samples::<f32>()
.map(|s| s.unwrap_or_default())
.collect::<Vec<f32>>(),
};
} else if spec.bits_per_sample == 16 {
// I noticed 16 bit can be LOUD so I tried to scale it
samples = match spec.sample_format {
hound::SampleFormat::Int => reader
.samples::<i16>()
.map(|s| {
util::db_to_gain(-36.0)
* ((s.unwrap_or_default() as f32 * 256.0) / i16::MAX as f32)
})
.collect::<Vec<f32>>(),
hound::SampleFormat::Float => reader
.samples::<f32>()
.map(|s| s.unwrap_or_default())
.collect::<Vec<f32>>(),
};
} else {
// Attempt 32 bit cast/decode if 8/16 are invalid - no scaling
samples = match spec.sample_format {
hound::SampleFormat::Int => reader
.samples::<i32>()
.map(|s| (s.unwrap_or_default() as f32 * 256.0) / i32::MAX as f32)
.collect::<Vec<f32>>(),
hound::SampleFormat::Float => reader
.samples::<f32>()
.map(|s| s.unwrap_or_default())
.collect::<Vec<f32>>(),
};
}
// Uninterleave sample format to chunks for resampling
let mut new_samples = vec![Vec::with_capacity(samples.len() / channels); channels];
for sample_chunk in samples.chunks(channels) {
// sample_chunk is a chunk like [a, b]
for (i, sample) in sample_chunk.into_iter().enumerate() {
new_samples[i].push(sample.clone());
}
}
self.loaded_sample = new_samples;
// Based off restretch vs non stretch use different algorithms
// To generate a sample library
self.regenerate_samples();
};
}
// This method performs the sample recalculations when restretch is toggled
pub fn regenerate_samples(&mut self) {
if !self.sample_lib.is_empty() {
if self.audio_module_type == AudioModuleType::Sampler {
// Compare our restretch change
if self.restretch != self.prev_restretch {
self.prev_restretch = self.restretch;
}
} else if self.audio_module_type == AudioModuleType::Granulizer {
self.restretch = true;
self.prev_restretch = false;
} else {
return;
}
self.sample_lib.clear();
}
if self.restretch {
let middle_c: f32 = 256.0;
// Generate our sample library from our sample
for i in 0..127 {
let target_pitch_factor = util::f32_midi_note_to_freq(i as f32) / middle_c;
// Calculate the number of samples in the shifted frame
let shifted_num_samples =
(self.loaded_sample[0].len() as f32 / target_pitch_factor).round() as usize;
// Apply pitch shifting by interpolating between the original samples
let mut shifted_samples_l = Vec::with_capacity(shifted_num_samples);
let mut shifted_samples_r = Vec::with_capacity(shifted_num_samples);
for j in 0..shifted_num_samples {
let original_index: usize;
let fractional_part: f32;
original_index = (j as f32 * target_pitch_factor).floor() as usize;
fractional_part = j as f32 * target_pitch_factor - original_index as f32;
if original_index < self.loaded_sample[0].len() - 1 {
// Linear interpolation between adjacent samples
let interpolated_sample_r;
let interpolated_sample_l = (1.0 - fractional_part)
* self.loaded_sample[0][original_index]
+ fractional_part * self.loaded_sample[0][original_index + 1];
if self.loaded_sample.len() > 1 {
interpolated_sample_r = (1.0 - fractional_part)
* self.loaded_sample[1][original_index]
+ fractional_part * self.loaded_sample[1][original_index + 1];
} else {
interpolated_sample_r = interpolated_sample_l;
}
shifted_samples_l.push(interpolated_sample_l);
shifted_samples_r.push(interpolated_sample_r);
} else {
// If somehow through buffer shenanigans we are past our length we shouldn't do anything here
if original_index < self.loaded_sample[0].len() {
shifted_samples_l.push(self.loaded_sample[0][original_index]);
if self.loaded_sample.len() > 1 {
shifted_samples_r.push(self.loaded_sample[1][original_index]);
} else {
shifted_samples_r.push(self.loaded_sample[0][original_index]);
}
}
}
}
let mut NoteVector = Vec::with_capacity(2);
NoteVector.insert(0, shifted_samples_l);
NoteVector.insert(1, shifted_samples_r);
self.sample_lib.insert(i, NoteVector);
}
}
// If we are just pitch shifting instead of restretching
else {
let mut shifter = PitchShifter::new(50, self.sample_rate as usize);
for i in 0..127 {
let translated_i = (i as i32 - 60_i32) as f32;
let mut out_buffer_left = vec![0.0; self.loaded_sample[0].len()];
let mut out_buffer_right = vec![0.0; self.loaded_sample[0].len()];
let loaded_left = self.loaded_sample[0].as_slice();
let loaded_right;
if self.loaded_sample.len() > 1 {
loaded_right = self.loaded_sample[1].as_slice();
} else {
loaded_right = self.loaded_sample[0].as_slice();
}
shifter.shift_pitch(1, translated_i, loaded_left, &mut out_buffer_left);
shifter.shift_pitch(1, translated_i, loaded_right, &mut out_buffer_right);
let mut NoteVector = Vec::with_capacity(2);
NoteVector.insert(0, out_buffer_left);
NoteVector.insert(1, out_buffer_right);
self.sample_lib.insert(i, NoteVector);
}
}
}
fn calculate_panning(&mut self, voice_index: i32, num_voices: i32) -> f32 {
// Ensure the voice index is within bounds.
let voice_index = voice_index.min(num_voices - 1);
let sign = if self.two_voice_stereo_flipper {
1.0
} else {
-1.0
};
// Handle the special case for 2 voices.
if num_voices == 2 {
// multiplied by sign of stereo flipper to avoid pan
return if voice_index == 0 {
-0.25 * std::f32::consts::PI * sign // First voice panned left
} else {
if self.two_voice_stereo_flipper {
self.two_voice_stereo_flipper = false;
} else {
self.two_voice_stereo_flipper = true;
}
0.25 * std::f32::consts::PI * sign // Second voice panned right
};
}
// Handle the special case for 3 voices.
if num_voices == 3 {
return match voice_index {
0 => {
if self.two_voice_stereo_flipper {
self.two_voice_stereo_flipper = false;
} else {
self.two_voice_stereo_flipper = true;
}
-0.25 * std::f32::consts::PI * sign
} // First voice panned left
1 => 0.0, // Second voice panned center
2 => 0.25 * std::f32::consts::PI * sign, // Third voice panned right
_ => 0.0, // Handle other cases gracefully
};
}
// Calculate the pan angle for voices with index 0 and 1.
let base_angle = ((voice_index / 2) as f32) / ((num_voices / 2) as f32 - 1.0) - 0.5;
// Determine the final angle based on even or odd index.
let angle = if voice_index % 2 == 0 {
-base_angle
} else {
base_angle
};
if voice_index == 0 {
if self.two_voice_stereo_flipper {
self.two_voice_stereo_flipper = false;
} else {
self.two_voice_stereo_flipper = true;
}
}
angle * std::f32::consts::PI * sign // Use full scale for other cases
}
}