devalang_wasm/engine/special_vars/
mod.rs1use crate::language::syntax::ast::Value;
4use std::collections::HashMap;
5
6#[cfg(feature = "cli")]
8fn gen_random_f32() -> f32 {
9 rand::random::<f32>()
10}
11
12#[cfg(feature = "wasm")]
13fn gen_random_f32() -> f32 {
14 let mut buf = [0u8; 4];
15 getrandom::getrandom(&mut buf).unwrap_or_default();
16 let val = u32::from_le_bytes(buf);
17 (val as f32) / (u32::MAX as f32)
18}
19
20#[cfg(feature = "cli")]
21fn gen_random_u32() -> u32 {
22 rand::random::<u32>()
23}
24
25#[cfg(feature = "wasm")]
26fn gen_random_u32() -> u32 {
27 let mut buf = [0u8; 4];
28 getrandom::getrandom(&mut buf).unwrap_or_default();
29 u32::from_le_bytes(buf)
30}
31
32#[cfg(feature = "cli")]
33fn gen_random_bool() -> bool {
34 rand::random::<bool>()
35}
36
37#[cfg(feature = "wasm")]
38fn gen_random_bool() -> bool {
39 gen_random_u32() % 2 == 0
40}
41
42#[cfg(feature = "cli")]
43pub fn gen_range_f32(min: f32, max: f32) -> f32 {
44 use rand::Rng;
45 let mut rng = rand::thread_rng();
46 rng.gen_range(min..max)
47}
48
49#[cfg(feature = "wasm")]
50pub fn gen_range_f32(min: f32, max: f32) -> f32 {
51 min + gen_random_f32() * (max - min)
52}
53
54pub const SPECIAL_VAR_PREFIX: char = '$';
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum SpecialVarCategory {
60 Time, Random, Music, Position, Midi, System, }
67
68#[derive(Debug, Clone)]
70pub struct SpecialVarContext {
71 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, }
81
82impl Default for SpecialVarContext {
83 fn default() -> Self {
84 Self {
85 current_time: 0.0,
86 current_beat: 0.0,
87 current_bar: 0.0,
88 bpm: 120.0,
89 duration: 0.5,
90 sample_rate: 44100,
91 channels: 2,
92 position: 0.0,
93 total_duration: 0.0,
94 }
95 }
96}
97
98impl SpecialVarContext {
99 pub fn new(bpm: f32, sample_rate: u32) -> Self {
100 Self {
101 bpm,
102 duration: 60.0 / bpm,
103 sample_rate,
104 ..Default::default()
105 }
106 }
107
108 pub fn update_time(&mut self, time: f32) {
110 self.current_time = time;
111 self.current_beat = time / self.duration;
112 self.current_bar = self.current_beat / 4.0;
113
114 if self.total_duration > 0.0 {
115 self.position = (time / self.total_duration).clamp(0.0, 1.0);
116 }
117 }
118
119 pub fn update_bpm(&mut self, bpm: f32) {
121 self.bpm = bpm;
122 self.duration = 60.0 / bpm;
123 }
124}
125
126pub fn is_special_var(name: &str) -> bool {
128 name.starts_with(SPECIAL_VAR_PREFIX)
129}
130
131pub fn resolve_special_var(name: &str, context: &SpecialVarContext) -> Option<Value> {
133 if !is_special_var(name) {
134 return None;
135 }
136
137 let eps = 1e-6_f32;
141
142 match name {
143 "$time" => Some(Value::Number(context.current_time)),
145 "$beat" => Some(Value::Number(context.current_beat)),
146 "$bar" => Some(Value::Number((context.current_bar + eps).floor() + 1.0)),
149 "$currentTime" => Some(Value::Number(context.current_time)),
150 "$currentBeat" => Some(Value::Number(context.current_beat)),
151 "$currentBar" => Some(Value::Number((context.current_bar + eps).floor() + 1.0)),
152
153 "$bpm" => Some(Value::Number(context.bpm)),
155 "$tempo" => Some(Value::Number(context.bpm)),
156 "$duration" => Some(Value::Number(context.duration)),
157
158 "$position" => Some(Value::Number(context.position)),
160 "$progress" => Some(Value::Number(context.position)),
161
162 "$sampleRate" => Some(Value::Number(context.sample_rate as f32)),
164 "$channels" => Some(Value::Number(context.channels as f32)),
165
166 #[cfg(any(feature = "cli", feature = "wasm"))]
168 "$random" | "$random.float" => Some(Value::Number(gen_random_f32())),
169 #[cfg(any(feature = "cli", feature = "wasm"))]
170 "$random.noise" => Some(Value::Number(gen_random_f32() * 2.0 - 1.0)), #[cfg(any(feature = "cli", feature = "wasm"))]
172 "$random.int" => Some(Value::Number((gen_random_u32() % 100) as f32)),
173 #[cfg(any(feature = "cli", feature = "wasm"))]
174 "$random.bool" => Some(Value::Boolean(gen_random_bool())),
175
176 #[cfg(any(feature = "cli", feature = "wasm"))]
178 _ if name.starts_with("$random.range(") => {
179 parse_random_range(name)
181 }
182
183 _ => None,
184 }
185}
186
187#[cfg(any(feature = "cli", feature = "wasm"))]
189fn parse_random_range(name: &str) -> Option<Value> {
190 let start = name.find('(')?;
192 let end = name.rfind(')')?;
193 let content = &name[start + 1..end];
194
195 let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
197 if parts.len() != 2 {
198 return None;
199 }
200
201 let min: f32 = parts[0].parse().ok()?;
203 let max: f32 = parts[1].parse().ok()?;
204
205 let value = min + gen_random_f32() * (max - min);
207 Some(Value::Number(value))
208}
209
210pub fn get_all_special_vars(context: &SpecialVarContext) -> HashMap<String, Value> {
212 let mut vars = HashMap::new();
213
214 let eps = 1e-6_f32;
216
217 vars.insert("$time".to_string(), Value::Number(context.current_time));
219 vars.insert("$beat".to_string(), Value::Number(context.current_beat));
220 vars.insert(
222 "$bar".to_string(),
223 Value::Number((context.current_bar + eps).floor() + 1.0),
224 );
225 vars.insert(
226 "$currentTime".to_string(),
227 Value::Number(context.current_time),
228 );
229 vars.insert(
230 "$currentBeat".to_string(),
231 Value::Number(context.current_beat),
232 );
233 vars.insert(
234 "$currentBar".to_string(),
235 Value::Number((context.current_bar + eps).floor() + 1.0),
236 );
237
238 vars.insert("$bpm".to_string(), Value::Number(context.bpm));
240 vars.insert("$tempo".to_string(), Value::Number(context.bpm));
241 vars.insert("$duration".to_string(), Value::Number(context.duration));
242
243 vars.insert("$position".to_string(), Value::Number(context.position));
245 vars.insert("$progress".to_string(), Value::Number(context.position));
246
247 vars.insert(
249 "$sampleRate".to_string(),
250 Value::Number(context.sample_rate as f32),
251 );
252 vars.insert(
253 "$channels".to_string(),
254 Value::Number(context.channels as f32),
255 );
256
257 vars
258}
259
260pub fn list_special_vars() -> HashMap<&'static str, Vec<(&'static str, &'static str)>> {
262 let mut categories = HashMap::new();
263
264 categories.insert(
265 "Time",
266 vec![
267 ("$time", "Current time in seconds"),
268 ("$beat", "Current beat position"),
269 ("$bar", "Current bar position"),
270 ("$currentTime", "Alias for $time"),
271 ("$currentBeat", "Alias for $beat"),
272 ("$currentBar", "Alias for $bar"),
273 ],
274 );
275
276 categories.insert(
277 "Music",
278 vec![
279 ("$bpm", "Current BPM"),
280 ("$tempo", "Alias for $bpm"),
281 ("$duration", "Beat duration in seconds"),
282 ],
283 );
284
285 categories.insert(
286 "Position",
287 vec![
288 ("$position", "Normalized position (0.0-1.0)"),
289 ("$progress", "Alias for $position"),
290 ],
291 );
292
293 categories.insert(
294 "Random",
295 vec![
296 ("$random", "Random float 0.0-1.0"),
297 ("$random.float", "Random float 0.0-1.0"),
298 ("$random.noise", "Random float -1.0 to 1.0"),
299 ("$random.int", "Random integer 0-99"),
300 ("$random.bool", "Random boolean"),
301 ("$random.range(min, max)", "Random float in range"),
302 ],
303 );
304
305 categories.insert(
306 "System",
307 vec![
308 ("$sampleRate", "Sample rate in Hz"),
309 ("$channels", "Number of audio channels"),
310 ],
311 );
312
313 categories
314}
315
316#[cfg(test)]
317#[path = "test_special_vars.rs"]
318mod tests;