@devaloop/devalang 0.0.1-alpha.12 → 0.0.1-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.devalang +8 -9
  2. package/Cargo.toml +8 -3
  3. package/README.md +36 -34
  4. package/docs/CHANGELOG.md +65 -1
  5. package/docs/CONTRIBUTING.md +1 -0
  6. package/docs/ROADMAP.md +2 -2
  7. package/docs/TODO.md +6 -5
  8. package/examples/bank.deva +2 -4
  9. package/examples/function.deva +15 -0
  10. package/examples/index.deva +25 -14
  11. package/out-tsc/bin/devalang.exe +0 -0
  12. package/package.json +6 -6
  13. package/project-version.json +3 -3
  14. package/rust/cli/bank.rs +2 -1
  15. package/rust/cli/build.rs +76 -14
  16. package/rust/cli/check.rs +71 -8
  17. package/rust/cli/driver.rs +40 -28
  18. package/rust/cli/install.rs +22 -7
  19. package/rust/cli/login.rs +134 -0
  20. package/rust/cli/mod.rs +2 -1
  21. package/rust/cli/play.rs +45 -20
  22. package/rust/common/api.rs +8 -0
  23. package/rust/common/cdn.rs +2 -5
  24. package/rust/common/mod.rs +3 -1
  25. package/rust/common/sso.rs +8 -0
  26. package/rust/config/driver.rs +19 -1
  27. package/rust/config/loader.rs +56 -10
  28. package/rust/core/audio/engine.rs +254 -91
  29. package/rust/core/audio/interpreter/arrow_call.rs +34 -15
  30. package/rust/core/audio/interpreter/call.rs +72 -47
  31. package/rust/core/audio/interpreter/condition.rs +14 -12
  32. package/rust/core/audio/interpreter/driver.rs +90 -128
  33. package/rust/core/audio/interpreter/function.rs +21 -0
  34. package/rust/core/audio/interpreter/load.rs +1 -1
  35. package/rust/core/audio/interpreter/loop_.rs +24 -18
  36. package/rust/core/audio/interpreter/mod.rs +2 -1
  37. package/rust/core/audio/interpreter/sleep.rs +0 -6
  38. package/rust/core/audio/interpreter/spawn.rs +78 -60
  39. package/rust/core/audio/interpreter/trigger.rs +157 -70
  40. package/rust/core/audio/loader/trigger.rs +37 -4
  41. package/rust/core/audio/player.rs +20 -10
  42. package/rust/core/audio/renderer.rs +24 -25
  43. package/rust/core/builder/mod.rs +11 -6
  44. package/rust/core/debugger/mod.rs +2 -0
  45. package/rust/core/debugger/module.rs +47 -0
  46. package/rust/core/debugger/store.rs +25 -11
  47. package/rust/core/error/mod.rs +6 -0
  48. package/rust/core/lexer/handler/driver.rs +23 -1
  49. package/rust/core/lexer/handler/identifier.rs +1 -0
  50. package/rust/core/lexer/handler/indent.rs +16 -2
  51. package/rust/core/lexer/handler/mod.rs +1 -0
  52. package/rust/core/lexer/handler/parenthesis.rs +41 -0
  53. package/rust/core/lexer/token.rs +4 -0
  54. package/rust/core/mod.rs +2 -1
  55. package/rust/core/parser/driver.rs +47 -4
  56. package/rust/core/parser/handler/arrow_call.rs +78 -18
  57. package/rust/core/parser/handler/bank.rs +35 -7
  58. package/rust/core/parser/handler/dot.rs +81 -123
  59. package/rust/core/parser/handler/identifier/call.rs +69 -22
  60. package/rust/core/parser/handler/identifier/function.rs +92 -0
  61. package/rust/core/parser/handler/identifier/let_.rs +13 -19
  62. package/rust/core/parser/handler/identifier/mod.rs +1 -0
  63. package/rust/core/parser/handler/identifier/spawn.rs +74 -27
  64. package/rust/core/parser/statement.rs +16 -4
  65. package/rust/core/plugin/loader.rs +48 -0
  66. package/rust/core/plugin/mod.rs +1 -0
  67. package/rust/core/preprocessor/loader.rs +50 -32
  68. package/rust/core/preprocessor/module.rs +3 -1
  69. package/rust/core/preprocessor/processor.rs +26 -1
  70. package/rust/core/preprocessor/resolver/call.rs +61 -84
  71. package/rust/core/preprocessor/resolver/condition.rs +11 -6
  72. package/rust/core/preprocessor/resolver/driver.rs +52 -6
  73. package/rust/core/preprocessor/resolver/function.rs +78 -0
  74. package/rust/core/preprocessor/resolver/group.rs +43 -13
  75. package/rust/core/preprocessor/resolver/let_.rs +7 -10
  76. package/rust/core/preprocessor/resolver/mod.rs +2 -1
  77. package/rust/core/preprocessor/resolver/spawn.rs +64 -30
  78. package/rust/core/preprocessor/resolver/trigger.rs +7 -3
  79. package/rust/core/preprocessor/resolver/value.rs +10 -1
  80. package/rust/core/shared/value.rs +4 -1
  81. package/rust/core/store/function.rs +34 -0
  82. package/rust/core/store/global.rs +9 -10
  83. package/rust/core/store/mod.rs +2 -1
  84. package/rust/core/store/variable.rs +6 -0
  85. package/rust/installer/addon.rs +80 -0
  86. package/rust/installer/bank.rs +24 -14
  87. package/rust/installer/mod.rs +4 -1
  88. package/rust/installer/plugin.rs +55 -0
  89. package/rust/lib.rs +10 -7
  90. package/rust/main.rs +32 -9
  91. package/rust/utils/logger.rs +16 -0
  92. package/rust/utils/mod.rs +45 -1
  93. package/rust/utils/spinner.rs +2 -4
@@ -1,6 +1,5 @@
1
1
  use std::{ fs, path::Path };
2
- use std::collections::HashMap;
3
- use crate::config::driver::{ BankEntry, Config };
2
+ use crate::config::driver::{ BankEntry, BankMetadata, Config };
4
3
 
5
4
  pub fn load_config(path: Option<&Path>) -> Option<Config> {
6
5
  let config_path = path.unwrap_or_else(|| Path::new(".devalang"));
@@ -56,6 +55,59 @@ pub fn remove_bank_from_config(config: &mut Config, dependency: &str) {
56
55
  }
57
56
  }
58
57
 
58
+ pub fn add_plugin_to_config(config: &mut Config, real_path: &Path, dependency: &str) {
59
+ if config.plugins.is_none() {
60
+ config.plugins = Some(Vec::new());
61
+ }
62
+
63
+ let plugins = config.plugins.as_mut().unwrap();
64
+
65
+ let exists = plugins.iter().any(|p| p.path == dependency);
66
+ if exists {
67
+ println!("Plugin '{}' already in config", dependency);
68
+ return;
69
+ }
70
+
71
+ let metadata_path = Path::new(real_path).join("plugin.toml");
72
+
73
+ if !metadata_path.exists() {
74
+ eprintln!("❌ Plugin metadata file '{}' does not exist", metadata_path.display());
75
+ return;
76
+ }
77
+
78
+ let metadata_content = std::fs
79
+ ::read_to_string(&metadata_path)
80
+ .expect("Failed to read plugin metadata file");
81
+
82
+ let metadata: std::collections::HashMap<String, String> = toml
83
+ ::from_str(&metadata_content)
84
+ .expect("Failed to parse plugin metadata file");
85
+
86
+ let plugin_entry = crate::config::driver::PluginEntry {
87
+ path: dependency.to_string(),
88
+ version: metadata
89
+ .get("version")
90
+ .cloned()
91
+ .unwrap_or_else(|| "0.0.1".to_string()),
92
+ author: metadata
93
+ .get("author")
94
+ .cloned()
95
+ .unwrap_or_else(|| "unknown".to_string()),
96
+ access: metadata
97
+ .get("access")
98
+ .cloned()
99
+ .unwrap_or_else(|| "public".to_string()),
100
+ };
101
+
102
+ plugins.push(plugin_entry);
103
+
104
+ if let Err(e) = config.write(config) {
105
+ eprintln!("❌ Failed to write config: {}", e);
106
+ } else {
107
+ println!("✅ Plugin '{}' added to config", dependency);
108
+ }
109
+ }
110
+
59
111
  pub fn add_bank_to_config(config: &mut Config, real_path: &Path, dependency: &str) {
60
112
  if config.banks.is_none() {
61
113
  config.banks = Some(Vec::new());
@@ -80,24 +132,18 @@ pub fn add_bank_to_config(config: &mut Config, real_path: &Path, dependency: &st
80
132
  ::read_to_string(&metadata_path)
81
133
  .expect("Failed to read bank metadata file");
82
134
 
83
- let metadata: HashMap<String, String> = toml
135
+ let metadata: BankMetadata = toml
84
136
  ::from_str(&metadata_content)
85
137
  .expect("Failed to parse bank metadata file");
86
138
 
87
139
  let bank_to_insert = BankEntry {
88
140
  path: dependency.to_string(),
89
141
  version: Some(
90
- metadata
142
+ metadata.bank
91
143
  .get("version")
92
144
  .cloned()
93
145
  .unwrap_or_else(|| "0.0.1".to_string())
94
146
  ),
95
- author: Some(
96
- metadata
97
- .get("author")
98
- .cloned()
99
- .unwrap_or_else(|| "unknown".to_string())
100
- ),
101
147
  };
102
148
 
103
149
  banks.push(bank_to_insert);
@@ -1,11 +1,10 @@
1
1
  use std::{ collections::HashMap, fs::File, io::BufReader, path::Path };
2
2
  use hound::{ SampleFormat, WavSpec, WavWriter };
3
3
  use rodio::{ Decoder, Source };
4
-
5
4
  use crate::core::{
6
5
  shared::value::Value,
7
6
  store::variable::VariableTable,
8
- utils::path::{ normalize_path, resolve_relative_path },
7
+ utils::path::normalize_path,
9
8
  };
10
9
 
11
10
  const SAMPLE_RATE: u32 = 44100;
@@ -14,7 +13,6 @@ const CHANNELS: u16 = 2;
14
13
  #[derive(Debug, Clone, PartialEq)]
15
14
  pub struct AudioEngine {
16
15
  pub volume: f32,
17
- pub variables: VariableTable,
18
16
  pub buffer: Vec<i16>,
19
17
  pub module_name: String,
20
18
  }
@@ -24,7 +22,6 @@ impl AudioEngine {
24
22
  AudioEngine {
25
23
  volume: 1.0,
26
24
  buffer: vec![],
27
- variables: VariableTable::new(),
28
25
  module_name,
29
26
  }
30
27
  }
@@ -57,12 +54,10 @@ impl AudioEngine {
57
54
 
58
55
  if self.buffer.iter().all(|&s| s == 0) {
59
56
  self.buffer = other.buffer;
60
- self.variables.variables.extend(other.variables.variables);
61
57
  return;
62
58
  }
63
59
 
64
60
  self.mix(&other);
65
- self.variables.variables.extend(other.variables.variables);
66
61
  }
67
62
 
68
63
  pub fn set_duration(&mut self, duration_secs: f32) {
@@ -73,10 +68,6 @@ impl AudioEngine {
73
68
  }
74
69
  }
75
70
 
76
- pub fn set_variables(&mut self, variables: VariableTable) {
77
- self.variables = variables;
78
- }
79
-
80
71
  pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
81
72
  if self.buffer.len() % (CHANNELS as usize) != 0 {
82
73
  self.buffer.push(0);
@@ -109,30 +100,141 @@ impl AudioEngine {
109
100
  freq: f32,
110
101
  amp: f32,
111
102
  start_time_ms: f32,
112
- duration_ms: f32
103
+ duration_ms: f32,
104
+ synth_params: HashMap<String, Value>,
105
+ note_params: HashMap<String, Value>
113
106
  ) {
107
+ let valid_synth_params = vec!["attack", "decay", "sustain", "release"];
108
+ let valid_note_params = vec![
109
+ "duration",
110
+ "velocity",
111
+ "glide",
112
+ "slide",
113
+ "amp",
114
+ "target_freq",
115
+ "target_amp",
116
+ "modulation",
117
+ "expression"
118
+ ];
119
+
120
+ // Synth params validation
121
+ for key in synth_params.keys() {
122
+ if !valid_synth_params.contains(&key.as_str()) {
123
+ eprintln!("⚠️ Unknown synth parameter: '{}'", key);
124
+ }
125
+ }
126
+
127
+ // Note params validation
128
+ for key in note_params.keys() {
129
+ if !valid_note_params.contains(&key.as_str()) {
130
+ eprintln!("⚠️ Unknown note parameter: '{}'", key);
131
+ }
132
+ }
133
+
134
+ // Synth parameters
135
+ let attack = self.extract_f32(&synth_params, "attack").unwrap_or(0.0);
136
+ let decay = self.extract_f32(&synth_params, "decay").unwrap_or(0.0);
137
+ let sustain = self.extract_f32(&synth_params, "sustain").unwrap_or(0.0);
138
+ let release = self.extract_f32(&synth_params, "release").unwrap_or(0.0);
139
+ let attack_s = if attack > 10.0 { attack / 1000.0 } else { attack };
140
+ let decay_s = if decay > 10.0 { decay / 1000.0 } else { decay };
141
+ let release_s = if release > 10.0 { release / 1000.0 } else { release };
142
+ let sustain_level = if sustain > 1.0 { (sustain / 100.0).clamp(0.0, 1.0) } else { sustain.clamp(0.0, 1.0) };
143
+
144
+ // Note parameters
145
+ let duration_ms = self.extract_f32(&note_params, "duration").unwrap_or(duration_ms);
146
+ let velocity = self.extract_f32(&note_params, "velocity").unwrap_or(1.0);
147
+ let glide = self.extract_boolean(&note_params, "glide").unwrap_or(false);
148
+ let slide = self.extract_boolean(&note_params, "slide").unwrap_or(false);
149
+
150
+ let _amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0) * velocity.clamp(0.0, 1.0);
151
+
152
+ // Logic for glide and slide
153
+ let freq_start = freq;
154
+ let mut freq_end = freq;
155
+ let amp_start = amp * velocity.clamp(0.0, 1.0);
156
+ let mut amp_end = amp_start;
157
+
158
+ if glide {
159
+ if let Some(Value::Number(target_freq)) = note_params.get("target_freq") {
160
+ freq_end = *target_freq;
161
+ } else {
162
+ freq_end = freq * 1.5; // Par défaut, glide vers une quinte
163
+ }
164
+ }
165
+
166
+ if slide {
167
+ if let Some(Value::Number(target_amp)) = note_params.get("target_amp") {
168
+ amp_end = *target_amp * velocity.clamp(0.0, 1.0);
169
+ } else {
170
+ amp_end = amp_start * 0.5; // Par défaut, slide vers la moitié
171
+ }
172
+ }
173
+
114
174
  let sample_rate = SAMPLE_RATE as f32;
115
175
  let channels = CHANNELS as usize;
116
176
 
117
177
  let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
118
178
  let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
119
- let amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0);
120
179
 
121
180
  let mut samples = Vec::with_capacity(total_samples);
122
- let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
181
+ let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
182
+
183
+ let attack_samples = (attack_s * sample_rate) as usize;
184
+ let decay_samples = (decay_s * sample_rate) as usize;
185
+ let release_samples = (release_s * sample_rate) as usize;
186
+ let sustain_samples = if total_samples > attack_samples + decay_samples + release_samples {
187
+ total_samples - attack_samples - decay_samples - release_samples
188
+ } else {
189
+ 0
190
+ };
123
191
 
124
192
  for i in 0..total_samples {
125
193
  let t = ((start_sample + i) as f32) / sample_rate;
126
- let phase = 2.0 * std::f32::consts::PI * freq * t;
194
+
195
+ // Glide
196
+ let current_freq = if glide {
197
+ freq_start + ((freq_end - freq_start) * (i as f32)) / (total_samples as f32)
198
+ } else {
199
+ freq
200
+ };
201
+
202
+ // Slide
203
+ let current_amp = if slide {
204
+ amp_start + ((amp_end - amp_start) * (i as f32)) / (total_samples as f32)
205
+ } else {
206
+ amp_start
207
+ };
208
+
209
+ let phase = 2.0 * std::f32::consts::PI * current_freq * t;
127
210
 
128
211
  let mut value = match waveform.as_str() {
129
212
  "sine" => phase.sin(),
130
- "square" => if phase.sin() >= 0.0 { 1.0 } else { -1.0 }
131
- "saw" => 2.0 * (freq * t - (freq * t + 0.5).floor()),
132
- "triangle" => (2.0 * (2.0 * (freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
213
+ "square" => if phase.sin() >= 0.0 { 1.0 } else { -1.0 },
214
+ "saw" => 2.0 * (current_freq * t - (current_freq * t + 0.5).floor()),
215
+ "triangle" => (2.0 * (2.0 * (current_freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
133
216
  _ => 0.0,
134
217
  };
135
218
 
219
+ // ADSR envelope
220
+ let envelope = if i < attack_samples {
221
+ (i as f32) / (attack_samples as f32)
222
+ } else if i < attack_samples + decay_samples {
223
+ 1.0 -
224
+ (1.0 - sustain_level) * (((i - attack_samples) as f32) / (decay_samples as f32))
225
+ } else if i < attack_samples + decay_samples + sustain_samples {
226
+ sustain_level
227
+ } else {
228
+ if release_samples > 0 {
229
+ sustain_level *
230
+ (1.0 -
231
+ ((i - attack_samples - decay_samples - sustain_samples) as f32) /
232
+ (release_samples as f32))
233
+ } else {
234
+ 0.0
235
+ }
236
+ };
237
+
136
238
  // Fade in/out
137
239
  if i < fade_len {
138
240
  value *= (i as f32) / (fade_len as f32);
@@ -140,7 +242,9 @@ impl AudioEngine {
140
242
  value *= ((total_samples - i) as f32) / (fade_len as f32);
141
243
  }
142
244
 
143
- samples.push((value * amplitude) as i16);
245
+ value *= envelope;
246
+ // Application de l'amplitude dynamique (slide + velocity)
247
+ samples.push((value * (i16::MAX as f32) * current_amp) as i16);
144
248
  }
145
249
 
146
250
  // Convert to stereo
@@ -157,8 +261,9 @@ impl AudioEngine {
157
261
  }
158
262
 
159
263
  for (i, sample) in stereo_samples.iter().enumerate() {
160
- if *sample != 0 {
161
- }
264
+ // Debug: note si on rencontre des samples non nuls
265
+ // (pour traquer les buffers silencieux)
266
+ // if i == 0 { eprintln!("[debug] first stereo sample: {}", sample); }
162
267
  self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
163
268
  }
164
269
  }
@@ -168,79 +273,95 @@ impl AudioEngine {
168
273
  filepath: &str,
169
274
  time_secs: f32,
170
275
  dur_sec: f32,
171
- effects: Option<HashMap<String, Value>>
276
+ effects: Option<HashMap<String, Value>>,
277
+ variable_table: &VariableTable
172
278
  ) {
173
279
  if filepath.is_empty() {
174
280
  eprintln!("❌ Empty file path provided for audio sample.");
175
281
  return;
176
282
  }
177
283
 
178
- let mut resolved_path = String::new();
179
-
180
- if filepath.starts_with("devalang://") {
181
- let root = Path::new(env!("CARGO_MANIFEST_DIR"));
182
- let parts = filepath.split("devalang://").collect::<Vec<&str>>();
183
- let object_parts = parts.get(1).unwrap_or(&"").split("/").collect::<Vec<&str>>();
184
- let object_type = object_parts.get(0).unwrap_or(&"").to_lowercase();
185
- let object_dir = object_parts.get(1).unwrap_or(&"").to_string();
186
- let object_name = object_parts.get(2).unwrap_or(&"").to_string();
187
-
188
- if object_type.contains("bank") {
189
- resolved_path = root
190
- .join(".deva")
191
- .join("bank")
192
- .join(object_dir)
193
- .join(format!("{}.wav", object_name))
194
- .to_str()
195
- .unwrap_or("")
196
- .to_string();
197
- } else {
198
- eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
284
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
285
+ let module_root = Path::new(&self.module_name);
286
+ let resolved_path: String;
287
+
288
+ // Get the variable path from the variable table
289
+ let mut var_path = filepath.to_string();
290
+ if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
291
+ var_path = variable_path.clone();
292
+ } else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
293
+ var_path = sample_path.clone();
294
+ }
295
+
296
+ // Handle devalang:// protocol
297
+ if var_path.starts_with("devalang://") {
298
+ let path_after_protocol = var_path.replace("devalang://", "");
299
+ let parts: Vec<&str> = path_after_protocol.split('/').collect();
300
+
301
+ if parts.len() < 3 {
302
+ eprintln!(
303
+ "❌ Invalid devalang:// path format. Expected devalang://<type>/<author>.<bank>/<entity>"
304
+ );
199
305
  return;
200
306
  }
307
+
308
+ let obj_type = parts[0];
309
+ let bank_name = parts[1];
310
+ let entity_name = parts[2];
311
+
312
+ resolved_path = root
313
+ .join(".deva")
314
+ .join(obj_type)
315
+ .join(bank_name)
316
+ .join(format!("{}.wav", entity_name))
317
+ .to_string_lossy()
318
+ .to_string();
201
319
  } else {
202
- let module_path = &self.module_name;
203
- let root = Path::new(module_path).parent();
320
+ // Else, resolve as a relative path
321
+ let entry_dir = module_root.parent().unwrap_or(root);
322
+ let absolute_path = root.join(entry_dir).join(&var_path);
204
323
 
205
- if let Some(root_path) = root {
206
- resolved_path = root_path.join(filepath).to_str().unwrap_or("").to_string();
207
- } else {
208
- eprintln!("❌ Could not resolve root path for module: {}", module_path);
209
- return;
210
- }
324
+ resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
211
325
  }
212
326
 
327
+ // Verify if the file exists
213
328
  if !Path::new(&resolved_path).exists() {
214
329
  eprintln!("❌ Audio file not found at: {}", resolved_path);
215
330
  return;
216
331
  }
217
332
 
218
- let file = BufReader::new(
219
- File::open(&resolved_path).expect(&format!("Failed to open audio file {}", filepath))
220
- );
221
- let decoder = Decoder::new(file).expect("Failed to decode audio file");
333
+ let file = match File::open(&resolved_path) {
334
+ Ok(f) => BufReader::new(f),
335
+ Err(e) => {
336
+ eprintln!("Failed to open audio file {}: {}", resolved_path, e);
337
+ return;
338
+ }
339
+ };
340
+
341
+ let decoder = match Decoder::new(file) {
342
+ Ok(d) => d,
343
+ Err(e) => {
344
+ eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
345
+ return;
346
+ }
347
+ };
222
348
 
223
- // Mono or stereo reading possible here, we will duplicate in L/R
224
349
  let max_mono_samples = (dur_sec * (SAMPLE_RATE as f32)) as usize;
225
350
  let samples: Vec<i16> = decoder.convert_samples().take(max_mono_samples).collect();
226
351
 
227
352
  if samples.is_empty() {
228
- eprintln!("No samples found in the audio file: {}", filepath);
353
+ eprintln!("No samples read from {}", resolved_path);
229
354
  return;
230
355
  }
231
356
 
232
- // Pad the buffer to ensure it can accommodate the new samples
357
+ // Calculate buffer offset and size
233
358
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
234
359
  let required_len = offset + samples.len() * (CHANNELS as usize);
235
- let padded_required_len = if required_len % 2 == 1 {
236
- required_len + 1
237
- } else {
238
- required_len
239
- };
240
-
241
- self.buffer.resize(padded_required_len, 0);
360
+ if self.buffer.len() < required_len {
361
+ self.buffer.resize(required_len, 0);
362
+ }
242
363
 
243
- // Apply effects
364
+ // Apply effects and mix
244
365
  if let Some(effects_map) = effects {
245
366
  self.pad_samples(&samples, time_secs, Some(effects_map));
246
367
  } else {
@@ -263,8 +384,10 @@ impl AudioEngine {
263
384
  let mut fade_in = 0.0;
264
385
  let mut fade_out = 0.0;
265
386
  let mut pitch = 1.0;
266
- let mut drive = 0.0;
387
+ let mut drive = 0.0;
267
388
  let mut reverb = 0.0;
389
+ let mut delay = 0.0; // delay time in seconds
390
+ let delay_feedback = 0.35; // default feedback
268
391
 
269
392
  if let Some(map) = &effects_map {
270
393
  for (key, val) in map {
@@ -285,12 +408,14 @@ impl AudioEngine {
285
408
  pitch = *v;
286
409
  }
287
410
  ("drive", Value::Number(v)) => {
288
- // Drive effect can be implemented here if needed
289
411
  drive = *v;
290
412
  }
291
413
  ("reverb", Value::Number(v)) => {
292
414
  reverb = *v;
293
415
  }
416
+ ("delay", Value::Number(v)) => {
417
+ delay = *v;
418
+ }
294
419
  _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
295
420
  }
296
421
  }
@@ -299,49 +424,64 @@ impl AudioEngine {
299
424
  let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
300
425
  let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
301
426
 
302
- for (i, &sample) in samples.iter().enumerate() {
303
- // Gain
304
- let mut adjusted = (sample as f32) * gain;
427
+ let delay_samples = if delay > 0.0 { (delay * (SAMPLE_RATE as f32)) as usize } else { 0 };
428
+ let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
429
+
430
+ for i in 0..total_samples {
431
+ // PITCH FIRST
432
+ let pitch_index = if pitch != 1.0 { ((i as f32) / pitch) as usize } else { i };
433
+
434
+ let mut adjusted = if pitch_index < total_samples {
435
+ samples[pitch_index] as f32
436
+ } else {
437
+ 0.0
438
+ };
305
439
 
306
- // Fade in
440
+ // GAIN
441
+ adjusted *= gain;
442
+
443
+ // FADE IN/OUT
307
444
  if fade_in_samples > 0 && i < fade_in_samples {
308
445
  adjusted *= (i as f32) / (fade_in_samples as f32);
309
446
  }
310
-
311
- // Fade out
312
447
  if fade_out_samples > 0 && i >= total_samples.saturating_sub(fade_out_samples) {
313
448
  adjusted *= ((total_samples - i) as f32) / (fade_out_samples as f32);
314
449
  }
315
450
 
316
- // Pitch adjustment
317
- if pitch != 1.0 {
318
- let pitch_adjusted_index = ((i as f32) / pitch) as usize;
319
- if pitch_adjusted_index < total_samples {
320
- adjusted = (samples[pitch_adjusted_index] as f32) * gain;
321
- } else {
322
- adjusted = 0.0; // Out of bounds, set to zero
323
- }
451
+ // DRIVE (soft)
452
+ if drive > 0.0 {
453
+ let normalized = adjusted / (i16::MAX as f32);
454
+ let pre_gain = (10f32).powf(drive / 20.0); // dB mapping
455
+ let driven = (normalized * pre_gain).tanh();
456
+ adjusted = driven * (i16::MAX as f32);
324
457
  }
325
458
 
326
- // Drive effect
327
- if drive > 0.0 {
328
- adjusted = adjusted.tanh() * (1.0 + drive);
459
+ // DELAY
460
+ if delay_samples > 0 && i >= delay_samples {
461
+ let echo = delay_buffer[i - delay_samples] * delay_feedback;
462
+ adjusted += echo;
463
+ }
464
+ if delay_samples > 0 {
465
+ delay_buffer[i] = adjusted;
329
466
  }
330
467
 
331
- // Reverb effect
468
+ // REVERB
332
469
  if reverb > 0.0 {
333
- let reverb_delay = (reverb * (SAMPLE_RATE as f32)) as usize;
470
+ let reverb_delay = (0.03 * (SAMPLE_RATE as f32)) as usize;
334
471
  if i >= reverb_delay {
335
- adjusted += self.buffer[offset + i - reverb_delay] as f32 * 0.5; // Simple feedback
472
+ adjusted += (self.buffer[offset + i - reverb_delay] as f32) * reverb;
336
473
  }
337
474
  }
338
475
 
339
- // Clamp
476
+ // CLAMP
340
477
  let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
341
478
 
342
- // Pan (L/R split)
343
- let left = ((adjusted_sample as f32) * (1.0 - pan.clamp(0.0, 1.0))) as i16;
344
- let right = ((adjusted_sample as f32) * (1.0 + pan.clamp(-1.0, 0.0)).abs()) as i16;
479
+ // PAN
480
+ let left_gain = 1.0 - pan.max(0.0); // Pan > 0 => reduce left
481
+ let right_gain = 1.0 + pan.min(0.0); // Pan < 0 => reduce right
482
+
483
+ let left = ((adjusted_sample as f32) * left_gain) as i16;
484
+ let right = ((adjusted_sample as f32) * right_gain) as i16;
345
485
 
346
486
  let left_pos = offset + i * 2;
347
487
  let right_pos = left_pos + 1;
@@ -352,4 +492,27 @@ impl AudioEngine {
352
492
  }
353
493
  }
354
494
  }
495
+
496
+ fn extract_f32(&self, map: &HashMap<String, Value>, key: &str) -> Option<f32> {
497
+ match map.get(key) {
498
+ Some(Value::Number(n)) => Some(*n),
499
+ Some(Value::String(s)) => s.parse::<f32>().ok(),
500
+ Some(Value::Boolean(b)) => Some(if *b { 1.0 } else { 0.0 }),
501
+ _ => None,
502
+ }
503
+ }
504
+
505
+ fn extract_boolean(&self, map: &HashMap<String, Value>, key: &str) -> Option<bool> {
506
+ match map.get(key) {
507
+ Some(Value::Boolean(b)) => Some(*b),
508
+ Some(Value::Number(n)) => Some(*n != 0.0),
509
+ Some(Value::Identifier(s)) => {
510
+ if s == "true" { Some(true) } else if s == "false" { Some(false) } else { None }
511
+ }
512
+ Some(Value::String(s)) => {
513
+ if s == "true" { Some(true) } else if s == "false" { Some(false) } else { None }
514
+ }
515
+ _ => None,
516
+ }
517
+ }
355
518
  }