lib.rs (17747B)
1 use nih_plug::prelude::*; 2 use std::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 struct Mooxide { 9 params: Arc<MooxideParams>, 10 sample_rate: f32, 11 phases: [f32; 3], 12 midi_note_id: u8, 13 midi_note_frequency: f32, 14 midi_note_velocity: Smoother<f32>, 15 filter_biquad: [f32; 4], 16 note_time: u32, 17 } 18 19 #[derive(Enum, PartialEq, Clone, Copy)] 20 pub enum Waveform { 21 #[name = "Triangle"] 22 Triangle, 23 #[name = "Triangle-Sawtooth"] 24 TriangleSawtooth, 25 #[name = "Sawtooth"] 26 Sawtooth, 27 #[name = "ReverseSawtooth"] 28 ReverseSawtooth, 29 #[name = "Square"] 30 Square, 31 #[name = "Wide-Pulse"] 32 WidePulse, 33 #[name = "Narrow-Pulse"] 34 NarrowPulse, 35 } 36 37 #[derive(Enum, PartialEq, Clone, Copy)] 38 pub enum Range { 39 #[name = "2"] 40 Two, 41 #[name = "4"] 42 Four, 43 #[name = "8"] 44 Eight, 45 #[name = "16"] 46 Sixteen, 47 #[name = "32"] 48 ThirtyTwo, 49 #[name = "64"] 50 SixtyFour, 51 } 52 #[derive(Enum, PartialEq, Clone, Copy)] 53 pub enum NoiseKind { 54 #[name = "White"] 55 White, 56 #[name = "Pink"] 57 Pink, 58 } 59 60 #[derive(Params)] 61 struct MooxideParams { 62 /// The parameter's ID is used to identify the parameter in the wrappred plugin API. As long as 63 /// these IDs remain constant, you can rename and reorder these fields as you wish. The 64 /// parameters are exposed to the host in the same order they were defined. In this case, this 65 /// gain parameter is stored as linear gain while the values are displayed in decibels. 66 #[id = "tune"] 67 pub tune: FloatParam, 68 #[id = "occ1_range"] 69 pub osc1_range: EnumParam<Range>, 70 #[id = "osc1_wave"] 71 pub osc1_wave: EnumParam<Waveform>, 72 #[id = "osc1_mix"] 73 pub osc1_mix: FloatParam, 74 #[id = "osc2_range"] 75 pub osc2_range: EnumParam<Range>, 76 #[id = "osc2_detune"] 77 pub osc2_detune: FloatParam, 78 #[id = "osc2_wave"] 79 pub osc2_wave: EnumParam<Waveform>, 80 #[id = "osc2_mix"] 81 pub osc2_mix: FloatParam, 82 #[id = "osc3_range"] 83 pub osc3_range: EnumParam<Range>, 84 #[id = "osc3_detune"] 85 pub osc3_detune: FloatParam, 86 #[id = "osc3_wave"] 87 pub osc3_wave: EnumParam<Waveform>, 88 #[id = "osc3_mix"] 89 pub osc3_mix: FloatParam, 90 91 #[id = "noise"] 92 pub noise: EnumParam<NoiseKind>, 93 #[id = "noise_mix"] 94 pub noise_mix: FloatParam, 95 96 #[id = "filter_cutoff"] 97 pub filter_cutoff: FloatParam, 98 #[id = "filter_emphasis"] 99 pub filter_emphasis: FloatParam, 100 #[id = "filter_contour"] 101 pub filter_contour: FloatParam, 102 #[id = "filter_attack"] 103 pub filter_attack: FloatParam, 104 #[id = "filter_decay"] 105 pub filter_decay: FloatParam, 106 #[id = "filter_sustain"] 107 pub filter_sustain: FloatParam, 108 #[id = "contour_attack"] 109 pub contour_attack: FloatParam, 110 #[id = "contour_decay"] 111 pub contour_decay: FloatParam, 112 #[id = "contour_sustain"] 113 pub contour_sustain: FloatParam, 114 115 #[id = "gain"] 116 pub gain: FloatParam, 117 } 118 119 impl Default for Mooxide { 120 fn default() -> Self { 121 Self { 122 params: Arc::new(MooxideParams::default()), 123 sample_rate: 1.0, 124 phases: [0.0; 3], 125 midi_note_id: 0, 126 midi_note_frequency: 1.0, 127 midi_note_velocity: Smoother::new(SmoothingStyle::Linear(5.0)), 128 filter_biquad: [0.0; 4], 129 note_time: 0, 130 } 131 } 132 } 133 134 impl Default for MooxideParams { 135 fn default() -> Self { 136 Self { 137 gain: FloatParam::new("Gain", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 }), 138 tune: FloatParam::new( 139 "Tune", 140 0.0, 141 FloatRange::Linear { 142 min: -1.0, 143 max: 1.0, 144 }, 145 ), 146 osc1_range: EnumParam::new("Range", Range::Sixteen), 147 osc1_wave: EnumParam::new("Waveform", Waveform::Triangle), 148 osc1_mix: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 }), 149 osc2_range: EnumParam::new("Range", Range::ThirtyTwo), 150 osc2_detune: FloatParam::new( 151 "Detune", 152 0.0, 153 FloatRange::Linear { 154 min: -1.0, 155 max: 1.0, 156 }, 157 ), 158 osc2_wave: EnumParam::new("Waveform", Waveform::Triangle), 159 osc2_mix: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 }), 160 osc3_range: EnumParam::new("Range", Range::ThirtyTwo), 161 osc3_detune: FloatParam::new( 162 "Detune", 163 0.0, 164 FloatRange::Linear { 165 min: -1.0, 166 max: 1.0, 167 }, 168 ), 169 osc3_wave: EnumParam::new("Waveform", Waveform::Triangle), 170 osc3_mix: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 }), 171 172 noise: EnumParam::new("Noise", NoiseKind::White), 173 noise_mix: FloatParam::new("Mix", 1.0, FloatRange::Linear { min: 0.0, max: 1.0 }), 174 175 filter_cutoff: FloatParam::new( 176 "Filter Cutoff Frequency", 177 0.0, 178 FloatRange::Linear { 179 min: -5.0, 180 max: 5.0, 181 }, 182 ), 183 filter_emphasis: FloatParam::new( 184 "Filter Emphasis", 185 0.5, 186 FloatRange::Linear { 187 min: 0.01, 188 max: 1.0, 189 }, 190 ), 191 filter_contour: FloatParam::new( 192 "Filter Contour", 193 0.5, 194 FloatRange::Linear { min: 0.0, max: 1.0 }, 195 ), 196 filter_attack: FloatParam::new( 197 "Filter Attack Time", 198 0.6, 199 FloatRange::Skewed { 200 min: 0.001, 201 max: 20.0, 202 factor: FloatRange::skew_factor(-0.9), 203 }, 204 ), 205 filter_decay: FloatParam::new( 206 "Filter Decay Time", 207 0.6, 208 FloatRange::Skewed { 209 min: 0.001, 210 max: 20.0, 211 factor: FloatRange::skew_factor(-0.9), 212 }, 213 ), 214 filter_sustain: FloatParam::new( 215 "Filter Sustain Level", 216 0.5, 217 FloatRange::Linear { min: 0.0, max: 1.0 }, 218 ), 219 contour_attack: FloatParam::new( 220 "Contour Attack Time", 221 0.6, 222 FloatRange::Skewed { 223 min: 0.001, 224 max: 20.0, 225 factor: FloatRange::skew_factor(-0.9), 226 }, 227 ), 228 contour_decay: FloatParam::new( 229 "Contour Decay Time", 230 0.6, 231 FloatRange::Skewed { 232 min: 0.001, 233 max: 20.0, 234 factor: FloatRange::skew_factor(-0.9), 235 }, 236 ), 237 contour_sustain: FloatParam::new( 238 "Contour Sustain Level", 239 0.5, 240 FloatRange::Linear { min: 0.0, max: 1.0 }, 241 ), 242 } 243 } 244 } 245 246 impl Mooxide { 247 fn osc(&mut self, index: usize, freq: f32, wave: Waveform) -> f32 { 248 let phase = &mut self.phases[index]; 249 fn triangle(phase: f32) -> f32 { 250 1.0 - 2.0 * (phase + 0.75).fract().abs() 251 } 252 fn sawtooth(phase: f32) -> f32 { 253 2.0 * phase - 1.0 254 } 255 fn square(phase: f32, width: f32) -> f32 { 256 (phase < width) as i32 as f32 * 2.0 - 1.0 257 } 258 let out = match wave { 259 Waveform::Triangle => triangle(*phase), 260 Waveform::TriangleSawtooth => 0.5 * triangle(*phase) + 0.5 * sawtooth(*phase), 261 Waveform::Sawtooth => sawtooth(*phase), 262 Waveform::ReverseSawtooth => 1.0 - *phase * 2.0, 263 Waveform::Square => square(*phase, 0.5), 264 Waveform::WidePulse => square(*phase, 0.25), 265 Waveform::NarrowPulse => square(*phase, 0.125), 266 }; 267 268 // update phase 269 *phase += freq / self.sample_rate; 270 if *phase >= 1.0 { 271 *phase -= 1.0; 272 } 273 out 274 } 275 fn get_range_mult(&self, range: Range) -> f32 { 276 match range { 277 Range::Two => 0.25, 278 Range::Four => 0.5, 279 Range::Eight => 1.0, 280 Range::Sixteen => 2.0, 281 Range::ThirtyTwo => 4.0, 282 Range::SixtyFour => 8.0, 283 } 284 } 285 286 fn noise(&self, kind: NoiseKind) -> f32 { 287 match kind { 288 NoiseKind::White => rand::random::<f32>() * 2.0 - 1.0, 289 NoiseKind::Pink => { 290 let mut sum = 0.0; 291 for _ in 0..10 { 292 sum += rand::random::<f32>() * 2.0 - 1.0; 293 } 294 sum / 10.0 295 } 296 } 297 } 298 299 fn envelope(&self) -> f32 { 300 let time = self.note_time as f32 / self.sample_rate; 301 let attack_phase = (time / self.params.contour_attack.value()).clamp(0.0, 1.0); 302 let decay_phase = ((time - self.params.contour_attack.value()) 303 / self.params.contour_decay.value()) 304 .clamp(0.0, 1.0); 305 attack_phase * (1.0 - decay_phase) + (self.params.contour_sustain.value() * decay_phase) 306 } 307 308 fn filter_envelope(&self) -> f32 { 309 let time = self.note_time as f32 / self.sample_rate; 310 let attack_phase = (time / self.params.filter_attack.value()).clamp(0.0, 1.0); 311 let decay_phase = ((time - self.params.filter_attack.value()) 312 / self.params.filter_decay.value()) 313 .clamp(0.0, 1.0); 314 attack_phase * (1.0 - decay_phase) + (self.params.filter_sustain.value() * decay_phase) 315 } 316 317 fn filter(&mut self, input: f32) -> f32 { 318 // https://www.utsbox.com/?page_id=523 319 let openture = self.midi_note_frequency 320 * (2.0 + self.filter_envelope() + (self.params.filter_cutoff.value() / 5.0)); 321 let omega = (2.0 * std::f32::consts::PI * openture / self.sample_rate) 322 .clamp(0.01, std::f32::consts::PI - 0.01); 323 let alpha = omega.sin() / 2.0 / self.params.filter_emphasis.value(); 324 let a0 = 1.0 + alpha; 325 let a1 = -2.0 * omega.cos(); 326 let a2 = 1.0 - alpha; 327 let b0 = (1.0 - omega.cos()) / 2.0; 328 let b1 = 1.0 - omega.cos(); 329 let b2 = (1.0 - omega.cos()) / 2.0; 330 let y = b0 / a0 * input + b1 / a0 * self.filter_biquad[0] + b2 / a0 * self.filter_biquad[1] 331 - a1 / a0 * self.filter_biquad[2] 332 - a2 / a0 * self.filter_biquad[3]; 333 self.filter_biquad[1] = self.filter_biquad[0]; 334 self.filter_biquad[0] = input; 335 self.filter_biquad[3] = self.filter_biquad[2]; 336 self.filter_biquad[2] = y; 337 y 338 } 339 } 340 341 impl Plugin for Mooxide { 342 const NAME: &'static str = "Mooxide"; 343 const VENDOR: &'static str = "minerva-jupiter"; 344 const URL: &'static str = env!("CARGO_PKG_HOMEPAGE"); 345 const EMAIL: &'static str = "ryouturn@gmail.com"; 346 347 const VERSION: &'static str = env!("CARGO_PKG_VERSION"); 348 349 // The first audio IO layout is used as the default. The other layouts may be selected either 350 // explicitly or automatically by the host or the user depending on the plugin API/backend. 351 const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout { 352 main_input_channels: NonZeroU32::new(2), 353 main_output_channels: NonZeroU32::new(2), 354 355 aux_input_ports: &[], 356 aux_output_ports: &[], 357 358 // Individual ports and the layout as a whole can be named here. By default these names 359 // are generated as needed. This layout will be called 'Stereo', while a layout with 360 // only one input and output channel would be called 'Mono'. 361 names: PortNames::const_default(), 362 }]; 363 364 const MIDI_INPUT: MidiConfig = MidiConfig::Basic; 365 const MIDI_OUTPUT: MidiConfig = MidiConfig::None; 366 367 const SAMPLE_ACCURATE_AUTOMATION: bool = true; 368 369 // If the plugin can send or receive SysEx messages, it can define a type to wrap around those 370 // messages here. The type implements the `SysExMessage` trait, which allows conversion to and 371 // from plain byte buffers. 372 type SysExMessage = (); 373 // More advanced plugins can use this to run expensive background tasks. See the field's 374 // documentation for more information. `()` means that the plugin does not have any background 375 // tasks. 376 type BackgroundTask = (); 377 378 fn params(&self) -> Arc<dyn Params> { 379 self.params.clone() 380 } 381 382 fn initialize( 383 &mut self, 384 _audio_io_layout: &AudioIOLayout, 385 buffer_config: &BufferConfig, 386 _context: &mut impl InitContext<Self>, 387 ) -> bool { 388 // Resize buffers and perform other potentially expensive initialization operations here. 389 // The `reset()` function is always called right after this function. You can remove this 390 // function if you do not need it. 391 self.sample_rate = buffer_config.sample_rate; 392 true 393 } 394 395 fn reset(&mut self) { 396 // Reset buffers and envelopes here. This can be called from the audio thread and may not 397 // allocate. You can remove this function if you do not need it. 398 self.phases = [0.0; 3]; 399 self.midi_note_id = 0; 400 self.midi_note_frequency = 1.0; 401 self.midi_note_velocity.reset(0.0); 402 } 403 fn process( 404 &mut self, 405 buffer: &mut Buffer, 406 _aux: &mut AuxiliaryBuffers, 407 context: &mut impl ProcessContext<Self>, 408 ) -> ProcessStatus { 409 let mut next_event = context.next_event(); 410 for (sample_id, channel_samples) in buffer.iter_samples().enumerate() { 411 // Smoothing is optionally built into the parameters themselves 412 let gain = self.params.gain.smoothed.next(); 413 414 // This plugin can be either triggered by MIDI or controleld by a parameter 415 let displacement = { 416 // Act on the next MIDI event 417 while let Some(event) = next_event { 418 if event.timing() > sample_id as u32 { 419 break; 420 } 421 422 match event { 423 NoteEvent::NoteOn { note, velocity, .. } => { 424 self.midi_note_id = note; 425 self.midi_note_frequency = util::midi_note_to_freq(note); 426 self.midi_note_velocity 427 .set_target(self.sample_rate, velocity); 428 self.filter_biquad = [0.0; 4]; 429 self.note_time = 0; 430 } 431 NoteEvent::NoteOff { note, .. } if note == self.midi_note_id => { 432 self.midi_note_velocity.set_target(self.sample_rate, 0.0); 433 } 434 NoteEvent::PolyPressure { note, pressure, .. } 435 if note == self.midi_note_id => 436 { 437 self.midi_note_velocity 438 .set_target(self.sample_rate, pressure); 439 } 440 _ => (), 441 } 442 443 next_event = context.next_event(); 444 } 445 let tuned_frequency = self.midi_note_frequency 446 * (2.0f64.powf(self.params.tune.value() as f64 / 12.0) as f32); 447 let osc1 = self.osc( 448 0, 449 tuned_frequency * self.get_range_mult(self.params.osc1_range.value()), 450 self.params.osc1_wave.value(), 451 ); 452 let osc2 = self.osc( 453 1, 454 tuned_frequency 455 * self.get_range_mult(self.params.osc2_range.value()) 456 * (1.0 + self.params.osc2_detune.value() * 0.01), 457 self.params.osc2_wave.value(), 458 ); 459 let osc3 = self.osc( 460 2, 461 tuned_frequency 462 * self.get_range_mult(self.params.osc3_range.value()) 463 * (1.0 + self.params.osc3_detune.value() * 0.01), 464 self.params.osc3_wave.value(), 465 ); 466 467 let noise = self.noise(self.params.noise.value()); 468 469 self.filter( 470 (osc1 * self.params.osc1_mix.value() 471 + osc2 * self.params.osc2_mix.value() 472 + osc3 * self.params.osc3_mix.value() 473 + noise * self.params.noise_mix.value()) 474 * self.midi_note_velocity.next(), 475 ) * self.envelope() 476 }; 477 let output = displacement * util::db_to_gain_fast(gain); 478 for sample in channel_samples { 479 *sample = output.clamp(-1.0, 1.0); 480 } 481 self.note_time += 1; 482 } 483 484 ProcessStatus::KeepAlive 485 } 486 } 487 488 impl ClapPlugin for Mooxide { 489 const CLAP_ID: &'static str = "net.minervajuppiter.mooxide"; 490 const CLAP_DESCRIPTION: Option<&'static str> = Some("simple synth"); 491 const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); 492 const CLAP_SUPPORT_URL: Option<&'static str> = None; 493 494 // Don't forget to change these features 495 const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect, ClapFeature::Stereo]; 496 } 497 498 nih_export_clap!(Mooxide);