@devaloop/devalang 0.0.1-alpha.11 → 0.0.1-alpha.13

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 (66) hide show
  1. package/.devalang +8 -8
  2. package/Cargo.toml +8 -8
  3. package/README.md +1 -14
  4. package/docs/CHANGELOG.md +44 -0
  5. package/docs/TODO.md +1 -1
  6. package/examples/index.deva +10 -11
  7. package/out-tsc/bin/devalang.exe +0 -0
  8. package/package.json +2 -1
  9. package/project-version.json +3 -3
  10. package/rust/cli/build.rs +25 -2
  11. package/rust/cli/check.rs +26 -3
  12. package/rust/cli/play.rs +1 -1
  13. package/rust/core/audio/engine.rs +207 -41
  14. package/rust/core/audio/interpreter/call.rs +72 -47
  15. package/rust/core/audio/interpreter/condition.rs +14 -12
  16. package/rust/core/audio/interpreter/driver.rs +84 -127
  17. package/rust/core/audio/interpreter/function.rs +21 -0
  18. package/rust/core/audio/interpreter/load.rs +1 -1
  19. package/rust/core/audio/interpreter/loop_.rs +24 -18
  20. package/rust/core/audio/interpreter/mod.rs +2 -1
  21. package/rust/core/audio/interpreter/sleep.rs +0 -6
  22. package/rust/core/audio/interpreter/spawn.rs +78 -60
  23. package/rust/core/audio/interpreter/trigger.rs +169 -61
  24. package/rust/core/audio/loader/trigger.rs +37 -4
  25. package/rust/core/audio/player.rs +20 -10
  26. package/rust/core/audio/renderer.rs +24 -25
  27. package/rust/core/debugger/mod.rs +2 -0
  28. package/rust/core/debugger/module.rs +47 -0
  29. package/rust/core/debugger/store.rs +25 -11
  30. package/rust/core/error/mod.rs +6 -0
  31. package/rust/core/lexer/handler/driver.rs +23 -1
  32. package/rust/core/lexer/handler/identifier.rs +1 -0
  33. package/rust/core/lexer/handler/mod.rs +1 -0
  34. package/rust/core/lexer/handler/parenthesis.rs +41 -0
  35. package/rust/core/lexer/token.rs +3 -0
  36. package/rust/core/parser/driver.rs +31 -3
  37. package/rust/core/parser/handler/dot.rs +65 -129
  38. package/rust/core/parser/handler/identifier/call.rs +69 -22
  39. package/rust/core/parser/handler/identifier/function.rs +92 -0
  40. package/rust/core/parser/handler/identifier/let_.rs +13 -19
  41. package/rust/core/parser/handler/identifier/mod.rs +1 -0
  42. package/rust/core/parser/handler/identifier/spawn.rs +74 -27
  43. package/rust/core/parser/statement.rs +16 -4
  44. package/rust/core/preprocessor/loader.rs +45 -29
  45. package/rust/core/preprocessor/module.rs +3 -1
  46. package/rust/core/preprocessor/processor.rs +26 -1
  47. package/rust/core/preprocessor/resolver/call.rs +61 -84
  48. package/rust/core/preprocessor/resolver/condition.rs +11 -6
  49. package/rust/core/preprocessor/resolver/driver.rs +52 -6
  50. package/rust/core/preprocessor/resolver/function.rs +78 -0
  51. package/rust/core/preprocessor/resolver/group.rs +43 -13
  52. package/rust/core/preprocessor/resolver/let_.rs +7 -10
  53. package/rust/core/preprocessor/resolver/mod.rs +2 -1
  54. package/rust/core/preprocessor/resolver/spawn.rs +64 -30
  55. package/rust/core/preprocessor/resolver/trigger.rs +7 -3
  56. package/rust/core/preprocessor/resolver/value.rs +10 -1
  57. package/rust/core/shared/value.rs +4 -1
  58. package/rust/core/store/function.rs +34 -0
  59. package/rust/core/store/global.rs +9 -10
  60. package/rust/core/store/mod.rs +2 -1
  61. package/rust/core/store/variable.rs +6 -0
  62. package/rust/installer/bank.rs +1 -1
  63. package/rust/installer/mod.rs +2 -1
  64. package/rust/lib.rs +10 -8
  65. package/rust/utils/mod.rs +44 -1
  66. /package/rust/{utils/installer.rs → installer/utils.rs} +0 -0
package/.devalang CHANGED
@@ -1,9 +1,9 @@
1
- [defaults]
2
- entry = "./examples"
3
- output = "./output"
4
- watch = false
5
-
6
- [[banks]]
7
- path = "devalang://bank/808"
8
- version = "0.0.1"
1
+ [defaults]
2
+ entry = "./examples"
3
+ output = "./output"
4
+ watch = false
5
+
6
+ [[banks]]
7
+ path = "devalang://bank/808"
8
+ version = "0.0.1"
9
9
  author = "devaloop"
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "devalang"
3
- version = "0.0.1-alpha.11"
3
+ version = "0.0.1-alpha.13"
4
4
  authors = ["Devaloop <contact@devaloop.com>"]
5
5
  description = "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text."
6
6
  license = "MIT"
@@ -20,14 +20,14 @@ required-features = ["cli"]
20
20
 
21
21
  [lib]
22
22
  path = "rust/lib.rs"
23
- crate-type = ["cdylib"]
23
+ crate-type = ["cdylib", "rlib"]
24
24
 
25
25
  [profile.release]
26
26
  opt-level = "s"
27
27
 
28
28
  [features]
29
29
  default = ["cli"]
30
- cli = ["crossterm", "indicatif", "inquire"]
30
+ cli = ["crossterm", "indicatif", "inquire", "zip", "reqwest", "flate2", "tokio"]
31
31
 
32
32
  [dependencies]
33
33
  clap = { version = "4.5", features = ["derive"] }
@@ -47,8 +47,8 @@ crossterm = { version = "0.27", optional = true }
47
47
  indicatif = { version = "0.17", optional = true }
48
48
  inquire = { version = "0.7.5", optional = true }
49
49
  js-sys = "0.3"
50
- reqwest = { version = "0.12.22", features = ["json"] }
51
- flate2 = "1.0"
52
- tar = "0.4"
53
- tokio = { version = "1", features = ["full"] }
54
- zip = "4.3.0"
50
+ reqwest = { version = "0.12.22", optional = true, features = ["json"] }
51
+ flate2 = { version = "1.0", optional = true }
52
+ tokio = { version = "1", features = ["full"], optional = true }
53
+ zip = { version = "4.3.0", optional = true }
54
+ rayon = "1.10.0"
package/README.md CHANGED
@@ -27,7 +27,7 @@ Compose loops, control samples, render and play audio — all in clean, readable
27
27
 
28
28
  From studio sketches to live sets, Devalang gives you rhythmic control — with the elegance of code.
29
29
 
30
- > 🚧 **v0.0.1-alpha.11 Notice** 🚧
30
+ > 🚧 **v0.0.1-alpha.13 Notice** 🚧
31
31
  >
32
32
  > NEW: Devalang is available in your browser at [playground.devalang.com](https://playground.devalang.com) !
33
33
  >
@@ -119,22 +119,9 @@ devalang play --repeat
119
119
 
120
120
  ### Please refer to the [online documentation](https://docs.devalang.com) for detailed information on syntax, features, and usage examples.
121
121
 
122
- ## 📜 Changelog Highlights
123
-
124
- For a complete list of changes, see [docs/CHANGELOG.md](./docs/CHANGELOG.md)
125
-
126
- - Implemented beat durations in `triggers` and `arrow_calls` statements
127
- - Implemented `bank` resolver to resolve banks of sounds in the code
128
- - Support for namespaced banks of sounds (e.g. `.808.myTrigger`)
129
- - Implemented multiple commands for `bank` management
130
- - `bank list`, `bank available`, `bank info <bank_name>`, `bank remove <bank_name>`, `bank update`, `bank update <bank_name>`
131
- - Implemented `install` command to install banks of sounds
132
- - `install bank <bank_name>`
133
-
134
122
  ## 🧯 Known issues
135
123
 
136
124
  - No smart modules yet, all groups, variables, and samples must be explicitly imported where used
137
- - No support yet for `pattern`, `function`, ... statements
138
125
  - No support yet for cross-platform builds (Linux, macOS)
139
126
 
140
127
  ## 🧪 Roadmap Highlights
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@
4
4
 
5
5
  # Changelog
6
6
 
7
+ ## Version 0.0.1-alpha.13 (2025-07-26)
8
+
9
+ ### 🧩 Language Features
10
+
11
+ - Added support for `fn` directive to define functions in Devalang.
12
+ - Example: `fn myFunction(param1, param2):`
13
+
14
+ ### 🧠 Core Engine
15
+
16
+ - Patched `trigger`, `call`, and `spawn`, `renderer` to handle correct cursor time in the audio interpreter.
17
+ - Refactored audio engine and interpreter to handle correct timing and execution while using `loop`, `call`, and `spawn` statements.
18
+ - Refactored `trigger` effects to apply more effects to triggers.
19
+ - Example: `.myTrigger auto { reverb: 0.25, pitch: 0.75, gain: 0.8 }`
20
+ - Refactored `preprocessor` to handle correct namespaced banks of sounds and triggers.
21
+ - Refactored `collect_errors_recursively` to provide detailed error reporting across nested statements.
22
+ - Optimized the `renderer` to handle silent buffers and improve performance.
23
+
24
+ ### 🛠️ Utilities
25
+
26
+ - Added the `extract_loop_body_statements` utility for better loop handling.
27
+ - Improved logging for module variables and functions.
28
+
29
+ ### 🧩 Web Assembly
30
+
31
+ - Patched `lib.rs` dependencies to ensure compatibility with the latest Rust and WASM standards.
32
+
33
+ ## Version 0.0.1-alpha.12 (2025-07-21)
34
+
35
+ ### 🧩 Language Features
36
+
37
+ - Implemented `trigger` effects to apply effects to triggers, allowing for more dynamic sound manipulation.
38
+ - Example: `.myTrigger auto { reverb: 1.0, pitch: 1.5, gain: 0.8 }`
39
+
40
+ ### 🧠 Core Engine
41
+
42
+ - Moved `utils::installer` to `installer::utils` to better organize the project structure.
43
+ - Set CLI dependencies as optional in `Cargo.toml` to allow for a cleaner build without CLI features.
44
+ - Patched `@load` relative path resolution to ensure correct loading of external resources.
45
+ - Patched `trigger` statement that was not correctly parsed when using namespaced banks of sounds.
46
+
47
+ ### 🧩 Web Assembly
48
+
49
+ - Patched `lib.rs` dependencies to ensure compatibility with the latest Rust and WASM standards.
50
+
7
51
  ## Version 0.0.1-alpha.11 (2025-07-20)
8
52
 
9
53
  ### 📖 Documentation
package/docs/TODO.md CHANGED
@@ -44,7 +44,7 @@ This is a list of tasks and features to be implemented in Devalang. Note that th
44
44
  - [x] @export
45
45
  - [x] @load
46
46
  - [ ] @include
47
- - [ ] function
47
+ - [x] function
48
48
  - [ ] pattern
49
49
  - [x] group
50
50
  - [x] call
@@ -1,16 +1,15 @@
1
1
  # This file demonstrates the use of main features in Devalang.
2
+ bpm 135
2
3
 
3
- @import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
4
- @import { myLead } from "./synth.deva"
5
- @import { myLoop } from "./loop.deva"
4
+ bank 808
6
5
 
7
- @load "./samples/kick-808.wav" as kickCustom
8
- @load "./samples/hat-808.wav" as hatCustom
6
+ let entityTest1 = .808.kick auto
7
+ let entityTest2 = .808.clap auto
8
+ let entityTest3 = .808.snare auto
9
9
 
10
- bpm tempo
10
+ fn myFirstGroup(entity1, entity2, entity3):
11
+ .entity1
12
+ .entity2
13
+ .entity3
11
14
 
12
- group myTrack:
13
- spawn myLoop
14
- spawn myLead
15
-
16
- call myTrack
15
+ call myFirstGroup(entityTest1, entityTest2, entityTest3)
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devaloop/devalang",
3
3
  "private": false,
4
- "version": "0.0.1-alpha.11",
4
+ "version": "0.0.1-alpha.13",
5
5
  "description": "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text.",
6
6
  "main": "out-tsc/index.js",
7
7
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "prepublish": "cargo build --release && npm run script:postbuild",
12
12
  "rust:dev:build": "cargo run build --entry examples --output output",
13
13
  "rust:dev:check": "cargo run check --entry examples --output output",
14
+ "rust:dev:play": "cargo run play --entry examples --output output --repeat",
14
15
  "rust:wasm:web": "wasm-pack build --target=web --no-default-features",
15
16
  "rust:wasm:node": "wasm-pack build --target=nodejs --no-default-features",
16
17
  "script:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.0.1-alpha.11",
2
+ "version": "0.0.1-alpha.13",
3
3
  "channel": "alpha",
4
- "lastCommit": "f643b05f7c5d97c5d22cb255c3641cfc9e385ea2",
5
- "build": 10
4
+ "lastCommit": "f25a91c6efe3dca91cc8fa3bfa829d3fc2271855",
5
+ "build": 12
6
6
  }
package/rust/cli/build.rs CHANGED
@@ -4,8 +4,9 @@ use crate::{
4
4
  builder::Builder,
5
5
  debugger::{
6
6
  lexer::write_lexer_log_file,
7
+ module::{ write_module_function_log_file, write_module_variable_log_file },
7
8
  preprocessor::write_preprocessor_log_file,
8
- store::write_store_log_file,
9
+ store::{ write_function_log_file, write_variables_log_file },
9
10
  },
10
11
  preprocessor::loader::ModuleLoader,
11
12
  store::global::GlobalStore,
@@ -111,13 +112,35 @@ fn begin_build(entry: String, output: String) {
111
112
  let (modules_tokens, modules_statements) = module_loader.load_all_modules(&mut global_store);
112
113
 
113
114
  // SECTION Write logs
115
+ for (module_path, module) in global_store.modules.clone() {
116
+ write_module_variable_log_file(
117
+ &normalized_output_dir,
118
+ &module_path,
119
+ &module.variable_table
120
+ );
121
+ write_module_function_log_file(
122
+ &normalized_output_dir,
123
+ &module_path,
124
+ &module.function_table
125
+ );
126
+ }
127
+
114
128
  write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
115
129
  write_preprocessor_log_file(
116
130
  &normalized_output_dir,
117
131
  "resolved_statements.log",
118
132
  modules_statements.clone()
119
133
  );
120
- write_store_log_file(&normalized_output_dir, "global_store.log", global_store.modules.clone());
134
+ write_variables_log_file(
135
+ &normalized_output_dir,
136
+ "global_variables.log",
137
+ global_store.variables.clone()
138
+ );
139
+ write_function_log_file(
140
+ &normalized_output_dir,
141
+ "global_functions.log",
142
+ global_store.functions.clone()
143
+ );
121
144
 
122
145
  // SECTION Building AST and Audio
123
146
  let builder = Builder::new();
package/rust/cli/check.rs CHANGED
@@ -5,7 +5,12 @@ use crate::{
5
5
  store::global::GlobalStore,
6
6
  utils::path::{ find_entry_file, normalize_path },
7
7
  },
8
- utils::{ logger::{ LogLevel, Logger }, spinner::with_spinner, watcher::watch_directory },
8
+ utils::{
9
+ collect_errors_recursively,
10
+ logger::{ LogLevel, Logger },
11
+ spinner::with_spinner,
12
+ watcher::watch_directory,
13
+ },
9
14
  };
10
15
  use std::{ thread, time::Duration };
11
16
 
@@ -104,7 +109,26 @@ fn begin_check(entry: String, output: String) {
104
109
  // NOTE: We don't use modules in the check command, but we still need to load them
105
110
  let modules = module_loader.load_all_modules(&mut global_store);
106
111
 
107
- // TODO: Implement debugging
112
+ // Debugging: Log loaded modules and errors
113
+ let logger = Logger::new();
114
+ logger.log_message(LogLevel::Info, "Loaded modules:");
115
+ for (module_name, _) in &modules.0 {
116
+ logger.log_message(LogLevel::Info, &format!("- {}", module_name));
117
+ }
118
+
119
+ let mut all_errors = Vec::new();
120
+ for (_, statements) in &modules.1 {
121
+ all_errors.extend(collect_errors_recursively(statements));
122
+ }
123
+
124
+ if !all_errors.is_empty() {
125
+ logger.log_message(LogLevel::Error, "Errors detected during check:");
126
+ for error in all_errors {
127
+ logger.log_message(LogLevel::Error, &format!("- {}", error.message));
128
+ }
129
+ } else {
130
+ logger.log_message(LogLevel::Success, "No errors detected.");
131
+ }
108
132
 
109
133
  let success_message = format!(
110
134
  "Check completed successfully in {:.2?}. Output files written to: '{}'",
@@ -112,6 +136,5 @@ fn begin_check(entry: String, output: String) {
112
136
  normalized_output_dir
113
137
  );
114
138
 
115
- let logger = Logger::new();
116
139
  logger.log_message(LogLevel::Success, &success_message);
117
140
  }
package/rust/cli/play.rs CHANGED
@@ -127,7 +127,7 @@ pub fn handle_play_command(
127
127
  } else {
128
128
  // Single execution
129
129
  begin_play(&config, &entry_file, &output_path);
130
-
130
+
131
131
  logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
132
132
 
133
133
  audio_player.play_file_once(&audio_file);
@@ -1,10 +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::{
5
+ shared::value::Value,
6
6
  store::variable::VariableTable,
7
- utils::path::{ normalize_path, resolve_relative_path },
7
+ utils::path::normalize_path,
8
8
  };
9
9
 
10
10
  const SAMPLE_RATE: u32 = 44100;
@@ -13,7 +13,6 @@ const CHANNELS: u16 = 2;
13
13
  #[derive(Debug, Clone, PartialEq)]
14
14
  pub struct AudioEngine {
15
15
  pub volume: f32,
16
- pub variables: VariableTable,
17
16
  pub buffer: Vec<i16>,
18
17
  pub module_name: String,
19
18
  }
@@ -23,7 +22,6 @@ impl AudioEngine {
23
22
  AudioEngine {
24
23
  volume: 1.0,
25
24
  buffer: vec![],
26
- variables: VariableTable::new(),
27
25
  module_name,
28
26
  }
29
27
  }
@@ -56,12 +54,10 @@ impl AudioEngine {
56
54
 
57
55
  if self.buffer.iter().all(|&s| s == 0) {
58
56
  self.buffer = other.buffer;
59
- self.variables.variables.extend(other.variables.variables);
60
57
  return;
61
58
  }
62
59
 
63
60
  self.mix(&other);
64
- self.variables.variables.extend(other.variables.variables);
65
61
  }
66
62
 
67
63
  pub fn set_duration(&mut self, duration_secs: f32) {
@@ -72,10 +68,6 @@ impl AudioEngine {
72
68
  }
73
69
  }
74
70
 
75
- pub fn set_variables(&mut self, variables: VariableTable) {
76
- self.variables = variables;
77
- }
78
-
79
71
  pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
80
72
  if self.buffer.len() % (CHANNELS as usize) != 0 {
81
73
  self.buffer.push(0);
@@ -167,75 +159,249 @@ impl AudioEngine {
167
159
  filepath: &str,
168
160
  time_secs: f32,
169
161
  dur_sec: f32,
170
- effects: Option<HashMap<String, f32>>
162
+ effects: Option<HashMap<String, Value>>,
163
+ variable_table: &VariableTable
171
164
  ) {
172
165
  if filepath.is_empty() {
173
166
  eprintln!("❌ Empty file path provided for audio sample.");
174
167
  return;
175
168
  }
176
169
 
170
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
171
+ let module_root = Path::new(&self.module_name);
177
172
  let mut resolved_path = String::new();
178
173
 
179
- if filepath.starts_with("devalang://") {
180
- let root = Path::new(env!("CARGO_MANIFEST_DIR"));
181
- let parts = filepath.split("devalang://").collect::<Vec<&str>>();
182
- let object_parts = parts.get(1).unwrap_or(&"").split("/").collect::<Vec<&str>>();
183
- let object_type = object_parts.get(0).unwrap_or(&"").to_lowercase();
184
- let object_dir = object_parts.get(1).unwrap_or(&"").to_string();
185
- let object_name = object_parts.get(2).unwrap_or(&"").to_string();
174
+ // Get the variable path from the variable table
175
+ let mut var_path = filepath.to_string();
176
+ if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
177
+ var_path = variable_path.clone();
178
+ } else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
179
+ var_path = sample_path.clone();
180
+ }
181
+
182
+ // If it's a namespace
183
+ if var_path.contains(".") {
184
+ let parts: Vec<&str> = var_path.trim_start_matches('.').split('.').collect();
185
+ if parts.len() == 2 {
186
+ let bank_name = parts[0];
187
+ let entity_name = parts[1];
188
+
189
+ // Verifies if the bank is declared
190
+ if !variable_table.variables.contains_key(bank_name) {
191
+ eprintln!(
192
+ "❌ Bank '{}' not declared. Please declare it first using : 'bank {}'",
193
+ bank_name,
194
+ bank_name
195
+ );
196
+ return;
197
+ }
186
198
 
187
- if object_type.contains("bank") {
188
199
  resolved_path = root
189
200
  .join(".deva")
190
201
  .join("bank")
191
- .join(object_dir)
192
- .join(format!("{}.wav", object_name))
193
- .to_str()
194
- .unwrap_or("")
202
+ .join(bank_name)
203
+ .join(format!("{}.wav", entity_name))
204
+ .to_string_lossy()
195
205
  .to_string();
196
206
  } else {
197
- eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
207
+ eprintln!("❌ Invalid namespace format: {}", var_path);
208
+ return;
209
+ }
210
+ } else if var_path.starts_with("devalang://") {
211
+ let path_after_protocol = var_path.replace("devalang://", "");
212
+ let parts: Vec<&str> = path_after_protocol.split('/').collect();
213
+
214
+ if parts.len() < 3 {
215
+ eprintln!(
216
+ "❌ Invalid devalang:// path format. Expected devalang://<type>/<bank>/<entity>"
217
+ );
198
218
  return;
199
219
  }
220
+
221
+ let obj_type = parts[0];
222
+ let bank_name = parts[1];
223
+ let entity_name = parts[2];
224
+
225
+ resolved_path = root
226
+ .join(".deva")
227
+ .join(obj_type)
228
+ .join(bank_name)
229
+ .join(format!("{}.wav", entity_name))
230
+ .to_string_lossy()
231
+ .to_string();
232
+ } else {
233
+ // Else, resolve as a relative path
234
+ let entry_dir = module_root.parent().unwrap_or(root);
235
+ let absolute_path = root.join(entry_dir).join(&var_path);
236
+
237
+ resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
238
+ }
239
+
240
+ // Verify if the file exists
241
+ if !Path::new(&resolved_path).exists() {
242
+ eprintln!("❌ Audio file not found at: {}", resolved_path);
243
+ return;
200
244
  }
201
245
 
202
- let file = BufReader::new(File::open(resolved_path).expect("Failed to open audio file"));
203
- let decoder = Decoder::new(file).expect("Failed to decode audio file");
246
+ let file = match File::open(&resolved_path) {
247
+ Ok(f) => BufReader::new(f),
248
+ Err(e) => {
249
+ eprintln!("❌ Failed to open audio file {}: {}", resolved_path, e);
250
+ return;
251
+ }
252
+ };
253
+
254
+ let decoder = match Decoder::new(file) {
255
+ Ok(d) => d,
256
+ Err(e) => {
257
+ eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
258
+ return;
259
+ }
260
+ };
204
261
 
205
- // Mono or stereo reading possible here, we will duplicate in L/R
206
262
  let max_mono_samples = (dur_sec * (SAMPLE_RATE as f32)) as usize;
207
263
  let samples: Vec<i16> = decoder.convert_samples().take(max_mono_samples).collect();
208
264
 
209
265
  if samples.is_empty() {
210
- eprintln!("No samples found in the audio file: {}", filepath);
266
+ eprintln!("No samples read from {}", resolved_path);
211
267
  return;
212
268
  }
213
269
 
214
- // TODO Apply effects here if needed
270
+ // Calculate buffer offset and size
215
271
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
216
272
  let required_len = offset + samples.len() * (CHANNELS as usize);
217
- let padded_required_len = if required_len % 2 == 1 {
218
- required_len + 1
219
- } else {
220
- required_len
221
- };
273
+ if self.buffer.len() < required_len {
274
+ self.buffer.resize(required_len, 0);
275
+ }
222
276
 
223
- self.buffer.resize(padded_required_len, 0);
224
- self.pad_samples(&samples, time_secs);
277
+ // Apply effects and mix
278
+ if let Some(effects_map) = effects {
279
+ self.pad_samples(&samples, time_secs, Some(effects_map));
280
+ } else {
281
+ self.pad_samples(&samples, time_secs, None);
282
+ }
225
283
  }
226
284
 
227
- fn pad_samples(&mut self, samples: &[i16], time_secs: f32) {
285
+ fn pad_samples(
286
+ &mut self,
287
+ samples: &[i16],
288
+ time_secs: f32,
289
+ effects_map: Option<HashMap<String, Value>>
290
+ ) {
228
291
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
292
+ let total_samples = samples.len();
293
+
294
+ // Default values
295
+ let mut gain = 1.0;
296
+ let mut pan = 0.0;
297
+ let mut fade_in = 0.0;
298
+ let mut fade_out = 0.0;
299
+ let mut pitch = 1.0;
300
+ let mut drive = 0.0;
301
+ let mut reverb = 0.0;
302
+ let mut delay = 0.0; // delay time in seconds
303
+ let delay_feedback = 0.35; // default feedback
304
+
305
+ if let Some(map) = &effects_map {
306
+ for (key, val) in map {
307
+ match (key.as_str(), val) {
308
+ ("gain", Value::Number(v)) => {
309
+ gain = *v;
310
+ }
311
+ ("pan", Value::Number(v)) => {
312
+ pan = *v;
313
+ }
314
+ ("fadeIn", Value::Number(v)) => {
315
+ fade_in = *v;
316
+ }
317
+ ("fadeOut", Value::Number(v)) => {
318
+ fade_out = *v;
319
+ }
320
+ ("pitch", Value::Number(v)) => {
321
+ pitch = *v;
322
+ }
323
+ ("drive", Value::Number(v)) => {
324
+ drive = *v;
325
+ }
326
+ ("reverb", Value::Number(v)) => {
327
+ reverb = *v;
328
+ }
329
+ ("delay", Value::Number(v)) => {
330
+ delay = *v;
331
+ }
332
+ _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
333
+ }
334
+ }
335
+ }
336
+
337
+ let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
338
+ let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
339
+
340
+ let delay_samples = if delay > 0.0 { (delay * (SAMPLE_RATE as f32)) as usize } else { 0 };
341
+ let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
342
+
343
+ for i in 0..total_samples {
344
+ // PITCH FIRST
345
+ let pitch_index = if pitch != 1.0 { ((i as f32) / pitch) as usize } else { i };
346
+
347
+ let mut adjusted = if pitch_index < total_samples {
348
+ samples[pitch_index] as f32
349
+ } else {
350
+ 0.0
351
+ };
352
+
353
+ // GAIN
354
+ adjusted *= gain;
355
+
356
+ // FADE IN/OUT
357
+ if fade_in_samples > 0 && i < fade_in_samples {
358
+ adjusted *= (i as f32) / (fade_in_samples as f32);
359
+ }
360
+ if fade_out_samples > 0 && i >= total_samples.saturating_sub(fade_out_samples) {
361
+ adjusted *= ((total_samples - i) as f32) / (fade_out_samples as f32);
362
+ }
363
+
364
+ // DRIVE (soft)
365
+ if drive > 0.0 {
366
+ let normalized = adjusted / (i16::MAX as f32);
367
+ let pre_gain = (10f32).powf(drive / 20.0); // dB mapping
368
+ let driven = (normalized * pre_gain).tanh();
369
+ adjusted = driven * (i16::MAX as f32);
370
+ }
371
+
372
+ // DELAY
373
+ if delay_samples > 0 && i >= delay_samples {
374
+ let echo = delay_buffer[i - delay_samples] * delay_feedback;
375
+ adjusted += echo;
376
+ }
377
+ if delay_samples > 0 {
378
+ delay_buffer[i] = adjusted;
379
+ }
380
+
381
+ // REVERB
382
+ if reverb > 0.0 {
383
+ let reverb_delay = (0.03 * (SAMPLE_RATE as f32)) as usize;
384
+ if i >= reverb_delay {
385
+ adjusted += (self.buffer[offset + i - reverb_delay] as f32) * reverb;
386
+ }
387
+ }
388
+
389
+ // CLAMP
390
+ let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
391
+
392
+ // PAN
393
+ let left_gain = 1.0 - pan.max(0.0); // Pan > 0 => reduce left
394
+ let right_gain = 1.0 + pan.min(0.0); // Pan < 0 => reduce right
229
395
 
230
- for (i, &sample) in samples.iter().enumerate() {
231
- let adjusted_sample = ((sample as f32) * self.volume).round() as i16;
396
+ let left = ((adjusted_sample as f32) * left_gain) as i16;
397
+ let right = ((adjusted_sample as f32) * right_gain) as i16;
232
398
 
233
399
  let left_pos = offset + i * 2;
234
400
  let right_pos = left_pos + 1;
235
401
 
236
402
  if right_pos < self.buffer.len() {
237
- self.buffer[left_pos] = self.buffer[left_pos].saturating_add(adjusted_sample); // gauche
238
- self.buffer[right_pos] = self.buffer[right_pos].saturating_add(adjusted_sample); // droite
403
+ self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
404
+ self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
239
405
  }
240
406
  }
241
407
  }