@devaloop/devalang 0.0.1-alpha.8 → 0.0.1-alpha.9

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 (59) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +4 -8
  3. package/docs/CHANGELOG.md +27 -0
  4. package/examples/condition.deva +8 -12
  5. package/examples/group.deva +3 -3
  6. package/examples/index.deva +10 -8
  7. package/examples/loop.deva +10 -8
  8. package/examples/synth.deva +14 -0
  9. package/examples/variables.deva +1 -1
  10. package/out-tsc/bin/devalang.exe +0 -0
  11. package/out-tsc/scripts/version/fetch.js +1 -5
  12. package/package.json +1 -1
  13. package/project-version.json +3 -3
  14. package/rust/core/audio/engine.rs +89 -12
  15. package/rust/core/audio/interpreter/arrow_call.rs +129 -0
  16. package/rust/core/audio/interpreter/call.rs +29 -7
  17. package/rust/core/audio/interpreter/condition.rs +5 -1
  18. package/rust/core/audio/interpreter/driver.rs +41 -29
  19. package/rust/core/audio/interpreter/loop_.rs +11 -3
  20. package/rust/core/audio/interpreter/mod.rs +1 -0
  21. package/rust/core/audio/interpreter/spawn.rs +43 -42
  22. package/rust/core/audio/interpreter/trigger.rs +1 -1
  23. package/rust/core/audio/renderer.rs +15 -18
  24. package/rust/core/lexer/handler/arrow.rs +31 -0
  25. package/rust/core/lexer/handler/driver.rs +12 -1
  26. package/rust/core/lexer/handler/identifier.rs +1 -0
  27. package/rust/core/lexer/handler/mod.rs +1 -0
  28. package/rust/core/lexer/mod.rs +24 -3
  29. package/rust/core/lexer/token.rs +4 -0
  30. package/rust/core/parser/driver.rs +23 -4
  31. package/rust/core/parser/handler/arrow_call.rs +126 -0
  32. package/rust/core/parser/handler/identifier/call.rs +41 -0
  33. package/rust/core/parser/handler/identifier/group.rs +75 -0
  34. package/rust/core/parser/handler/identifier/let_.rs +133 -0
  35. package/rust/core/parser/handler/identifier/mod.rs +51 -0
  36. package/rust/core/parser/handler/identifier/sleep.rs +33 -0
  37. package/rust/core/parser/handler/identifier/spawn.rs +41 -0
  38. package/rust/core/parser/handler/identifier/synth.rs +65 -0
  39. package/rust/core/parser/handler/loop_.rs +24 -18
  40. package/rust/core/parser/handler/mod.rs +2 -1
  41. package/rust/core/parser/statement.rs +8 -0
  42. package/rust/core/preprocessor/loader.rs +57 -43
  43. package/rust/core/preprocessor/module.rs +3 -6
  44. package/rust/core/preprocessor/processor.rs +13 -4
  45. package/rust/core/preprocessor/resolver/call.rs +99 -29
  46. package/rust/core/preprocessor/resolver/condition.rs +38 -12
  47. package/rust/core/preprocessor/resolver/driver.rs +74 -29
  48. package/rust/core/preprocessor/resolver/group.rs +24 -81
  49. package/rust/core/preprocessor/resolver/let_.rs +31 -0
  50. package/rust/core/preprocessor/resolver/loop_.rs +62 -116
  51. package/rust/core/preprocessor/resolver/mod.rs +5 -1
  52. package/rust/core/preprocessor/resolver/spawn.rs +41 -36
  53. package/rust/core/preprocessor/resolver/synth.rs +50 -0
  54. package/rust/core/preprocessor/resolver/trigger.rs +51 -50
  55. package/rust/core/preprocessor/resolver/value.rs +78 -0
  56. package/rust/core/utils/path.rs +17 -32
  57. package/rust/core/utils/validation.rs +30 -28
  58. package/typescript/scripts/version/fetch.ts +1 -6
  59. package/rust/core/parser/handler/identifier.rs +0 -262
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "devalang"
3
- version = "0.0.1-alpha.8"
3
+ version = "0.0.1-alpha.9"
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"
package/README.md CHANGED
@@ -27,12 +27,12 @@ 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.8 Notice** 🚧
30
+ > 🚧 **v0.0.1-alpha.9 Notice** 🚧
31
31
  >
32
32
  > NEW: Devalang VSCode extension is now available !
33
33
  > [Get it here](https://marketplace.visualstudio.com/items?itemName=devaloop.devalang-vscode).
34
34
  >
35
- > NEW: Devalang supports conditional statements (`if`, `else`, `else if`) for more dynamic compositions !
35
+ > Includes synthesis, playback, and rendering features, but is still in early development.
36
36
  >
37
37
  > Currently, Devalang CLI is only available for **Windows**.
38
38
  > Linux and macOS binaries will be added in future releases via cross-platform builds.
@@ -181,24 +181,20 @@ bpm globalBpm
181
181
  bank globalBank
182
182
  # Will declare a custom instrument bank using the globalBank variable
183
183
 
184
- # Loops
185
-
186
184
  loop 5:
187
185
  .customKick kickDuration {reverb=50, drive=25}
188
186
  # Will play 5 times a custom sample for 500ms with reverb and overdrive effects
189
187
 
190
- # Groups
191
-
192
188
  group myGroup:
193
189
  .customKick kickDuration {reverb=50, drive=25}
194
190
  # Will play the same sample in a group, allowing for more complex patterns
195
191
 
196
- # Will be executed line by line (sequentially)
197
192
  call myGroup
193
+ # Will be executed line by line (sequentially)
198
194
 
195
+ # spawn myGroup
199
196
  # Will be executed in parallel (concurrently)
200
197
  # ⚠️ Note: `spawn` runs the entire group in parallel, but the group’s internal logic remains sequential unless it uses `spawn` internally.
201
- # spawn myGroup
202
198
  ```
203
199
 
204
200
  > 🧠 Note: `call` and `spawn` only work with `group` blocks. They do not apply to individual samples or other statements.
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@
4
4
 
5
5
  # Changelog
6
6
 
7
+ ## Version 0.0.1-alpha.9 (2025-07-14)
8
+
9
+ ### ✨ Syntax
10
+
11
+ - Added support for `synth` directives to define synthesizer sounds directly in code
12
+ → Example:
13
+
14
+ ```deva
15
+ let mySynth = synth sine
16
+ ```
17
+
18
+ ### 🧠 Core Engine
19
+
20
+ - ✅ **Major refactor** of the resolution layer:
21
+
22
+ - `condition`, `loop`, `group`, `trigger`, `call`, `spawn`, and `driver` were fully rewritten
23
+ - Ensures **correct variable scoping**, **cross-module references**, and **imported symbol resolution**
24
+
25
+ - ✅ Audio interpreter updated:
26
+ - `call` and `spawn` now execute correctly in parallel, preserving **temporal alignment** across groups
27
+
28
+ ### 🧩 Language Features
29
+
30
+ - Improved `@import` behavior:
31
+ - Modules can now be imported using **relative paths only**
32
+ - No need for absolute or root-based imports
33
+
7
34
  ## Version 0.0.1-alpha.8 (2025-07-12)
8
35
 
9
36
  ### Syntax
@@ -1,24 +1,20 @@
1
1
  # This file demonstrates the use of conditional blocks in Devalang.
2
2
 
3
- @import { myGroup } from "./examples/group.deva"
3
+ @import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
4
+ @import { myGroup } from "./group.deva"
4
5
 
5
- @load "./examples/samples/kick-808.wav" as kickCustom
6
- @load "./examples/samples/hat-808.wav" as hatCustom
6
+ @load "./samples/kick-808.wav" as kickCustom
7
+ @load "./samples/hat-808.wav" as hatCustom
7
8
 
8
9
  group conditionBlock:
9
10
  if tempo > 120:
10
11
  # Will be executed if the condition is true
11
- .kickCustom
12
+ .kickCustom auto
12
13
  else if tempo > 155:
13
14
  # Will be executed if the condition is false
14
- .hatCustom
15
+ .hatCustom auto
15
16
  else:
16
- # Following lines will be executed if the condition is neither true or false
17
-
18
- # This will call myGroup sequentially (in the main thread)
19
- call myGroup
20
-
21
- # This will spawn a group in parallel to the main thread (kick + hat simultaneously)
22
- # spawn myGroup
17
+ .kickCustom auto
18
+ .hatCustom auto
23
19
 
24
20
  @export { conditionBlock }
@@ -1,9 +1,9 @@
1
1
  # This file demonstrates the use of grouping in Devalang.
2
2
 
3
- @import { duration, default_bank, params, loopCount, tempo } from "./examples/variables.deva"
3
+ @import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
4
4
 
5
- @load "./examples/samples/kick-808.wav" as kickCustom
6
- @load "./examples/samples/hat-808.wav" as hatCustom
5
+ @load "./samples/kick-808.wav" as kickCustom
6
+ @load "./samples/hat-808.wav" as hatCustom
7
7
 
8
8
  group myGroup:
9
9
  .kickCustom duration params
@@ -1,16 +1,18 @@
1
1
  # This file demonstrates the use of main features in Devalang.
2
2
 
3
- @import { duration, default_bank, params, loopCount, tempo } from "./examples/variables.deva"
4
- @import { conditionBlock } from "./examples/condition.deva"
5
- @import { myGroup } from "./examples/group.deva"
3
+ @import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
4
+ @import { myLead } from "./synth.deva"
5
+ @import { myLoop } from "./loop.deva"
6
6
 
7
-
8
- @load "./examples/samples/kick-808.wav" as kickCustom
9
- @load "./examples/samples/hat-808.wav" as hatCustom
7
+ @load "./samples/kick-808.wav" as kickCustom
8
+ @load "./samples/hat-808.wav" as hatCustom
10
9
 
11
10
  bpm tempo
12
11
 
13
12
  bank default_bank
14
13
 
15
- # This will call the group sequentially (line by line)
16
- call conditionBlock
14
+ group myTrack:
15
+ spawn myLoop
16
+ spawn myLead
17
+
18
+ call myTrack
@@ -1,15 +1,17 @@
1
1
  # This file demonstrates the use of a loop in Devalang.
2
2
 
3
- @import { duration, default_bank, params, loopCount, tempo } from "./examples/variables.deva"
3
+ @import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
4
4
 
5
- @load "./examples/samples/kick-808.wav" as kickCustom
6
- @load "./examples/samples/hat-808.wav" as hatCustom
5
+ @load "./samples/kick-808.wav" as kickCustom
6
+ @load "./samples/hat-808.wav" as hatCustom
7
7
 
8
- loop loopCount:
9
- .kickCustom duration params
8
+ group myLoop:
9
+ loop loopCount:
10
+ .kickCustom duration params
10
11
 
11
- # Uncomment the next line (.hat) while executing "play" command
12
- # with `--repeat` option to see magic happen !
12
+ # Uncomment the next line (.hat) while executing "play" command
13
+ # with `--repeat` option to see magic happen !
13
14
 
14
- .hatCustom duration params
15
+ .hatCustom duration params
15
16
 
17
+ @export { myLoop }
@@ -0,0 +1,14 @@
1
+ # This file defines a simple synth lead using Devalang's syntax.
2
+
3
+ group myLead:
4
+ let mySynth = synth sine
5
+
6
+ mySynth -> note(C4, { duration: 400 })
7
+ mySynth -> note(G4, { duration: 400 })
8
+ mySynth -> note(E4, { duration: 600 })
9
+ mySynth -> note(A4, { duration: 400 })
10
+ mySynth -> note(F4, { duration: 800 })
11
+ mySynth -> note(D4, { duration: 400 })
12
+ mySynth -> note(B3, { duration: 600 })
13
+
14
+ @export { myLead }
@@ -2,7 +2,7 @@
2
2
 
3
3
  let duration = auto
4
4
  let default_bank = 808
5
- let params = {decay:10, delay:30}
5
+ let params = { decay: 10, delay: 30 }
6
6
  let loopCount = 5
7
7
  let tempo = 115
8
8
 
Binary file
@@ -16,19 +16,15 @@ exports.fetchVersion = void 0;
16
16
  const fs_1 = __importDefault(require("fs"));
17
17
  const child_process_1 = require("child_process");
18
18
  const fetchVersion = (projectVersionPath) => __awaiter(void 0, void 0, void 0, function* () {
19
- // Lire le fichier
20
19
  const data = JSON.parse(fs_1.default.readFileSync(projectVersionPath, "utf-8"));
21
- // Incrémenter le numéro de build
22
20
  data.build = (data.build || 0) + 1;
23
- // Récupérer le dernier hash git
24
21
  try {
25
22
  const commit = (0, child_process_1.execSync)("git rev-parse HEAD").toString().trim();
26
23
  data.lastCommit = commit;
27
24
  }
28
25
  catch (err) {
29
- console.warn("⚠️ Impossible de récupérer le hash git.");
26
+ console.warn("⚠️ Unable to fetch git commit hash. Ensure you are in a git repository.");
30
27
  }
31
- // Écrire la mise à jour
32
28
  fs_1.default.writeFileSync(projectVersionPath, JSON.stringify(data, null, 2));
33
29
  });
34
30
  exports.fetchVersion = fetchVersion;
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.8",
4
+ "version": "0.0.1-alpha.9",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.0.1-alpha.8",
2
+ "version": "0.0.1-alpha.9",
3
3
  "channel": "alpha",
4
- "lastCommit": "fdb440173acc82227067864903aabc83e1d1570c",
5
- "build": 7
4
+ "lastCommit": "61ab816a54275f2617ffd4327eb388ed822c36eb",
5
+ "build": 8
6
6
  }
@@ -2,7 +2,10 @@ use std::{ collections::HashMap, fs::File, io::BufReader };
2
2
  use hound::{ SampleFormat, WavSpec, WavWriter };
3
3
  use rodio::{ Decoder, Source };
4
4
 
5
- use crate::core::{ store::variable::VariableTable, utils::path::normalize_path };
5
+ use crate::core::{
6
+ store::variable::VariableTable,
7
+ utils::path::{ normalize_path, resolve_relative_path },
8
+ };
6
9
 
7
10
  const SAMPLE_RATE: u32 = 44100;
8
11
  const CHANNELS: u16 = 2;
@@ -12,14 +15,16 @@ pub struct AudioEngine {
12
15
  pub volume: f32,
13
16
  pub variables: VariableTable,
14
17
  pub buffer: Vec<i16>,
18
+ pub module_name: String,
15
19
  }
16
20
 
17
21
  impl AudioEngine {
18
- pub fn new() -> Self {
22
+ pub fn new(module_name: String) -> Self {
19
23
  AudioEngine {
20
24
  volume: 1.0,
21
25
  buffer: vec![],
22
26
  variables: VariableTable::new(),
27
+ module_name,
23
28
  }
24
29
  }
25
30
 
@@ -32,14 +37,28 @@ impl AudioEngine {
32
37
  }
33
38
  }
34
39
 
35
- pub fn set_duration(&mut self, duration_secs: f32) {
36
- let mut total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
40
+ pub fn merge_with(&mut self, other: AudioEngine) {
41
+ if other.buffer.iter().all(|&s| s == 0) {
42
+ eprintln!("⚠️ Skipping merge: other buffer is silent");
43
+ return;
44
+ }
37
45
 
38
- if total_samples % (CHANNELS as usize) != 0 {
39
- total_samples += 1;
46
+ if self.buffer.iter().all(|&s| s == 0) {
47
+ self.buffer = other.buffer;
48
+ self.variables.variables.extend(other.variables.variables);
49
+ return;
40
50
  }
41
51
 
42
- self.buffer.resize(total_samples, 0);
52
+ self.mix(&other);
53
+ self.variables.variables.extend(other.variables.variables);
54
+ }
55
+
56
+ pub fn set_duration(&mut self, duration_secs: f32) {
57
+ let total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
58
+
59
+ if self.buffer.len() < total_samples {
60
+ self.buffer.resize(total_samples, 0);
61
+ }
43
62
  }
44
63
 
45
64
  pub fn set_variables(&mut self, variables: VariableTable) {
@@ -72,18 +91,76 @@ impl AudioEngine {
72
91
  Ok(())
73
92
  }
74
93
 
75
- pub fn insert(
94
+ pub fn insert_note(
95
+ &mut self,
96
+ waveform: String,
97
+ freq: f32,
98
+ amp: f32,
99
+ start_time_ms: f32,
100
+ duration_ms: f32
101
+ ) {
102
+ let sample_rate = SAMPLE_RATE as f32;
103
+ let channels = CHANNELS as usize;
104
+
105
+ let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
106
+ let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
107
+ let amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0);
108
+
109
+ let mut samples = Vec::with_capacity(total_samples);
110
+ let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
111
+
112
+ for i in 0..total_samples {
113
+ let t = ((start_sample + i) as f32) / sample_rate;
114
+ let phase = 2.0 * std::f32::consts::PI * freq * t;
115
+
116
+ let mut value = match waveform.as_str() {
117
+ "sine" => phase.sin(),
118
+ "square" => if phase.sin() >= 0.0 { 1.0 } else { -1.0 }
119
+ "saw" => 2.0 * (freq * t - (freq * t + 0.5).floor()),
120
+ "triangle" => (2.0 * (2.0 * (freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
121
+ _ => 0.0,
122
+ };
123
+
124
+ // Fade in/out
125
+ if i < fade_len {
126
+ value *= (i as f32) / (fade_len as f32);
127
+ } else if i >= total_samples - fade_len {
128
+ value *= ((total_samples - i) as f32) / (fade_len as f32);
129
+ }
130
+
131
+ samples.push((value * amplitude) as i16);
132
+ }
133
+
134
+ // Convert to stereo
135
+ let stereo_samples: Vec<i16> = samples
136
+ .iter()
137
+ .flat_map(|s| vec![*s, *s])
138
+ .collect();
139
+
140
+ let offset = start_sample * channels;
141
+ let required_len = offset + stereo_samples.len();
142
+
143
+ if self.buffer.len() < required_len {
144
+ self.buffer.resize(required_len, 0);
145
+ }
146
+
147
+ for (i, sample) in stereo_samples.iter().enumerate() {
148
+ if *sample != 0 {
149
+ }
150
+ self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
151
+ }
152
+ }
153
+
154
+ pub fn insert_sample(
76
155
  &mut self,
77
156
  filepath: &str,
78
157
  time_secs: f32,
79
158
  dur_sec: f32,
80
159
  effects: Option<HashMap<String, f32>>
81
160
  ) {
82
- let normalized_filepath = normalize_path(filepath);
161
+ let resolved = resolve_relative_path(&self.module_name.clone(), filepath);
83
162
 
84
- let file = BufReader::new(
85
- File::open(normalized_filepath).expect("Failed to open audio file")
86
- );
163
+ let file = BufReader::new(File::open(resolved).expect("Failed to open audio file"));
87
164
  let decoder = Decoder::new(file).expect("Failed to decode audio file");
88
165
 
89
166
  // Mono or stereo reading possible here, we will duplicate in L/R
@@ -0,0 +1,129 @@
1
+ use crate::core::{
2
+ audio::engine::AudioEngine,
3
+ parser::statement::{ Statement, StatementKind },
4
+ shared::value::Value,
5
+ store::variable::VariableTable,
6
+ };
7
+
8
+ use std::collections::HashMap;
9
+
10
+ pub fn interprete_call_arrow_statement(
11
+ stmt: &Statement,
12
+ audio_engine: &mut AudioEngine,
13
+ variable_table: &VariableTable,
14
+ base_bpm: f32,
15
+ base_duration: f32,
16
+ max_end_time: &mut f32,
17
+ mut cursor_time: Option<&mut f32>,
18
+ update_cursor: bool
19
+ ) -> (f32, f32) {
20
+ let cursor_copy = cursor_time
21
+ .as_ref()
22
+ .map(|c| **c)
23
+ .unwrap_or(0.0);
24
+
25
+ if let StatementKind::ArrowCall { target, method, args } = &stmt.kind {
26
+ let Some(Value::Map(synth_map)) = variable_table.get(target) else {
27
+ println!("❌ Synth '{}' not found in variable table", target);
28
+ return (*max_end_time, cursor_copy);
29
+ };
30
+
31
+ let Some(Value::String(entity)) = synth_map.get("entity") else {
32
+ println!("❌ Missing 'entity' key in synth '{}'.", target);
33
+ return (*max_end_time, cursor_copy);
34
+ };
35
+
36
+ if entity != "synth" {
37
+ println!("❌ '{}' is not a synth, entity is '{}'.", target, entity);
38
+ return (*max_end_time, cursor_copy);
39
+ }
40
+
41
+ let Some(Value::Map(value_map)) = synth_map.get("value") else {
42
+ println!("❌ Missing 'value' map in synth '{}'.", target);
43
+ return (*max_end_time, cursor_copy);
44
+ };
45
+
46
+ let Some(Value::String(waveform)) = value_map.get("waveform") else {
47
+ println!("❌ Missing or invalid 'waveform' in synth '{}'.", target);
48
+ return (*max_end_time, cursor_copy);
49
+ };
50
+
51
+ let Some(Value::Map(params)) = value_map.get("parameters") else {
52
+ println!("❌ Missing or invalid 'parameters' in synth '{}'.", target);
53
+ return (*max_end_time, cursor_copy);
54
+ };
55
+
56
+ let freq = extract_f32(params, "freq").unwrap_or(440.0);
57
+ let amp = extract_f32(params, "amp").unwrap_or(1.0);
58
+
59
+ if method == "note" {
60
+ let Some(Value::Identifier(note_name)) = args.get(0) else {
61
+ println!("❌ Invalid or missing argument for 'note' method on '{}'.", target);
62
+ return (*max_end_time, cursor_copy);
63
+ };
64
+
65
+ let mut final_note_params = HashMap::new();
66
+ if let Some(Value::Map(note_params)) = args.get(1) {
67
+ for (key, value) in note_params {
68
+ final_note_params.insert(key.clone(), value.clone());
69
+ }
70
+ }
71
+
72
+ let duration_ms = extract_f32(&final_note_params, "duration").unwrap_or(base_duration);
73
+ let duration_secs = duration_ms / 1000.0;
74
+
75
+ let final_freq = note_to_freq(note_name);
76
+ let start_time = cursor_copy;
77
+ let end_time = start_time + duration_secs;
78
+
79
+ audio_engine.insert_note(
80
+ waveform.clone(),
81
+ final_freq,
82
+ amp,
83
+ start_time * 1000.0,
84
+ duration_ms
85
+ );
86
+
87
+ *max_end_time = (*max_end_time).max(end_time);
88
+
89
+ if update_cursor {
90
+ if let Some(c) = cursor_time.as_mut() {
91
+ **c = end_time;
92
+ }
93
+ }
94
+
95
+ return (*max_end_time, end_time);
96
+ } else {
97
+ println!("❌ Unknown method '{}' on synth '{}'.", method, target);
98
+ }
99
+ }
100
+
101
+ (*max_end_time, cursor_copy)
102
+ }
103
+
104
+ fn extract_f32(map: &HashMap<String, Value>, key: &str) -> Option<f32> {
105
+ map.get(key).and_then(|v| {
106
+ match v {
107
+ Value::Number(n) => Some(*n),
108
+ _ => None,
109
+ }
110
+ })
111
+ }
112
+
113
+ fn note_to_freq(note: &str) -> f32 {
114
+ let notes = vec!["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
115
+
116
+ if note.len() < 2 || note.len() > 3 {
117
+ return 440.0;
118
+ }
119
+
120
+ let (name, octave_str) = note.split_at(note.len() - 1);
121
+ let semitone = notes
122
+ .iter()
123
+ .position(|&n| n == name)
124
+ .unwrap_or(9) as i32;
125
+ let octave = octave_str.parse::<i32>().unwrap_or(4);
126
+ let midi_note = (octave + 1) * 12 + semitone;
127
+
128
+ 440.0 * (2.0_f32).powf(((midi_note as f32) - 69.0) / 12.0)
129
+ }
@@ -14,8 +14,30 @@ pub fn interprete_call_statement(
14
14
  max_end_time: f32,
15
15
  cursor_time: f32
16
16
  ) -> (AudioEngine, f32, f32) {
17
- if let Value::String(identifier) = &stmt.value {
18
- if let Some(Value::Map(map)) = variable_table.clone().get(identifier) {
17
+ match &stmt.value {
18
+ Value::String(identifier) | Value::Identifier(identifier) => {
19
+ if let Some(Value::Map(map)) = variable_table.clone().get(identifier) {
20
+ if let Some(Value::Block(block)) = map.get("body") {
21
+ let (eng, _, end_time) = execute_audio_block(
22
+ audio_engine,
23
+ variable_table,
24
+ block.clone(),
25
+ base_bpm,
26
+ base_duration,
27
+ max_end_time,
28
+ cursor_time
29
+ );
30
+
31
+ return (eng, max_end_time.max(end_time), end_time);
32
+ } else {
33
+ eprintln!("❌ Group '{}' has no 'body' block", identifier);
34
+ }
35
+ } else {
36
+ eprintln!("❌ Group '{}' not found or not a map", identifier);
37
+ }
38
+ }
39
+
40
+ Value::Map(map) => {
19
41
  if let Some(Value::Block(block)) = map.get("body") {
20
42
  let (eng, _, end_time) = execute_audio_block(
21
43
  audio_engine,
@@ -29,13 +51,13 @@ pub fn interprete_call_statement(
29
51
 
30
52
  return (eng, max_end_time.max(end_time), end_time);
31
53
  } else {
32
- eprintln!("❌ 'body' must be a block");
54
+ eprintln!("❌ Call map has no 'body' block");
33
55
  }
34
- } else {
35
- eprintln!("❌ Call target '{}' not found or invalid", identifier);
36
56
  }
37
- } else {
38
- eprintln!("❌ Invalid call statement: expected string identifier");
57
+
58
+ other => {
59
+ eprintln!("❌ Invalid call statement: expected identifier or map, found {:?}", other);
60
+ }
39
61
  }
40
62
 
41
63
  (audio_engine, max_end_time, cursor_time)
@@ -1,5 +1,9 @@
1
1
  use crate::core::{
2
- audio::{ engine::AudioEngine, evaluator::evaluate_condition_string, interpreter::driver::execute_audio_block },
2
+ audio::{
3
+ engine::AudioEngine,
4
+ evaluator::evaluate_condition_string,
5
+ interpreter::driver::execute_audio_block,
6
+ },
3
7
  parser::statement::Statement,
4
8
  shared::value::Value,
5
9
  store::variable::VariableTable,