use crate::language::syntax::ast::Value;
use std::collections::HashMap;
#[cfg(feature = "cli")]
fn gen_random_f32() -> f32 {
rand::random::<f32>()
}
#[cfg(all(feature = "wasm", not(feature = "cli")))]
fn gen_random_f32() -> f32 {
let mut buf = [0u8; 4];
getrandom::getrandom(&mut buf).unwrap_or_default();
let val = u32::from_le_bytes(buf);
(val as f32) / (u32::MAX as f32)
}
#[cfg(feature = "cli")]
fn gen_random_u32() -> u32 {
rand::random::<u32>()
}
#[cfg(all(feature = "wasm", not(feature = "cli")))]
fn gen_random_u32() -> u32 {
let mut buf = [0u8; 4];
getrandom::getrandom(&mut buf).unwrap_or_default();
u32::from_le_bytes(buf)
}
#[cfg(feature = "cli")]
fn gen_random_bool() -> bool {
rand::random::<bool>()
}
#[cfg(all(feature = "wasm", not(feature = "cli")))]
fn gen_random_bool() -> bool {
gen_random_u32() % 2 == 0
}
#[cfg(feature = "cli")]
pub fn gen_range_f32(min: f32, max: f32) -> f32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(min..max)
}
#[cfg(all(feature = "wasm", not(feature = "cli")))]
pub fn gen_range_f32(min: f32, max: f32) -> f32 {
min + gen_random_f32() * (max - min)
}
pub const SPECIAL_VAR_PREFIX: char = '$';
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpecialVarCategory {
Time, Random, Music, Position, Midi, System, }
#[derive(Debug, Clone)]
pub struct SpecialVarContext {
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, }
impl Default for SpecialVarContext {
fn default() -> Self {
Self {
current_time: 0.0,
current_beat: 0.0,
current_bar: 0.0,
bpm: 120.0,
duration: 0.5,
sample_rate: 44100,
channels: 2,
position: 0.0,
total_duration: 0.0,
}
}
}
impl SpecialVarContext {
pub fn new(bpm: f32, sample_rate: u32) -> Self {
Self {
bpm,
duration: crate::engine::audio::duration::seconds_per_beat(bpm).unwrap_or(0.0),
sample_rate,
..Default::default()
}
}
pub fn update_time(&mut self, time: f32) {
self.current_time = time;
self.current_beat = time / self.duration;
self.current_bar = self.current_beat / 4.0;
if self.total_duration > 0.0 {
self.position = (time / self.total_duration).clamp(0.0, 1.0);
}
}
pub fn update_bpm(&mut self, bpm: f32) {
self.bpm = bpm;
self.duration = crate::engine::audio::duration::seconds_per_beat(bpm).unwrap_or(0.0);
}
}
pub fn is_special_var(name: &str) -> bool {
name.starts_with(SPECIAL_VAR_PREFIX)
}
pub fn resolve_special_var(name: &str, context: &SpecialVarContext) -> Option<Value> {
if !is_special_var(name) {
return None;
}
let eps = 1e-6_f32;
match name {
"$time" => Some(Value::Number(context.current_time)),
"$beat" => Some(Value::Number(context.current_beat)),
"$bar" => Some(Value::Number((context.current_bar + eps).floor() + 1.0)),
"$currentTime" => Some(Value::Number(context.current_time)),
"$currentBeat" => Some(Value::Number(context.current_beat)),
"$currentBar" => Some(Value::Number((context.current_bar + eps).floor() + 1.0)),
"$bpm" => Some(Value::Number(context.bpm)),
"$tempo" => Some(Value::Number(context.bpm)),
"$duration" => Some(Value::Number(context.duration)),
"$position" => Some(Value::Number(context.position)),
"$progress" => Some(Value::Number(context.position)),
"$sampleRate" => Some(Value::Number(context.sample_rate as f32)),
"$channels" => Some(Value::Number(context.channels as f32)),
#[cfg(any(feature = "cli", feature = "wasm"))]
"$random" | "$random.float" => Some(Value::Number(gen_random_f32())),
#[cfg(any(feature = "cli", feature = "wasm"))]
"$random.noise" => Some(Value::Number(gen_random_f32() * 2.0 - 1.0)), #[cfg(any(feature = "cli", feature = "wasm"))]
"$random.int" => Some(Value::Number((gen_random_u32() % 100) as f32)),
#[cfg(any(feature = "cli", feature = "wasm"))]
"$random.bool" => Some(Value::Boolean(gen_random_bool())),
#[cfg(any(feature = "cli", feature = "wasm"))]
_ if name.starts_with("$random.range(") => {
parse_random_range(name)
}
_ => None,
}
}
#[cfg(any(feature = "cli", feature = "wasm"))]
fn parse_random_range(name: &str) -> Option<Value> {
let start = name.find('(')?;
let end = name.rfind(')')?;
let content = &name[start + 1..end];
let parts: Vec<&str> = content.split(',').map(|s| s.trim()).collect();
if parts.len() != 2 {
return None;
}
let min: f32 = parts[0].parse().ok()?;
let max: f32 = parts[1].parse().ok()?;
let value = min + gen_random_f32() * (max - min);
Some(Value::Number(value))
}
pub fn get_all_special_vars(context: &SpecialVarContext) -> HashMap<String, Value> {
let mut vars = HashMap::new();
let eps = 1e-6_f32;
vars.insert("$time".to_string(), Value::Number(context.current_time));
vars.insert("$beat".to_string(), Value::Number(context.current_beat));
vars.insert(
"$bar".to_string(),
Value::Number((context.current_bar + eps).floor() + 1.0),
);
vars.insert(
"$currentTime".to_string(),
Value::Number(context.current_time),
);
vars.insert(
"$currentBeat".to_string(),
Value::Number(context.current_beat),
);
vars.insert(
"$currentBar".to_string(),
Value::Number((context.current_bar + eps).floor() + 1.0),
);
vars.insert("$bpm".to_string(), Value::Number(context.bpm));
vars.insert("$tempo".to_string(), Value::Number(context.bpm));
vars.insert("$duration".to_string(), Value::Number(context.duration));
vars.insert("$position".to_string(), Value::Number(context.position));
vars.insert("$progress".to_string(), Value::Number(context.position));
vars.insert(
"$sampleRate".to_string(),
Value::Number(context.sample_rate as f32),
);
vars.insert(
"$channels".to_string(),
Value::Number(context.channels as f32),
);
vars
}
pub fn list_special_vars() -> HashMap<&'static str, Vec<(&'static str, &'static str)>> {
let mut categories = HashMap::new();
categories.insert(
"Time",
vec![
("$time", "Current time in seconds"),
("$beat", "Current beat position"),
("$bar", "Current bar position"),
("$currentTime", "Alias for $time"),
("$currentBeat", "Alias for $beat"),
("$currentBar", "Alias for $bar"),
],
);
categories.insert(
"Music",
vec![
("$bpm", "Current BPM"),
("$tempo", "Alias for $bpm"),
("$duration", "Beat duration in seconds"),
],
);
categories.insert(
"Position",
vec![
("$position", "Normalized position (0.0-1.0)"),
("$progress", "Alias for $position"),
],
);
categories.insert(
"Random",
vec![
("$random", "Random float 0.0-1.0"),
("$random.float", "Random float 0.0-1.0"),
("$random.noise", "Random float -1.0 to 1.0"),
("$random.int", "Random integer 0-99"),
("$random.bool", "Random boolean"),
("$random.range(min, max)", "Random float in range"),
],
);
categories.insert(
"System",
vec![
("$sampleRate", "Sample rate in Hz"),
("$channels", "Number of audio channels"),
],
);
categories
}
#[cfg(test)]
#[path = "test_special_vars.rs"]
mod tests;