devalang_core/core/audio/engine/sample/
padding.rs

1use devalang_types::Value;
2use std::collections::HashMap;
3
4pub fn pad_samples_impl(
5    engine: &mut crate::core::audio::engine::driver::AudioEngine,
6    samples: &[i16],
7    time_secs: f32,
8    effects_map: Option<HashMap<String, Value>>,
9) {
10    let sample_rate = engine.sample_rate as f32;
11    let channels = engine.channels as usize;
12
13    let offset = (time_secs * (sample_rate) * (channels as f32)) as usize;
14    let total_samples = samples.len();
15
16    let mut gain = 1.0;
17    let mut pan = 0.0;
18    let mut fade_in = 0.0;
19    let mut fade_out = 0.0;
20    let mut pitch = 1.0;
21    let mut drive = 0.0;
22    let mut reverb = 0.0;
23    let mut delay = 0.0; // delay time in seconds
24    let delay_feedback = 0.35; // default feedback
25
26    if let Some(map) = &effects_map {
27        for (key, val) in map {
28            match (key.as_str(), val) {
29                ("gain", Value::Number(v)) => {
30                    gain = *v;
31                }
32                ("pan", Value::Number(v)) => {
33                    pan = *v;
34                }
35                ("fadeIn", Value::Number(v)) => {
36                    fade_in = *v;
37                }
38                ("fadeOut", Value::Number(v)) => {
39                    fade_out = *v;
40                }
41                ("pitch", Value::Number(v)) => {
42                    pitch = *v;
43                }
44                ("drive", Value::Number(v)) => {
45                    drive = *v;
46                }
47                ("reverb", Value::Number(v)) => {
48                    reverb = *v;
49                }
50                ("delay", Value::Number(v)) => {
51                    delay = *v;
52                }
53                _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
54            }
55        }
56    }
57
58    let fade_in_samples = (fade_in * (sample_rate)) as usize;
59    let fade_out_samples = (fade_out * (sample_rate)) as usize;
60
61    // If no fade specified, apply a tiny default fade (2 ms) when sample boundaries are non-zero
62    let default_boundary_fade_ms = 1.0_f32; // 1 ms
63    let default_fade_samples = (default_boundary_fade_ms * (sample_rate)) as usize;
64    let mut effective_fade_in = fade_in_samples;
65    let mut effective_fade_out = fade_out_samples;
66    if effective_fade_in == 0 {
67        if let Some(&first) = samples.first() {
68            if first.abs() > 64 {
69                // increased threshold to detect only strong abrupt starts
70                effective_fade_in = default_fade_samples.max(1);
71            }
72        }
73    }
74    if effective_fade_out == 0 {
75        if let Some(&last) = samples.last() {
76            if last.abs() > 64 {
77                // increased threshold to detect only strong abrupt ends
78                effective_fade_out = default_fade_samples.max(1);
79            }
80        }
81    }
82
83    // Ensure fades do not exceed half the sample length to avoid silencing short samples
84    if total_samples > 0 {
85        let cap = total_samples / 2;
86        if effective_fade_in > cap {
87            effective_fade_in = cap.max(1);
88        }
89        if effective_fade_out > cap {
90            effective_fade_out = cap.max(1);
91        }
92    }
93
94    let delay_samples = if delay > 0.0 {
95        (delay * (sample_rate)) as usize
96    } else {
97        0
98    };
99    let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
100
101    for i in 0..total_samples {
102        let pitch_index = if pitch != 1.0 {
103            ((i as f32) / pitch) as usize
104        } else {
105            i
106        };
107
108        let mut adjusted = if pitch_index < total_samples {
109            samples[pitch_index] as f32
110        } else {
111            0.0
112        };
113
114        adjusted *= gain;
115
116        if effective_fade_in > 0 && i < effective_fade_in {
117            if effective_fade_in == 1 {
118                adjusted *= 0.0;
119            } else {
120                adjusted *= (i as f32) / (effective_fade_in as f32);
121            }
122        }
123        if effective_fade_out > 0 && i >= total_samples.saturating_sub(effective_fade_out) {
124            if effective_fade_out == 1 {
125                adjusted *= 0.0;
126            } else {
127                adjusted *= ((total_samples - 1 - i) as f32) / ((effective_fade_out - 1) as f32);
128            }
129        }
130
131        if drive > 0.0 {
132            let normalized = adjusted / (i16::MAX as f32);
133            let pre_gain = (10f32).powf(drive / 20.0);
134            let driven = (normalized * pre_gain).tanh();
135            adjusted = driven * (i16::MAX as f32);
136        }
137
138        if delay_samples > 0 && i >= delay_samples {
139            let echo = delay_buffer[i - delay_samples] * delay_feedback;
140            adjusted += echo;
141        }
142        if delay_samples > 0 {
143            delay_buffer[i] = adjusted;
144        }
145
146        if reverb > 0.0 {
147            let reverb_delay = (0.03 * (sample_rate)) as usize;
148            if i >= reverb_delay {
149                adjusted += (engine.buffer[offset + i - reverb_delay] as f32) * reverb;
150            }
151        }
152
153        let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
154
155        let (left_gain, right_gain) = crate::core::audio::engine::helpers::pan_gains(pan);
156
157        let left = ((adjusted_sample as f32) * left_gain) as i16;
158        let right = ((adjusted_sample as f32) * right_gain) as i16;
159
160        // For interleaved buffer with `channels` channels, each frame has `channels` samples.
161        // left channel at frame i is at offset + i * channels, right at +1.
162        let left_pos = offset + i * channels;
163        let right_pos = left_pos + 1;
164
165        if right_pos < engine.buffer.len() {
166            engine.buffer[left_pos] = engine.buffer[left_pos].saturating_add(left);
167            engine.buffer[right_pos] = engine.buffer[right_pos].saturating_add(right);
168        }
169    }
170}