devalang_core/core/audio/engine/notes/
params.rs

1use devalang_types::Value;
2use std::collections::HashMap;
3
4pub struct FilterSpec {
5    pub kind: String,
6    pub cutoff: f32,
7}
8
9pub struct FilterState {
10    pub prev_l: f32,
11    pub prev_r: f32,
12    pub prev_in_l: f32,
13    pub prev_in_r: f32,
14    pub prev_out_l: f32,
15    pub prev_out_r: f32,
16}
17
18pub struct NoteSetup {
19    pub sample_rate: f32,
20    pub channels: usize,
21    pub total_samples: usize,
22    pub start_sample: usize,
23    pub attack_samples: usize,
24    pub decay_samples: usize,
25    pub release_samples: usize,
26    pub sustain_level: f32,
27    pub pluck_click: f32,
28    pub pluck_click_samples: usize,
29    pub drive: f32,
30    pub filters: Vec<FilterSpec>,
31    pub filter_states: Vec<FilterState>,
32    pub lfo_rate: f32,
33    pub lfo_depth: f32,
34    pub lfo_target: Option<String>,
35    pub voices: usize,
36    pub unison_detune: f32,
37    pub volume_env: HashMap<String, Value>,
38    pub pan_env: HashMap<String, Value>,
39    pub pitch_env: HashMap<String, Value>,
40}
41
42pub fn build_note_setup(
43    engine: &mut crate::core::audio::engine::AudioEngine,
44    _waveform: &str,
45    freq: f32,
46    amp: f32,
47    start_time_ms: f32,
48    mut duration_ms: f32,
49    synth_params: &HashMap<String, Value>,
50    note_params: &HashMap<String, Value>,
51    automation: &Option<HashMap<String, Value>>,
52) -> NoteSetup {
53    use crate::core::audio::engine::helpers;
54
55    // Extract ADSR and normalize units
56    let attack = engine.extract_f32(synth_params, "attack").unwrap_or(0.0);
57    let decay = engine.extract_f32(synth_params, "decay").unwrap_or(0.0);
58    let sustain = engine.extract_f32(synth_params, "sustain").unwrap_or(1.0);
59    let release = engine.extract_f32(synth_params, "release").unwrap_or(0.0);
60    let _sustain_level = if sustain > 1.0 {
61        (sustain / 100.0).clamp(0.0, 1.0)
62    } else {
63        sustain.clamp(0.0, 1.0)
64    };
65
66    if let Some(g) = engine.extract_f32(note_params, "gate") {
67        if g > 0.0 && g <= 1.0 {
68            duration_ms = duration_ms * g;
69        } else if g > 1.0 {
70            duration_ms = duration_ms * (g / 100.0);
71        }
72    } else if let Some(g) = engine.extract_f32(synth_params, "gate") {
73        if g > 0.0 && g <= 1.0 {
74            duration_ms = duration_ms * g;
75        } else if g > 1.0 {
76            duration_ms = duration_ms * (g / 100.0);
77        }
78    }
79
80    let velocity = engine.extract_f32(note_params, "velocity").unwrap_or(1.0);
81
82    let detune_cents = engine
83        .extract_f32(note_params, "detune")
84        .or(engine.extract_f32(synth_params, "detune"))
85        .unwrap_or(0.0);
86
87    let _lowpass_cut = engine
88        .extract_f32(note_params, "lowpass")
89        .or(engine.extract_f32(synth_params, "lowpass"))
90        .unwrap_or(0.0);
91
92    let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
93
94    let _freq_start = freq;
95    let mut _freq_end = freq;
96    let _amp_start = amp * velocity.clamp(0.0, 1.0);
97    let mut _amp_end = _amp_start;
98
99    let glide = engine
100        .extract_boolean(note_params, "glide")
101        .unwrap_or(false);
102    let slide = engine
103        .extract_boolean(note_params, "slide")
104        .unwrap_or(false);
105    if glide {
106        if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
107            _freq_end = *target_freq;
108        } else {
109            _freq_end = freq * 1.5;
110        }
111    }
112    if slide {
113        if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
114            _amp_end = *target_amp * velocity.clamp(0.0, 1.0);
115        } else {
116            _amp_end = _amp_start * 0.5;
117        }
118    }
119
120    let sample_rate = engine.sample_rate as f32;
121    let channels = engine.channels as usize;
122
123    let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
124    let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
125
126    // MIDI event
127    let midi_note_f = 69.0 + 12.0 * (_freq_start / 440.0).log2();
128    let midi_note = midi_note_f.round().clamp(0.0, 127.0) as u8;
129    let midi_vel = (velocity.clamp(0.0, 1.0) * 127.0).round().clamp(0.0, 127.0) as u8;
130    engine
131        .midi_events
132        .push(crate::core::audio::engine::driver::MidiNoteEvent {
133            key: midi_note,
134            vel: midi_vel,
135            start_ms: start_time_ms as u32,
136            duration_ms: duration_ms as u32,
137            channel: 0,
138        });
139
140    let _detune_factor = (2.0_f32).powf(detune_cents / 1200.0);
141
142    let (_volume_env, _pan_env, _pitch_env) = helpers::env_maps_from_automation(automation);
143
144    let attack_s = if attack > 10.0 {
145        attack / 1000.0
146    } else {
147        attack
148    };
149    let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
150    let release_s = if release > 10.0 {
151        release / 1000.0
152    } else {
153        release
154    };
155    let sustain_level = _sustain_level;
156
157    let attack_samples = (attack_s * sample_rate) as usize;
158    let decay_samples = (decay_s * sample_rate) as usize;
159    let release_samples = (release_s * sample_rate) as usize;
160
161    // optional pluck click
162    let pluck_click = engine
163        .extract_f32(note_params, "pluck_click")
164        .or(engine.extract_f32(synth_params, "pluck_click"))
165        .unwrap_or(0.0);
166    let pluck_click_ms = engine
167        .extract_f32(note_params, "pluck_click_ms")
168        .or(engine.extract_f32(synth_params, "pluck_click_ms"))
169        .unwrap_or(10.0);
170    let pluck_click_samples = ((pluck_click_ms / 1000.0) * sample_rate) as usize;
171
172    let drive = engine
173        .extract_f32(note_params, "drive")
174        .or(engine.extract_f32(synth_params, "drive"))
175        .unwrap_or(0.0);
176
177    // parse filter specs
178    let mut raw_filters: Vec<HashMap<String, Value>> = Vec::new();
179    if let Some(Value::Array(arr)) = synth_params.get("filters") {
180        for v in arr {
181            if let Value::Map(m) = v {
182                raw_filters.push(m.clone());
183            }
184        }
185    }
186    if let Some(Value::Array(arr)) = note_params.get("filters") {
187        for v in arr {
188            if let Value::Map(m) = v {
189                raw_filters.push(m.clone());
190            }
191        }
192    }
193
194    let mut filters: Vec<FilterSpec> = Vec::new();
195    let mut filter_states: Vec<FilterState> = Vec::new();
196    for rf in raw_filters.into_iter() {
197        let kind = rf
198            .get("type")
199            .and_then(|v| match v {
200                Value::String(s) => Some(s.clone()),
201                Value::Identifier(s) => Some(s.clone()),
202                _ => None,
203            })
204            .unwrap_or_else(|| "lowpass".to_string());
205        let cutoff = rf
206            .get("cutoff")
207            .and_then(|v| match v {
208                Value::Number(n) => Some(*n),
209                Value::String(s) => s.parse::<f32>().ok(),
210                _ => None,
211            })
212            .unwrap_or(1000.0);
213        filters.push(FilterSpec {
214            kind: kind.to_lowercase(),
215            cutoff,
216        });
217        filter_states.push(FilterState {
218            prev_l: 0.0,
219            prev_r: 0.0,
220            prev_in_l: 0.0,
221            prev_in_r: 0.0,
222            prev_out_l: 0.0,
223            prev_out_r: 0.0,
224        });
225    }
226
227    // LFO parsing (from synth or note) - simplified: prefer note params over synth params
228    let mut lfo_rate = 0.0f32;
229    let mut lfo_depth = 0.0f32;
230    let mut lfo_target: Option<String> = None;
231    if let Some(Value::Map(m)) = synth_params.get("lfo") {
232        if let Some(Value::Number(r)) = m.get("rate") {
233            lfo_rate = *r;
234        }
235        if let Some(Value::Number(d)) = m.get("depth") {
236            lfo_depth = *d;
237        }
238        if let Some(Value::String(t)) = m.get("target") {
239            lfo_target = Some(t.clone());
240        }
241    }
242    if let Some(Value::Map(m)) = note_params.get("lfo") {
243        if let Some(Value::Number(r)) = m.get("rate") {
244            lfo_rate = *r;
245        }
246        if let Some(Value::Number(d)) = m.get("depth") {
247            lfo_depth = *d;
248        }
249        if let Some(Value::String(t)) = m.get("target") {
250            lfo_target = Some(t.clone());
251        }
252    }
253
254    let voices = engine
255        .extract_f32(note_params, "voices")
256        .or(engine.extract_f32(synth_params, "voices"))
257        .unwrap_or(1.0)
258        .max(1.0)
259        .round() as usize;
260    let unison_detune = engine
261        .extract_f32(note_params, "unison_detune")
262        .or(engine.extract_f32(synth_params, "unison_detune"))
263        .unwrap_or(0.0);
264
265    let (volume_env, pan_env, pitch_env) = (
266        helpers::env_map_to_hash(&_volume_env),
267        helpers::env_map_to_hash(&_pan_env),
268        helpers::env_map_to_hash(&_pitch_env),
269    );
270
271    NoteSetup {
272        sample_rate,
273        channels,
274        total_samples,
275        start_sample,
276        attack_samples,
277        decay_samples,
278        release_samples,
279        sustain_level,
280        pluck_click,
281        pluck_click_samples,
282        drive,
283        filters,
284        filter_states,
285        lfo_rate,
286        lfo_depth,
287        lfo_target,
288        voices,
289        unison_detune,
290        volume_env,
291        pan_env,
292        pitch_env,
293    }
294}