mooxide

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

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