Initial commit

This commit is contained in:
2026-01-18 15:39:46 +01:00
commit 587f2bd7e7
106 changed files with 14918 additions and 0 deletions

462
src/voice/mod.rs Normal file
View 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
View 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
View 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(
&params,
&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,
};
}
}