@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
@@ -2,18 +2,20 @@ use crate::core::{
2
2
  audio::{ engine::AudioEngine, interpreter::driver::execute_audio_block },
3
3
  parser::statement::{ Statement, StatementKind },
4
4
  shared::{ duration::Duration, value::Value },
5
- store::variable::VariableTable,
5
+ store::{function::FunctionTable, global::GlobalStore, variable::VariableTable},
6
6
  };
7
7
 
8
8
  pub fn interprete_loop_statement(
9
9
  stmt: &Statement,
10
- audio_engine: AudioEngine,
11
- variable_table: VariableTable,
10
+ audio_engine: &mut AudioEngine,
11
+ global_store: &GlobalStore,
12
+ variable_table: &VariableTable,
13
+ functions_table: &FunctionTable,
12
14
  base_bpm: f32,
13
15
  base_duration: f32,
14
16
  max_end_time: f32,
15
17
  cursor_time: f32
16
- ) -> (AudioEngine, f32, f32) {
18
+ ) -> (f32, f32) {
17
19
  if let Value::Map(loop_value) = &stmt.value {
18
20
  let loop_count = match loop_value.get("iterator") {
19
21
  Some(Value::Number(n)) => *n as usize,
@@ -22,12 +24,15 @@ pub fn interprete_loop_statement(
22
24
  *n as usize
23
25
  } else {
24
26
  eprintln!("❌ Loop iterator must be a number, found: {:?}", ident);
25
- return (audio_engine, max_end_time, cursor_time);
27
+ return (max_end_time, cursor_time);
26
28
  }
27
29
  }
28
30
  _ => {
29
- eprintln!("❌ Loop iterator must be a number, found: {:?}", loop_value.get("iterator"));
30
- return (audio_engine, max_end_time, cursor_time);
31
+ eprintln!(
32
+ "❌ Loop iterator must be a number, found: {:?}",
33
+ loop_value.get("iterator")
34
+ );
35
+ return (max_end_time, cursor_time);
31
36
  }
32
37
  };
33
38
 
@@ -35,7 +40,7 @@ pub fn interprete_loop_statement(
35
40
  Some(Value::Block(body)) => body.clone(),
36
41
  _ => {
37
42
  eprintln!("❌ Loop body must be a block, found: {:?}", loop_value.get("body"));
38
- return (audio_engine, max_end_time, cursor_time);
43
+ return (max_end_time, cursor_time);
39
44
  }
40
45
  };
41
46
 
@@ -43,10 +48,12 @@ pub fn interprete_loop_statement(
43
48
  let mut cur_time = cursor_time;
44
49
  let mut max_time = max_end_time;
45
50
 
46
- for _ in 0..loop_count {
47
- let (eng, _, end_time) = execute_audio_block(
48
- engine,
51
+ for i in 0..loop_count {
52
+ let (block_end_time, cursor_time) = execute_audio_block(
53
+ &mut engine,
54
+ global_store,
49
55
  variable_table.clone(),
56
+ functions_table.clone(),
50
57
  loop_body.clone(),
51
58
  base_bpm,
52
59
  base_duration,
@@ -54,14 +61,13 @@ pub fn interprete_loop_statement(
54
61
  cur_time
55
62
  );
56
63
 
57
- engine = eng;
58
- cur_time = end_time;
59
- max_time = max_time.max(end_time);
64
+ cur_time = block_end_time;
65
+ max_time = max_time.max(cur_time);
60
66
  }
61
67
 
62
- (engine, max_time, cur_time)
63
- } else {
64
- eprintln!("❌ Loop statement value is not a map");
65
- (audio_engine, max_end_time, cursor_time)
68
+ return (max_time, cur_time);
66
69
  }
70
+
71
+ eprintln!("❌ Loop statement value is not a map");
72
+ (max_end_time, cursor_time)
67
73
  }
@@ -9,4 +9,5 @@ pub mod sleep;
9
9
  pub mod loop_;
10
10
  pub mod call;
11
11
  pub mod condition;
12
- pub mod arrow_call;
12
+ pub mod arrow_call;
13
+ pub mod function;
@@ -18,12 +18,6 @@ pub fn interprete_sleep_statement(
18
18
  0.0
19
19
  })
20
20
  }
21
- Value::String(s) if s.ends_with("s") => {
22
- s.trim_end_matches("s").parse::<f32>().unwrap_or_else(|_| {
23
- eprintln!("❌ Invalid sleep value (s): {}", s);
24
- 0.0
25
- })
26
- }
27
21
  other => {
28
22
  eprintln!("❌ Invalid sleep value: {:?}", other);
29
23
  0.0
@@ -1,84 +1,102 @@
1
1
  use crate::core::{
2
- audio::{ engine::AudioEngine, interpreter::driver::execute_audio_block },
3
- parser::statement::Statement,
2
+ audio::{engine::AudioEngine, interpreter::driver::execute_audio_block},
3
+ parser::statement::{Statement, StatementKind},
4
4
  shared::value::Value,
5
- store::variable::VariableTable,
5
+ store::{function::FunctionTable, global::GlobalStore, variable::VariableTable},
6
6
  };
7
7
 
8
8
  pub fn interprete_spawn_statement(
9
9
  stmt: &Statement,
10
- audio_engine: AudioEngine,
10
+ audio_engine: &mut AudioEngine,
11
11
  variable_table: &VariableTable,
12
+ functions: &FunctionTable,
13
+ global_store: &GlobalStore,
12
14
  base_bpm: f32,
13
15
  base_duration: f32,
16
+ max_end_time: f32,
14
17
  cursor_time: f32,
15
- max_end_time: f32
16
- ) -> Option<(f32, f32, AudioEngine)> {
17
- match &stmt.value {
18
- Value::String(identifier) | Value::Identifier(identifier) => {
19
- handle_spawn_identifier(
20
- identifier,
21
- audio_engine,
22
- variable_table,
23
- base_bpm,
24
- base_duration,
25
- cursor_time,
26
- max_end_time
27
- )
28
- }
18
+ ) -> (f32, f32) {
19
+ match &stmt.kind {
20
+ StatementKind::Spawn { name, args } => {
21
+ let mut local_engine = AudioEngine::new(audio_engine.module_name.clone());
22
+
23
+ // Function case
24
+ if let Some(func) = functions.functions.get(name) {
25
+ if func.parameters.len() != args.len() {
26
+ eprintln!(
27
+ "❌ Function '{}' expects {} args, got {}",
28
+ name,
29
+ func.parameters.len(),
30
+ args.len()
31
+ );
32
+ return (max_end_time, cursor_time);
33
+ }
29
34
 
30
- Value::Map(map) => {
31
- if let Some(Value::Block(block)) = map.get("body") {
32
- let (eng, _, end_time) = execute_audio_block(
33
- audio_engine.clone(),
34
- variable_table.clone(),
35
- block.clone(),
35
+ let mut local_vars = VariableTable::with_parent(variable_table.clone());
36
+ for (param, arg) in func.parameters.iter().zip(args) {
37
+ local_vars.set(param.clone(), arg.clone());
38
+ }
39
+
40
+ let (spawn_max, _) = execute_audio_block(
41
+ &mut local_engine,
42
+ global_store,
43
+ local_vars,
44
+ functions.clone(),
45
+ func.body.clone(),
36
46
  base_bpm,
37
47
  base_duration,
38
- max_end_time,
39
- cursor_time
48
+ 0.0,
49
+ 0.0,
40
50
  );
41
- return Some((max_end_time.max(end_time), end_time, eng));
42
- } else {
43
- eprintln!("❌ Spawn map has no 'body' block");
51
+
52
+ audio_engine.merge_with(local_engine);
53
+ return (spawn_max.max(max_end_time), cursor_time);
54
+ }
55
+
56
+ // Group case
57
+ if let Some(group_stmt) = find_group(name, variable_table, global_store) {
58
+ if let Value::Map(map) = &group_stmt.value {
59
+ if let Some(Value::Block(body)) = map.get("body") {
60
+ let (spawn_max, _) = execute_audio_block(
61
+ &mut local_engine,
62
+ global_store,
63
+ variable_table.clone(),
64
+ functions.clone(),
65
+ body.clone(),
66
+ base_bpm,
67
+ base_duration,
68
+ 0.0,
69
+ 0.0,
70
+ );
71
+ audio_engine.merge_with(local_engine);
72
+ return (spawn_max.max(max_end_time), cursor_time);
73
+ }
74
+ }
44
75
  }
45
- None
46
- }
47
76
 
48
- _ => {
49
- eprintln!("❌ Invalid spawn statement: expected identifier, found {:?}", stmt.value);
50
- None
77
+ eprintln!("❌ Function or group '{}' not found", name);
51
78
  }
79
+
80
+ _ => eprintln!("❌ interprete_spawn_statement expected Spawn, got {:?}", stmt.kind),
52
81
  }
82
+
83
+ (max_end_time, cursor_time)
53
84
  }
54
85
 
55
- fn handle_spawn_identifier(
56
- identifier: &str,
57
- audio_engine: AudioEngine,
58
- variable_table: &VariableTable,
59
- base_bpm: f32,
60
- base_duration: f32,
61
- cursor_time: f32,
62
- max_end_time: f32
63
- ) -> Option<(f32, f32, AudioEngine)> {
64
- if let Some(Value::Map(map)) = variable_table.get(identifier) {
65
- if let Some(Value::Block(block)) = map.get("body") {
66
- let (eng, _, end_time) = execute_audio_block(
67
- audio_engine.clone(),
68
- variable_table.clone(),
69
- block.clone(),
70
- base_bpm,
71
- base_duration,
72
- max_end_time,
73
- cursor_time
74
- );
75
- return Some((max_end_time.max(end_time), end_time, eng));
76
- } else {
77
- eprintln!("❌ Spawn group '{}' has no 'body' block", identifier);
86
+ fn find_group<'a>(
87
+ name: &str,
88
+ variable_table: &'a VariableTable,
89
+ global_store: &'a GlobalStore,
90
+ ) -> Option<&'a Statement> {
91
+ if let Some(Value::Statement(stmt_box)) = variable_table.get(name) {
92
+ if let StatementKind::Group = stmt_box.kind {
93
+ return Some(stmt_box);
94
+ }
95
+ }
96
+ if let Some(Value::Statement(stmt_box)) = global_store.variables.variables.get(name) {
97
+ if let StatementKind::Group = stmt_box.kind {
98
+ return Some(stmt_box);
78
99
  }
79
- } else {
80
- eprintln!("❌ Spawn group '{}' not found or not a map", identifier);
81
100
  }
82
-
83
101
  None
84
102
  }
@@ -15,105 +15,192 @@ pub fn interprete_trigger_statement(
15
15
  cursor_time: f32,
16
16
  max_end_time: f32
17
17
  ) -> Option<(f32, f32, AudioEngine)> {
18
- if let StatementKind::Trigger { entity, duration } = &stmt.kind {
19
- if let Some(trigger_val) = resolve_namespaced_variable(entity, variable_table) {
20
- let duration_secs = match duration {
21
- Duration::Number(n) => *n,
22
-
23
- Duration::Identifier(id) => {
24
- if id == "auto" {
25
- 1.0
18
+ if let StatementKind::Trigger { entity, duration, effects } = &stmt.kind {
19
+ let mut trigger_val = Value::String(entity.clone());
20
+ let mut trigger_src = String::new();
21
+
22
+ match variable_table.variables.get(entity) {
23
+ Some(Value::Identifier(id)) => {
24
+ // Get real value from global variable table
25
+ if let Some(global_table) = &variable_table.parent {
26
+ if let Some(val) = global_table.get(id) {
27
+ trigger_val = val.clone();
26
28
  } else {
27
- match variable_table.get(id) {
28
- Some(Value::Number(n)) => *n,
29
- Some(Value::Identifier(other)) if other == "auto" => 1.0,
30
- Some(other) => {
31
- eprintln!(
32
- "❌ Invalid duration reference '{}': expected number, got {:?}",
33
- id,
34
- other
35
- );
36
- return None;
37
- }
38
- None => {
39
- eprintln!("❌ Duration identifier '{}' not found", id);
40
- return None;
41
- }
42
- }
29
+ eprintln!("❌ Trigger entity '{}' not found in global variable table", id);
30
+ return None;
43
31
  }
32
+ } else if let Some(val) = variable_table.get(id) {
33
+ trigger_val = val.clone();
34
+ } else {
35
+ eprintln!("❌ Trigger entity '{}' not found in variable table", id);
36
+ return None;
44
37
  }
38
+ }
39
+ Some(Value::Sample(sample_src)) => {
40
+ // If the entity is a sample, we use its path directly
41
+ trigger_src = sample_src.clone();
42
+ }
43
+ Some(Value::Map(map)) => {
44
+ // If the entity is a map, we assume it contains an "entity" key
45
+ if let Some(Value::String(src)) = map.get("entity") {
46
+ trigger_val = Value::String(src.clone());
47
+ } else if let Some(Value::Identifier(src)) = map.get("entity") {
48
+ trigger_val = Value::Identifier(src.clone());
49
+ } else {
50
+ eprintln!(
51
+ "❌ Trigger map must contain an 'entity' key with a string or identifier value."
52
+ );
53
+ return None;
54
+ }
55
+ }
56
+ _ => {
57
+ trigger_val = Value::String(entity.clone());
58
+ }
59
+ }
45
60
 
46
- Duration::Beat(beat_str) => {
47
- // Assuming beat_str is in the format "numerator/denominator"
48
- let parts: Vec<&str> = beat_str.split('/').collect();
49
-
50
- if parts.len() != 2 {
51
- eprintln!("❌ Invalid beat duration format: {}", beat_str);
52
- return None;
61
+ let duration_secs = match duration {
62
+ Duration::Number(n) => *n,
63
+
64
+ Duration::Identifier(id) => {
65
+ if id == "auto" {
66
+ 1.0
67
+ } else {
68
+ match variable_table.get(id) {
69
+ Some(Value::Number(n)) => *n,
70
+ Some(Value::Identifier(other)) if other == "auto" => 1.0,
71
+ Some(other) => {
72
+ eprintln!(
73
+ "❌ Invalid duration reference '{}': expected number, got {:?}",
74
+ id,
75
+ other
76
+ );
77
+ return None;
78
+ }
79
+ None => {
80
+ eprintln!("❌ Duration identifier '{}' not found", id);
81
+ return None;
82
+ }
53
83
  }
54
-
55
- let numerator: f32 = parts[0].parse().unwrap_or(1.0);
56
- let denominator: f32 = parts[1].parse().unwrap_or(1.0);
57
- numerator / denominator
58
84
  }
85
+ }
59
86
 
60
- Duration::Auto => 1.0,
61
- };
87
+ Duration::Beat(beat_str) => {
88
+ let parts: Vec<&str> = beat_str.split('/').collect();
89
+ if parts.len() != 2 {
90
+ eprintln!("❌ Invalid beat duration format: {}", beat_str);
91
+ return None;
92
+ }
62
93
 
63
- let duration_final = duration_secs * base_duration;
94
+ let numerator: f32 = parts[0].parse().unwrap_or(1.0);
95
+ let denominator: f32 = parts[1].parse().unwrap_or(4.0);
64
96
 
65
- let (src, _) = load_trigger(
66
- trigger_val,
67
- duration,
68
- base_duration,
69
- variable_table.clone()
70
- );
97
+ let beats = (numerator / denominator) * 4.0;
71
98
 
72
- if let Some(effects) = extract_effects(stmt.value.clone()) {
73
- audio_engine.insert_sample(&src, cursor_time, duration_final, Some(effects));
74
- } else {
75
- audio_engine.insert_sample(&src, cursor_time, duration_final, None);
99
+ beats * base_duration
76
100
  }
77
101
 
78
- let mut updated_engine = audio_engine.clone();
102
+ Duration::Auto => base_duration,
103
+ };
79
104
 
80
- let new_cursor_time = cursor_time + duration_final;
81
- let new_max_end_time = new_cursor_time.max(max_end_time);
82
-
83
- return Some((new_cursor_time, new_max_end_time, updated_engine));
105
+ let final_variable_table = if let Some(parent) = &variable_table.parent {
106
+ VariableTable {
107
+ variables: parent.variables.clone(),
108
+ parent: None,
109
+ }
84
110
  } else {
85
- eprintln!("❌ Unknown trigger entity: {}", entity);
111
+ variable_table.clone()
112
+ };
113
+
114
+ let (src, sample_length) = load_trigger(
115
+ &trigger_val,
116
+ duration,
117
+ effects,
118
+ base_duration,
119
+ final_variable_table.clone()
120
+ );
121
+
122
+ if trigger_src.is_empty() {
123
+ trigger_src = src;
86
124
  }
87
- }
88
-
89
- None
90
- }
91
125
 
92
- fn resolve_namespaced_variable<'a>(path: &str, variables: &'a VariableTable) -> Option<&'a Value> {
93
- let mut current: Option<&Value> = None;
126
+ let effects = extract_effects(stmt.value.clone());
127
+ let one_shot = effects
128
+ .as_ref()
129
+ .and_then(|map| map.get("one_shot"))
130
+ .and_then(|v| {
131
+ match v {
132
+ Value::Identifier(id) if id == "true" => Some(true),
133
+ Value::String(s) if s == "true" => Some(true),
134
+ _ => None,
135
+ }
136
+ })
137
+ .unwrap_or(false);
94
138
 
95
- for (i, part) in path.split('.').enumerate() {
96
- if i == 0 {
97
- current = variables.get(part);
139
+ let play_length = if one_shot {
140
+ sample_length // play entire sample
98
141
  } else {
99
- current = match current {
100
- Some(Value::Map(map)) => map.get(part),
101
- _ => {
142
+ duration_secs.min(sample_length)
143
+ };
144
+
145
+ let trigger_src = match trigger_val.get("entity") {
146
+ Some(Value::String(src)) => src.clone(),
147
+ Some(Value::Identifier(id)) => id.clone(),
148
+ Some(Value::Statement(stmt)) => {
149
+ if let StatementKind::Trigger { entity, .. } = &stmt.kind {
150
+ entity.clone()
151
+ } else {
152
+ eprintln!("❌ Invalid trigger statement in map: expected 'Trigger' kind");
102
153
  return None;
103
154
  }
104
- };
155
+ }
156
+ _ => trigger_src,
157
+ };
158
+
159
+ if let Some(effects_map) = effects {
160
+ audio_engine.insert_sample(
161
+ &trigger_src,
162
+ cursor_time,
163
+ play_length,
164
+ Some(effects_map),
165
+ &final_variable_table
166
+ );
167
+ } else {
168
+ audio_engine.insert_sample(
169
+ &trigger_src,
170
+ cursor_time,
171
+ play_length,
172
+ None,
173
+ &final_variable_table
174
+ );
105
175
  }
176
+
177
+ let new_cursor_time = cursor_time + duration_secs; // advance by beat duration
178
+ let new_max_end_time = (cursor_time + play_length).max(max_end_time);
179
+
180
+ let updated_engine = audio_engine.clone();
181
+
182
+ return Some((new_cursor_time, new_max_end_time, updated_engine));
106
183
  }
107
184
 
108
- current
185
+ None
109
186
  }
110
187
 
111
188
  fn extract_effects(value: Value) -> Option<HashMap<String, Value>> {
112
- if let Value::Map(map) = value.clone() {
189
+ if let Value::Map(map) = value {
113
190
  let mut effects = HashMap::new();
114
191
 
115
192
  for (key, val) in map {
116
- effects.insert(key.clone(), val);
193
+ if key == "effects" {
194
+ if let Value::Map(effect_map) = val {
195
+ for (effect_key, effect_val) in effect_map {
196
+ effects.insert(effect_key, effect_val);
197
+ }
198
+ } else {
199
+ return None; // effects must be a map
200
+ }
201
+ } else {
202
+ return Some(effects);
203
+ }
117
204
  }
118
205
 
119
206
  Some(effects)
@@ -1,8 +1,13 @@
1
- use crate::core::{ shared::{ duration::Duration, value::Value }, store::variable::VariableTable };
1
+ use crate::core::{
2
+ parser::statement::StatementKind,
3
+ shared::{ duration::Duration, value::Value },
4
+ store::variable::VariableTable,
5
+ };
2
6
 
3
7
  pub fn load_trigger(
4
8
  trigger: &Value,
5
9
  duration: &Duration,
10
+ effects: &Option<Value>,
6
11
  base_duration: f32,
7
12
  variable_table: VariableTable
8
13
  ) -> (String, f32) {
@@ -13,8 +18,36 @@ pub fn load_trigger(
13
18
  Value::String(src) => {
14
19
  trigger_path = src.to_string();
15
20
  }
21
+ Value::Identifier(src) => {
22
+ trigger_path = src.to_string();
23
+ }
24
+
25
+ Value::Map(map) => {
26
+ if let Some(Value::String(src)) = map.get("entity") {
27
+ trigger_path = format!("devalang://bank/{}", src.to_string());
28
+ } else if let Some(Value::Identifier(src)) = map.get("entity") {
29
+ trigger_path = format!("devalang://bank/{}", src.to_string());
30
+ } else {
31
+ eprintln!(
32
+ "❌ Trigger map must contain an 'entity' key with a string or identifier value."
33
+ );
34
+ }
35
+ }
36
+ Value::Sample(src) => {
37
+ trigger_path = src.to_string();
38
+ }
39
+ Value::Statement(stmt) => {
40
+ if let StatementKind::Trigger { entity, duration, effects } = &stmt.kind {
41
+ trigger_path = entity.clone();
42
+ } else {
43
+ eprintln!("❌ Trigger statement must be of type 'Trigger', found: {:?}", stmt.kind);
44
+ }
45
+ }
16
46
  _ => {
17
- eprintln!("❌ Invalid trigger type. Expected a text variable.");
47
+ eprintln!(
48
+ "❌ Invalid trigger type. Expected a string or identifier variable, found: {:?}",
49
+ trigger
50
+ );
18
51
  }
19
52
  }
20
53
 
@@ -45,11 +78,11 @@ pub fn load_trigger(
45
78
 
46
79
  Duration::Beat(beat_str) => {
47
80
  let parts: Vec<&str> = beat_str.split('/').collect();
48
-
81
+
49
82
  if parts.len() == 2 {
50
83
  let numerator: f32 = parts[0].parse().unwrap_or(1.0);
51
84
  let denominator: f32 = parts[1].parse().unwrap_or(1.0);
52
- duration_as_secs = numerator / denominator * base_duration;
85
+ duration_as_secs = (numerator / denominator) * base_duration;
53
86
  } else {
54
87
  eprintln!("❌ Invalid beat duration format: {}", beat_str);
55
88
  }
@@ -21,23 +21,33 @@ impl AudioPlayer {
21
21
  }
22
22
  }
23
23
 
24
- fn load_source(&self, path: &str) -> impl Source<Item = f32> + Send + 'static {
25
- let file = File::open(path).unwrap();
26
- let reader = BufReader::new(file);
27
-
28
- Decoder::new(reader).unwrap().convert_samples()
24
+ fn load_source(&self, path: &str) -> Option<impl Source<Item = f32> + Send + 'static> {
25
+ if let Ok(file) = File::open(path) {
26
+ let reader = BufReader::new(file);
27
+ match Decoder::new(reader) {
28
+ Ok(decoder) => Some(decoder.convert_samples()),
29
+ Err(e) => {
30
+ eprintln!("❌ Failed to decode audio file '{}': {}", path, e);
31
+ None
32
+ }
33
+ }
34
+ } else {
35
+ eprintln!("❌ Could not open audio file: {}", path);
36
+ None
37
+ }
29
38
  }
30
39
 
31
40
  pub fn play_file_once(&mut self, path: &str) {
32
41
  self.sink.stop();
33
42
  self.sink = Sink::try_new(&self.handle).unwrap();
34
-
35
43
  self.sink.set_volume(1.0);
36
44
 
37
- let source = self.load_source(path);
38
-
39
- self.sink.append(source);
40
- self.last_path = Some(path.to_string());
45
+ if let Some(source) = self.load_source(path) {
46
+ self.sink.append(source);
47
+ self.last_path = Some(path.to_string());
48
+ } else {
49
+ eprintln!("⚠️ Skipping playback: failed to load '{}'", path);
50
+ }
41
51
  }
42
52
 
43
53
  pub fn replay_last(&mut self) {