Files
doux-copy/src/lib.rs
2026-01-19 14:42:14 +01:00

680 lines
23 KiB
Rust

#[cfg(feature = "native")]
pub mod audio;
#[cfg(feature = "native")]
pub mod config;
pub mod effects;
pub mod envelope;
#[cfg(feature = "native")]
pub mod error;
pub mod event;
pub mod fastmath;
pub mod filter;
#[cfg(feature = "native")]
pub mod loader;
pub mod noise;
pub mod orbit;
#[cfg(feature = "native")]
pub mod osc;
pub mod oscillator;
pub mod plaits;
pub mod sample;
pub mod schedule;
#[cfg(feature = "native")]
pub mod telemetry;
pub mod types;
pub mod voice;
#[cfg(target_arch = "wasm32")]
mod wasm;
use envelope::init_envelope;
use event::Event;
use orbit::{EffectParams, Orbit};
use sample::{FileSource, SampleEntry, SampleInfo, SamplePool, WebSampleSource};
use schedule::Schedule;
#[cfg(feature = "native")]
use telemetry::EngineMetrics;
use types::{DelayType, Source, BLOCK_SIZE, CHANNELS, MAX_ORBITS, MAX_VOICES};
use voice::{Voice, VoiceParams};
pub struct Engine {
pub sr: f32,
pub isr: f32,
pub voices: Vec<Voice>,
pub active_voices: usize,
pub orbits: Vec<Orbit>,
pub schedule: Schedule,
pub time: f64,
pub tick: u64,
pub output_channels: usize,
pub output: Vec<f32>,
// Sample storage
pub sample_pool: SamplePool,
pub samples: Vec<SampleInfo>,
pub sample_index: Vec<SampleEntry>,
// Default orbit params (used when voice doesn't specify)
pub effect_params: EffectParams,
// Telemetry (native only)
#[cfg(feature = "native")]
pub metrics: EngineMetrics,
}
impl Engine {
pub fn new(sample_rate: f32) -> Self {
Self::new_with_channels(sample_rate, CHANNELS)
}
pub fn new_with_channels(sample_rate: f32, output_channels: usize) -> Self {
let mut orbits = Vec::with_capacity(MAX_ORBITS);
for _ in 0..MAX_ORBITS {
orbits.push(Orbit::new(sample_rate));
}
Self {
sr: sample_rate,
isr: 1.0 / sample_rate,
voices: vec![Voice::default(); MAX_VOICES],
active_voices: 0,
orbits,
schedule: Schedule::new(),
time: 0.0,
tick: 0,
output_channels,
output: vec![0.0; BLOCK_SIZE * output_channels],
sample_pool: SamplePool::new(),
samples: Vec::with_capacity(256),
sample_index: Vec::new(),
effect_params: EffectParams {
delay_time: 0.333,
delay_feedback: 0.6,
delay_type: DelayType::Standard,
verb_decay: 0.75,
verb_damp: 0.95,
verb_predelay: 0.1,
verb_diff: 0.7,
comb_freq: 220.0,
comb_feedback: 0.9,
comb_damp: 0.1,
},
#[cfg(feature = "native")]
metrics: EngineMetrics::default(),
}
}
pub fn load_sample(&mut self, samples: &[f32], channels: u8, freq: f32) -> Option<usize> {
let info = self.sample_pool.add(samples, channels, freq)?;
let idx = self.samples.len();
self.samples.push(info);
Some(idx)
}
/// Look up sample by name (e.g., "wave_tek") and n (e.g., 0 for "wave_tek/0")
/// n wraps around using modulo if it exceeds the folder count
fn find_sample_index(&self, name: &str, n: usize) -> Option<usize> {
let prefix = format!("{name}/");
let count = self
.sample_index
.iter()
.filter(|e| e.name.starts_with(&prefix))
.count();
if count == 0 {
return None;
}
let wrapped_n = n % count;
let target = format!("{name}/{wrapped_n}");
self.sample_index.iter().position(|e| e.name == target)
}
/// Get a loaded sample index, loading lazily if needed (native only)
fn get_or_load_sample(&mut self, name: &str, n: usize) -> Option<usize> {
// First check if this is a direct index into already-loaded samples (WASM path)
// For WASM, treat `name` as numeric index if sample_index is empty
if self.sample_index.is_empty() {
let idx: usize = name.parse().ok()?;
if idx < self.samples.len() {
return Some(idx);
}
return None;
}
// Find the sample in the index by name
let index_idx = self.find_sample_index(name, n)?;
// If already loaded, return the loaded index
if let Some(loaded_idx) = self.sample_index[index_idx].loaded {
return Some(loaded_idx);
}
// Load the sample now (native only)
#[cfg(feature = "native")]
{
let path = self.sample_index[index_idx].path.clone();
match loader::load_sample_file(self, &path) {
Ok(loaded_idx) => {
self.sample_index[index_idx].loaded = Some(loaded_idx);
Some(loaded_idx)
}
Err(e) => {
eprintln!(
"Failed to load sample {}: {e}",
self.sample_index[index_idx].name
);
None
}
}
}
#[cfg(not(feature = "native"))]
{
None
}
}
pub fn evaluate(&mut self, input: &str) -> Option<usize> {
let event = Event::parse(input);
// Default to "play" if no explicit command - matches dough's JS wrapper behavior
let cmd = event.cmd.as_deref().unwrap_or("play");
match cmd {
"play" => self.play_event(event),
"hush" => {
self.hush();
None
}
"panic" => {
self.panic();
None
}
"reset" => {
self.panic();
self.schedule.clear();
self.time = 0.0;
self.tick = 0;
None
}
"release" => {
if let Some(v) = event.voice {
if v < self.active_voices {
self.voices[v].params.gate = 0.0;
}
}
None
}
"hush_endless" => {
for i in 0..self.active_voices {
if self.voices[i].params.duration.is_none() {
self.voices[i].params.gate = 0.0;
}
}
None
}
"reset_time" => {
self.time = 0.0;
self.tick = 0;
None
}
"reset_schedule" => {
self.schedule.clear();
None
}
_ => None,
}
}
fn play_event(&mut self, event: Event) -> Option<usize> {
if event.time.is_some() {
// ALL events with time go to schedule (like dough.c)
// This ensures repeat works correctly for time=0 events
self.schedule.push(event);
return None;
}
self.process_event(&event)
}
pub fn play(&mut self, params: VoiceParams) -> Option<usize> {
if self.active_voices >= MAX_VOICES {
return None;
}
let i = self.active_voices;
self.voices[i] = Voice::default();
self.voices[i].params = params;
self.voices[i].sr = self.sr;
self.active_voices += 1;
Some(i)
}
/// Process an event, handling voice selection like dough.c's process_engine_event()
fn process_event(&mut self, event: &Event) -> Option<usize> {
// Cut group: release any voices in the same cut group
if let Some(cut) = event.cut {
for i in 0..self.active_voices {
if self.voices[i].params.cut == Some(cut) {
self.voices[i].params.gate = 0.0;
}
}
}
let (voice_idx, is_new_voice) = if let Some(v) = event.voice {
if v < self.active_voices {
// Voice exists - reuse it
(v, false)
} else {
// Voice index out of range - allocate new
if self.active_voices >= MAX_VOICES {
return None;
}
let i = self.active_voices;
self.active_voices += 1;
(i, true)
}
} else {
// No voice specified - allocate new
if self.active_voices >= MAX_VOICES {
return None;
}
let i = self.active_voices;
self.active_voices += 1;
(i, true)
};
let should_reset = is_new_voice || event.reset.unwrap_or(false);
if should_reset {
self.voices[voice_idx] = Voice::default();
self.voices[voice_idx].sr = self.sr;
// Initialize glide_lag to target freq to prevent glide from 0
if let Some(freq) = event.freq {
self.voices[voice_idx].glide_lag.s = freq;
}
}
// Update voice params (only the ones explicitly set in event)
self.update_voice_params(voice_idx, event);
Some(voice_idx)
}
/// Update voice params - only updates fields that are explicitly set in the event
fn update_voice_params(&mut self, idx: usize, event: &Event) {
macro_rules! copy_opt {
($src:expr, $dst:expr, $($field:ident),+ $(,)?) => {
$(if let Some(val) = $src.$field { $dst.$field = val; })+
};
}
macro_rules! copy_opt_some {
($src:expr, $dst:expr, $($field:ident),+ $(,)?) => {
$(if let Some(val) = $src.$field { $dst.$field = Some(val); })+
};
}
// Resolve sound/sample first (before borrowing voice)
// If sound parses as a Source, use it; otherwise treat as sample folder name
let (parsed_source, loaded_sample) = if let Some(ref sound_str) = event.sound {
if let Ok(source) = sound_str.parse::<Source>() {
(Some(source), None)
} else {
// Treat as sample folder name
let sample = self.get_or_load_sample(sound_str, event.n.unwrap_or(0));
(None, sample)
}
} else {
(None, None)
};
let v = &mut self.voices[idx];
// --- Pitch ---
copy_opt!(event, v.params, freq, detune, speed);
copy_opt_some!(event, v.params, glide);
// --- Source ---
if let Some(source) = parsed_source {
v.params.sound = source;
}
copy_opt!(event, v.params, pw, spread);
if let Some(size) = event.size {
v.params.shape.size = size.min(256);
}
if let Some(mult) = event.mult {
v.params.shape.mult = mult.clamp(0.25, 16.0);
}
if let Some(warp) = event.warp {
v.params.shape.warp = warp.clamp(-1.0, 1.0);
}
if let Some(mirror) = event.mirror {
v.params.shape.mirror = mirror.clamp(0.0, 1.0);
}
if let Some(harmonics) = event.harmonics {
v.params.harmonics = harmonics.clamp(0.01, 0.999);
}
if let Some(timbre) = event.timbre {
v.params.timbre = timbre.clamp(0.01, 0.999);
}
if let Some(morph) = event.morph {
v.params.morph = morph.clamp(0.01, 0.999);
}
copy_opt_some!(event, v.params, cut);
// Sample playback (native)
if let Some(sample_idx) = loaded_sample {
v.params.sound = Source::Sample;
let begin = event.begin.unwrap_or(0.0);
let end = event.end.unwrap_or(1.0);
v.file_source = Some(FileSource::new(sample_idx, begin, end));
} else if event.begin.is_some() || event.end.is_some() {
// Update begin/end on existing file_source
if let Some(ref mut fs) = v.file_source {
if let Some(begin) = event.begin {
fs.begin = begin.clamp(0.0, 1.0);
}
if let Some(end) = event.end {
fs.end = end.clamp(fs.begin, 1.0);
}
}
}
// Web sample playback (WASM - set by JavaScript)
if let (Some(offset), Some(frames)) = (event.file_pcm, event.file_frames) {
v.params.sound = Source::WebSample;
v.web_sample = Some(WebSampleSource::new(
SampleInfo {
offset,
frames: frames as u32,
channels: event.file_channels.unwrap_or(1),
freq: event.file_freq.unwrap_or(65.406),
},
event.begin.unwrap_or(0.0),
event.end.unwrap_or(1.0),
));
}
// --- Gain ---
copy_opt!(event, v.params, gain, postgain, velocity, pan, gate);
copy_opt_some!(event, v.params, duration);
// --- Gain Envelope ---
let gain_env = init_envelope(
None,
event.attack,
event.decay,
event.sustain,
event.release,
);
if gain_env.active {
v.params.attack = gain_env.att;
v.params.decay = gain_env.dec;
v.params.sustain = gain_env.sus;
v.params.release = gain_env.rel;
}
// --- Filters ---
// Macro to apply envelope params (env amount + ADSR) to a target
macro_rules! apply_env {
($src:expr, $dst:expr, $e:ident, $a:ident, $d:ident, $s:ident, $r:ident, $active:ident) => {
let env = init_envelope($src.$e, $src.$a, $src.$d, $src.$s, $src.$r);
if env.active {
$dst.$e = env.env;
$dst.$a = env.att;
$dst.$d = env.dec;
$dst.$s = env.sus;
$dst.$r = env.rel;
$dst.$active = true;
}
};
}
copy_opt_some!(event, v.params, lpf);
copy_opt!(event, v.params, lpq);
apply_env!(event, v.params, lpe, lpa, lpd, lps, lpr, lp_env_active);
copy_opt_some!(event, v.params, hpf);
copy_opt!(event, v.params, hpq);
apply_env!(event, v.params, hpe, hpa, hpd, hps, hpr, hp_env_active);
copy_opt_some!(event, v.params, bpf);
copy_opt!(event, v.params, bpq);
apply_env!(event, v.params, bpe, bpa, bpd, bps, bpr, bp_env_active);
copy_opt!(event, v.params, ftype);
// --- Modulation ---
apply_env!(
event,
v.params,
penv,
patt,
pdec,
psus,
prel,
pitch_env_active
);
copy_opt!(event, v.params, vib, vibmod, vibshape);
copy_opt!(event, v.params, fm, fmh, fmshape);
apply_env!(event, v.params, fme, fma, fmd, fms, fmr, fm_env_active);
copy_opt!(event, v.params, am, amdepth, amshape);
copy_opt!(event, v.params, rm, rmdepth, rmshape);
// --- Effects ---
copy_opt!(
event,
v.params,
phaser,
phaserdepth,
phasersweep,
phasercenter
);
copy_opt!(event, v.params, flanger, flangerdepth, flangerfeedback);
copy_opt!(event, v.params, chorus, chorusdepth, chorusdelay);
copy_opt!(event, v.params, comb, combfreq, combfeedback, combdamp);
copy_opt_some!(event, v.params, coarse, crush, fold, wrap, distort);
copy_opt!(event, v.params, distortvol);
// --- Sends ---
copy_opt!(
event,
v.params,
orbit,
delay,
delaytime,
delayfeedback,
delaytype
);
copy_opt!(
event,
v.params,
verb,
verbdecay,
verbdamp,
verbpredelay,
verbdiff
);
}
fn free_voice(&mut self, i: usize) {
if self.active_voices > 0 {
self.active_voices -= 1;
self.voices.swap(i, self.active_voices);
}
}
fn process_schedule(&mut self) {
loop {
// O(1) early-exit: check only the first (earliest) event
let t = match self.schedule.peek_time() {
Some(t) if t <= self.time => t,
_ => return,
};
let diff = self.time - t;
let mut event = self.schedule.pop_front().unwrap();
// Fire only if event is fresh (within 1ms) - matches dough.c WASM
// Old events are silently rescheduled to catch up
if diff < 0.001 {
self.process_event(&event);
}
// Reschedule repeating events (re-insert in sorted order)
if let Some(rep) = event.repeat {
event.time = Some(t + rep as f64);
self.schedule.push(event);
}
// Loop continues for catch-up behavior
}
}
pub fn gen_sample(
&mut self,
output: &mut [f32],
sample_idx: usize,
web_pcm: &[f32],
live_input: &[f32],
) {
let base_idx = sample_idx * self.output_channels;
let num_pairs = self.output_channels / 2;
for c in 0..self.output_channels {
output[base_idx + c] = 0.0;
}
// Clear orbit sends
for orbit in &mut self.orbits {
orbit.clear_sends();
}
// Process voices - matches dough.c behavior exactly:
// When a voice dies, it's freed immediately and the loop continues,
// which means the swapped-in voice (from the end) gets skipped this frame.
let isr = self.isr;
let num_orbits = self.orbits.len();
let mut i = 0;
while i < self.active_voices {
// Reborrow for each iteration to allow free_voice during loop
let pool = self.sample_pool.data.as_slice();
let samples = self.samples.as_slice();
let alive = self.voices[i].process(isr, pool, samples, web_pcm, sample_idx, live_input);
if !alive {
self.free_voice(i);
// Match dough.c: increment i, skipping the swapped-in voice
i += 1;
continue;
}
let orbit_idx = self.voices[i].params.orbit % num_orbits;
let out_pair = orbit_idx % num_pairs;
let pair_offset = out_pair * 2;
output[base_idx + pair_offset] += self.voices[i].ch[0];
output[base_idx + pair_offset + 1] += self.voices[i].ch[1];
// Add to orbit sends
if self.voices[i].params.delay > 0.0 {
for c in 0..CHANNELS {
self.orbits[orbit_idx]
.add_delay_send(c, self.voices[i].ch[c] * self.voices[i].params.delay);
}
// Update orbit delay params from voice
self.effect_params.delay_time = self.voices[i].params.delaytime;
self.effect_params.delay_feedback = self.voices[i].params.delayfeedback;
self.effect_params.delay_type = self.voices[i].params.delaytype;
}
if self.voices[i].params.verb > 0.0 {
for c in 0..CHANNELS {
self.orbits[orbit_idx]
.add_verb_send(c, self.voices[i].ch[c] * self.voices[i].params.verb);
}
// Update orbit verb params from voice
self.effect_params.verb_decay = self.voices[i].params.verbdecay;
self.effect_params.verb_damp = self.voices[i].params.verbdamp;
self.effect_params.verb_predelay = self.voices[i].params.verbpredelay;
self.effect_params.verb_diff = self.voices[i].params.verbdiff;
}
if self.voices[i].params.comb > 0.0 {
for c in 0..CHANNELS {
self.orbits[orbit_idx]
.add_comb_send(c, self.voices[i].ch[c] * self.voices[i].params.comb);
}
// Update orbit comb params from voice
self.effect_params.comb_freq = self.voices[i].params.combfreq;
self.effect_params.comb_feedback = self.voices[i].params.combfeedback;
self.effect_params.comb_damp = self.voices[i].params.combdamp;
}
i += 1;
}
for (orbit_idx, orbit) in self.orbits.iter_mut().enumerate() {
orbit.process(&self.effect_params);
let out_pair = orbit_idx % num_pairs;
let pair_offset = out_pair * 2;
output[base_idx + pair_offset] +=
orbit.delay_out[0] + orbit.verb_out[0] + orbit.comb_out[0];
output[base_idx + pair_offset + 1] +=
orbit.delay_out[1] + orbit.verb_out[1] + orbit.comb_out[1];
}
for c in 0..self.output_channels {
output[base_idx + c] = (output[base_idx + c] * 0.5).clamp(-1.0, 1.0);
}
}
pub fn process_block(&mut self, output: &mut [f32], web_pcm: &[f32], live_input: &[f32]) {
#[cfg(feature = "native")]
let start = std::time::Instant::now();
let samples = output.len() / self.output_channels;
for i in 0..samples {
self.process_schedule();
self.tick += 1;
self.time = self.tick as f64 / self.sr as f64;
self.gen_sample(output, i, web_pcm, live_input);
}
#[cfg(feature = "native")]
{
use std::sync::atomic::Ordering;
let elapsed_ns = start.elapsed().as_nanos() as u64;
self.metrics.load.record_sample(elapsed_ns);
self.metrics
.active_voices
.store(self.active_voices as u32, Ordering::Relaxed);
self.metrics
.peak_voices
.fetch_max(self.active_voices as u32, Ordering::Relaxed);
self.metrics
.schedule_depth
.store(self.schedule.len() as u32, Ordering::Relaxed);
}
let copy_len = output.len().min(self.output.len());
self.output[..copy_len].copy_from_slice(&output[..copy_len]);
}
pub fn dsp(&mut self) {
let mut output = std::mem::take(&mut self.output);
self.process_block(&mut output, &[], &[]);
self.output = output;
}
pub fn dsp_with_web_pcm(&mut self, web_pcm: &[f32], live_input: &[f32]) {
let mut output = std::mem::take(&mut self.output);
self.process_block(&mut output, web_pcm, live_input);
self.output = output;
}
pub fn get_time(&self) -> f64 {
self.time
}
pub fn hush(&mut self) {
for i in 0..self.active_voices {
self.voices[i].params.gate = 0.0;
}
}
pub fn panic(&mut self) {
self.active_voices = 0;
}
}