use super::lfo::{LfoParams, apply_lfo_modulation, generate_lfo_value};
use super::synth::types::{SynthType, get_synth_type};
use super::synth::{adsr_envelope, midi_to_frequency, oscillator_sample, time_to_samples};
use anyhow::Result;
use std::collections::HashMap;
#[cfg(feature = "cli")]
use crate::engine::plugin::{loader::load_plugin, runner::WasmPluginRunner};
#[derive(Debug, Clone)]
pub struct FilterDef {
pub filter_type: String, pub cutoff: f32, pub resonance: f32, }
#[derive(Debug, Clone)]
pub struct SynthParams {
pub waveform: String,
pub attack: f32, pub decay: f32, pub sustain: f32, pub release: f32, pub synth_type: Option<String>, pub filters: Vec<FilterDef>, pub options: HashMap<String, f32>, pub lfo: Option<LfoParams>, pub plugin_author: Option<String>,
pub plugin_name: Option<String>,
pub plugin_export: Option<String>,
}
impl Default for SynthParams {
fn default() -> Self {
Self {
waveform: "sine".to_string(),
attack: 0.01,
decay: 0.1,
sustain: 0.7,
release: 0.2,
synth_type: None,
filters: Vec::new(),
options: HashMap::new(),
lfo: None,
plugin_author: None,
plugin_name: None,
plugin_export: None,
}
}
}
pub fn generate_note(
midi_note: u8,
duration_ms: f32,
velocity: f32,
params: &SynthParams,
sample_rate: u32,
) -> Result<Vec<f32>> {
generate_note_with_options(
midi_note,
duration_ms,
velocity,
params,
sample_rate,
0.0,
0.0,
)
}
pub fn generate_note_with_options(
midi_note: u8,
duration_ms: f32,
velocity: f32,
params: &SynthParams,
sample_rate: u32,
pan: f32, detune: f32, ) -> Result<Vec<f32>> {
#[cfg(feature = "cli")]
{
if let Some(ref author) = params.plugin_author {
if let Some(ref name) = params.plugin_name {
return generate_note_with_plugin(
midi_note,
duration_ms,
velocity,
params,
sample_rate,
pan,
detune,
author,
name,
params.plugin_export.as_deref(),
);
}
}
}
let base_frequency = midi_to_frequency(midi_note);
let frequency = if detune.abs() > 0.01 {
base_frequency * 2.0_f32.powf(detune / 1200.0)
} else {
base_frequency
};
let velocity = velocity.clamp(0.0, 1.0);
let mut modified_params = params.clone();
let synth_type: Option<Box<dyn SynthType>> = if let Some(ref type_name) = params.synth_type {
get_synth_type(type_name)
} else {
None
};
if let Some(ref stype) = synth_type {
stype.modify_params(&mut modified_params);
}
let attack_samples = time_to_samples(modified_params.attack, sample_rate);
let decay_samples = time_to_samples(modified_params.decay, sample_rate);
let release_samples = time_to_samples(modified_params.release, sample_rate);
let duration_seconds = duration_ms / 1000.0;
let total_samples = time_to_samples(duration_seconds, sample_rate);
let envelope_samples = attack_samples + decay_samples + release_samples;
let sustain_samples = total_samples.saturating_sub(envelope_samples);
let mut samples = Vec::with_capacity(total_samples * 2);
let pan = pan.clamp(-1.0, 1.0);
let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI; let left_gain = pan_angle.cos();
let right_gain = pan_angle.sin();
let bpm = 120.0;
for i in 0..total_samples {
let time = i as f32 / sample_rate as f32;
let mut osc_frequency = frequency;
if let Some(ref lfo) = modified_params.lfo {
use crate::engine::audio::lfo::LfoTarget;
if lfo.target == LfoTarget::Pitch {
let lfo_cents = generate_lfo_value(lfo, time, bpm) * 100.0; osc_frequency = frequency * 2.0_f32.powf(lfo_cents / 1200.0);
}
}
let osc_sample = oscillator_sample(&modified_params.waveform, osc_frequency, time);
let envelope = adsr_envelope(
i,
attack_samples,
decay_samples,
sustain_samples,
release_samples,
modified_params.sustain,
);
let mut amplitude = osc_sample * envelope * velocity * 0.3;
if let Some(ref lfo) = modified_params.lfo {
use crate::engine::audio::lfo::LfoTarget;
if lfo.target == LfoTarget::Volume {
let lfo_value = generate_lfo_value(lfo, time, bpm);
amplitude *= 1.0 + lfo_value; }
}
samples.push(amplitude * left_gain);
samples.push(amplitude * right_gain);
}
for filter in &modified_params.filters {
let mut modulated_filter = filter.clone();
if let Some(ref lfo) = modified_params.lfo {
use crate::engine::audio::lfo::LfoTarget;
if lfo.target == LfoTarget::FilterCutoff {
let cutoff_range = filter.cutoff * 0.5; modulated_filter.cutoff = apply_lfo_modulation(
lfo,
0.0, bpm,
filter.cutoff,
cutoff_range,
);
}
}
apply_filter(&mut samples, &modulated_filter, sample_rate)?;
}
if let Some(stype) = synth_type {
stype.post_process(&mut samples, sample_rate, &modified_params.options)?;
}
Ok(samples)
}
fn apply_filter(samples: &mut [f32], filter: &FilterDef, sample_rate: u32) -> Result<()> {
match filter.filter_type.to_lowercase().as_str() {
"lowpass" => apply_lowpass(samples, filter.cutoff, sample_rate),
"highpass" => apply_highpass(samples, filter.cutoff, sample_rate),
"bandpass" => apply_bandpass(samples, filter.cutoff, sample_rate),
_ => Ok(()),
}
}
fn apply_lowpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
let dt = 1.0 / sample_rate as f32;
let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
let alpha = dt / (rc + dt);
let mut prev = 0.0f32;
for i in (0..samples.len()).step_by(2) {
let filtered = prev + alpha * (samples[i] - prev);
prev = filtered;
samples[i] = filtered;
if i + 1 < samples.len() {
samples[i + 1] = filtered;
}
}
Ok(())
}
fn apply_highpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
let dt = 1.0 / sample_rate as f32;
let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
let alpha = rc / (rc + dt);
let mut prev_input = 0.0f32;
let mut prev_output = 0.0f32;
for i in (0..samples.len()).step_by(2) {
let current = samples[i];
let filtered = alpha * (prev_output + current - prev_input);
prev_input = current;
prev_output = filtered;
samples[i] = filtered;
if i + 1 < samples.len() {
samples[i + 1] = filtered;
}
}
Ok(())
}
fn apply_bandpass(samples: &mut [f32], center: f32, sample_rate: u32) -> Result<()> {
let bandwidth = center * 0.5;
apply_highpass(samples, center - bandwidth, sample_rate)?;
apply_lowpass(samples, center + bandwidth, sample_rate)?;
Ok(())
}
pub fn generate_chord(
midi_notes: &[u8],
duration_ms: f32,
velocity: f32,
params: &SynthParams,
sample_rate: u32,
) -> Result<Vec<f32>> {
generate_chord_with_options(
midi_notes,
duration_ms,
velocity,
params,
sample_rate,
0.0,
0.0,
0.0,
)
}
pub fn generate_chord_with_options(
midi_notes: &[u8],
duration_ms: f32,
velocity: f32,
params: &SynthParams,
sample_rate: u32,
pan: f32, detune: f32, spread: f32, ) -> Result<Vec<f32>> {
if midi_notes.is_empty() {
return Ok(Vec::new());
}
let num_notes = midi_notes.len();
let spread = spread.clamp(0.0, 1.0);
let mut result: Option<Vec<f32>> = None;
for (i, &midi_note) in midi_notes.iter().enumerate() {
let note_pan = if num_notes > 1 && spread > 0.0 {
let position = i as f32 / (num_notes - 1) as f32; let spread_amount = (position - 0.5) * 2.0 * spread; (pan + spread_amount).clamp(-1.0, 1.0)
} else {
pan
};
let note_samples = generate_note_with_options(
midi_note,
duration_ms,
velocity,
params,
sample_rate,
note_pan,
detune,
)?;
match result {
None => {
result = Some(note_samples);
}
Some(ref mut buffer) => {
for (j, sample) in note_samples.iter().enumerate() {
if j < buffer.len() {
buffer[j] = (buffer[j] + sample) / 2.0;
}
}
}
}
}
Ok(result.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_note() {
let params = SynthParams::default();
let samples = generate_note(60, 500.0, 0.8, ¶ms, 44100).unwrap();
assert!(samples.len() > 0);
assert_eq!(samples.len() % 2, 0);
let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
assert!(has_audio);
}
#[test]
fn test_generate_chord() {
let params = SynthParams::default();
let samples = generate_chord(&[60, 64, 67], 500.0, 0.8, ¶ms, 44100).unwrap();
assert!(samples.len() > 0);
let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
assert!(has_audio);
}
}
#[cfg(feature = "cli")]
fn generate_note_with_plugin(
midi_note: u8,
duration_ms: f32,
velocity: f32,
params: &SynthParams,
sample_rate: u32,
pan: f32,
detune: f32,
plugin_author: &str,
plugin_name: &str,
plugin_export: Option<&str>,
) -> Result<Vec<f32>> {
use once_cell::sync::Lazy;
use std::sync::Mutex;
static PLUGIN_RUNNER: Lazy<Mutex<WasmPluginRunner>> =
Lazy::new(|| Mutex::new(WasmPluginRunner::new()));
static PLUGIN_CACHE: Lazy<Mutex<HashMap<String, Vec<u8>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
let plugin_key = format!("{}.{}", plugin_author, plugin_name);
let mut cache = PLUGIN_CACHE.lock().unwrap();
let wasm_bytes = if let Some(bytes) = cache.get(&plugin_key) {
bytes.clone()
} else {
let (_info, bytes) = load_plugin(plugin_author, plugin_name)
.map_err(|e| anyhow::anyhow!("Failed to load plugin: {}", e))?;
cache.insert(plugin_key.clone(), bytes.clone());
bytes
};
drop(cache);
let base_frequency = midi_to_frequency(midi_note);
let frequency = if detune.abs() > 0.01 {
base_frequency * 2.0_f32.powf(detune / 1200.0)
} else {
base_frequency
};
let duration_seconds = duration_ms / 1000.0;
let total_samples = (duration_seconds * sample_rate as f32) as usize;
let mut buffer = vec![0.0f32; total_samples * 2];
let mut plugin_options = params.options.clone();
let audio_params = ["pan", "gain", "volume", "detune", "spread", "decay_mode"];
for param in &audio_params {
plugin_options.remove(*param);
}
if !plugin_options.contains_key("waveform") && params.waveform != "plugin" {
let waveform_value = match params.waveform.as_str() {
"sine" => 0.0,
"saw" => 1.0,
"square" => 2.0,
"triangle" => 3.0,
_ => 0.0, };
plugin_options.insert("waveform".to_string(), waveform_value);
}
let runner = PLUGIN_RUNNER.lock().unwrap();
let synth_id = format!("{}_{}", plugin_key, plugin_export.unwrap_or("default"));
runner
.render_note_in_place(
&wasm_bytes,
&mut buffer,
Some(&synth_id), plugin_export,
frequency,
velocity,
duration_ms as i32,
sample_rate as i32,
2, Some(&plugin_options),
)
.map_err(|e| anyhow::anyhow!("Plugin render error: {}", e))?;
if pan.abs() > 0.01 {
let pan = pan.clamp(-1.0, 1.0);
let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI;
let left_gain = pan_angle.cos();
let right_gain = pan_angle.sin();
for i in (0..buffer.len()).step_by(2) {
if i + 1 < buffer.len() {
buffer[i] *= left_gain;
buffer[i + 1] *= right_gain;
}
}
}
for filter in ¶ms.filters {
apply_filter(&mut buffer, filter, sample_rate)?;
}
Ok(buffer)
}