commit 4d672ff9b5a54f0b4c7c51fcb4209f26a7a2877b
parent 71ec7f4243a55739cf05b792476300c1d4a483b0
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Fri, 5 Jun 2026 18:55:10 +0900
feat: implement synthesis engine with sensor-mapped parameters
- Added Waveform enum and oscillators with phase tracking.
- Replaced gain parameters with accelerometer and quaternion controls (accel_x/y/z, quat_x/y/z).
- Implemented core synthesis logic including FM modulation, morphing, and saturation.
- Updated CLAP features to identify the plugin as an instrument/synthesizer.
Diffstat:
| M | src/lib.rs | | | 214 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- |
1 file changed, 151 insertions(+), 63 deletions(-)
diff --git a/src/lib.rs b/src/lib.rs
@@ -1,28 +1,56 @@
use nih_plug::prelude::*;
use std::sync::Arc;
-// This is a shortened version of the gain example with most comments removed, check out
-// https://github.com/robbert-vdh/nih-plug/blob/master/plugins/examples/gain/src/lib.rs to get
-// started
+#[derive(PartialEq, Clone, Copy)]
+pub enum Waveform {
+ Sine,
+ Triangle,
+ TriangleSawtooth,
+ Sawtooth,
+ ReverseSawtooth,
+ Square,
+ WidePulse,
+ NarrowPulse,
+}
struct GeedbackClap {
params: Arc<GeedbackClapParams>,
+
+ // Oscillator States
+ phases: [f64; 3],
+ phases_mod: [f64; 3],
+ waveforms: [Waveform; 3],
+ mixes: [f64; 3],
+
+ sample_rate: f32,
}
#[derive(Params)]
struct GeedbackClapParams {
- /// The parameter's ID is used to identify the parameter in the wrappred plugin API. As long as
- /// 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 = "gain"]
- pub gain: FloatParam,
+ #[id = "accel_x"]
+ pub accel_x: FloatParam,
+ #[id = "accel_y"]
+ pub accel_y: FloatParam,
+ #[id = "accel_z"]
+ pub accel_z: FloatParam,
+
+ #[id = "quat_x"]
+ pub quat_x: FloatParam,
+ #[id = "quat_y"]
+ pub quat_y: FloatParam,
+ #[id = "quat_z"]
+ pub quat_z: FloatParam,
}
impl Default for GeedbackClap {
fn default() -> Self {
Self {
params: Arc::new(GeedbackClapParams::default()),
+ phases: [0.0; 3],
+ phases_mod: [0.0; 3],
+ waveforms: [Waveform::Sine, Waveform::Sine, Waveform::Sine],
+ mixes: [0.3, 0.3, 0.3],
+ sample_rate: 44100.0,
}
}
}
@@ -30,29 +58,70 @@ impl Default for GeedbackClap {
impl Default for GeedbackClapParams {
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),
+ accel_x: FloatParam::new("Accel X", 0.5, FloatRange::Linear { min: 0.0, max: 1.0 })
+ .with_smoother(SmoothingStyle::Linear(50.0)),
+ accel_y: FloatParam::new("Accel Y", 0.5, FloatRange::Linear { min: 0.0, max: 1.0 })
+ .with_smoother(SmoothingStyle::Linear(50.0)),
+ accel_z: FloatParam::new("Accel Z", 0.5, FloatRange::Linear { min: 0.0, max: 1.0 })
+ .with_smoother(SmoothingStyle::Linear(50.0)),
+ quat_x: FloatParam::new(
+ "Quat X",
+ 0.0,
+ FloatRange::Linear {
+ min: -1.0,
+ max: 1.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(50.0)),
+ quat_y: FloatParam::new(
+ "Quat Y",
+ 0.0,
+ FloatRange::Linear {
+ min: -1.0,
+ max: 1.0,
+ },
+ )
+ .with_smoother(SmoothingStyle::Linear(50.0)),
+ quat_z: FloatParam::new(
+ "Quat Z",
+ 0.0,
+ FloatRange::Linear {
+ min: -1.0,
+ max: 1.0,
+ },
+ )
+ .with_smoother(SmoothingStyle::Linear(50.0)),
+ }
+ }
+}
+
+impl GeedbackClap {
+ fn calculate_wave(&self, phase: f64, wave: Waveform, morph: f64) -> f64 {
+ let m = (morph + 1.0) * 0.5;
+ let p = phase.fract();
+
+ match wave {
+ Waveform::Sine => {
+ let raw = (p * 2.0 * std::f64::consts::PI).sin();
+ if m > 0.5 {
+ let drive = 1.0 + (m - 0.5) * 2.0;
+ (raw * drive).tanh()
+ } else {
+ raw
+ }
+ }
+ Waveform::Triangle => 1.0 - 2.0 * (p + 0.75).fract().abs(),
+ Waveform::TriangleSawtooth => {
+ let tri = 1.0 - 2.0 * (p + 0.75).fract().abs();
+ let saw = 2.0 * p - 1.0;
+ tri * (1.0 - m) + saw * m
+ }
+ Waveform::Sawtooth => 2.0 * p - 1.0,
+ Waveform::ReverseSawtooth => 1.0 - p * 2.0,
+ Waveform::Square | Waveform::WidePulse | Waveform::NarrowPulse => {
+ let width = 0.02 + m * 0.48;
+ if p < width { 1.0 } else { -1.0 }
+ }
}
}
}
@@ -65,18 +134,11 @@ impl Plugin for GeedbackClap {
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
- // The first audio IO layout is used as the default. The other layouts may be selected either
- // explicitly or automatically by the host or the user depending on the plugin API/backend.
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
main_input_channels: NonZeroU32::new(2),
main_output_channels: NonZeroU32::new(2),
-
aux_input_ports: &[],
aux_output_ports: &[],
-
- // Individual ports and the layout as a whole can be named here. By default these names
- // are generated as needed. This layout will be called 'Stereo', while a layout with
- // only one input and output channel would be called 'Mono'.
names: PortNames::const_default(),
}];
@@ -85,13 +147,7 @@ impl Plugin for GeedbackClap {
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
- // If the plugin can send or receive SysEx messages, it can define a type to wrap around those
- // messages here. The type implements the `SysExMessage` trait, which allows conversion to and
- // from plain byte buffers.
type SysExMessage = ();
- // More advanced plugins can use this to run expensive background tasks. See the field's
- // documentation for more information. `()` means that the plugin does not have any background
- // tasks.
type BackgroundTask = ();
fn params(&self) -> Arc<dyn Params> {
@@ -101,18 +157,16 @@ impl Plugin for GeedbackClap {
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.phases = [0.0; 3];
+ self.phases_mod = [0.0; 3];
}
fn process(
@@ -122,11 +176,51 @@ impl Plugin for GeedbackClap {
_context: &mut impl ProcessContext<Self>,
) -> ProcessStatus {
for channel_samples in buffer.iter_samples() {
- // Smoothing is optionally built into the parameters themselves
- let gain = self.params.gain.smoothed.next();
+ // Get smoothed parameters
+ let ax = self.params.accel_x.smoothed.next() as f64 * 2.0 - 1.0;
+ let ay = self.params.accel_y.smoothed.next() as f64 * 2.0 - 1.0;
+ let az = self.params.accel_z.smoothed.next() as f64 * 2.0 - 1.0;
+
+ let qx = self.params.quat_x.smoothed.next() as f64;
+ let qy = self.params.quat_y.smoothed.next() as f64;
+ let qz = self.params.quat_z.smoothed.next() as f64;
+
+ // Mapping Quaternions (X, Y, Z) to Oscillator frequencies and morphs
+ let freqs = [
+ 110.0 * 2.0_f64.powf(qy * 2.5),
+ 164.8 * 2.0_f64.powf(qz * 4.0),
+ 220.0 * 2.0_f64.powf(qx * 2.5),
+ ];
+
+ let morphs = [qy, qz, qx];
+
+ let fm_depths = [
+ ay.abs().powi(3) * 1.0,
+ az.abs().powi(3) * 1.0,
+ ax.abs().powi(3) * 1.0,
+ ];
+
+ let mut mixed_output = 0.0;
+ let sample_rate = self.sample_rate as f64;
+
+ for i in 0..3 {
+ self.phases_mod[i] = (self.phases_mod[i] + freqs[i] / sample_rate) % 1.0;
+ let modulation =
+ (self.phases_mod[i] * 2.0 * std::f64::consts::PI).sin() * fm_depths[i];
+ let osc_out =
+ self.calculate_wave(self.phases[i] + modulation, self.waveforms[i], morphs[i]);
+ mixed_output += osc_out * self.mixes[i];
+ self.phases[i] = (self.phases[i] + freqs[i] / sample_rate) % 1.0;
+ }
+
+ // Master Gain / Saturation based on Accel only
+ let shake_intensity = (ax.abs() + ay.abs() + az.abs()) * 4.0;
+ let master_gain = 0.6 + shake_intensity;
+
+ let final_output = (mixed_output * master_gain).tanh() as f32;
for sample in channel_samples {
- *sample *= gain;
+ *sample = final_output;
}
}
@@ -140,17 +234,11 @@ impl ClapPlugin for GeedbackClap {
const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL);
const CLAP_SUPPORT_URL: Option<&'static str> = None;
- // Don't forget to change these features
- const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect, ClapFeature::Stereo];
+ const CLAP_FEATURES: &'static [ClapFeature] = &[
+ ClapFeature::Instrument,
+ ClapFeature::Synthesizer,
+ ClapFeature::Stereo,
+ ];
}
-// impl Vst3Plugin for GeedbackClap {
-// const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!";
-
-// // And also don't forget to change these categories
-// const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
-// &[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics];
-// }
-
nih_export_clap!(GeedbackClap);
-// nih_export_vst3!(GeedbackClap);