aerothesis

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

lib.rs (17300B)


      1 use nih_plug::prelude::*;
      2 use std::{collections::VecDeque, sync::Arc};
      3 
      4 // This is a shortened version of the gain example with most comments removed, check out
      5 // https://github.com/robbert-vdh/nih-plug/blob/master/plugins/examples/gain/src/lib.rs to get
      6 // started
      7 
      8 pub struct Aerothesis {
      9     pub params: Arc<AerothesisParams>,
     10 
     11     pub x_prev: f32,
     12     pub x_prev2: f32,
     13 
     14     pub v_prev: f32,
     15 
     16     pub f_prev: f32,
     17     pub f_prev2: f32,
     18     pub sample_rate: f32,
     19 
     20     pub v_breath: f32,
     21     pub v_bite: f32,
     22 
     23     pub v_fluid_prev: f32,
     24 
     25     pub x_history: VecDeque<f32>,
     26 
     27     pub note_frequency: f32,
     28 
     29     pub displacement_prev: f32,
     30     pub velocity_prev: f32,
     31     pub displacement_prev2: f32,
     32     pub accel_prev: f32,
     33     pub f: f32,
     34     pub resonance: f32,
     35 }
     36 
     37 #[derive(Enum, PartialEq, Clone, Copy)]
     38 pub enum InstrumentType {
     39     #[name = "Single Reed"]
     40     SingleReed,
     41     #[name = "Rip Reed"]
     42     LipReed,
     43 }
     44 
     45 #[derive(Enum, PartialEq, Clone, Copy)]
     46 pub enum ResonanceType {
     47     #[name = "Open Pipe"]
     48     OpenPipe,
     49     #[name = "Closed Pipe"]
     50     ClosedPipe,
     51 }
     52 
     53 #[derive(Params)]
     54 pub struct AerothesisParams {
     55     /// The parameter's ID is used to identify the parameter in the wrappred plugin API. As long as
     56     /// these IDs remain constant, you can rename and reorder these fields as you wish. The
     57     /// parameters are exposed to the host in the same order they were defined. In this case, this
     58     /// gain parameter is stored as linear gain while the values are displayed in decibels.
     59     #[id = "gain"]
     60     pub gain: FloatParam,
     61 
     62     #[id = "instrument_type"]
     63     pub instrument_type: EnumParam<InstrumentType>,
     64 
     65     #[id = "ReedLength"]
     66     pub reed_length: FloatParam,
     67 
     68     #[id = "base_mass"]
     69     pub base_mass: FloatParam,
     70     #[id = "bite_mass_scale"]
     71     pub bite_mass_scale: FloatParam,
     72     #[id = "base_stiffness"]
     73     pub base_stiffness: FloatParam,
     74     #[id = "bite_stiffness_scale"]
     75     pub bite_stiffness_scale: FloatParam,
     76     #[id = "base_damping"]
     77     pub base_damping: FloatParam,
     78     #[id = "bite_damping_scale"]
     79     pub bite_damping_scale: FloatParam,
     80     #[id = "breath_damping"]
     81     pub breath_damping: FloatParam,
     82     #[id = "pressure_scale"]
     83     pub pressure_scale: FloatParam,
     84     #[id = "feedback_gain"]
     85     pub feedback_gain: FloatParam,
     86 
     87     #[id = "breath_cc"]
     88     pub breath_cc: IntParam,
     89     #[id = "bite_cc"]
     90     pub bite_cc: IntParam,
     91 
     92     #[id = "resonance_type"]
     93     pub resonance_type: EnumParam<ResonanceType>,
     94 
     95     #[id = "resonance_decay"]
     96     pub resonance_decay: FloatParam,
     97 }
     98 
     99 impl Default for Aerothesis {
    100     fn default() -> Self {
    101         Self {
    102             params: Arc::new(AerothesisParams::default()),
    103             x_prev: 0.0,
    104             x_prev2: 0.0,
    105             v_prev: 0.0,
    106             f_prev: 0.0,
    107             f_prev2: 0.0,
    108             sample_rate: 44100.0,
    109 
    110             v_breath: 0.1,
    111             v_bite: 0.0,
    112             v_fluid_prev: 0.0,
    113 
    114             x_history: VecDeque::new(),
    115 
    116             note_frequency: 0.0,
    117 
    118             displacement_prev: 0.0,
    119             velocity_prev: 0.0,
    120             displacement_prev2: 0.0,
    121             accel_prev: 0.0,
    122             f: 0.0,
    123             resonance: 0.0,
    124         }
    125     }
    126 }
    127 
    128 impl Default for AerothesisParams {
    129     fn default() -> Self {
    130         Self {
    131             // This gain is stored as linear gain. NIH-plug comes with useful conversion functions
    132             // to treat these kinds of parameters as if we were dealing with decibels. Storing this
    133             // as decibels is easier to work with, but requires a conversion for every sample.
    134             gain: FloatParam::new(
    135                 "Gain",
    136                 util::db_to_gain(0.0),
    137                 FloatRange::Skewed {
    138                     min: util::db_to_gain(-30.0),
    139                     max: util::db_to_gain(30.0),
    140                     // This makes the range appear as if it was linear when displaying the values as
    141                     // decibels
    142                     factor: FloatRange::gain_skew_factor(-30.0, 30.0),
    143                 },
    144             )
    145             // Because the gain parameter is stored as linear gain instead of storing the value as
    146             // decibels, we need logarithmic smoothing
    147             .with_smoother(SmoothingStyle::Logarithmic(50.0))
    148             .with_unit(" dB")
    149             // There are many predefined formatters we can use here. If the gain was stored as
    150             // decibels instead of as a linear gain value, we could have also used the
    151             // `.with_step_size(0.1)` function to get internal rounding.
    152             .with_value_to_string(formatters::v2s_f32_gain_to_db(2))
    153             .with_string_to_value(formatters::s2v_f32_gain_to_db()),
    154 
    155             instrument_type: EnumParam::new("Instrument Type", InstrumentType::SingleReed),
    156 
    157             reed_length: FloatParam::new(
    158                 "Reed Length",
    159                 0.01,
    160                 FloatRange::Skewed {
    161                     min: 0.001,
    162                     max: 0.1,
    163                     factor: 0.2,
    164                 },
    165             ),
    166 
    167             base_mass: FloatParam::new(
    168                 "Base Mass",
    169                 0.0005,
    170                 FloatRange::Skewed {
    171                     min: 0.0001,
    172                     max: 0.01,
    173                     factor: 0.2,
    174                 },
    175             ),
    176             bite_mass_scale: FloatParam::new(
    177                 "Bite Mass Reduction",
    178                 0.5,
    179                 FloatRange::Linear { min: 0.0, max: 0.9 },
    180             ),
    181             base_stiffness: FloatParam::new(
    182                 "Base Stiffness",
    183                 2000.0,
    184                 FloatRange::Skewed {
    185                     min: 100.0,
    186                     max: 20000.0,
    187                     factor: 0.3,
    188                 },
    189             ),
    190             bite_stiffness_scale: FloatParam::new(
    191                 "Bite Stiffness Incr.",
    192                 2.0,
    193                 FloatRange::Linear {
    194                     min: 0.0,
    195                     max: 10.0,
    196                 },
    197             ),
    198             base_damping: FloatParam::new(
    199                 "Base Damping",
    200                 0.001,
    201                 FloatRange::Skewed {
    202                     min: 0.0001,
    203                     max: 0.1,
    204                     factor: 0.2,
    205                 },
    206             ),
    207             bite_damping_scale: FloatParam::new(
    208                 "Bite Damping Incr.",
    209                 1.0,
    210                 FloatRange::Linear {
    211                     min: 0.0,
    212                     max: 10.0,
    213                 },
    214             ),
    215             breath_damping: FloatParam::new(
    216                 "Breath Damping",
    217                 0.1,
    218                 FloatRange::Linear { min: 0.0, max: 1.0 },
    219             ),
    220             pressure_scale: FloatParam::new(
    221                 "Pressure Gain",
    222                 50.0,
    223                 FloatRange::Linear {
    224                     min: 0.0,
    225                     max: 200.0,
    226                 },
    227             ),
    228             feedback_gain: FloatParam::new(
    229                 "Feedback Gain",
    230                 5.0,
    231                 FloatRange::Linear {
    232                     min: 0.0,
    233                     max: 10.0,
    234                 },
    235             ),
    236 
    237             breath_cc: IntParam::new("Breath CC", 2, IntRange::Linear { min: 0, max: 127 }),
    238             bite_cc: IntParam::new("Bite CC", 11, IntRange::Linear { min: 0, max: 127 }),
    239 
    240             resonance_type: EnumParam::new("Resonance Type", ResonanceType::OpenPipe),
    241             resonance_decay: FloatParam::new(
    242                 "Resonance Decay",
    243                 0.9,
    244                 FloatRange::Skewed {
    245                     min: 0.0,
    246                     max: 1.0,
    247                     factor: 0.8,
    248                 },
    249             ),
    250         }
    251     }
    252 }
    253 
    254 impl Aerothesis {
    255     pub fn osc(&mut self) -> f32 {
    256         let x_n = self.osc_x();
    257         let v_n = self.v(x_n);
    258 
    259         self.x_prev2 = self.x_prev;
    260         self.x_prev = x_n;
    261 
    262         self.f_prev2 = self.f_prev;
    263         self.f_prev = self.f;
    264 
    265         self.v_prev = v_n;
    266         self.v_fluid_prev = self.vf();
    267 
    268         x_n
    269     }
    270 
    271     pub fn m(&self) -> f32 {
    272         self.params.base_mass.value() * (1.0 - self.params.bite_mass_scale.value() * self.v_bite)
    273     }
    274 
    275     pub fn r(&self) -> f32 {
    276         self.params.base_damping.value()
    277             * (1.0 + self.params.bite_damping_scale.value() * self.v_bite)
    278     }
    279 
    280     pub fn k(&self) -> f32 {
    281         self.params.base_stiffness.value()
    282             * (1.0 + self.params.bite_stiffness_scale.value() * self.v_bite)
    283     }
    284     pub fn vf(&self) -> f32 {
    285         const EPS: f32 = 1e-5;
    286         const RHO: f32 = 1.2;
    287         let t = 1.0 / self.sample_rate;
    288 
    289         let a_fluid = (RHO * self.params.reed_length.value()) / t;
    290 
    291         let gap_prev = (2.0 - self.x_prev).clamp(EPS, 2.0);
    292         let b_prev = RHO / (4.0 * (gap_prev * gap_prev));
    293         let pressure = self.params.base_damping.value()
    294             * (self.v_breath + self.resonance * 100.0 * self.params.feedback_gain.value());
    295         let c_prev = pressure - b_prev * (self.v_fluid_prev * self.v_fluid_prev);
    296 
    297         // Current gap is also based on x_prev in this discrete model for stability
    298         let gap_curr = (2.0 - self.x_prev).clamp(EPS, 2.0);
    299         let b_curr = RHO / (4.0 * (gap_curr * gap_curr));
    300 
    301         if gap_curr <= EPS {
    302             0.0
    303         } else {
    304             let discriminant = (a_fluid * a_fluid
    305                 + 4.0 * b_curr * (a_fluid * self.v_fluid_prev + c_prev))
    306                 .max(0.0);
    307             let numerator = -a_fluid + discriminant.sqrt();
    308             numerator / (2.0 * b_curr)
    309         }
    310     }
    311     pub fn f(&self) -> f32 {
    312         const EPS: f32 = 1e-5;
    313         const RHO: f32 = 1.2;
    314 
    315         let gap_curr = (2.0 - self.x_prev).clamp(EPS, 2.0);
    316 
    317         let v_fluid_current = self.vf();
    318 
    319         let f_current = if self.x_prev >= 2.0 {
    320             0.0
    321         } else {
    322             0.5 * RHO * (v_fluid_current * v_fluid_current) * gap_curr
    323         };
    324 
    325         f_current
    326     }
    327     pub fn osc_x(&mut self) -> f32 {
    328         let m = self.m();
    329         let r = self.r();
    330         let k = self.k();
    331         let t = 1.0 / self.sample_rate;
    332         let f_current = self.f;
    333 
    334         let b0 = t * t;
    335         let b1 = 2.0 * t * t;
    336         let b2 = t * t;
    337 
    338         let a0 = 4.0 * m + 2.0 * r * t + k * t * t;
    339         let a1 = -8.0 * m + 2.0 * k * t * t;
    340         let a2 = 4.0 * m - 2.0 * r * t + k * t * t;
    341 
    342         ((b0 * f_current + b1 * self.f_prev + b2 * self.f_prev2
    343             - a1 * self.x_prev
    344             - a2 * self.x_prev2)
    345             / a0)
    346             .clamp(0.0, 2.0)
    347     }
    348 
    349     pub fn v(&self, x: f32) -> f32 {
    350         let t = 1.0 / self.sample_rate;
    351         (2.0 / t) * (x - self.x_prev) - self.v_prev
    352     }
    353 
    354     pub fn resonance(&mut self) -> f32 {
    355         if self.resonance_delay_samples() > self.x_history.len() as f32 {
    356             0.0
    357         } else {
    358             if self.resonance_delay_samples() < self.x_history.len() as f32 {
    359                 self.x_history
    360                     .truncate(self.resonance_delay_samples() as usize);
    361             }
    362             let decay: f32 = if self.params.resonance_type.value() == ResonanceType::OpenPipe {
    363                 1.0
    364             } else {
    365                 -1.0
    366             };
    367             let x_delay = self.x_history.pop_back().unwrap_or(0.0);
    368             decay * x_delay
    369         }
    370     }
    371     pub fn displacement(&mut self) -> f32 {
    372         self.resonance = self.resonance();
    373         self.f = self.f();
    374         let x_n = self.osc();
    375         let x_current = x_n - self.equilibrium_offset();
    376 
    377         // let displacement = x_current
    378         //     * (self.params.resonance_decay.value()
    379         //         * (x_current - self.displacement_prev)
    380         //         * (x_current - self.displacement_prev))
    381         //         .clamp(0.0, 1.0);
    382         let displacement = x_current;
    383 
    384         self.displacement_prev = displacement;
    385 
    386         self.x_history.push_front(displacement);
    387         // displacement + self.resonance
    388         displacement
    389         // self.resonance
    390     }
    391 
    392     fn equilibrium_offset(&self) -> f32 {
    393         let f = self.f;
    394         let k = self.k();
    395         if k > 0.0 {
    396             (f / k).clamp(0.0, 1.8)
    397         } else {
    398             0.0
    399         }
    400     }
    401     pub fn resonance_delay_samples(&self) -> f32 {
    402         if self.params.resonance_type.value() == ResonanceType::OpenPipe {
    403             self.sample_rate / self.note_frequency
    404         } else {
    405             self.sample_rate / 2.0 / self.note_frequency
    406         }
    407     }
    408     pub fn avg_x_history(&self) -> f32 {
    409         self.x_history.iter().sum::<f32>() / self.x_history.len() as f32
    410     }
    411 }
    412 
    413 impl Plugin for Aerothesis {
    414     const NAME: &'static str = "Aerothesis";
    415     const VENDOR: &'static str = "Minerva_Juppiter";
    416     const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
    417     const EMAIL: &'static str = "aerothesis@minervajuppiter.net";
    418 
    419     const VERSION: &'static str = env!("CARGO_PKG_VERSION");
    420 
    421     // The first audio IO layout is used as the default. The other layouts may be selected either
    422     // explicitly or automatically by the host or the user depending on the plugin API/backend.
    423     const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
    424         main_input_channels: NonZeroU32::new(2),
    425         main_output_channels: NonZeroU32::new(2),
    426 
    427         aux_input_ports: &[],
    428         aux_output_ports: &[],
    429 
    430         // Individual ports and the layout as a whole can be named here. By default these names
    431         // are generated as needed. This layout will be called 'Stereo', while a layout with
    432         // only one input and output channel would be called 'Mono'.
    433         names: PortNames::const_default(),
    434     }];
    435 
    436     const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
    437     const MIDI_OUTPUT: MidiConfig = MidiConfig::None;
    438 
    439     const SAMPLE_ACCURATE_AUTOMATION: bool = true;
    440 
    441     // If the plugin can send or receive SysEx messages, it can define a type to wrap around those
    442     // messages here. The type implements the `SysExMessage` trait, which allows conversion to and
    443     // from plain byte buffers.
    444     type SysExMessage = ();
    445     // More advanced plugins can use this to run expensive background tasks. See the field's
    446     // documentation for more information. `()` means that the plugin does not have any background
    447     // tasks.
    448     type BackgroundTask = ();
    449 
    450     fn params(&self) -> Arc<dyn Params> {
    451         self.params.clone()
    452     }
    453 
    454     fn initialize(
    455         &mut self,
    456         _audio_io_layout: &AudioIOLayout,
    457         buffer_config: &BufferConfig,
    458         _context: &mut impl InitContext<Self>,
    459     ) -> bool {
    460         self.sample_rate = buffer_config.sample_rate;
    461         true
    462     }
    463 
    464     fn reset(&mut self) {
    465         // Reset buffers and envelopes here. This can be called from the audio thread and may not
    466         // allocate. You can remove this function if you do not need it.
    467         self.x_history.clear();
    468     }
    469 
    470     fn process(
    471         &mut self,
    472         buffer: &mut Buffer,
    473         _aux: &mut AuxiliaryBuffers,
    474         context: &mut impl ProcessContext<Self>,
    475     ) -> ProcessStatus {
    476         while let Some(event) = context.next_event() {
    477             match event {
    478                 NoteEvent::MidiCC {
    479                     timing: _,
    480                     channel: _,
    481                     cc,
    482                     value,
    483                 } => {
    484                     if cc as i32 == self.params.breath_cc.value() {
    485                         self.v_breath = value;
    486                     } else if cc as i32 == self.params.bite_cc.value() {
    487                         self.v_bite = value;
    488                     }
    489                 }
    490                 NoteEvent::NoteOn {
    491                     timing: _,
    492                     voice_id: _,
    493                     channel: _,
    494                     note,
    495                     velocity: _,
    496                 } => {
    497                     self.reset();
    498                     self.note_frequency = util::midi_note_to_freq(note);
    499                 }
    500                 _ => (),
    501             }
    502         }
    503 
    504         for channel_samples in buffer.iter_samples() {
    505             let gain = self.params.gain.smoothed.next();
    506             let x_current = self.displacement();
    507 
    508             for sample in channel_samples {
    509                 if self.note_frequency == 0.0 {
    510                     self.x_history.clear();
    511                     *sample = 0.0;
    512                 } else {
    513                     *sample = (x_current * gain).clamp(-1.0, 1.0);
    514                 }
    515             }
    516         }
    517 
    518         ProcessStatus::Normal
    519     }
    520 }
    521 
    522 impl ClapPlugin for Aerothesis {
    523     const CLAP_ID: &'static str = "com.your-domain.aerothesis";
    524     const CLAP_DESCRIPTION: Option<&'static str> = Some("A short description of your plugin");
    525     const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
    526     const CLAP_SUPPORT_URL: Option<&'static str> = None;
    527 
    528     // Don't forget to change these features
    529     const CLAP_FEATURES: &'static [ClapFeature] = &[
    530         ClapFeature::AudioEffect,
    531         ClapFeature::Stereo,
    532         ClapFeature::NoteDetector,
    533     ];
    534 }
    535 
    536 // impl Vst3Plugin for Aerothesis {
    537 //     const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!";
    538 
    539 //     // And also don't forget to change these categories
    540 //     const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
    541 //         &[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
    542 // }
    543 
    544 nih_export_clap!(Aerothesis);
    545 // nih_export_vst3!(Aerothesis);