strombos

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

lib.rs (10650B)


      1 use nih_plug::prelude::*;
      2 use std::f32::consts::PI;
      3 use std::sync::Arc;
      4 
      5 // This is a shortened version of the gain example with most comments removed, check out
      6 // https://github.com/robbert-vdh/nih-plug/blob/master/plugins/examples/gain/src/lib.rs to get
      7 // started
      8 
      9 struct Strombos {
     10     params: Arc<StrombosParams>,
     11     sample_rate: f32,
     12     phase: f32,
     13     breath: f32,
     14     bite: f32,
     15     notefreq: f32,
     16     note_time: u32,
     17     a0: f32,
     18     a1: f32,
     19     a2: f32,
     20     b0: f32,
     21     b1: f32,
     22     b2: f32,
     23     in1: f32,
     24     in2: f32,
     25     out1: f32,
     26     out2: f32,
     27 }
     28 
     29 #[derive(Params)]
     30 struct StrombosParams {
     31     /// The parameter's ID is used to identify the parameter in the wrappred plugin API. As long as
     32     /// these IDs remain constant, you can rename and reorder these fields as you wish. The
     33     /// parameters are exposed to the host in the same order they were defined. In this case, this
     34     /// gain parameter is stored as linear gain while the values are displayed in decibels.
     35     #[id = "gain"]
     36     pub gain: FloatParam,
     37     #[id = "breath cc"]
     38     pub breath_cc: IntParam,
     39     #[id = "bite"]
     40     pub bite_cc: IntParam,
     41 }
     42 
     43 impl Default for Strombos {
     44     fn default() -> Self {
     45         Self {
     46             params: Arc::new(StrombosParams::default()),
     47             sample_rate: 44100.0,
     48             phase: 0.0,
     49             breath: 0.0,
     50             bite: 0.0,
     51             notefreq: 440.0,
     52             note_time: 0,
     53             a0: 1.0,
     54             a1: 0.0,
     55             a2: 0.0,
     56             b0: 1.0,
     57             b1: 0.0,
     58             b2: 0.0,
     59             in1: 0.0,
     60             in2: 0.0,
     61             out1: 0.0,
     62             out2: 0.0,
     63         }
     64     }
     65 }
     66 
     67 impl Default for StrombosParams {
     68     fn default() -> Self {
     69         Self {
     70             // This gain is stored as linear gain. NIH-plug comes with useful conversion functions
     71             // to treat these kinds of parameters as if we were dealing with decibels. Storing this
     72             // as decibels is easier to work with, but requires a conversion for every sample.
     73             gain: FloatParam::new(
     74                 "Gain",
     75                 util::db_to_gain(0.0),
     76                 FloatRange::Skewed {
     77                     min: util::db_to_gain(-30.0),
     78                     max: util::db_to_gain(30.0),
     79                     // This makes the range appear as if it was linear when displaying the values as
     80                     // decibels
     81                     factor: FloatRange::gain_skew_factor(-30.0, 30.0),
     82                 },
     83             )
     84             // Because the gain parameter is stored as linear gain instead of storing the value as
     85             // decibels, we need logarithmic smoothing
     86             .with_smoother(SmoothingStyle::Logarithmic(50.0))
     87             .with_unit(" dB")
     88             // There are many predefined formatters we can use here. If the gain was stored as
     89             // decibels instead of as a linear gain value, we could have also used the
     90             // `.with_step_size(0.1)` function to get internal rounding.
     91             .with_value_to_string(formatters::v2s_f32_gain_to_db(2))
     92             .with_string_to_value(formatters::s2v_f32_gain_to_db()),
     93 
     94             breath_cc: IntParam::new("breath_cc", 2, IntRange::Linear { min: 0, max: 127 }),
     95             bite_cc: IntParam::new("bite", 11, IntRange::Linear { min: 0, max: 127 }),
     96         }
     97     }
     98 }
     99 
    100 impl Strombos {
    101     fn base_osc(&self) -> f32 {
    102         const PROPORION: f32 = 0.8;
    103         let phi = self.phase % 1.0;
    104         if 0.0 <= phi && phi < 0.5 * PROPORION {
    105             2.0 / PROPORION * phi
    106         } else if 0.5 * PROPORION <= phi && phi < 1.0 - 0.5 * PROPORION {
    107             -2.0 / (1.0 - PROPORION) * phi + (1.0 + PROPORION / (1.0 - PROPORION))
    108         } else {
    109             2.0 / PROPORION * phi - 2.0 / PROPORION
    110         }
    111     }
    112 
    113     fn osc(&self) -> f32 {
    114         let cycle: i32 = (self.phase % 3.0 as f32) as i32;
    115         let amplitudes = [0.25, 1.0, 0.25];
    116         amplitudes[cycle as usize] * self.base_osc()
    117     }
    118     fn update_phase(&mut self) {
    119         let phase = 3.0 * self.notefreq / self.sample_rate as f32;
    120         if self.phase + phase > 3.0 {
    121             self.phase += phase - 3.0;
    122         } else {
    123             self.phase += phase;
    124         }
    125     }
    126     fn cutoff(&self) -> f32 {
    127         // 2.0 * self.notefreq * self.breath * self.bit
    128         2.0 * self.notefreq * (1.0 + self.breath) * (1.0 + self.bite)
    129     }
    130     fn resonance(&self) -> f32 {
    131         // self.bite
    132         10.0_f32.powf(1.0 + self.bite)
    133     }
    134     fn lowpass(&mut self, sample: f32) -> f32 {
    135         let cutoff = self.cutoff();
    136         let resonance = self.resonance();
    137         let omega = 2.0 * PI * cutoff / self.sample_rate;
    138         let alpha = (omega.sin()) / (2.0 * resonance);
    139 
    140         self.a0 = 1.0 + alpha;
    141         self.a1 = -2.0 * omega.cos();
    142         self.a2 = 1.0 - alpha;
    143         self.b0 = (1.0 - omega.cos()) / 2.0;
    144         self.b1 = 1.0 - omega.cos();
    145         self.b2 = (1.0 - omega.cos()) / 2.0;
    146 
    147         let input = sample;
    148         let output =
    149             self.b0 / self.a0 * input + self.b1 / self.a0 * self.in1 + self.b2 / self.a0 * self.in2
    150                 - self.a1 / self.a0 * self.out1
    151                 - self.a2 / self.a0 * self.out2;
    152 
    153         self.in2 = self.in1;
    154         self.in1 = input;
    155         self.out2 = self.out1;
    156         self.out1 = output;
    157 
    158         return output;
    159     }
    160 }
    161 
    162 impl Plugin for Strombos {
    163     const NAME: &'static str = "Strombos";
    164     const VENDOR: &'static str = "Minerva_Juppiter";
    165     const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
    166     const EMAIL: &'static str = "strombos@minervajuppiter.net";
    167 
    168     const VERSION: &'static str = env!("CARGO_PKG_VERSION");
    169 
    170     // The first audio IO layout is used as the default. The other layouts may be selected either
    171     // explicitly or automatically by the host or the user depending on the plugin API/backend.
    172     const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
    173         main_input_channels: NonZeroU32::new(2),
    174         main_output_channels: NonZeroU32::new(2),
    175 
    176         aux_input_ports: &[],
    177         aux_output_ports: &[],
    178 
    179         // Individual ports and the layout as a whole can be named here. By default these names
    180         // are generated as needed. This layout will be called 'Stereo', while a layout with
    181         // only one input and output channel would be called 'Mono'.
    182         names: PortNames::const_default(),
    183     }];
    184 
    185     const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
    186     const MIDI_OUTPUT: MidiConfig = MidiConfig::None;
    187 
    188     const SAMPLE_ACCURATE_AUTOMATION: bool = true;
    189 
    190     // If the plugin can send or receive SysEx messages, it can define a type to wrap around those
    191     // messages here. The type implements the `SysExMessage` trait, which allows conversion to and
    192     // from plain byte buffers.
    193     type SysExMessage = ();
    194     // More advanced plugins can use this to run expensive background tasks. See the field's
    195     // documentation for more information. `()` means that the plugin does not have any background
    196     // tasks.
    197     type BackgroundTask = ();
    198 
    199     fn params(&self) -> Arc<dyn Params> {
    200         self.params.clone()
    201     }
    202 
    203     fn initialize(
    204         &mut self,
    205         _audio_io_layout: &AudioIOLayout,
    206         buffer_config: &BufferConfig,
    207         _context: &mut impl InitContext<Self>,
    208     ) -> bool {
    209         // Resize buffers and perform other potentially expensive initialization operations here.
    210         // The `reset()` function is always called right after this function. You can remove this
    211         // function if you do not need it.
    212         self.sample_rate = buffer_config.sample_rate as f32;
    213         true
    214     }
    215 
    216     fn reset(&mut self) {
    217         // Reset buffers and envelopes here. This can be called from the audio thread and may not
    218         // allocate. You can remove this function if you do not need it.
    219         self.note_time = 0;
    220         self.phase = 0.0;
    221     }
    222 
    223     fn process(
    224         &mut self,
    225         buffer: &mut Buffer,
    226         _aux: &mut AuxiliaryBuffers,
    227         context: &mut impl ProcessContext<Self>,
    228     ) -> ProcessStatus {
    229         while let Some(event) = context.next_event() {
    230             match event {
    231                 NoteEvent::MidiCC {
    232                     timing: _,
    233                     channel: _,
    234                     cc,
    235                     value,
    236                 } => {
    237                     if cc as i32 == self.params.breath_cc.value() {
    238                         self.breath = value as f32 / 127.0;
    239                     } else if cc as i32 == self.params.bite_cc.value() {
    240                         self.bite = value as f32 / 127.0;
    241                     }
    242                 }
    243                 NoteEvent::NoteOff {
    244                     timing: _,
    245                     voice_id: _,
    246                     channel: _,
    247                     note: _,
    248                     velocity: _,
    249                 } => {
    250                     self.reset();
    251                 }
    252                 NoteEvent::NoteOn {
    253                     timing: _,
    254                     voice_id: _,
    255                     channel: _,
    256                     note,
    257                     velocity: _,
    258                 } => {
    259                     self.notefreq = util::midi_note_to_freq(note);
    260                 }
    261                 _ => {}
    262             }
    263         }
    264         for channel_samples in buffer.iter_samples() {
    265             // Smoothing is optionally built into the parameters themselves
    266             let gain = self.params.gain.smoothed.next();
    267             for sample in channel_samples {
    268                 if self.notefreq == 0.0 {
    269                     *sample = 0.0;
    270                 } else {
    271                     *sample =
    272                         (gain * self.lowpass(self.osc()) * 1000.0 * self.breath).clamp(-1.0, 1.0);
    273                 }
    274             }
    275             self.note_time += 1;
    276             self.update_phase();
    277         }
    278 
    279         ProcessStatus::Normal
    280     }
    281 }
    282 
    283 impl ClapPlugin for Strombos {
    284     const CLAP_ID: &'static str = "net.minervajuppiter.strombos";
    285     const CLAP_DESCRIPTION: Option<&'static str> = Some("A short description of your plugin");
    286     const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
    287     const CLAP_SUPPORT_URL: Option<&'static str> = None;
    288 
    289     // Don't forget to change these features
    290     const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect, ClapFeature::Stereo];
    291 }
    292 
    293 // impl Vst3Plugin for Strombos {
    294 //     const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!";
    295 
    296 //     // And also don't forget to change these categories
    297 //     const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
    298 //         &[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
    299 // }
    300 
    301 nih_export_clap!(Strombos);
    302 // nih_export_vst3!(Strombos);