devalang_wasm/engine/audio/interpreter/driver/
patterns.rs1use anyhow::Result;
2use std::collections::HashMap;
3
4use crate::engine::audio::events::AudioEvent;
5use crate::language::syntax::ast::Value;
6
7use super::AudioInterpreter;
8
9pub fn extract_pattern_data(
10 _interpreter: &AudioInterpreter,
11 value: &Value,
12) -> (Option<String>, Option<HashMap<String, f32>>) {
13 match value {
14 Value::String(pattern) => (Some(pattern.clone()), None),
15 Value::Map(map) => {
16 let pattern = map.get("pattern").and_then(|v| {
17 if let Value::String(s) = v {
18 Some(s.clone())
19 } else {
20 None
21 }
22 });
23
24 let mut options = HashMap::new();
25 for (key, val) in map.iter() {
26 if key != "pattern" {
27 if let Value::Number(num) = val {
28 options.insert(key.clone(), *num);
29 }
30 }
31 }
32
33 let opts = if options.is_empty() {
34 None
35 } else {
36 Some(options)
37 };
38 (pattern, opts)
39 }
40 _ => (None, None),
41 }
42}
43
44pub fn execute_pattern(
45 interpreter: &mut AudioInterpreter,
46 target: &str,
47 pattern: &str,
48 options: Option<HashMap<String, f32>>,
49) -> Result<()> {
50 execute_pattern_with_source(interpreter, target, pattern, options, None)
51}
52
53pub fn execute_pattern_with_source(
54 interpreter: &mut AudioInterpreter,
55 target: &str,
56 pattern: &str,
57 options: Option<HashMap<String, f32>>,
58 source: Option<&str>,
59) -> Result<()> {
60 let swing = options
61 .as_ref()
62 .and_then(|o| o.get("swing").copied())
63 .unwrap_or(0.0);
64 let humanize = options
65 .as_ref()
66 .and_then(|o| o.get("humanize").copied())
67 .unwrap_or(0.0);
68 let velocity_mult = options
69 .as_ref()
70 .and_then(|o| o.get("velocity").copied())
71 .unwrap_or(1.0);
72 let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
73
74 let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
75 let resolved_uri = resolve_sample_uri(interpreter, target);
76
77 let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
78 let step_count = pattern_chars.len() as f32;
79 if step_count == 0.0 {
80 return Ok(());
81 }
82
83 let beat_duration =
84 crate::engine::audio::duration::seconds_per_beat(effective_bpm).unwrap_or(0.0);
85 let bar_duration = beat_duration * 4.0;
86 let step_duration = if step_count > 0.0 {
87 bar_duration / step_count
88 } else {
89 0.0
90 };
91
92 for (i, &ch) in pattern_chars.iter().enumerate() {
93 let is_hit = match ch {
94 'x' | 'X' | 'o' => true,
95 _ => false,
96 };
97
98 if is_hit {
99 let mut time = interpreter.cursor_time + (i as f32 * step_duration);
100 if swing > 0.0 && i % 2 == 1 {
101 time += step_duration * swing;
102 }
103
104 #[cfg(any(feature = "cli", feature = "wasm"))]
105 if humanize > 0.0 {
106 let offset = crate::engine::special_vars::gen_range_f32(-humanize, humanize);
107 time += offset;
108 }
109
110 let velocity_modifier = match ch {
113 'x' => 1.0, 'X' => 1.3, 'o' => 0.6, _ => 1.0,
117 };
118
119 let event = AudioEvent::Sample {
120 node_id: interpreter.resolve_event_node_id(source.unwrap_or(target)),
121 uri: resolved_uri.clone(),
122 start_time: time,
123 velocity: velocity_mult * velocity_modifier,
124 effects: None,
125 source: source.map(|s| s.to_string()),
126 };
127 interpreter.events.events.push(event);
128 }
129 }
130
131 interpreter.cursor_time += bar_duration;
132 Ok(())
133}
134
135pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
136 if let Some(dot_pos) = target.find('.') {
137 let bank_alias = &target[..dot_pos];
138 let trigger_name = &target[dot_pos + 1..];
139 if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
140 if let Some(Value::String(bank_name)) = bank_map.get("_name") {
141 return format!("devalang://bank/{}/{}", bank_name, trigger_name);
142 }
143 }
144 }
145 target.to_string()
146}