Skip to main content

devalang_wasm/engine/audio/interpreter/driver/
patterns.rs

1use 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            // v0.2.0: Support velocity variations based on symbol
111            // x = normal (1.0), X = accent (1.3), o = soft (0.6), . = visual (skip)
112            let velocity_modifier = match ch {
113                'x' => 1.0, // Normal hit
114                'X' => 1.3, // Accent (louder, may saturate)
115                'o' => 0.6, // Soft hit (quieter)
116                _ => 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}