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);