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:
| M | src/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
}
}