Initial commit
This commit is contained in:
462
src/voice/mod.rs
Normal file
462
src/voice/mod.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
//! Voice - the core synthesis unit.
|
||||
|
||||
mod params;
|
||||
mod source;
|
||||
|
||||
pub use params::VoiceParams;
|
||||
|
||||
use std::f32::consts::PI;
|
||||
|
||||
use crate::effects::{crush, distort, fold, wrap, Chorus, Coarse, Flanger, Lag, Phaser};
|
||||
use crate::envelope::Adsr;
|
||||
use crate::fastmath::{cosf, exp2f, sinf};
|
||||
use crate::filter::FilterState;
|
||||
use crate::noise::{BrownNoise, PinkNoise};
|
||||
use crate::oscillator::Phasor;
|
||||
use crate::plaits::PlaitsEngine;
|
||||
use crate::sample::{FileSource, SampleInfo, WebSampleSource};
|
||||
use crate::types::{FilterSlope, FilterType, BLOCK_SIZE, CHANNELS};
|
||||
|
||||
fn apply_filter(
|
||||
signal: f32,
|
||||
filter: &mut FilterState,
|
||||
ftype: FilterType,
|
||||
q: f32,
|
||||
num_stages: usize,
|
||||
sr: f32,
|
||||
) -> f32 {
|
||||
let mut out = signal;
|
||||
for stage in 0..num_stages {
|
||||
out = filter.biquads[stage].process(out, ftype, filter.cutoff, q, sr);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub struct Voice {
|
||||
pub params: VoiceParams,
|
||||
pub phasor: Phasor,
|
||||
pub spread_phasors: [Phasor; 7],
|
||||
pub adsr: Adsr,
|
||||
pub lp_adsr: Adsr,
|
||||
pub hp_adsr: Adsr,
|
||||
pub bp_adsr: Adsr,
|
||||
pub lp: FilterState,
|
||||
pub hp: FilterState,
|
||||
pub bp: FilterState,
|
||||
// Modulation
|
||||
pub pitch_adsr: Adsr,
|
||||
pub fm_adsr: Adsr,
|
||||
pub vib_lfo: Phasor,
|
||||
pub fm_phasor: Phasor,
|
||||
pub am_lfo: Phasor,
|
||||
pub rm_lfo: Phasor,
|
||||
pub glide_lag: Lag,
|
||||
pub current_freq: f32,
|
||||
// Noise
|
||||
pub pink_noise: PinkNoise,
|
||||
pub brown_noise: BrownNoise,
|
||||
// Sample playback (native)
|
||||
pub file_source: Option<FileSource>,
|
||||
// Sample playback (web)
|
||||
pub web_sample: Option<WebSampleSource>,
|
||||
// Effects
|
||||
pub phaser: Phaser,
|
||||
pub flanger: Flanger,
|
||||
pub chorus: Chorus,
|
||||
pub coarse: Coarse,
|
||||
|
||||
pub time: f32,
|
||||
pub ch: [f32; CHANNELS],
|
||||
pub spread_side: f32,
|
||||
pub sr: f32,
|
||||
pub lag_unit: f32,
|
||||
pub(super) seed: u32,
|
||||
|
||||
// Plaits engines
|
||||
pub(super) plaits_engine: Option<PlaitsEngine>,
|
||||
pub(super) plaits_out: [f32; BLOCK_SIZE],
|
||||
pub(super) plaits_aux: [f32; BLOCK_SIZE],
|
||||
pub(super) plaits_idx: usize,
|
||||
pub(super) plaits_prev_gate: bool,
|
||||
}
|
||||
|
||||
impl Default for Voice {
|
||||
fn default() -> Self {
|
||||
let sr = 44100.0;
|
||||
Self {
|
||||
params: VoiceParams::default(),
|
||||
phasor: Phasor::default(),
|
||||
spread_phasors: std::array::from_fn(|i| {
|
||||
let mut p = Phasor::default();
|
||||
p.phase = i as f32 / 7.0;
|
||||
p
|
||||
}),
|
||||
adsr: Adsr::default(),
|
||||
lp_adsr: Adsr::default(),
|
||||
hp_adsr: Adsr::default(),
|
||||
bp_adsr: Adsr::default(),
|
||||
lp: FilterState::default(),
|
||||
hp: FilterState::default(),
|
||||
bp: FilterState::default(),
|
||||
pitch_adsr: Adsr::default(),
|
||||
fm_adsr: Adsr::default(),
|
||||
vib_lfo: Phasor::default(),
|
||||
fm_phasor: Phasor::default(),
|
||||
am_lfo: Phasor::default(),
|
||||
rm_lfo: Phasor::default(),
|
||||
glide_lag: Lag::default(),
|
||||
current_freq: 330.0,
|
||||
pink_noise: PinkNoise::default(),
|
||||
brown_noise: BrownNoise::default(),
|
||||
file_source: None,
|
||||
web_sample: None,
|
||||
phaser: Phaser::default(),
|
||||
flanger: Flanger::default(),
|
||||
chorus: Chorus::default(),
|
||||
coarse: Coarse::default(),
|
||||
time: 0.0,
|
||||
ch: [0.0; CHANNELS],
|
||||
spread_side: 0.0,
|
||||
sr,
|
||||
lag_unit: sr / 10.0,
|
||||
seed: 123456789,
|
||||
plaits_engine: None,
|
||||
plaits_out: [0.0; BLOCK_SIZE],
|
||||
plaits_aux: [0.0; BLOCK_SIZE],
|
||||
plaits_idx: BLOCK_SIZE,
|
||||
plaits_prev_gate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Voice {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
params: self.params,
|
||||
phasor: self.phasor,
|
||||
spread_phasors: self.spread_phasors,
|
||||
adsr: self.adsr,
|
||||
lp_adsr: self.lp_adsr,
|
||||
hp_adsr: self.hp_adsr,
|
||||
bp_adsr: self.bp_adsr,
|
||||
lp: self.lp,
|
||||
hp: self.hp,
|
||||
bp: self.bp,
|
||||
pitch_adsr: self.pitch_adsr,
|
||||
fm_adsr: self.fm_adsr,
|
||||
vib_lfo: self.vib_lfo,
|
||||
fm_phasor: self.fm_phasor,
|
||||
am_lfo: self.am_lfo,
|
||||
rm_lfo: self.rm_lfo,
|
||||
glide_lag: self.glide_lag,
|
||||
current_freq: self.current_freq,
|
||||
pink_noise: self.pink_noise,
|
||||
brown_noise: self.brown_noise,
|
||||
file_source: self.file_source,
|
||||
web_sample: self.web_sample,
|
||||
phaser: self.phaser,
|
||||
flanger: self.flanger,
|
||||
chorus: self.chorus,
|
||||
coarse: self.coarse,
|
||||
time: self.time,
|
||||
ch: self.ch,
|
||||
spread_side: self.spread_side,
|
||||
sr: self.sr,
|
||||
lag_unit: self.lag_unit,
|
||||
seed: self.seed,
|
||||
plaits_engine: None,
|
||||
plaits_out: [0.0; BLOCK_SIZE],
|
||||
plaits_aux: [0.0; BLOCK_SIZE],
|
||||
plaits_idx: BLOCK_SIZE,
|
||||
plaits_prev_gate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
pub(super) fn rand(&mut self) -> f32 {
|
||||
self.seed = self.seed.wrapping_mul(1103515245).wrapping_add(12345);
|
||||
((self.seed >> 16) & 0x7fff) as f32 / 32767.0
|
||||
}
|
||||
|
||||
pub(super) fn white(&mut self) -> f32 {
|
||||
self.rand() * 2.0 - 1.0
|
||||
}
|
||||
|
||||
fn compute_freq(&mut self, isr: f32) -> f32 {
|
||||
let mut freq = self.params.freq;
|
||||
|
||||
// Detune (cents offset)
|
||||
if self.params.detune != 0.0 {
|
||||
freq *= exp2f(self.params.detune / 1200.0);
|
||||
}
|
||||
|
||||
// Speed multiplier
|
||||
freq *= self.params.speed;
|
||||
|
||||
// Glide
|
||||
if let Some(glide_time) = self.params.glide {
|
||||
freq = self.glide_lag.update(freq, glide_time, self.lag_unit);
|
||||
}
|
||||
|
||||
// FM synthesis
|
||||
if self.params.fm > 0.0 {
|
||||
let mut fm_amount = self.params.fm;
|
||||
if self.params.fm_env_active {
|
||||
let env = self.fm_adsr.update(
|
||||
self.time,
|
||||
self.params.gate,
|
||||
self.params.fma,
|
||||
self.params.fmd,
|
||||
self.params.fms,
|
||||
self.params.fmr,
|
||||
);
|
||||
fm_amount = self.params.fme * env * fm_amount + fm_amount;
|
||||
}
|
||||
let mod_freq = freq * self.params.fmh;
|
||||
let mod_gain = mod_freq * fm_amount;
|
||||
let modulator = self.fm_phasor.lfo(self.params.fmshape, mod_freq, isr);
|
||||
freq += modulator * mod_gain;
|
||||
}
|
||||
|
||||
// Pitch envelope
|
||||
if self.params.pitch_env_active && self.params.penv != 0.0 {
|
||||
let env = self.pitch_adsr.update(
|
||||
self.time,
|
||||
1.0,
|
||||
self.params.patt,
|
||||
self.params.pdec,
|
||||
self.params.psus,
|
||||
self.params.prel,
|
||||
);
|
||||
let env_adj = if self.params.psus == 1.0 {
|
||||
env - 1.0
|
||||
} else {
|
||||
env
|
||||
};
|
||||
freq *= exp2f(env_adj * self.params.penv / 12.0);
|
||||
}
|
||||
|
||||
// Vibrato
|
||||
if self.params.vib > 0.0 && self.params.vibmod > 0.0 {
|
||||
let mod_val = self.vib_lfo.lfo(self.params.vibshape, self.params.vib, isr);
|
||||
freq *= exp2f(mod_val * self.params.vibmod / 12.0);
|
||||
}
|
||||
|
||||
self.current_freq = freq;
|
||||
freq
|
||||
}
|
||||
|
||||
fn num_stages(&self) -> usize {
|
||||
match self.params.ftype {
|
||||
FilterSlope::Db12 => 1,
|
||||
FilterSlope::Db24 => 2,
|
||||
FilterSlope::Db48 => 4,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(
|
||||
&mut self,
|
||||
isr: f32,
|
||||
pool: &[f32],
|
||||
samples: &[SampleInfo],
|
||||
web_pcm: &[f32],
|
||||
sample_idx: usize,
|
||||
live_input: &[f32],
|
||||
) -> bool {
|
||||
let env = self.adsr.update(
|
||||
self.time,
|
||||
self.params.gate,
|
||||
self.params.attack,
|
||||
self.params.decay,
|
||||
self.params.sustain,
|
||||
self.params.release,
|
||||
);
|
||||
if self.adsr.is_off() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let freq = self.compute_freq(isr);
|
||||
if !self.run_source(freq, isr, pool, samples, web_pcm, sample_idx, live_input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update filter envelopes
|
||||
if let Some(lpf) = self.params.lpf {
|
||||
self.lp.cutoff = lpf;
|
||||
if self.params.lp_env_active {
|
||||
let lp_env = self.lp_adsr.update(
|
||||
self.time,
|
||||
self.params.gate,
|
||||
self.params.lpa,
|
||||
self.params.lpd,
|
||||
self.params.lps,
|
||||
self.params.lpr,
|
||||
);
|
||||
self.lp.cutoff = self.params.lpe * lp_env * lpf + lpf;
|
||||
}
|
||||
}
|
||||
if let Some(hpf) = self.params.hpf {
|
||||
self.hp.cutoff = hpf;
|
||||
if self.params.hp_env_active {
|
||||
let hp_env = self.hp_adsr.update(
|
||||
self.time,
|
||||
self.params.gate,
|
||||
self.params.hpa,
|
||||
self.params.hpd,
|
||||
self.params.hps,
|
||||
self.params.hpr,
|
||||
);
|
||||
self.hp.cutoff = self.params.hpe * hp_env * hpf + hpf;
|
||||
}
|
||||
}
|
||||
if let Some(bpf) = self.params.bpf {
|
||||
self.bp.cutoff = bpf;
|
||||
if self.params.bp_env_active {
|
||||
let bp_env = self.bp_adsr.update(
|
||||
self.time,
|
||||
self.params.gate,
|
||||
self.params.bpa,
|
||||
self.params.bpd,
|
||||
self.params.bps,
|
||||
self.params.bpr,
|
||||
);
|
||||
self.bp.cutoff = self.params.bpe * bp_env * bpf + bpf;
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-filter gain
|
||||
self.ch[0] *= self.params.gain * self.params.velocity;
|
||||
|
||||
// Apply filters (LP -> HP -> BP)
|
||||
let num_stages = self.num_stages();
|
||||
if self.params.lpf.is_some() {
|
||||
self.ch[0] = apply_filter(
|
||||
self.ch[0],
|
||||
&mut self.lp,
|
||||
FilterType::Lowpass,
|
||||
self.params.lpq,
|
||||
num_stages,
|
||||
self.sr,
|
||||
);
|
||||
}
|
||||
if self.params.hpf.is_some() {
|
||||
self.ch[0] = apply_filter(
|
||||
self.ch[0],
|
||||
&mut self.hp,
|
||||
FilterType::Highpass,
|
||||
self.params.hpq,
|
||||
num_stages,
|
||||
self.sr,
|
||||
);
|
||||
}
|
||||
if self.params.bpf.is_some() {
|
||||
self.ch[0] = apply_filter(
|
||||
self.ch[0],
|
||||
&mut self.bp,
|
||||
FilterType::Bandpass,
|
||||
self.params.bpq,
|
||||
num_stages,
|
||||
self.sr,
|
||||
);
|
||||
}
|
||||
|
||||
// Distortion effects
|
||||
if let Some(coarse_factor) = self.params.coarse {
|
||||
self.ch[0] = self.coarse.process(self.ch[0], coarse_factor);
|
||||
}
|
||||
if let Some(crush_bits) = self.params.crush {
|
||||
self.ch[0] = crush(self.ch[0], crush_bits);
|
||||
}
|
||||
if let Some(fold_amount) = self.params.fold {
|
||||
self.ch[0] = fold(self.ch[0], fold_amount);
|
||||
}
|
||||
if let Some(wrap_amount) = self.params.wrap {
|
||||
self.ch[0] = wrap(self.ch[0], wrap_amount);
|
||||
}
|
||||
if let Some(dist_amount) = self.params.distort {
|
||||
self.ch[0] = distort(self.ch[0], dist_amount, self.params.distortvol);
|
||||
}
|
||||
|
||||
// AM modulation
|
||||
if self.params.am > 0.0 {
|
||||
let modulator = self.am_lfo.lfo(self.params.amshape, self.params.am, isr);
|
||||
let depth = self.params.amdepth.clamp(0.0, 1.0);
|
||||
self.ch[0] *= 1.0 + modulator * depth;
|
||||
}
|
||||
|
||||
// Ring modulation
|
||||
if self.params.rm > 0.0 {
|
||||
let modulator = self.rm_lfo.lfo(self.params.rmshape, self.params.rm, isr);
|
||||
let depth = self.params.rmdepth.clamp(0.0, 1.0);
|
||||
self.ch[0] *= (1.0 - depth) + modulator * depth;
|
||||
}
|
||||
|
||||
// Phaser
|
||||
if self.params.phaser > 0.0 {
|
||||
self.ch[0] = self.phaser.process(
|
||||
self.ch[0],
|
||||
self.params.phaser,
|
||||
self.params.phaserdepth,
|
||||
self.params.phasercenter,
|
||||
self.params.phasersweep,
|
||||
self.sr,
|
||||
isr,
|
||||
);
|
||||
}
|
||||
|
||||
// Flanger
|
||||
if self.params.flanger > 0.0 {
|
||||
self.ch[0] = self.flanger.process(
|
||||
self.ch[0],
|
||||
self.params.flanger,
|
||||
self.params.flangerdepth,
|
||||
self.params.flangerfeedback,
|
||||
self.sr,
|
||||
isr,
|
||||
);
|
||||
}
|
||||
|
||||
// Apply gain envelope and postgain
|
||||
self.ch[0] *= env * self.params.postgain;
|
||||
|
||||
// Restore stereo for spread mode
|
||||
if self.params.spread > 0.0 {
|
||||
let side = self.spread_side * env * self.params.postgain;
|
||||
self.ch[1] = self.ch[0] - side;
|
||||
self.ch[0] += side;
|
||||
} else {
|
||||
self.ch[1] = self.ch[0];
|
||||
}
|
||||
|
||||
// Chorus
|
||||
if self.params.chorus > 0.0 {
|
||||
let stereo = self.chorus.process(
|
||||
self.ch[0],
|
||||
self.ch[1],
|
||||
self.params.chorus,
|
||||
self.params.chorusdepth,
|
||||
self.params.chorusdelay,
|
||||
self.sr,
|
||||
isr,
|
||||
);
|
||||
self.ch[0] = stereo[0];
|
||||
self.ch[1] = stereo[1];
|
||||
}
|
||||
|
||||
// Panning
|
||||
if self.params.pan != 0.5 {
|
||||
let pan_pos = self.params.pan * PI / 2.0;
|
||||
self.ch[0] *= cosf(pan_pos);
|
||||
self.ch[1] *= sinf(pan_pos);
|
||||
}
|
||||
|
||||
self.time += isr;
|
||||
if let Some(dur) = self.params.duration {
|
||||
if dur > 0.0 && self.time > dur {
|
||||
self.params.gate = 0.0;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
405
src/voice/params.rs
Normal file
405
src/voice/params.rs
Normal file
@@ -0,0 +1,405 @@
|
||||
//! Voice parameters - pure data structure for synthesis configuration.
|
||||
//!
|
||||
//! This module contains [`VoiceParams`], which holds all parameters that control
|
||||
//! a single voice's sound. Parameters are grouped by function:
|
||||
//!
|
||||
//! - **Core** - frequency, gain, panning, gate
|
||||
//! - **Oscillator** - sound source, pulse width, spread, waveshaping
|
||||
//! - **Amplitude Envelope** - ADSR for volume
|
||||
//! - **Filters** - lowpass, highpass, bandpass with optional envelopes
|
||||
//! - **Pitch Modulation** - glide, pitch envelope, vibrato, FM
|
||||
//! - **Amplitude Modulation** - AM, ring modulation
|
||||
//! - **Effects** - phaser, flanger, chorus, distortion
|
||||
//! - **Routing** - orbit assignment, effect sends
|
||||
|
||||
use crate::oscillator::PhaseShape;
|
||||
use crate::types::{DelayType, FilterSlope, LfoShape, Source};
|
||||
|
||||
/// All parameters that control a voice's sound generation.
|
||||
///
|
||||
/// This is a pure data structure with no methods beyond [`Default`].
|
||||
/// The actual signal processing happens in [`Voice`](super::Voice).
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct VoiceParams {
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Core
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Base frequency in Hz.
|
||||
pub freq: f32,
|
||||
/// Pitch offset in cents (1/100th of a semitone).
|
||||
pub detune: f32,
|
||||
/// Playback speed multiplier (also affects pitch for samples).
|
||||
pub speed: f32,
|
||||
/// Pre-filter gain (0.0 to 1.0+).
|
||||
pub gain: f32,
|
||||
/// MIDI velocity (0.0 to 1.0), multiplied with gain.
|
||||
pub velocity: f32,
|
||||
/// Post-envelope gain (0.0 to 1.0+).
|
||||
pub postgain: f32,
|
||||
/// Stereo pan position (0.0 = left, 0.5 = center, 1.0 = right).
|
||||
pub pan: f32,
|
||||
/// Gate signal (> 0.0 = note on, 0.0 = note off).
|
||||
pub gate: f32,
|
||||
/// Optional note duration in seconds. Voice releases when exceeded.
|
||||
pub duration: Option<f32>,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Oscillator
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Sound source type (oscillator waveform, sample, or Plaits engine).
|
||||
pub sound: Source,
|
||||
/// Pulse width for pulse/square waves (0.0 to 1.0).
|
||||
pub pw: f32,
|
||||
/// Unison spread amount in cents. Enables 7-voice supersaw when > 0.
|
||||
pub spread: f32,
|
||||
/// Phase shaping parameters for waveform modification.
|
||||
pub shape: PhaseShape,
|
||||
/// Harmonics control for Plaits engines (0.0 to 1.0).
|
||||
pub harmonics: f32,
|
||||
/// Timbre control for Plaits engines (0.0 to 1.0).
|
||||
pub timbre: f32,
|
||||
/// Morph control for Plaits engines (0.0 to 1.0).
|
||||
pub morph: f32,
|
||||
/// Sample slice/cut index for sample playback.
|
||||
pub cut: Option<usize>,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Amplitude Envelope (ADSR)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Attack time in seconds.
|
||||
pub attack: f32,
|
||||
/// Decay time in seconds.
|
||||
pub decay: f32,
|
||||
/// Sustain level (0.0 to 1.0).
|
||||
pub sustain: f32,
|
||||
/// Release time in seconds.
|
||||
pub release: f32,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Lowpass Filter
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Lowpass cutoff frequency in Hz. `None` = filter bypassed.
|
||||
pub lpf: Option<f32>,
|
||||
/// Lowpass resonance/Q (0.0 to 1.0).
|
||||
pub lpq: f32,
|
||||
/// Lowpass envelope depth multiplier.
|
||||
pub lpe: f32,
|
||||
/// Lowpass envelope attack time.
|
||||
pub lpa: f32,
|
||||
/// Lowpass envelope decay time.
|
||||
pub lpd: f32,
|
||||
/// Lowpass envelope sustain level.
|
||||
pub lps: f32,
|
||||
/// Lowpass envelope release time.
|
||||
pub lpr: f32,
|
||||
/// Enable lowpass filter envelope modulation.
|
||||
pub lp_env_active: bool,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Highpass Filter
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Highpass cutoff frequency in Hz. `None` = filter bypassed.
|
||||
pub hpf: Option<f32>,
|
||||
/// Highpass resonance/Q (0.0 to 1.0).
|
||||
pub hpq: f32,
|
||||
/// Highpass envelope depth multiplier.
|
||||
pub hpe: f32,
|
||||
/// Highpass envelope attack time.
|
||||
pub hpa: f32,
|
||||
/// Highpass envelope decay time.
|
||||
pub hpd: f32,
|
||||
/// Highpass envelope sustain level.
|
||||
pub hps: f32,
|
||||
/// Highpass envelope release time.
|
||||
pub hpr: f32,
|
||||
/// Enable highpass filter envelope modulation.
|
||||
pub hp_env_active: bool,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Bandpass Filter
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Bandpass center frequency in Hz. `None` = filter bypassed.
|
||||
pub bpf: Option<f32>,
|
||||
/// Bandpass resonance/Q (0.0 to 1.0).
|
||||
pub bpq: f32,
|
||||
/// Bandpass envelope depth multiplier.
|
||||
pub bpe: f32,
|
||||
/// Bandpass envelope attack time.
|
||||
pub bpa: f32,
|
||||
/// Bandpass envelope decay time.
|
||||
pub bpd: f32,
|
||||
/// Bandpass envelope sustain level.
|
||||
pub bps: f32,
|
||||
/// Bandpass envelope release time.
|
||||
pub bpr: f32,
|
||||
/// Enable bandpass filter envelope modulation.
|
||||
pub bp_env_active: bool,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Filter Slope
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Filter slope (12/24/48 dB per octave) for all filters.
|
||||
pub ftype: FilterSlope,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Glide (Portamento)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Glide time in seconds. `None` = no glide.
|
||||
pub glide: Option<f32>,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Pitch Envelope
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Pitch envelope depth in semitones.
|
||||
pub penv: f32,
|
||||
/// Pitch envelope attack time.
|
||||
pub patt: f32,
|
||||
/// Pitch envelope decay time.
|
||||
pub pdec: f32,
|
||||
/// Pitch envelope sustain level.
|
||||
pub psus: f32,
|
||||
/// Pitch envelope release time.
|
||||
pub prel: f32,
|
||||
/// Enable pitch envelope modulation.
|
||||
pub pitch_env_active: bool,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Vibrato
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Vibrato LFO rate in Hz.
|
||||
pub vib: f32,
|
||||
/// Vibrato depth in semitones.
|
||||
pub vibmod: f32,
|
||||
/// Vibrato LFO waveform.
|
||||
pub vibshape: LfoShape,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// FM Synthesis
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// FM modulation index (depth).
|
||||
pub fm: f32,
|
||||
/// FM harmonic ratio (modulator freq = carrier freq * fmh).
|
||||
pub fmh: f32,
|
||||
/// FM modulator waveform.
|
||||
pub fmshape: LfoShape,
|
||||
/// FM envelope depth multiplier.
|
||||
pub fme: f32,
|
||||
/// FM envelope attack time.
|
||||
pub fma: f32,
|
||||
/// FM envelope decay time.
|
||||
pub fmd: f32,
|
||||
/// FM envelope sustain level.
|
||||
pub fms: f32,
|
||||
/// FM envelope release time.
|
||||
pub fmr: f32,
|
||||
/// Enable FM envelope modulation.
|
||||
pub fm_env_active: bool,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Amplitude Modulation
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// AM LFO rate in Hz.
|
||||
pub am: f32,
|
||||
/// AM depth (0.0 to 1.0).
|
||||
pub amdepth: f32,
|
||||
/// AM LFO waveform.
|
||||
pub amshape: LfoShape,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Ring Modulation
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Ring modulator frequency in Hz.
|
||||
pub rm: f32,
|
||||
/// Ring modulation depth (0.0 to 1.0).
|
||||
pub rmdepth: f32,
|
||||
/// Ring modulator waveform.
|
||||
pub rmshape: LfoShape,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Phaser
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Phaser LFO rate in Hz. 0 = bypassed.
|
||||
pub phaser: f32,
|
||||
/// Phaser depth/feedback (0.0 to 1.0).
|
||||
pub phaserdepth: f32,
|
||||
/// Phaser sweep range in Hz.
|
||||
pub phasersweep: f32,
|
||||
/// Phaser center frequency in Hz.
|
||||
pub phasercenter: f32,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Flanger
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Flanger LFO rate in Hz. 0 = bypassed.
|
||||
pub flanger: f32,
|
||||
/// Flanger depth (0.0 to 1.0).
|
||||
pub flangerdepth: f32,
|
||||
/// Flanger feedback amount (0.0 to 1.0).
|
||||
pub flangerfeedback: f32,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Chorus
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Chorus LFO rate in Hz. 0 = bypassed.
|
||||
pub chorus: f32,
|
||||
/// Chorus depth/modulation amount (0.0 to 1.0).
|
||||
pub chorusdepth: f32,
|
||||
/// Chorus base delay time in ms.
|
||||
pub chorusdelay: f32,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Distortion
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Coarse sample rate reduction factor. `None` = bypassed.
|
||||
pub coarse: Option<f32>,
|
||||
/// Bit crush depth (bits). `None` = bypassed.
|
||||
pub crush: Option<f32>,
|
||||
/// Wavefolding amount. `None` = bypassed.
|
||||
pub fold: Option<f32>,
|
||||
/// Wavewrapping amount. `None` = bypassed.
|
||||
pub wrap: Option<f32>,
|
||||
/// Distortion/saturation amount. `None` = bypassed.
|
||||
pub distort: Option<f32>,
|
||||
/// Distortion output volume compensation.
|
||||
pub distortvol: f32,
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Routing / Sends
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// Orbit index for effect bus routing (0 to MAX_ORBITS-1).
|
||||
pub orbit: usize,
|
||||
/// Delay send level (0.0 to 1.0).
|
||||
pub delay: f32,
|
||||
/// Delay time in seconds (overrides orbit default).
|
||||
pub delaytime: f32,
|
||||
/// Delay feedback amount (overrides orbit default).
|
||||
pub delayfeedback: f32,
|
||||
/// Delay type (overrides orbit default).
|
||||
pub delaytype: DelayType,
|
||||
/// Reverb send level (0.0 to 1.0).
|
||||
pub verb: f32,
|
||||
/// Reverb decay time (overrides orbit default).
|
||||
pub verbdecay: f32,
|
||||
/// Reverb damping (overrides orbit default).
|
||||
pub verbdamp: f32,
|
||||
/// Reverb pre-delay in seconds.
|
||||
pub verbpredelay: f32,
|
||||
/// Reverb diffusion amount.
|
||||
pub verbdiff: f32,
|
||||
/// Comb filter send level (0.0 to 1.0).
|
||||
pub comb: f32,
|
||||
/// Comb filter frequency in Hz.
|
||||
pub combfreq: f32,
|
||||
/// Comb filter feedback amount.
|
||||
pub combfeedback: f32,
|
||||
/// Comb filter damping.
|
||||
pub combdamp: f32,
|
||||
}
|
||||
|
||||
impl Default for VoiceParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
freq: 330.0,
|
||||
detune: 0.0,
|
||||
speed: 1.0,
|
||||
gain: 1.0,
|
||||
velocity: 1.0,
|
||||
postgain: 1.0,
|
||||
pan: 0.5,
|
||||
gate: 1.0,
|
||||
duration: None,
|
||||
sound: Source::Tri,
|
||||
pw: 0.5,
|
||||
spread: 0.0,
|
||||
shape: PhaseShape::default(),
|
||||
harmonics: 0.5,
|
||||
timbre: 0.5,
|
||||
morph: 0.5,
|
||||
cut: None,
|
||||
attack: 0.001,
|
||||
decay: 0.0,
|
||||
sustain: 1.0,
|
||||
release: 0.005,
|
||||
lpf: None,
|
||||
lpq: 0.2,
|
||||
lpe: 1.0,
|
||||
lpa: 0.001,
|
||||
lpd: 0.0,
|
||||
lps: 1.0,
|
||||
lpr: 0.005,
|
||||
lp_env_active: false,
|
||||
hpf: None,
|
||||
hpq: 0.2,
|
||||
hpe: 1.0,
|
||||
hpa: 0.001,
|
||||
hpd: 0.0,
|
||||
hps: 1.0,
|
||||
hpr: 0.005,
|
||||
hp_env_active: false,
|
||||
bpf: None,
|
||||
bpq: 0.2,
|
||||
bpe: 1.0,
|
||||
bpa: 0.001,
|
||||
bpd: 0.0,
|
||||
bps: 1.0,
|
||||
bpr: 0.005,
|
||||
bp_env_active: false,
|
||||
ftype: FilterSlope::Db12,
|
||||
glide: None,
|
||||
penv: 1.0,
|
||||
patt: 0.001,
|
||||
pdec: 0.0,
|
||||
psus: 1.0,
|
||||
prel: 0.005,
|
||||
pitch_env_active: false,
|
||||
vib: 0.0,
|
||||
vibmod: 0.5,
|
||||
vibshape: LfoShape::Sine,
|
||||
fm: 0.0,
|
||||
fmh: 1.0,
|
||||
fmshape: LfoShape::Sine,
|
||||
fme: 1.0,
|
||||
fma: 0.001,
|
||||
fmd: 0.0,
|
||||
fms: 1.0,
|
||||
fmr: 0.005,
|
||||
fm_env_active: false,
|
||||
am: 0.0,
|
||||
amdepth: 0.5,
|
||||
amshape: LfoShape::Sine,
|
||||
rm: 0.0,
|
||||
rmdepth: 1.0,
|
||||
rmshape: LfoShape::Sine,
|
||||
phaser: 0.0,
|
||||
phaserdepth: 0.75,
|
||||
phasersweep: 2000.0,
|
||||
phasercenter: 1000.0,
|
||||
flanger: 0.0,
|
||||
flangerdepth: 0.5,
|
||||
flangerfeedback: 0.5,
|
||||
chorus: 0.0,
|
||||
chorusdepth: 0.5,
|
||||
chorusdelay: 25.0,
|
||||
coarse: None,
|
||||
crush: None,
|
||||
fold: None,
|
||||
wrap: None,
|
||||
distort: None,
|
||||
distortvol: 1.0,
|
||||
orbit: 0,
|
||||
delay: 0.0,
|
||||
delaytime: 0.333,
|
||||
delayfeedback: 0.6,
|
||||
delaytype: DelayType::Standard,
|
||||
verb: 0.0,
|
||||
verbdecay: 0.75,
|
||||
verbdamp: 0.95,
|
||||
verbpredelay: 0.1,
|
||||
verbdiff: 0.7,
|
||||
comb: 0.0,
|
||||
combfreq: 220.0,
|
||||
combfeedback: 0.9,
|
||||
combdamp: 0.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
213
src/voice/source.rs
Normal file
213
src/voice/source.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
//! Source generation - oscillators, samples, Plaits engines, spread mode.
|
||||
|
||||
use crate::fastmath::exp2f;
|
||||
use crate::oscillator::Phasor;
|
||||
use crate::plaits::PlaitsEngine;
|
||||
use crate::sample::SampleInfo;
|
||||
use crate::types::{freq2midi, Source, BLOCK_SIZE, CHANNELS};
|
||||
use mi_plaits_dsp::engine::{EngineParameters, TriggerState};
|
||||
|
||||
use super::Voice;
|
||||
|
||||
impl Voice {
|
||||
#[inline]
|
||||
pub(super) fn osc_at(&self, phasor: &Phasor, phase: f32) -> f32 {
|
||||
match self.params.sound {
|
||||
Source::Tri => phasor.tri_at(phase, &self.params.shape),
|
||||
Source::Sine => phasor.sine_at(phase, &self.params.shape),
|
||||
Source::Saw => phasor.saw_at(phase, &self.params.shape),
|
||||
Source::Zaw => phasor.zaw_at(phase, &self.params.shape),
|
||||
Source::Pulse => phasor.pulse_at(phase, self.params.pw, &self.params.shape),
|
||||
Source::Pulze => phasor.pulze_at(phase, self.params.pw, &self.params.shape),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn run_source(
|
||||
&mut self,
|
||||
freq: f32,
|
||||
isr: f32,
|
||||
pool: &[f32],
|
||||
samples: &[SampleInfo],
|
||||
web_pcm: &[f32],
|
||||
sample_idx: usize,
|
||||
live_input: &[f32],
|
||||
) -> bool {
|
||||
match self.params.sound {
|
||||
Source::Sample => {
|
||||
if let Some(ref mut fs) = self.file_source {
|
||||
if let Some(info) = samples.get(fs.sample_idx) {
|
||||
if fs.is_done(info) {
|
||||
return false;
|
||||
}
|
||||
for c in 0..CHANNELS {
|
||||
self.ch[c] = fs.update(pool, info, self.params.speed, c) * 0.2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
self.ch[0] = 0.0;
|
||||
self.ch[1] = 0.0;
|
||||
}
|
||||
Source::WebSample => {
|
||||
if let Some(ref mut ws) = self.web_sample {
|
||||
if ws.is_done() {
|
||||
return false;
|
||||
}
|
||||
for c in 0..CHANNELS {
|
||||
self.ch[c] = ws.update(web_pcm, self.params.speed, c) * 0.2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
self.ch[0] = 0.0;
|
||||
self.ch[1] = 0.0;
|
||||
}
|
||||
Source::LiveInput => {
|
||||
let input_idx = sample_idx * CHANNELS;
|
||||
for c in 0..CHANNELS {
|
||||
let idx = input_idx + c;
|
||||
self.ch[c] = live_input.get(idx).copied().unwrap_or(0.0) * 0.2;
|
||||
}
|
||||
}
|
||||
Source::PlModal
|
||||
| Source::PlVa
|
||||
| Source::PlWs
|
||||
| Source::PlFm
|
||||
| Source::PlGrain
|
||||
| Source::PlAdd
|
||||
| Source::PlWt
|
||||
| Source::PlChord
|
||||
| Source::PlSwarm
|
||||
| Source::PlNoise
|
||||
| Source::PlBass
|
||||
| Source::PlSnare
|
||||
| Source::PlHat => {
|
||||
if self.plaits_idx >= BLOCK_SIZE {
|
||||
let need_new = self
|
||||
.plaits_engine
|
||||
.as_ref()
|
||||
.is_none_or(|e| e.source() != self.params.sound);
|
||||
if need_new {
|
||||
let sample_rate = 1.0 / isr;
|
||||
self.plaits_engine = Some(PlaitsEngine::new(self.params.sound, sample_rate));
|
||||
}
|
||||
let engine = self.plaits_engine.as_mut().unwrap();
|
||||
|
||||
let trigger = if self.params.sound.is_plaits_percussion() {
|
||||
TriggerState::Unpatched
|
||||
} else {
|
||||
let gate_high = self.params.gate > 0.5;
|
||||
let t = if gate_high && !self.plaits_prev_gate {
|
||||
TriggerState::RisingEdge
|
||||
} else if gate_high {
|
||||
TriggerState::High
|
||||
} else {
|
||||
TriggerState::Low
|
||||
};
|
||||
self.plaits_prev_gate = gate_high;
|
||||
t
|
||||
};
|
||||
|
||||
let params = EngineParameters {
|
||||
trigger,
|
||||
note: freq2midi(freq),
|
||||
timbre: self.params.timbre,
|
||||
morph: self.params.morph,
|
||||
harmonics: self.params.harmonics,
|
||||
accent: self.params.velocity,
|
||||
a0_normalized: 55.0 * isr,
|
||||
};
|
||||
|
||||
let mut already_enveloped = false;
|
||||
engine.render(
|
||||
¶ms,
|
||||
&mut self.plaits_out,
|
||||
&mut self.plaits_aux,
|
||||
&mut already_enveloped,
|
||||
);
|
||||
self.plaits_idx = 0;
|
||||
}
|
||||
|
||||
self.ch[0] = self.plaits_out[self.plaits_idx] * 0.2;
|
||||
self.ch[1] = self.ch[0];
|
||||
self.plaits_idx += 1;
|
||||
}
|
||||
_ => {
|
||||
let spread = self.params.spread;
|
||||
if spread > 0.0 {
|
||||
self.run_spread(freq, isr);
|
||||
} else {
|
||||
self.run_single_osc(freq, isr);
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn run_spread(&mut self, freq: f32, isr: f32) {
|
||||
let mut left = 0.0;
|
||||
let mut right = 0.0;
|
||||
const PAN: [f32; 3] = [0.3, 0.6, 0.9];
|
||||
|
||||
// Center oscillator
|
||||
let phase_c = self.spread_phasors[3].phase;
|
||||
let center = self.osc_at(&self.spread_phasors[3], phase_c);
|
||||
self.spread_phasors[3].phase = (phase_c + freq * isr) % 1.0;
|
||||
left += center;
|
||||
right += center;
|
||||
|
||||
// Symmetric pairs with parabolic detuning + stereo spread
|
||||
for i in 1..=3 {
|
||||
let detune_cents = (i * i) as f32 * self.params.spread;
|
||||
let ratio_up = exp2f(detune_cents / 1200.0);
|
||||
let ratio_down = exp2f(-detune_cents / 1200.0);
|
||||
|
||||
let phase_up = self.spread_phasors[3 + i].phase;
|
||||
let voice_up = self.osc_at(&self.spread_phasors[3 + i], phase_up);
|
||||
self.spread_phasors[3 + i].phase = (phase_up + freq * ratio_up * isr) % 1.0;
|
||||
|
||||
let phase_down = self.spread_phasors[3 - i].phase;
|
||||
let voice_down = self.osc_at(&self.spread_phasors[3 - i], phase_down);
|
||||
self.spread_phasors[3 - i].phase = (phase_down + freq * ratio_down * isr) % 1.0;
|
||||
|
||||
let pan = PAN[i - 1];
|
||||
left += voice_down * (0.5 + pan * 0.5) + voice_up * (0.5 - pan * 0.5);
|
||||
right += voice_up * (0.5 + pan * 0.5) + voice_down * (0.5 - pan * 0.5);
|
||||
}
|
||||
|
||||
// Store as mid/side - effects process mid, stereo restored later
|
||||
let mid = (left + right) / 2.0;
|
||||
let side = (left - right) / 2.0;
|
||||
self.ch[0] = mid / 4.0 * 0.2;
|
||||
self.spread_side = side / 4.0 * 0.2;
|
||||
}
|
||||
|
||||
fn run_single_osc(&mut self, freq: f32, isr: f32) {
|
||||
self.ch[0] = match self.params.sound {
|
||||
Source::Tri => self.phasor.tri_shaped(freq, isr, &self.params.shape) * 0.2,
|
||||
Source::Sine => self.phasor.sine_shaped(freq, isr, &self.params.shape) * 0.2,
|
||||
Source::Saw => self.phasor.saw_shaped(freq, isr, &self.params.shape) * 0.2,
|
||||
Source::Zaw => self.phasor.zaw_shaped(freq, isr, &self.params.shape) * 0.2,
|
||||
Source::Pulse => {
|
||||
self.phasor
|
||||
.pulse_shaped(freq, self.params.pw, isr, &self.params.shape)
|
||||
* 0.2
|
||||
}
|
||||
Source::Pulze => {
|
||||
self.phasor
|
||||
.pulze_shaped(freq, self.params.pw, isr, &self.params.shape)
|
||||
* 0.2
|
||||
}
|
||||
Source::White => self.white() * 0.2,
|
||||
Source::Pink => {
|
||||
let w = self.white();
|
||||
self.pink_noise.next(w) * 0.2
|
||||
}
|
||||
Source::Brown => {
|
||||
let w = self.white();
|
||||
self.brown_noise.next(w) * 0.2
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user