1#[cfg(feature = "wide-backend")]
33const TWENTY_OVER_LOG2_10: f32 = 6.020_6;
34
35#[inline]
38pub fn db_to_linear_block(out: &mut [f32], src: &[f32]) {
39 #[cfg(feature = "wide-backend")]
40 {
41 use wide::f32x8;
42 let n = out.len().min(src.len());
43 let n8 = n / 8 * 8;
44 let scale = f32x8::splat(core::f32::consts::LN_10 / 20.0);
48 let (head_out, tail_out) = out[..n].split_at_mut(n8);
49 for (out_chunk, src_chunk) in head_out.chunks_exact_mut(8).zip(src[..n8].chunks_exact(8)) {
50 let v = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
51 out_chunk.copy_from_slice((v * scale).exp().as_array_ref());
52 }
53 db_to_linear_block_scalar(tail_out, &src[n8..n]);
54 }
55 #[cfg(not(feature = "wide-backend"))]
56 db_to_linear_block_scalar(out, src);
57}
58
59#[inline]
63pub fn db_to_linear_block_scalar(out: &mut [f32], src: &[f32]) {
64 let n = out.len().min(src.len());
65 for i in 0..n {
66 out[i] = 10.0_f32.powf(src[i] / 20.0);
67 }
68}
69
70#[inline]
73pub fn linear_to_db_block(out: &mut [f32], src: &[f32]) {
74 #[cfg(feature = "wide-backend")]
75 {
76 use wide::f32x8;
77 let n = out.len().min(src.len());
78 let n8 = n / 8 * 8;
79 let scale = f32x8::splat(TWENTY_OVER_LOG2_10);
80 let (head_out, tail_out) = out[..n].split_at_mut(n8);
81 for (out_chunk, src_chunk) in head_out.chunks_exact_mut(8).zip(src[..n8].chunks_exact(8)) {
82 let v = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
83 out_chunk.copy_from_slice((v.log2() * scale).as_array_ref());
84 }
85 linear_to_db_block_scalar(tail_out, &src[n8..n]);
86 }
87 #[cfg(not(feature = "wide-backend"))]
88 linear_to_db_block_scalar(out, src);
89}
90
91#[inline]
93pub fn linear_to_db_block_scalar(out: &mut [f32], src: &[f32]) {
94 let n = out.len().min(src.len());
95 for i in 0..n {
96 out[i] = 20.0 * src[i].log10();
97 }
98}
99
100#[inline]
103pub fn exp2_block(out: &mut [f32], src: &[f32]) {
104 #[cfg(feature = "wide-backend")]
105 {
106 use wide::f32x8;
107 let n = out.len().min(src.len());
108 let n8 = n / 8 * 8;
109 let ln2 = f32x8::splat(core::f32::consts::LN_2);
110 let (head_out, tail_out) = out[..n].split_at_mut(n8);
111 for (out_chunk, src_chunk) in head_out.chunks_exact_mut(8).zip(src[..n8].chunks_exact(8)) {
112 let v = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
116 out_chunk.copy_from_slice((v * ln2).exp().as_array_ref());
117 }
118 exp2_block_scalar(tail_out, &src[n8..n]);
119 }
120 #[cfg(not(feature = "wide-backend"))]
121 exp2_block_scalar(out, src);
122}
123
124#[inline]
126pub fn exp2_block_scalar(out: &mut [f32], src: &[f32]) {
127 let n = out.len().min(src.len());
128 for i in 0..n {
129 out[i] = src[i].exp2();
130 }
131}
132
133#[inline]
136pub fn log2_block(out: &mut [f32], src: &[f32]) {
137 #[cfg(feature = "wide-backend")]
138 {
139 use wide::f32x8;
140 let n = out.len().min(src.len());
141 let n8 = n / 8 * 8;
142 let (head_out, tail_out) = out[..n].split_at_mut(n8);
143 for (out_chunk, src_chunk) in head_out.chunks_exact_mut(8).zip(src[..n8].chunks_exact(8)) {
144 let v = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
145 out_chunk.copy_from_slice(v.log2().as_array_ref());
146 }
147 log2_block_scalar(tail_out, &src[n8..n]);
148 }
149 #[cfg(not(feature = "wide-backend"))]
150 log2_block_scalar(out, src);
151}
152
153#[inline]
155pub fn log2_block_scalar(out: &mut [f32], src: &[f32]) {
156 let n = out.len().min(src.len());
157 for i in 0..n {
158 out[i] = src[i].log2();
159 }
160}
161
162#[inline]
172pub fn tanh_block(out: &mut [f32], src: &[f32]) {
173 #[cfg(feature = "wide-backend")]
174 {
175 use wide::f32x8;
176 let n = out.len().min(src.len());
177 let n8 = n / 8 * 8;
178 let bound = f32x8::splat(10.0);
179 let neg_bound = f32x8::splat(-10.0);
180 let two = f32x8::splat(2.0);
181 let one = f32x8::splat(1.0);
182 let (head_out, tail_out) = out[..n].split_at_mut(n8);
183 for (out_chunk, src_chunk) in head_out.chunks_exact_mut(8).zip(src[..n8].chunks_exact(8)) {
184 let x = f32x8::from(<[f32; 8]>::try_from(src_chunk).unwrap_or_default());
185 let x_clamped = x.fast_max(neg_bound).fast_min(bound);
186 let e2x = (x_clamped * two).exp();
187 let result = (e2x - one) / (e2x + one);
188 out_chunk.copy_from_slice(result.as_array_ref());
189 }
190 tanh_block_scalar(tail_out, &src[n8..n]);
191 }
192 #[cfg(not(feature = "wide-backend"))]
193 tanh_block_scalar(out, src);
194}
195
196#[inline]
201pub fn tanh_block_scalar(out: &mut [f32], src: &[f32]) {
202 let n = out.len().min(src.len());
203 for i in 0..n {
204 out[i] = src[i].tanh();
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 #![allow(clippy::float_cmp, clippy::cast_precision_loss)]
213
214 use super::*;
215
216 fn max_abs_err(a: &[f32], b: &[f32]) -> f32 {
217 a.iter()
218 .zip(b.iter())
219 .map(|(x, y)| (x - y).abs())
220 .fold(0.0_f32, f32::max)
221 }
222
223 fn max_rel_err(a: &[f32], b: &[f32]) -> f32 {
224 a.iter()
225 .zip(b.iter())
226 .filter(|(_, y)| y.abs() > 1e-6)
227 .map(|(x, y)| ((x - y) / y).abs())
228 .fold(0.0_f32, f32::max)
229 }
230
231 #[test]
232 fn db_to_linear_block_matches_libm() {
233 let src: Vec<f32> = (-120..=24).map(|i| i as f32).collect();
234 let mut out = vec![0.0; src.len()];
235 db_to_linear_block(&mut out, &src);
236 let expected: Vec<f32> = src.iter().map(|&x| 10.0_f32.powf(x / 20.0)).collect();
237 assert!(
240 max_rel_err(&out, &expected) < 1e-5,
241 "rel err = {}",
242 max_rel_err(&out, &expected)
243 );
244 }
245
246 #[test]
247 fn linear_to_db_round_trips() {
248 let db: Vec<f32> = (-100..=20).map(|i| i as f32).collect();
249 let mut lin = vec![0.0; db.len()];
250 let mut roundtrip = vec![0.0; db.len()];
251 db_to_linear_block(&mut lin, &db);
252 linear_to_db_block(&mut roundtrip, &lin);
253 let err = max_abs_err(&db, &roundtrip);
255 assert!(err < 1e-4, "round-trip err = {err} dB");
256 }
257
258 #[test]
259 fn exp2_block_matches_libm() {
260 let src: Vec<f32> = (-100..=100).map(|i| i as f32 * 0.1).collect();
261 let mut out = vec![0.0; src.len()];
262 exp2_block(&mut out, &src);
263 let expected: Vec<f32> = src.iter().map(|&x| x.exp2()).collect();
264 assert!(
265 max_rel_err(&out, &expected) < 1e-5,
266 "rel err = {}",
267 max_rel_err(&out, &expected)
268 );
269 }
270
271 #[test]
272 fn log2_block_matches_libm() {
273 let src: Vec<f32> = (1..=200).map(|i| i as f32).collect();
274 let mut out = vec![0.0; src.len()];
275 log2_block(&mut out, &src);
276 let expected: Vec<f32> = src.iter().map(|&x| x.log2()).collect();
277 assert!(
278 max_abs_err(&out, &expected) < 1e-5,
279 "abs err = {}",
280 max_abs_err(&out, &expected)
281 );
282 }
283
284 #[test]
285 fn tanh_block_matches_libm() {
286 let src: Vec<f32> = (-100..=100).map(|i| i as f32 * 0.1).collect();
287 let mut out = vec![0.0; src.len()];
288 tanh_block(&mut out, &src);
289 let expected: Vec<f32> = src.iter().map(|&x| x.tanh()).collect();
290 let err = max_abs_err(&out, &expected);
291 assert!(err < 5e-6, "abs err = {err}");
292 }
293
294 #[test]
295 fn tanh_block_saturates_for_large_inputs() {
296 let src = [-50.0, -20.0, 20.0, 50.0];
297 let mut out = [0.0; 4];
298 tanh_block(&mut out, &src);
299 for &y in &out {
300 assert!(
301 (y.abs() - 1.0).abs() < 1e-4,
302 "expected saturation near ±1, got {y}"
303 );
304 }
305 }
306
307 #[test]
308 fn lengths_min_clamped() {
309 let src = [1.0_f32, 2.0, 3.0];
310 let mut out = [0.0_f32; 5];
311 db_to_linear_block(&mut out, &src);
312 assert_eq!(out[3], 0.0);
314 assert_eq!(out[4], 0.0);
315 }
316}