mooxide

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

commit a2b82534f023d2b4537938abb3dcb64a405534f4
parent b9406fbce17ead3f25c9e3600eef349993289944
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Tue, 10 Mar 2026 21:22:59 +0900

feat(synth): Add MIDI-controlled oscillator with waveform selection

Diffstat:
Msrc/lib.rs | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 114 insertions(+), 26 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs @@ -1,4 +1,6 @@ use nih_plug::prelude::*; +use std::f32::consts; +use std::str::SplitAsciiWhitespace; use std::sync::Arc; // This is a shortened version of the gain example with most comments removed, check out @@ -7,6 +9,21 @@ use std::sync::Arc; struct Mooxide { params: Arc<MooxideParams>, + sample_rate: f32, + phase: f32, + midi_note_id: u8, + midi_note_frequency: f32, + midi_note_velocity: Smoother<f32>, +} + +#[derive(Enum, PartialEq, Clone, Copy)] +pub enum Waveform { + #[name = "Sine"] + Sine, + #[name = "Triangle"] + Triangle, + #[name = "Sawtooth"] + Sawtooth, } #[derive(Params)] @@ -15,6 +32,8 @@ struct MooxideParams { /// these IDs remain constant, you can rename and reorder these fields as you wish. The /// parameters are exposed to the host in the same order they were defined. In this case, this /// gain parameter is stored as linear gain while the values are displayed in decibels. + #[id = "osc_wave"] + pub wave: EnumParam<Waveform>, #[id = "gain"] pub gain: FloatParam, } @@ -23,6 +42,11 @@ impl Default for Mooxide { fn default() -> Self { Self { params: Arc::new(MooxideParams::default()), + sample_rate: 1.0, + phase: 0.0, + midi_note_id: 0, + midi_note_frequency: 1.0, + midi_note_velocity: Smoother::new(SmoothingStyle::Linear(5.0)), } } } @@ -30,29 +54,43 @@ impl Default for Mooxide { impl Default for MooxideParams { fn default() -> Self { Self { - // This gain is stored as linear gain. NIH-plug comes with useful conversion functions - // to treat these kinds of parameters as if we were dealing with decibels. Storing this - // as decibels is easier to work with, but requires a conversion for every sample. gain: FloatParam::new( "Gain", - util::db_to_gain(0.0), - FloatRange::Skewed { - min: util::db_to_gain(-30.0), - max: util::db_to_gain(30.0), - // This makes the range appear as if it was linear when displaying the values as - // decibels - factor: FloatRange::gain_skew_factor(-30.0, 30.0), + -10.0, + FloatRange::Linear { + min: -30.0, + max: 0.0, }, ) - // Because the gain parameter is stored as linear gain instead of storing the value as - // decibels, we need logarithmic smoothing - .with_smoother(SmoothingStyle::Logarithmic(50.0)) - .with_unit(" dB") - // There are many predefined formatters we can use here. If the gain was stored as - // decibels instead of as a linear gain value, we could have also used the - // `.with_step_size(0.1)` function to get internal rounding. - .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) - .with_string_to_value(formatters::s2v_f32_gain_to_db()), + .with_smoother(SmoothingStyle::Linear(3.0)) + .with_step_size(0.01) + .with_unit(" dB"), + wave: EnumParam::new("Waveform", Waveform::Sine), + } + } +} + +impl Mooxide { + fn sin(&mut self, frequency: f32) -> f32 { + let sine = (self.phase * consts::TAU).sin(); + self.update_phase(frequency); + sine + } + fn triangle(&mut self, frequency: f32) -> f32 { + let triangle = 2.0 * (1.0 - self.phase).abs() - 1.0; + self.update_phase(frequency); + triangle + } + fn sawtooth(&mut self, frequency: f32) -> f32 { + let sawtooth = self.phase * 2.0 - 1.0; + self.update_phase(frequency); + sawtooth + } + + fn update_phase(&mut self, frequency: f32) { + self.phase += frequency / self.sample_rate; + if self.phase >= 1.0 { + self.phase -= 1.0; } } } @@ -80,7 +118,7 @@ impl Plugin for Mooxide { names: PortNames::const_default(), }]; - const MIDI_INPUT: MidiConfig = MidiConfig::None; + const MIDI_INPUT: MidiConfig = MidiConfig::Basic; const MIDI_OUTPUT: MidiConfig = MidiConfig::None; const SAMPLE_ACCURATE_AUTOMATION: bool = true; @@ -101,36 +139,86 @@ impl Plugin for Mooxide { fn initialize( &mut self, _audio_io_layout: &AudioIOLayout, - _buffer_config: &BufferConfig, + buffer_config: &BufferConfig, _context: &mut impl InitContext<Self>, ) -> bool { // Resize buffers and perform other potentially expensive initialization operations here. // The `reset()` function is always called right after this function. You can remove this // function if you do not need it. + self.sample_rate = buffer_config.sample_rate; true } fn reset(&mut self) { // Reset buffers and envelopes here. This can be called from the audio thread and may not // allocate. You can remove this function if you do not need it. + self.phase = 0.0; + self.midi_note_id = 0; + self.midi_note_frequency = 1.0; + self.midi_note_velocity.reset(0.0); } - fn process( &mut self, buffer: &mut Buffer, _aux: &mut AuxiliaryBuffers, - _context: &mut impl ProcessContext<Self>, + context: &mut impl ProcessContext<Self>, ) -> ProcessStatus { - for channel_samples in buffer.iter_samples() { + let mut next_event = context.next_event(); + for (sample_id, channel_samples) in buffer.iter_samples().enumerate() { // Smoothing is optionally built into the parameters themselves let gain = self.params.gain.smoothed.next(); + // This plugin can be either triggered by MIDI or controleld by a parameter + let sine = { + // Act on the next MIDI event + while let Some(event) = next_event { + if event.timing() > sample_id as u32 { + break; + } + + match event { + NoteEvent::NoteOn { note, velocity, .. } => { + self.midi_note_id = note; + self.midi_note_frequency = util::midi_note_to_freq(note); + self.midi_note_velocity + .set_target(self.sample_rate, velocity); + } + NoteEvent::NoteOff { note, .. } if note == self.midi_note_id => { + self.midi_note_velocity.set_target(self.sample_rate, 0.0); + } + NoteEvent::PolyPressure { note, pressure, .. } + if note == self.midi_note_id => + { + self.midi_note_velocity + .set_target(self.sample_rate, pressure); + } + _ => (), + } + + next_event = context.next_event(); + } + + // This gain envelope prevents clicks with new notes and with released notes + let current_waveform = self.params.wave.value(); + match current_waveform { + Waveform::Sine => { + self.sin(self.midi_note_frequency) * self.midi_note_velocity.next() + } + Waveform::Triangle => { + self.triangle(self.midi_note_frequency) * self.midi_note_velocity.next() + } + Waveform::Sawtooth => { + self.sawtooth(self.midi_note_frequency) * self.midi_note_velocity.next() + } + _ => self.sin(self.midi_note_frequency) * self.midi_note_velocity.next(), + } + }; for sample in channel_samples { - *sample *= gain; + *sample = sine * util::db_to_gain_fast(gain); } } - ProcessStatus::Normal + ProcessStatus::KeepAlive } }