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