@devaloop/devalang 0.0.1-beta.1 → 0.0.1-beta.3

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 (207) hide show
  1. package/.devalang +9 -10
  2. package/Cargo.toml +84 -80
  3. package/README.md +10 -7
  4. package/docs/CHANGELOG.md +83 -0
  5. package/docs/ROADMAP.md +6 -2
  6. package/docs/TODO.md +3 -14
  7. package/examples/bus.deva +10 -0
  8. package/examples/chain.deva +19 -0
  9. package/examples/effect.deva +2 -0
  10. package/examples/filter.deva +11 -0
  11. package/examples/lfo.deva +9 -0
  12. package/examples/plugin.deva +10 -10
  13. package/examples/routing.deva +23 -0
  14. package/examples/synth.deva +11 -1
  15. package/examples/synth_types.deva +17 -0
  16. package/out-tsc/bin/project-version.json +6 -0
  17. package/out-tsc/core/functions/index.d.ts +5 -0
  18. package/out-tsc/core/functions/index.js +11 -0
  19. package/out-tsc/pkg/devalang_core.d.ts +2 -0
  20. package/out-tsc/pkg/devalang_core.js +17 -2
  21. package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +1 -0
  22. package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
  23. package/out-tsc/scripts/version/copy-to-binary.js +79 -0
  24. package/package.json +23 -10
  25. package/project-version.json +3 -3
  26. package/rust/bindings/Cargo.toml +9 -0
  27. package/rust/bindings/src/lib.rs +86 -0
  28. package/rust/cli/addon/commands.rs +35 -0
  29. package/rust/cli/addon/download.rs +234 -0
  30. package/rust/cli/addon/install.rs +33 -0
  31. package/rust/cli/addon/list.rs +224 -0
  32. package/rust/cli/addon/metadata.rs +124 -0
  33. package/rust/cli/addon/mod.rs +8 -0
  34. package/rust/cli/addon/remove.rs +271 -0
  35. package/rust/cli/addon/update.rs +305 -0
  36. package/rust/cli/{install/addon.rs → addon/utils.rs} +34 -43
  37. package/rust/cli/build/commands.rs +153 -103
  38. package/rust/cli/build/mod.rs +2 -2
  39. package/rust/cli/build/process.rs +165 -146
  40. package/rust/cli/check/mod.rs +208 -208
  41. package/rust/cli/discover/commands.rs +53 -31
  42. package/rust/cli/discover/config.rs +2 -4
  43. package/rust/cli/discover/install.rs +139 -28
  44. package/rust/cli/discover/metadata.rs +3 -3
  45. package/rust/cli/login/commands.rs +124 -124
  46. package/rust/cli/me/commands.rs +52 -0
  47. package/rust/cli/me/mod.rs +1 -0
  48. package/rust/cli/mod.rs +2 -2
  49. package/rust/cli/parser.rs +76 -70
  50. package/rust/cli/play/commands.rs +375 -324
  51. package/rust/cli/play/mod.rs +5 -5
  52. package/rust/cli/play/process.rs +159 -150
  53. package/rust/cli/play/realtime.rs +91 -91
  54. package/rust/cli/telemetry/commands.rs +22 -22
  55. package/rust/cli/telemetry/event_creator.rs +80 -80
  56. package/rust/cli/telemetry/mod.rs +3 -3
  57. package/rust/cli/telemetry/send.rs +51 -51
  58. package/rust/cli/template/commands.rs +69 -69
  59. package/rust/config/driver.rs +112 -103
  60. package/rust/config/mod.rs +3 -3
  61. package/rust/config/ops.rs +26 -26
  62. package/rust/config/settings.rs +101 -101
  63. package/rust/core/audio/engine/driver.rs +237 -0
  64. package/rust/core/audio/engine/export.rs +169 -0
  65. package/rust/core/audio/engine/helpers.rs +178 -170
  66. package/rust/core/audio/engine/mod.rs +56 -7
  67. package/rust/core/audio/engine/notes/dsp.rs +88 -0
  68. package/rust/core/audio/engine/notes/mod.rs +53 -0
  69. package/rust/core/audio/engine/notes/params.rs +294 -0
  70. package/rust/core/audio/engine/sample/insert.rs +300 -0
  71. package/rust/core/audio/engine/sample/mod.rs +40 -0
  72. package/rust/core/audio/engine/sample/padding.rs +170 -0
  73. package/rust/core/audio/evaluator/condition.rs +61 -0
  74. package/rust/core/audio/evaluator/mod.rs +9 -0
  75. package/rust/core/audio/{evaluator.rs → evaluator/numeric.rs} +152 -310
  76. package/rust/core/audio/evaluator/rhs.rs +16 -0
  77. package/rust/core/audio/evaluator/string_expr.rs +94 -0
  78. package/rust/core/audio/interpreter/driver.rs +574 -542
  79. package/rust/core/audio/interpreter/mod.rs +2 -14
  80. package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +179 -0
  81. package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -0
  82. package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
  83. package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +3 -0
  84. package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +371 -0
  85. package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -0
  86. package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -0
  87. package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -0
  88. package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -0
  89. package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -0
  90. package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -0
  91. package/rust/core/audio/interpreter/{automate.rs → statements/automate.rs} +2 -4
  92. package/rust/core/audio/interpreter/{call.rs → statements/call.rs} +36 -5
  93. package/rust/core/audio/interpreter/{condition.rs → statements/condition.rs} +72 -71
  94. package/rust/core/audio/interpreter/{function.rs → statements/function.rs} +24 -26
  95. package/rust/core/audio/interpreter/{let_.rs → statements/let_.rs} +36 -38
  96. package/rust/core/audio/interpreter/{load.rs → statements/load.rs} +17 -19
  97. package/rust/core/audio/interpreter/{loop_.rs → statements/loop_.rs} +115 -114
  98. package/rust/core/audio/interpreter/statements/mod.rs +12 -0
  99. package/rust/core/audio/interpreter/{sleep.rs → statements/sleep.rs} +28 -28
  100. package/rust/core/audio/interpreter/{spawn.rs → statements/spawn.rs} +54 -4
  101. package/rust/core/audio/interpreter/{tempo.rs → statements/tempo.rs} +40 -40
  102. package/rust/core/audio/interpreter/{trigger.rs → statements/trigger.rs} +242 -239
  103. package/rust/core/audio/loader/trigger.rs +98 -97
  104. package/rust/core/audio/mod.rs +6 -7
  105. package/rust/core/audio/special/easing.rs +189 -189
  106. package/rust/core/audio/special/env.rs +45 -45
  107. package/rust/core/audio/special/math.rs +134 -134
  108. package/rust/core/audio/special/modulator.rs +143 -143
  109. package/rust/core/builder/mod.rs +129 -86
  110. package/rust/core/debugger/{module.rs → logs.rs} +52 -55
  111. package/rust/core/debugger/mod.rs +30 -30
  112. package/rust/core/debugger/store.rs +38 -40
  113. package/rust/core/error/mod.rs +269 -269
  114. package/rust/core/lexer/driver.rs +2 -4
  115. package/rust/core/mod.rs +9 -10
  116. package/rust/core/parser/driver/block.rs +111 -0
  117. package/rust/core/parser/driver/cursor.rs +82 -0
  118. package/rust/core/parser/driver/driver_impl.rs +159 -0
  119. package/rust/core/parser/driver/mod.rs +6 -0
  120. package/rust/core/parser/driver/parse_array.rs +120 -0
  121. package/rust/core/parser/driver/parse_map.rs +247 -0
  122. package/rust/core/parser/driver/parser.rs +160 -0
  123. package/rust/core/parser/handler/arrow_call.rs +90 -15
  124. package/rust/core/parser/handler/at.rs +279 -279
  125. package/rust/core/parser/handler/bank.rs +104 -104
  126. package/rust/core/parser/handler/condition.rs +83 -83
  127. package/rust/core/parser/handler/dot.rs +148 -148
  128. package/rust/core/parser/handler/identifier/automate.rs +254 -254
  129. package/rust/core/parser/handler/identifier/call.rs +91 -91
  130. package/rust/core/parser/handler/identifier/emit.rs +70 -70
  131. package/rust/core/parser/handler/identifier/function.rs +113 -113
  132. package/rust/core/parser/handler/identifier/group.rs +89 -89
  133. package/rust/core/parser/handler/identifier/let_.rs +173 -173
  134. package/rust/core/parser/handler/identifier/mod.rs +55 -55
  135. package/rust/core/parser/handler/identifier/on.rs +107 -107
  136. package/rust/core/parser/handler/identifier/print.rs +49 -49
  137. package/rust/core/parser/handler/identifier/sleep.rs +96 -43
  138. package/rust/core/parser/handler/identifier/spawn.rs +91 -91
  139. package/rust/core/parser/handler/identifier/synth.rs +39 -3
  140. package/rust/core/parser/handler/loop_.rs +194 -194
  141. package/rust/core/parser/handler/pattern.rs +25 -2
  142. package/rust/core/parser/handler/tempo.rs +105 -57
  143. package/rust/core/parser/statement.rs +10 -11
  144. package/rust/core/plugin/loader.rs +137 -137
  145. package/rust/core/plugin/runner/mod.rs +11 -0
  146. package/rust/core/plugin/{runner.rs → runner/non_wasm.rs} +206 -72
  147. package/rust/core/plugin/runner/wasm32.rs +44 -0
  148. package/rust/core/preprocessor/loader/inject.rs +313 -0
  149. package/rust/core/preprocessor/loader/loader_helpers.rs +110 -0
  150. package/rust/core/preprocessor/loader/mod.rs +235 -0
  151. package/rust/core/preprocessor/module.rs +55 -60
  152. package/rust/core/preprocessor/{processor.rs → processor/handlers.rs} +107 -114
  153. package/rust/core/preprocessor/processor/mod.rs +1 -0
  154. package/rust/core/preprocessor/resolver/function.rs +69 -69
  155. package/rust/core/preprocessor/resolver/group.rs +122 -94
  156. package/rust/core/preprocessor/resolver/pattern.rs +14 -2
  157. package/rust/core/store/global.rs +57 -61
  158. package/rust/core/store/mod.rs +1 -5
  159. package/rust/lib.rs +323 -308
  160. package/rust/macros/Cargo.toml +14 -0
  161. package/rust/macros/src/lib.rs +52 -0
  162. package/rust/main.rs +336 -143
  163. package/rust/types/Cargo.toml +1 -1
  164. package/rust/types/src/addons.rs +57 -55
  165. package/rust/types/src/config.rs +82 -74
  166. package/rust/types/src/lib.rs +15 -12
  167. package/rust/types/src/plugin.rs +20 -0
  168. package/rust/types/src/store.rs +139 -0
  169. package/rust/types/src/telemetry.rs +85 -85
  170. package/rust/utils/Cargo.toml +5 -2
  171. package/rust/utils/src/file.rs +477 -94
  172. package/rust/utils/src/first_usage.rs +97 -97
  173. package/rust/utils/src/lib.rs +9 -9
  174. package/rust/utils/src/logger.rs +200 -200
  175. package/rust/utils/src/path.rs +158 -88
  176. package/rust/utils/src/signature.rs +41 -41
  177. package/rust/utils/src/spinner.rs +20 -20
  178. package/rust/utils/src/version.rs +58 -27
  179. package/rust/utils/src/watcher.rs +46 -46
  180. package/rust/web/api.rs +5 -5
  181. package/rust/web/auth.rs +5 -0
  182. package/rust/web/cdn.rs +34 -34
  183. package/rust/web/forge.rs +5 -0
  184. package/rust/web/mod.rs +2 -0
  185. package/tests/integration.rs +21 -21
  186. package/typescript/core/functions/index.ts +11 -0
  187. package/typescript/pkg/devalang_core.ts +20 -4
  188. package/typescript/scripts/version/copy-to-binary.ts +82 -0
  189. package/rust/cli/bank/api.rs +0 -122
  190. package/rust/cli/bank/commands.rs +0 -275
  191. package/rust/cli/bank/mod.rs +0 -29
  192. package/rust/cli/install/bank.rs +0 -53
  193. package/rust/cli/install/commands.rs +0 -35
  194. package/rust/cli/install/mod.rs +0 -4
  195. package/rust/cli/install/plugin.rs +0 -61
  196. package/rust/core/audio/engine/sample.rs +0 -366
  197. package/rust/core/audio/engine/synth.rs +0 -325
  198. package/rust/core/audio/interpreter/arrow_call.rs +0 -311
  199. package/rust/core/audio/renderer.rs +0 -54
  200. package/rust/core/parser/driver.rs +0 -584
  201. package/rust/core/preprocessor/loader.rs +0 -637
  202. package/rust/core/store/export.rs +0 -28
  203. package/rust/core/store/function.rs +0 -40
  204. package/rust/core/store/import.rs +0 -28
  205. package/rust/core/store/variable.rs +0 -51
  206. package/rust/core/utils/mod.rs +0 -1
  207. package/rust/core/utils/path.rs +0 -37
@@ -1,324 +1,375 @@
1
- use crate::config::driver::ProjectConfig;
2
- use devalang_utils::logger::{LogLevel, Logger};
3
- use std::{sync::mpsc::channel, thread};
4
-
5
- pub use crate::cli::play::io::wav_duration_seconds;
6
- pub use crate::cli::play::realtime::{
7
- RtContext, join_realtime_runner, start_realtime_runner, stop_realtime_runner,
8
- };
9
-
10
- use super::process::process_play;
11
- use super::utils::{files_changed, snapshot_files};
12
-
13
- use crate::core::audio::player::AudioPlayer;
14
-
15
- #[cfg(feature = "cli")]
16
- pub fn handle_play_command(
17
- config: Option<ProjectConfig>,
18
- entry: Option<String>,
19
- output: Option<String>,
20
- watch: bool,
21
- repeat: bool,
22
- debug: bool,
23
- ) -> Result<(), String> {
24
- let logger = Logger::new();
25
-
26
- let entry_path = entry
27
- .or_else(|| config.as_ref().and_then(|c| c.defaults.entry.clone()))
28
- .unwrap_or_default();
29
-
30
- let output_path = output
31
- .or_else(|| config.as_ref().and_then(|c| c.defaults.output.clone()))
32
- .unwrap_or_default();
33
-
34
- let fetched_repeat = if repeat {
35
- true
36
- } else {
37
- config
38
- .as_ref()
39
- .and_then(|c| c.defaults.repeat)
40
- .unwrap_or(false)
41
- };
42
-
43
- if entry_path.is_empty() || output_path.is_empty() {
44
- logger.log_message(LogLevel::Error, "Entry or output path not specified.");
45
- return Err("missing entry or output".to_string());
46
- }
47
-
48
- let entry_file = match crate::core::utils::path::find_entry_file(&entry_path) {
49
- Some(p) => p,
50
- None => {
51
- logger.log_message(LogLevel::Error, "index.deva not found");
52
- return Err("index.deva not found".to_string());
53
- }
54
- };
55
-
56
- let audio_file = format!(
57
- "{}/audio/index.wav",
58
- crate::core::utils::path::normalize_path(&output_path)
59
- );
60
- let mut audio_player = AudioPlayer::new();
61
-
62
- if watch && fetched_repeat {
63
- logger.log_message(
64
- LogLevel::Error,
65
- "Watch and repeat cannot be used together. Use repeat instead.",
66
- );
67
- return Err("invalid options: watch and repeat cannot be combined".to_string());
68
- }
69
-
70
- if watch {
71
- let (tx, rx) = channel::<()>();
72
-
73
- // Thread 1 : Watcher sending changes
74
- let entry_clone = entry_path.clone();
75
- thread::spawn(move || {
76
- let _ = devalang_utils::watcher::watch_directory(entry_clone, move || {
77
- let _ = tx.send(()); // signal a change
78
- });
79
- });
80
-
81
- // Main thread: build + play in a loop
82
- let (bpm, entry_stmts, variables, functions, global_store) =
83
- process_play(&config, &entry_file, &output_path, debug)?;
84
- audio_player.play_file_once(&audio_file);
85
- // Estimate duration: base on statement count plus extra for loop iterations (1 beat per iter)
86
- let loop_iters: usize = entry_stmts
87
- .iter()
88
- .map(|s| match &s.kind {
89
- crate::core::parser::statement::StatementKind::Loop => {
90
- use devalang_types::Value;
91
- if let Value::Map(m) = &s.value {
92
- if let Some(Value::Array(items)) = m.get("array") {
93
- items.len()
94
- } else if let Some(Value::Number(n)) = m.get("iterator") {
95
- (*n).max(0.0) as usize
96
- } else {
97
- 0
98
- }
99
- } else {
100
- 0
101
- }
102
- }
103
- _ => 0,
104
- })
105
- .sum();
106
- let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
107
- let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
108
- let total_secs = wav_duration_seconds(&audio_file)
109
- .unwrap_or(0.0)
110
- .max(est_by_len);
111
- let mut rt_runner = Some(start_realtime_runner(
112
- RtContext {
113
- bpm,
114
- entry_stmts,
115
- variables,
116
- functions,
117
- global_store,
118
- },
119
- total_secs,
120
- ));
121
-
122
- logger.log_message(
123
- LogLevel::Watcher,
124
- "Watching for changes... Press Ctrl+C to exit.",
125
- );
126
-
127
- while rx.recv().is_ok() {
128
- logger.log_message(LogLevel::Watcher, "Change detected, rebuilding...");
129
-
130
- // Stop previous real-time runner before restarting playback
131
- stop_realtime_runner(&mut rt_runner);
132
-
133
- let (bpm, entry_stmts, variables, functions, global_store) =
134
- match process_play(&config, &entry_file, &output_path, debug) {
135
- Ok(v) => v,
136
- Err(e) => {
137
- logger.log_message(LogLevel::Error, &format!("Rebuild failed: {}", e));
138
- continue;
139
- }
140
- };
141
-
142
- logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
143
-
144
- audio_player.play_file_once(&audio_file);
145
- let loop_iters: usize = entry_stmts
146
- .iter()
147
- .map(|s| match &s.kind {
148
- crate::core::parser::statement::StatementKind::Loop => {
149
- use devalang_types::Value;
150
- if let Value::Map(m) = &s.value {
151
- if let Some(Value::Array(items)) = m.get("array") {
152
- items.len()
153
- } else if let Some(Value::Number(n)) = m.get("iterator") {
154
- (*n).max(0.0) as usize
155
- } else {
156
- 0
157
- }
158
- } else {
159
- 0
160
- }
161
- }
162
- _ => 0,
163
- })
164
- .sum();
165
- let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
166
- let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
167
- let total_secs = wav_duration_seconds(&audio_file)
168
- .unwrap_or(0.0)
169
- .max(est_by_len);
170
- rt_runner = Some(start_realtime_runner(
171
- RtContext {
172
- bpm,
173
- entry_stmts: entry_stmts.clone(),
174
- variables: variables.clone(),
175
- functions: functions.clone(),
176
- global_store: global_store.clone(),
177
- },
178
- total_secs,
179
- ));
180
- }
181
- } else if fetched_repeat {
182
- // Initial build to start from a clean slate
183
- let (bpm, entry_stmts, variables, functions, global_store) =
184
- process_play(&config, &entry_file, &output_path, debug)?;
185
-
186
- logger.log_message(LogLevel::Info, "🎵 Playback started (repeat mode)...");
187
-
188
- let mut last_snapshot = snapshot_files(&entry_path);
189
- let mut audio_player = AudioPlayer::new();
190
- audio_player.play_file_once(&audio_file);
191
-
192
- let loop_iters: usize = entry_stmts
193
- .iter()
194
- .map(|s| match &s.kind {
195
- crate::core::parser::statement::StatementKind::Loop => {
196
- use devalang_types::Value;
197
-
198
- if let Value::Map(m) = &s.value {
199
- if let Some(Value::Array(items)) = m.get("array") {
200
- items.len()
201
- } else if let Some(Value::Number(n)) = m.get("iterator") {
202
- (*n).max(0.0) as usize
203
- } else {
204
- 0
205
- }
206
- } else {
207
- 0
208
- }
209
- }
210
- _ => 0,
211
- })
212
- .sum();
213
- let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
214
- let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
215
- let total_secs = wav_duration_seconds(&audio_file)
216
- .unwrap_or(0.0)
217
- .max(est_by_len);
218
- let mut rt_runner = Some(start_realtime_runner(
219
- RtContext {
220
- bpm,
221
- entry_stmts: entry_stmts.clone(),
222
- variables: variables.clone(),
223
- functions: functions.clone(),
224
- global_store: global_store.clone(),
225
- },
226
- total_secs,
227
- ));
228
-
229
- loop {
230
- let current_snapshot = snapshot_files(&entry_path);
231
- let has_changed = files_changed(&last_snapshot, &current_snapshot);
232
-
233
- if has_changed {
234
- logger.log_message(
235
- LogLevel::Info,
236
- "Change detected, rebuilding in background...",
237
- );
238
- let entry_file = entry_file.clone();
239
- let output_path = output_path.clone();
240
- let config_clone = config.clone();
241
-
242
- // Rebuild in a separate thread
243
- std::thread::spawn(move || {
244
- if let Err(e) = process_play(&config_clone, &entry_file, &output_path, debug) {
245
- eprintln!("Rebuild failed in background: {}", e);
246
- }
247
- });
248
-
249
- last_snapshot = current_snapshot;
250
- }
251
-
252
- // Wait for the audio to finish
253
- audio_player.wait_until_end();
254
- // Stop the current real-time runner
255
- stop_realtime_runner(&mut rt_runner);
256
-
257
- // Then replay the audio (rebuilt or not)
258
- audio_player.play_file_once(&audio_file);
259
- let loop_iters: usize = entry_stmts
260
- .iter()
261
- .map(|s| match &s.kind {
262
- crate::core::parser::statement::StatementKind::Loop => {
263
- use devalang_types::Value;
264
- if let Value::Map(m) = &s.value {
265
- if let Some(Value::Array(items)) = m.get("array") {
266
- items.len()
267
- } else if let Some(Value::Number(n)) = m.get("iterator") {
268
- (*n).max(0.0) as usize
269
- } else {
270
- 0
271
- }
272
- } else {
273
- 0
274
- }
275
- }
276
- _ => 0,
277
- })
278
- .sum();
279
- let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
280
- let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
281
- let total_secs = wav_duration_seconds(&audio_file)
282
- .unwrap_or(0.0)
283
- .max(est_by_len);
284
- rt_runner = Some(start_realtime_runner(
285
- RtContext {
286
- bpm,
287
- entry_stmts: entry_stmts.clone(),
288
- variables: variables.clone(),
289
- functions: functions.clone(),
290
- global_store: global_store.clone(),
291
- },
292
- total_secs,
293
- ));
294
- }
295
- } else {
296
- // Single execution
297
- let (bpm, entry_stmts, variables, functions, global_store) =
298
- process_play(&config, &entry_file, &output_path, debug)?;
299
-
300
- logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
301
-
302
- audio_player.play_file_once(&audio_file);
303
-
304
- let est_by_len = ((60.0 / bpm).max(0.01) * (entry_stmts.len() as f32)).max(1.0);
305
- let total_secs = wav_duration_seconds(&audio_file)
306
- .unwrap_or(0.0)
307
- .max(est_by_len);
308
- let mut rt_runner = Some(start_realtime_runner(
309
- RtContext {
310
- bpm,
311
- entry_stmts,
312
- variables,
313
- functions,
314
- global_store,
315
- },
316
- total_secs,
317
- ));
318
-
319
- audio_player.wait_until_end();
320
- // Let the runner finish naturally to execute all remaining statements (e.g., loop prints)
321
- join_realtime_runner(&mut rt_runner);
322
- }
323
- Ok(())
324
- }
1
+ use crate::config::driver::ProjectConfig;
2
+ use devalang_utils::logger::{LogLevel, Logger};
3
+ use devalang_utils::path::{find_entry_file, normalize_path};
4
+ use std::{sync::mpsc::channel, thread};
5
+
6
+ pub use crate::cli::play::io::wav_duration_seconds;
7
+ pub use crate::cli::play::realtime::{
8
+ RtContext, join_realtime_runner, start_realtime_runner, stop_realtime_runner,
9
+ };
10
+
11
+ use super::process::process_play;
12
+ use super::utils::{files_changed, snapshot_files};
13
+
14
+ use crate::core::audio::player::AudioPlayer;
15
+
16
+ #[cfg(feature = "cli")]
17
+ pub fn handle_play_command(
18
+ config: Option<ProjectConfig>,
19
+ entry: Option<String>,
20
+ output: Option<String>,
21
+ audio_format: crate::cli::parser::AudioFormat,
22
+ sample_rate: u32,
23
+ watch: bool,
24
+ repeat: bool,
25
+ debug: bool,
26
+ ) -> Result<(), String> {
27
+ let logger = Logger::new();
28
+
29
+ // Determine final audio_format and sample_rate, preferring CLI values but falling back to config defaults
30
+ let mut final_audio_format = audio_format;
31
+ let mut final_sample_rate = sample_rate;
32
+ if let Some(cfg) = config.as_ref() {
33
+ if let Some(af) = cfg.defaults.audio_format.as_ref() {
34
+ let af_low = af.to_lowercase();
35
+ final_audio_format = match af_low.as_str() {
36
+ "wav24" => crate::cli::parser::AudioFormat::Wav24,
37
+ "wav32" => crate::cli::parser::AudioFormat::Wav32,
38
+ _ => crate::cli::parser::AudioFormat::Wav16,
39
+ };
40
+ }
41
+ if let Some(sr) = cfg.defaults.sample_rate {
42
+ // Only override if CLI provided a 0 or unrealistic value (we assume CLI provides a valid value)
43
+ if final_sample_rate == 0 {
44
+ final_sample_rate = sr;
45
+ }
46
+ }
47
+ }
48
+
49
+ let entry_path = entry
50
+ .or_else(|| config.as_ref().and_then(|c| c.defaults.entry.clone()))
51
+ .unwrap_or_default();
52
+
53
+ let output_path = output
54
+ .or_else(|| config.as_ref().and_then(|c| c.defaults.output.clone()))
55
+ .unwrap_or_default();
56
+
57
+ let fetched_repeat = if repeat {
58
+ true
59
+ } else {
60
+ config
61
+ .as_ref()
62
+ .and_then(|c| c.defaults.repeat)
63
+ .unwrap_or(false)
64
+ };
65
+
66
+ if entry_path.is_empty() || output_path.is_empty() {
67
+ logger.log_message(LogLevel::Error, "Entry or output path not specified.");
68
+ return Err("missing entry or output".to_string());
69
+ }
70
+
71
+ let entry_file = match find_entry_file(&entry_path) {
72
+ Some(p) => p,
73
+ None => {
74
+ logger.log_message(LogLevel::Error, "index.deva not found");
75
+ return Err("index.deva not found".to_string());
76
+ }
77
+ };
78
+
79
+ let audio_file = format!("{}/audio/index.wav", normalize_path(&output_path));
80
+ let mut audio_player = AudioPlayer::new();
81
+
82
+ if watch && fetched_repeat {
83
+ logger.log_message(
84
+ LogLevel::Error,
85
+ "Watch and repeat cannot be used together. Use repeat instead.",
86
+ );
87
+ return Err("invalid options: watch and repeat cannot be combined".to_string());
88
+ }
89
+
90
+ if watch {
91
+ let (tx, rx) = channel::<()>();
92
+
93
+ // Thread 1 : Watcher sending changes
94
+ let entry_clone = entry_path.clone();
95
+ thread::spawn(move || {
96
+ let _ = devalang_utils::watcher::watch_directory(entry_clone, move || {
97
+ let _ = tx.send(()); // signal a change
98
+ });
99
+ });
100
+
101
+ // Main thread: build + play in a loop
102
+ let (bpm, entry_stmts, variables, functions, global_store) = process_play(
103
+ &config,
104
+ &entry_file,
105
+ &output_path,
106
+ final_audio_format,
107
+ final_sample_rate,
108
+ debug,
109
+ )?;
110
+ audio_player.play_file_once(&audio_file);
111
+ // Estimate duration: base on statement count plus extra for loop iterations (1 beat per iter)
112
+ let loop_iters: usize = entry_stmts
113
+ .iter()
114
+ .map(|s| match &s.kind {
115
+ crate::core::parser::statement::StatementKind::Loop => {
116
+ use devalang_types::Value;
117
+ if let Value::Map(m) = &s.value {
118
+ if let Some(Value::Array(items)) = m.get("array") {
119
+ items.len()
120
+ } else if let Some(Value::Number(n)) = m.get("iterator") {
121
+ (*n).max(0.0) as usize
122
+ } else {
123
+ 0
124
+ }
125
+ } else {
126
+ 0
127
+ }
128
+ }
129
+ _ => 0,
130
+ })
131
+ .sum();
132
+ let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
133
+ let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
134
+ let total_secs = wav_duration_seconds(&audio_file)
135
+ .unwrap_or(0.0)
136
+ .max(est_by_len);
137
+ let mut rt_runner = Some(start_realtime_runner(
138
+ RtContext {
139
+ bpm,
140
+ entry_stmts,
141
+ variables,
142
+ functions,
143
+ global_store,
144
+ },
145
+ total_secs,
146
+ ));
147
+
148
+ logger.log_message(
149
+ LogLevel::Watcher,
150
+ "Watching for changes... Press Ctrl+C to exit.",
151
+ );
152
+
153
+ while rx.recv().is_ok() {
154
+ logger.log_message(LogLevel::Watcher, "Change detected, rebuilding...");
155
+
156
+ // Stop previous real-time runner before restarting playback
157
+ stop_realtime_runner(&mut rt_runner);
158
+
159
+ let (bpm, entry_stmts, variables, functions, global_store) = match process_play(
160
+ &config,
161
+ &entry_file,
162
+ &output_path,
163
+ final_audio_format,
164
+ final_sample_rate,
165
+ debug,
166
+ ) {
167
+ Ok(v) => v,
168
+ Err(e) => {
169
+ logger.log_message(LogLevel::Error, &format!("Rebuild failed: {}", e));
170
+ continue;
171
+ }
172
+ };
173
+
174
+ logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
175
+
176
+ audio_player.play_file_once(&audio_file);
177
+ let loop_iters: usize = entry_stmts
178
+ .iter()
179
+ .map(|s| match &s.kind {
180
+ crate::core::parser::statement::StatementKind::Loop => {
181
+ use devalang_types::Value;
182
+ if let Value::Map(m) = &s.value {
183
+ if let Some(Value::Array(items)) = m.get("array") {
184
+ items.len()
185
+ } else if let Some(Value::Number(n)) = m.get("iterator") {
186
+ (*n).max(0.0) as usize
187
+ } else {
188
+ 0
189
+ }
190
+ } else {
191
+ 0
192
+ }
193
+ }
194
+ _ => 0,
195
+ })
196
+ .sum();
197
+ let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
198
+ let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
199
+ let total_secs = wav_duration_seconds(&audio_file)
200
+ .unwrap_or(0.0)
201
+ .max(est_by_len);
202
+ rt_runner = Some(start_realtime_runner(
203
+ RtContext {
204
+ bpm,
205
+ entry_stmts: entry_stmts.clone(),
206
+ variables: variables.clone(),
207
+ functions: functions.clone(),
208
+ global_store: global_store.clone(),
209
+ },
210
+ total_secs,
211
+ ));
212
+ }
213
+ } else if fetched_repeat {
214
+ // Initial build to start from a clean slate
215
+ let (bpm, entry_stmts, variables, functions, global_store) = process_play(
216
+ &config,
217
+ &entry_file,
218
+ &output_path,
219
+ final_audio_format,
220
+ final_sample_rate,
221
+ debug,
222
+ )?;
223
+
224
+ logger.log_message(LogLevel::Info, "🎵 Playback started (repeat mode)...");
225
+
226
+ let mut last_snapshot = snapshot_files(&entry_path);
227
+ let mut audio_player = AudioPlayer::new();
228
+ audio_player.play_file_once(&audio_file);
229
+
230
+ let loop_iters: usize = entry_stmts
231
+ .iter()
232
+ .map(|s| match &s.kind {
233
+ crate::core::parser::statement::StatementKind::Loop => {
234
+ use devalang_types::Value;
235
+
236
+ if let Value::Map(m) = &s.value {
237
+ if let Some(Value::Array(items)) = m.get("array") {
238
+ items.len()
239
+ } else if let Some(Value::Number(n)) = m.get("iterator") {
240
+ (*n).max(0.0) as usize
241
+ } else {
242
+ 0
243
+ }
244
+ } else {
245
+ 0
246
+ }
247
+ }
248
+ _ => 0,
249
+ })
250
+ .sum();
251
+ let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
252
+ let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
253
+ let total_secs = wav_duration_seconds(&audio_file)
254
+ .unwrap_or(0.0)
255
+ .max(est_by_len);
256
+ let mut rt_runner = Some(start_realtime_runner(
257
+ RtContext {
258
+ bpm,
259
+ entry_stmts: entry_stmts.clone(),
260
+ variables: variables.clone(),
261
+ functions: functions.clone(),
262
+ global_store: global_store.clone(),
263
+ },
264
+ total_secs,
265
+ ));
266
+
267
+ loop {
268
+ let current_snapshot = snapshot_files(&entry_path);
269
+ let has_changed = files_changed(&last_snapshot, &current_snapshot);
270
+
271
+ if has_changed {
272
+ logger.log_message(
273
+ LogLevel::Info,
274
+ "Change detected, rebuilding in background...",
275
+ );
276
+ let entry_file = entry_file.clone();
277
+ let output_path = output_path.clone();
278
+ let config_clone = config.clone();
279
+
280
+ // Rebuild in a separate thread
281
+ std::thread::spawn(move || {
282
+ if let Err(e) = process_play(
283
+ &config_clone,
284
+ &entry_file,
285
+ &output_path,
286
+ final_audio_format,
287
+ final_sample_rate,
288
+ debug,
289
+ ) {
290
+ eprintln!("Rebuild failed in background: {}", e);
291
+ }
292
+ });
293
+
294
+ last_snapshot = current_snapshot;
295
+ }
296
+
297
+ // Wait for the audio to finish
298
+ audio_player.wait_until_end();
299
+ // Stop the current real-time runner
300
+ stop_realtime_runner(&mut rt_runner);
301
+
302
+ // Then replay the audio (rebuilt or not)
303
+ audio_player.play_file_once(&audio_file);
304
+ let loop_iters: usize = entry_stmts
305
+ .iter()
306
+ .map(|s| match &s.kind {
307
+ crate::core::parser::statement::StatementKind::Loop => {
308
+ use devalang_types::Value;
309
+ if let Value::Map(m) = &s.value {
310
+ if let Some(Value::Array(items)) = m.get("array") {
311
+ items.len()
312
+ } else if let Some(Value::Number(n)) = m.get("iterator") {
313
+ (*n).max(0.0) as usize
314
+ } else {
315
+ 0
316
+ }
317
+ } else {
318
+ 0
319
+ }
320
+ }
321
+ _ => 0,
322
+ })
323
+ .sum();
324
+ let est_beats = (entry_stmts.len() as f32) + (loop_iters as f32);
325
+ let est_by_len = ((60.0 / bpm).max(0.01) * est_beats).max(1.0);
326
+ let total_secs = wav_duration_seconds(&audio_file)
327
+ .unwrap_or(0.0)
328
+ .max(est_by_len);
329
+ rt_runner = Some(start_realtime_runner(
330
+ RtContext {
331
+ bpm,
332
+ entry_stmts: entry_stmts.clone(),
333
+ variables: variables.clone(),
334
+ functions: functions.clone(),
335
+ global_store: global_store.clone(),
336
+ },
337
+ total_secs,
338
+ ));
339
+ }
340
+ } else {
341
+ // Single execution
342
+ let (bpm, entry_stmts, variables, functions, global_store) = process_play(
343
+ &config,
344
+ &entry_file,
345
+ &output_path,
346
+ final_audio_format,
347
+ final_sample_rate,
348
+ debug,
349
+ )?;
350
+
351
+ logger.log_message(LogLevel::Info, "🎵 Playback started (once mode)...");
352
+
353
+ audio_player.play_file_once(&audio_file);
354
+
355
+ let est_by_len = ((60.0 / bpm).max(0.01) * (entry_stmts.len() as f32)).max(1.0);
356
+ let total_secs = wav_duration_seconds(&audio_file)
357
+ .unwrap_or(0.0)
358
+ .max(est_by_len);
359
+ let mut rt_runner = Some(start_realtime_runner(
360
+ RtContext {
361
+ bpm,
362
+ entry_stmts,
363
+ variables,
364
+ functions,
365
+ global_store,
366
+ },
367
+ total_secs,
368
+ ));
369
+
370
+ audio_player.wait_until_end();
371
+ // Let the runner finish naturally to execute all remaining statements (e.g., loop prints)
372
+ join_realtime_runner(&mut rt_runner);
373
+ }
374
+ Ok(())
375
+ }