1use super::lfo::{LfoParams, apply_lfo_modulation, generate_lfo_value};
2use super::synth::types::{SynthType, get_synth_type};
3use super::synth::{adsr_envelope, midi_to_frequency, oscillator_sample, time_to_samples};
4use anyhow::Result;
6use std::collections::HashMap;
7
8#[cfg(feature = "cli")]
9use crate::engine::plugin::{loader::load_plugin, runner::WasmPluginRunner};
10
11#[derive(Debug, Clone)]
13pub struct FilterDef {
14 pub filter_type: String, pub cutoff: f32, pub resonance: f32, }
18
19#[derive(Debug, Clone)]
21pub struct SynthParams {
22 pub waveform: String,
23 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 bpm: f32, pub plugin_author: Option<String>,
34 pub plugin_name: Option<String>,
35 pub plugin_export: Option<String>,
36}
37
38impl Default for SynthParams {
39 fn default() -> Self {
40 Self {
41 waveform: "sine".to_string(),
42 attack: 0.01,
43 decay: 0.1,
44 sustain: 0.7,
45 release: 0.2,
46 synth_type: None,
47 filters: Vec::new(),
48 options: HashMap::new(),
49 lfo: None,
50 bpm: 120.0,
51 plugin_author: None,
52 plugin_name: None,
53 plugin_export: None,
54 }
55 }
56}
57
58pub fn generate_note(
60 midi_note: u8,
61 duration_ms: f32,
62 velocity: f32,
63 params: &SynthParams,
64 sample_rate: u32,
65) -> Result<Vec<f32>> {
66 generate_note_with_options(
67 midi_note,
68 duration_ms,
69 velocity,
70 params,
71 sample_rate,
72 0.0,
73 0.0,
74 )
75}
76
77pub fn generate_note_with_options(
81 midi_note: u8,
82 duration_ms: f32,
83 velocity: f32,
84 params: &SynthParams,
85 sample_rate: u32,
86 pan: f32, detune: f32, ) -> Result<Vec<f32>> {
89 #[cfg(feature = "cli")]
93 {
94 if let Some(ref author) = params.plugin_author {
95 if let Some(ref name) = params.plugin_name {
96 return generate_note_with_plugin(
98 midi_note,
99 duration_ms,
100 velocity,
101 params,
102 sample_rate,
103 pan,
104 detune,
105 author,
106 name,
107 params.plugin_export.as_deref(),
108 );
109 }
110 }
111 }
112
113 let base_frequency = midi_to_frequency(midi_note);
116
117 let frequency = if detune.abs() > 0.01 {
119 base_frequency * 2.0_f32.powf(detune / 1200.0)
120 } else {
121 base_frequency
122 };
123
124 let velocity = velocity.clamp(0.0, 1.0);
125
126 let mut modified_params = params.clone();
128
129 let synth_type: Option<Box<dyn SynthType>> = if let Some(ref type_name) = params.synth_type {
131 get_synth_type(type_name)
132 } else {
133 None
134 };
135
136 if let Some(ref stype) = synth_type {
137 stype.modify_params(&mut modified_params);
138 }
139
140 let attack_samples = time_to_samples(modified_params.attack, sample_rate);
142 let decay_samples = time_to_samples(modified_params.decay, sample_rate);
143 let release_samples = time_to_samples(modified_params.release, sample_rate);
144
145 let duration_seconds = duration_ms / 1000.0;
147 let total_samples = time_to_samples(duration_seconds, sample_rate);
148 let envelope_samples = attack_samples + decay_samples + release_samples;
149 let sustain_samples = total_samples.saturating_sub(envelope_samples);
150
151 let mut samples = Vec::with_capacity(total_samples * 2); let pan = pan.clamp(-1.0, 1.0);
155 let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI; let left_gain = pan_angle.cos();
157 let right_gain = pan_angle.sin();
158
159 let bpm = modified_params.bpm;
161
162 for i in 0..total_samples {
163 let time = i as f32 / sample_rate as f32;
164
165 let mut osc_frequency = frequency;
167
168 if let Some(ref lfo) = modified_params.lfo {
170 use crate::engine::audio::lfo::LfoTarget;
171 if lfo.target == LfoTarget::Pitch {
172 let lfo_cents = generate_lfo_value(lfo, time, bpm) * 100.0; osc_frequency = frequency * 2.0_f32.powf(lfo_cents / 1200.0);
174 }
175 }
176
177 let osc_sample = oscillator_sample(&modified_params.waveform, osc_frequency, time);
178
179 let envelope = adsr_envelope(
181 i,
182 attack_samples,
183 decay_samples,
184 sustain_samples,
185 release_samples,
186 modified_params.sustain,
187 );
188
189 let mut amplitude = osc_sample * envelope * velocity * 0.3; if let Some(ref lfo) = modified_params.lfo {
194 use crate::engine::audio::lfo::LfoTarget;
195 if lfo.target == LfoTarget::Volume {
196 let lfo_value = generate_lfo_value(lfo, time, bpm);
197 amplitude *= 1.0 + lfo_value; }
199 }
200
201 samples.push(amplitude * left_gain);
203 samples.push(amplitude * right_gain);
204 }
205
206 for filter in &modified_params.filters {
208 let mut modulated_filter = filter.clone();
209
210 if let Some(ref lfo) = modified_params.lfo {
212 use crate::engine::audio::lfo::LfoTarget;
213 if lfo.target == LfoTarget::FilterCutoff {
214 let cutoff_range = filter.cutoff * 0.5; modulated_filter.cutoff = apply_lfo_modulation(
217 lfo,
218 0.0, bpm,
220 filter.cutoff,
221 cutoff_range,
222 );
223 }
224 }
225
226 apply_filter(&mut samples, &modulated_filter, sample_rate)?;
227 }
228
229 if let Some(stype) = synth_type {
231 stype.post_process(&mut samples, sample_rate, &modified_params.options)?;
232 }
233
234 Ok(samples)
235}
236
237fn apply_filter(samples: &mut [f32], filter: &FilterDef, sample_rate: u32) -> Result<()> {
239 match filter.filter_type.to_lowercase().as_str() {
240 "lowpass" => apply_lowpass(samples, filter.cutoff, sample_rate),
241 "highpass" => apply_highpass(samples, filter.cutoff, sample_rate),
242 "bandpass" => apply_bandpass(samples, filter.cutoff, sample_rate),
243 _ => Ok(()),
244 }
245}
246
247fn apply_lowpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
249 let dt = 1.0 / sample_rate as f32;
250 let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
251 let alpha = dt / (rc + dt);
252
253 let mut prev = 0.0f32;
254 for i in (0..samples.len()).step_by(2) {
255 let filtered = prev + alpha * (samples[i] - prev);
257 prev = filtered;
258 samples[i] = filtered;
259
260 if i + 1 < samples.len() {
262 samples[i + 1] = filtered;
263 }
264 }
265
266 Ok(())
267}
268
269fn apply_highpass(samples: &mut [f32], cutoff: f32, sample_rate: u32) -> Result<()> {
271 let dt = 1.0 / sample_rate as f32;
272 let rc = 1.0 / (2.0 * std::f32::consts::PI * cutoff);
273 let alpha = rc / (rc + dt);
274
275 let mut prev_input = 0.0f32;
276 let mut prev_output = 0.0f32;
277
278 for i in (0..samples.len()).step_by(2) {
279 let current = samples[i];
280 let filtered = alpha * (prev_output + current - prev_input);
281
282 prev_input = current;
283 prev_output = filtered;
284
285 samples[i] = filtered;
286 if i + 1 < samples.len() {
287 samples[i + 1] = filtered;
288 }
289 }
290
291 Ok(())
292}
293
294fn apply_bandpass(samples: &mut [f32], center: f32, sample_rate: u32) -> Result<()> {
296 let bandwidth = center * 0.5; apply_highpass(samples, center - bandwidth, sample_rate)?;
300 apply_lowpass(samples, center + bandwidth, sample_rate)?;
301
302 Ok(())
303}
304
305pub fn generate_chord(
307 midi_notes: &[u8],
308 duration_ms: f32,
309 velocity: f32,
310 params: &SynthParams,
311 sample_rate: u32,
312) -> Result<Vec<f32>> {
313 generate_chord_with_options(
314 midi_notes,
315 duration_ms,
316 velocity,
317 params,
318 sample_rate,
319 0.0,
320 0.0,
321 0.0,
322 )
323}
324
325pub fn generate_chord_with_options(
327 midi_notes: &[u8],
328 duration_ms: f32,
329 velocity: f32,
330 params: &SynthParams,
331 sample_rate: u32,
332 pan: f32, detune: f32, spread: f32, ) -> Result<Vec<f32>> {
336 if midi_notes.is_empty() {
337 return Ok(Vec::new());
338 }
339
340 let num_notes = midi_notes.len();
341 let spread = spread.clamp(0.0, 1.0);
342
343 let mut result: Option<Vec<f32>> = None;
345
346 for (i, &midi_note) in midi_notes.iter().enumerate() {
347 let note_pan = if num_notes > 1 && spread > 0.0 {
349 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)
353 } else {
354 pan
355 };
356
357 let note_samples = generate_note_with_options(
359 midi_note,
360 duration_ms,
361 velocity,
362 params,
363 sample_rate,
364 note_pan,
365 detune,
366 )?;
367
368 match result {
370 None => {
371 result = Some(note_samples);
372 }
373 Some(ref mut buffer) => {
374 for (j, sample) in note_samples.iter().enumerate() {
376 if j < buffer.len() {
377 buffer[j] = (buffer[j] + sample) / 2.0;
378 }
379 }
380 }
381 }
382 }
383
384 Ok(result.unwrap_or_default())
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_generate_note() {
393 let params = SynthParams::default();
394 let samples = generate_note(60, 500.0, 0.8, ¶ms, 44100).unwrap();
395
396 assert!(samples.len() > 0);
398 assert_eq!(samples.len() % 2, 0); let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
402 assert!(has_audio);
403 }
404
405 #[test]
406 fn test_generate_chord() {
407 let params = SynthParams::default();
408 let samples = generate_chord(&[60, 64, 67], 500.0, 0.8, ¶ms, 44100).unwrap();
409
410 assert!(samples.len() > 0);
412
413 let has_audio = samples.iter().any(|&s| s.abs() > 0.001);
415 assert!(has_audio);
416 }
417}
418
419#[cfg(feature = "cli")]
421fn generate_note_with_plugin(
422 midi_note: u8,
423 duration_ms: f32,
424 velocity: f32,
425 params: &SynthParams,
426 sample_rate: u32,
427 pan: f32,
428 detune: f32,
429 plugin_author: &str,
430 plugin_name: &str,
431 plugin_export: Option<&str>,
432) -> Result<Vec<f32>> {
433 use once_cell::sync::Lazy;
434 use std::sync::Mutex;
435
436 static PLUGIN_RUNNER: Lazy<Mutex<WasmPluginRunner>> =
438 Lazy::new(|| Mutex::new(WasmPluginRunner::new()));
439
440 static PLUGIN_CACHE: Lazy<Mutex<HashMap<String, Vec<u8>>>> =
442 Lazy::new(|| Mutex::new(HashMap::new()));
443
444 let plugin_key = format!("{}.{}", plugin_author, plugin_name);
446 let mut cache = PLUGIN_CACHE
447 .lock()
448 .map_err(|_| anyhow::anyhow!("Plugin cache lock poisoned"))?;
449
450 let wasm_bytes = if let Some(bytes) = cache.get(&plugin_key) {
451 bytes.clone()
453 } else {
454 let (_info, bytes) = load_plugin(plugin_author, plugin_name)
456 .map_err(|e| anyhow::anyhow!("Failed to load plugin: {}", e))?;
457
458 cache.insert(plugin_key.clone(), bytes.clone());
459 bytes
460 };
461 drop(cache);
462
463 let base_frequency = midi_to_frequency(midi_note);
465 let frequency = if detune.abs() > 0.01 {
466 base_frequency * 2.0_f32.powf(detune / 1200.0)
467 } else {
468 base_frequency
469 };
470
471 let duration_seconds = duration_ms / 1000.0;
472 let total_samples = (duration_seconds * sample_rate as f32) as usize;
473 let mut buffer = vec![0.0f32; total_samples * 2]; let mut plugin_options = params.options.clone();
477
478 let audio_params = ["pan", "gain", "volume", "detune", "spread", "decay_mode"];
480 for param in &audio_params {
481 plugin_options.remove(*param);
482 }
483
484 if !plugin_options.contains_key("waveform") && params.waveform != "plugin" {
487 let waveform_value = match params.waveform.as_str() {
489 "sine" => 0.0,
490 "saw" => 1.0,
491 "square" => 2.0,
492 "triangle" => 3.0,
493 _ => 0.0, };
495 plugin_options.insert("waveform".to_string(), waveform_value);
496 }
497
498 let runner = PLUGIN_RUNNER
500 .lock()
501 .map_err(|_| anyhow::anyhow!("Plugin runner lock poisoned"))?;
502 let synth_id = format!("{}_{}", plugin_key, plugin_export.unwrap_or("default"));
503
504 runner
505 .render_note_in_place(
506 &wasm_bytes,
507 &mut buffer,
508 Some(&synth_id), plugin_export,
510 frequency,
511 velocity,
512 duration_ms as i32,
513 sample_rate as i32,
514 2, Some(&plugin_options),
516 )
517 .map_err(|e| anyhow::anyhow!("Plugin render error: {}", e))?;
518
519 if pan.abs() > 0.01 {
521 let pan = pan.clamp(-1.0, 1.0);
522 let pan_angle = (pan + 1.0) * 0.25 * std::f32::consts::PI;
523 let left_gain = pan_angle.cos();
524 let right_gain = pan_angle.sin();
525
526 for i in (0..buffer.len()).step_by(2) {
527 if i + 1 < buffer.len() {
528 buffer[i] *= left_gain;
529 buffer[i + 1] *= right_gain;
530 }
531 }
532 }
533
534 for filter in ¶ms.filters {
536 apply_filter(&mut buffer, filter, sample_rate)?;
537 }
538
539 Ok(buffer)
540}