geedback_clap

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

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:
Msrc/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);