devalang_wasm/engine/special_vars/
mod.rs1use crate::language::syntax::ast::Value;
4use std::collections::HashMap;
5
6pub const SPECIAL_VAR_PREFIX: char = '$';
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SpecialVarCategory {
12 Time, Random, Music, Position, Midi, System, }
19
20#[derive(Debug, Clone)]
22pub struct SpecialVarContext {
23 pub current_time: f32, pub current_beat: f32, pub current_bar: f32, pub bpm: f32, pub duration: f32, pub sample_rate: u32, pub channels: usize, pub position: f32, pub total_duration: f32, }
33
34impl Default for SpecialVarContext {
35 fn default() -> Self {
36 Self {
37 current_time: 0.0,
38 current_beat: 0.0,
39 current_bar: 0.0,
40 bpm: 120.0,
41 duration: 0.5,
42 sample_rate: 44100,
43 channels: 2,
44 position: 0.0,
45 total_duration: 0.0,
46 }
47 }
48}
49
50impl SpecialVarContext {
51 pub fn new(bpm: f32, sample_rate: u32) -> Self {
52 Self {
53 bpm,
54 duration: 60.0 / bpm,
55 sample_rate,
56 ..Default::default()
57 }
58 }
59
60 pub fn update_time(&mut self, time: f32) {
62 self.current_time = time;
63 self.current_beat = time / self.duration;
64 self.current_bar = self.current_beat / 4.0;
65
66 if self.total_duration > 0.0 {
67 self.position = (time / self.total_duration).clamp(0.0, 1.0);
68 }
69 }
70
71 pub fn update_bpm(&mut self, bpm: f32) {
73 self.bpm = bpm;
74 self.duration = 60.0 / bpm;
75 }
76}
77
78pub fn is_special_var(name: &str) -> bool {
80 name.starts_with(SPECIAL_VAR_PREFIX)
81}
82
83pub fn resolve_special_var(name: &str, context: &SpecialVarContext) -> Option<Value> {
85 if !is_special_var(name) {
86 return None;
87 }
88
89 match name {
90 "$time" => Some(Value::Number(context.current_time)),
92 "$beat" => Some(Value::Number(context.current_beat)),
93 "$bar" => Some(Value::Number(context.current_bar)),
94 "$currentTime" => Some(Value::Number(context.current_time)),
95 "$currentBeat" => Some(Value::Number(context.current_beat)),
96 "$currentBar" => Some(Value::Number(context.current_bar)),
97
98 "$bpm" => Some(Value::Number(context.bpm)),
100 "$tempo" => Some(Value::Number(context.bpm)),
101 "$duration" => Some(Value::Number(context.duration)),
102
103 "$position" => Some(Value::Number(context.position)),
105 "$progress" => Some(Value::Number(context.position)),
106
107 "$sampleRate" => Some(Value::Number(context.sample_rate as f32)),
109 "$channels" => Some(Value::Number(context.channels as f32)),
110
111 #[cfg(any(feature = "cli", feature = "wasm"))]
113 "$random" | "$random.float" => Some(Value::Number(rand::random::<f32>())),
114 #[cfg(any(feature = "cli", feature = "wasm"))]
115 "$random.noise" => Some(Value::Number(rand::random::<f32>() * 2.0 - 1.0)), #[cfg(any(feature = "cli", feature = "wasm"))]
117 "$random.int" => Some(Value::Number((rand::random::<u32>() % 100) as f32)),
118 #[cfg(any(feature = "cli", feature = "wasm"))]
119 "$random.bool" => Some(Value::Boolean(rand::random::<bool>())),
120
121 #[cfg(any(feature = "cli", feature = "wasm"))]
123 _ if name.starts_with("$random.range(") => {
124 parse_random_range(name)
126 }
127
128 _ => None,
129 }
130}
131
132#[cfg(any(feature = "cli", feature = "wasm"))]
134fn parse_random_range(name: &str) -> Option<Value> {
135 let start = name.find('(')?;
137 let end = name.rfind(')')?;
138 let content = &name[start + 1..end];
139
140 let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
142 if parts.len() != 2 {
143 return None;
144 }
145
146 let min: f32 = parts[0].parse().ok()?;
148 let max: f32 = parts[1].parse().ok()?;
149
150 let value = min + rand::random::<f32>() * (max - min);
152 Some(Value::Number(value))
153}
154
155pub fn get_all_special_vars(context: &SpecialVarContext) -> HashMap<String, Value> {
157 let mut vars = HashMap::new();
158
159 vars.insert("$time".to_string(), Value::Number(context.current_time));
161 vars.insert("$beat".to_string(), Value::Number(context.current_beat));
162 vars.insert("$bar".to_string(), Value::Number(context.current_bar));
163 vars.insert(
164 "$currentTime".to_string(),
165 Value::Number(context.current_time),
166 );
167 vars.insert(
168 "$currentBeat".to_string(),
169 Value::Number(context.current_beat),
170 );
171 vars.insert(
172 "$currentBar".to_string(),
173 Value::Number(context.current_bar),
174 );
175
176 vars.insert("$bpm".to_string(), Value::Number(context.bpm));
178 vars.insert("$tempo".to_string(), Value::Number(context.bpm));
179 vars.insert("$duration".to_string(), Value::Number(context.duration));
180
181 vars.insert("$position".to_string(), Value::Number(context.position));
183 vars.insert("$progress".to_string(), Value::Number(context.position));
184
185 vars.insert(
187 "$sampleRate".to_string(),
188 Value::Number(context.sample_rate as f32),
189 );
190 vars.insert(
191 "$channels".to_string(),
192 Value::Number(context.channels as f32),
193 );
194
195 vars
196}
197
198pub fn list_special_vars() -> HashMap<&'static str, Vec<(&'static str, &'static str)>> {
200 let mut categories = HashMap::new();
201
202 categories.insert(
203 "Time",
204 vec![
205 ("$time", "Current time in seconds"),
206 ("$beat", "Current beat position"),
207 ("$bar", "Current bar position"),
208 ("$currentTime", "Alias for $time"),
209 ("$currentBeat", "Alias for $beat"),
210 ("$currentBar", "Alias for $bar"),
211 ],
212 );
213
214 categories.insert(
215 "Music",
216 vec![
217 ("$bpm", "Current BPM"),
218 ("$tempo", "Alias for $bpm"),
219 ("$duration", "Beat duration in seconds"),
220 ],
221 );
222
223 categories.insert(
224 "Position",
225 vec![
226 ("$position", "Normalized position (0.0-1.0)"),
227 ("$progress", "Alias for $position"),
228 ],
229 );
230
231 categories.insert(
232 "Random",
233 vec![
234 ("$random", "Random float 0.0-1.0"),
235 ("$random.float", "Random float 0.0-1.0"),
236 ("$random.noise", "Random float -1.0 to 1.0"),
237 ("$random.int", "Random integer 0-99"),
238 ("$random.bool", "Random boolean"),
239 ("$random.range(min, max)", "Random float in range"),
240 ],
241 );
242
243 categories.insert(
244 "System",
245 vec![
246 ("$sampleRate", "Sample rate in Hz"),
247 ("$channels", "Number of audio channels"),
248 ],
249 );
250
251 categories
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_is_special_var() {
260 assert!(is_special_var("$time"));
261 assert!(is_special_var("$beat"));
262 assert!(is_special_var("$random"));
263 assert!(!is_special_var("time"));
264 assert!(!is_special_var("myVar"));
265 }
266
267 #[test]
268 fn test_resolve_time_vars() {
269 let mut context = SpecialVarContext::default();
270 context.update_time(2.0);
271
272 let time = resolve_special_var("$time", &context);
273 assert_eq!(time, Some(Value::Number(2.0)));
274
275 let beat = resolve_special_var("$beat", &context);
276 assert!(matches!(beat, Some(Value::Number(_))));
277 }
278
279 #[test]
280 fn test_resolve_random_vars() {
281 let context = SpecialVarContext::default();
282
283 let rand1 = resolve_special_var("$random", &context);
284 assert!(matches!(rand1, Some(Value::Number(_))));
285
286 let rand2 = resolve_special_var("$random.noise", &context);
287 assert!(matches!(rand2, Some(Value::Number(_))));
288 }
289
290 #[test]
291 fn test_context_update_time() {
292 let mut context = SpecialVarContext::new(120.0, 44100);
293 context.update_time(1.0);
294
295 assert_eq!(context.current_time, 1.0);
296 assert!(context.current_beat > 0.0);
297 }
298
299 #[test]
300 fn test_parse_random_range() {
301 let result = parse_random_range("$random.range(0, 10)");
302 assert!(result.is_some());
303
304 if let Some(Value::Number(n)) = result {
305 assert!(n >= 0.0 && n <= 10.0);
306 }
307 }
308}