devalang_core/core/audio/engine/notes/
params.rs1use 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 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 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 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 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 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}