Skip to main content

codec_core/utils/
simd.rs

1//! SIMD utilities for cross-platform optimizations
2//!
3//! This module provides SIMD capability detection and optimized operations
4//! for audio processing across different architectures.
5
6use std::sync::OnceLock;
7
8/// SIMD support information
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct SimdSupport {
11    /// `x86_64` SSE2 support
12    pub sse2: bool,
13    /// `x86_64` AVX2 support
14    pub avx2: bool,
15    /// `AArch64` NEON support
16    pub neon: bool,
17}
18
19/// Global SIMD support detection
20static SIMD_SUPPORT: OnceLock<SimdSupport> = OnceLock::new();
21
22#[cfg(target_arch = "x86_64")]
23const fn extracted_i16(value: i32) -> i16 {
24    let bytes = value.to_le_bytes();
25    i16::from_le_bytes([bytes[0], bytes[1]])
26}
27
28/// Initialize SIMD support detection
29pub fn init_simd_support() {
30    SIMD_SUPPORT.get_or_init(detect_simd_support);
31}
32
33/// Internal function to detect SIMD support
34fn detect_simd_support() -> SimdSupport {
35    #[cfg(target_arch = "x86_64")]
36    {
37        SimdSupport {
38            sse2: is_x86_feature_detected!("sse2"),
39            avx2: is_x86_feature_detected!("avx2"),
40            neon: false,
41        }
42    }
43    #[cfg(target_arch = "aarch64")]
44    {
45        SimdSupport {
46            sse2: false,
47            avx2: false,
48            neon: std::arch::is_aarch64_feature_detected!("neon"),
49        }
50    }
51    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
52    {
53        SimdSupport {
54            sse2: false,
55            avx2: false,
56            neon: false,
57        }
58    }
59}
60
61/// Get SIMD support information
62#[must_use]
63pub fn get_simd_support() -> SimdSupport {
64    *SIMD_SUPPORT.get_or_init(detect_simd_support)
65}
66
67/// Check if any SIMD support is available
68#[must_use]
69pub fn has_simd_support() -> bool {
70    let support = get_simd_support();
71    support.sse2 || support.avx2 || support.neon
72}
73
74/// SIMD-optimized μ-law encoding (x86_64 SSE2)
75#[cfg(target_arch = "x86_64")]
76pub fn encode_mulaw_simd_sse2(samples: &[i16], output: &mut [u8]) {
77    use std::arch::x86_64::*;
78
79    if !get_simd_support().sse2 {
80        return encode_mulaw_scalar(samples, output);
81    }
82
83    let mut chunks = samples.chunks_exact(8);
84    let mut out_idx = 0;
85
86    unsafe {
87        for chunk in chunks.by_ref() {
88            // Load 8 samples at once
89            let samples_vec = _mm_loadu_si128(chunk.as_ptr().cast::<__m128i>());
90
91            // Process each sample - need to unroll or use different approach
92            // _mm_extract_epi16 requires compile-time constant, so we unroll
93            output[out_idx] =
94                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 0)));
95            output[out_idx + 1] =
96                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 1)));
97            output[out_idx + 2] =
98                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 2)));
99            output[out_idx + 3] =
100                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 3)));
101            output[out_idx + 4] =
102                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 4)));
103            output[out_idx + 5] =
104                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 5)));
105            output[out_idx + 6] =
106                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 6)));
107            output[out_idx + 7] =
108                linear_to_mulaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 7)));
109            out_idx += 8;
110        }
111    }
112
113    // Handle remainder
114    for &sample in chunks.remainder() {
115        output[out_idx] = linear_to_mulaw_scalar(sample);
116        out_idx += 1;
117    }
118}
119
120/// SIMD-optimized μ-law encoding (`AArch64` NEON)
121#[cfg(target_arch = "aarch64")]
122pub fn encode_mulaw_simd_neon(samples: &[i16], output: &mut [u8]) {
123    if !get_simd_support().neon {
124        return encode_mulaw_scalar(samples, output);
125    }
126
127    // For now, fall back to scalar implementation for simplicity
128    encode_mulaw_scalar(samples, output);
129}
130
131/// Scalar μ-law encoding fallback
132pub fn encode_mulaw_scalar(samples: &[i16], output: &mut [u8]) {
133    for (i, &sample) in samples.iter().enumerate() {
134        output[i] = linear_to_mulaw_scalar(sample);
135    }
136}
137
138/// SIMD-optimized A-law encoding (x86_64 SSE2)
139#[cfg(target_arch = "x86_64")]
140pub fn encode_alaw_simd_sse2(samples: &[i16], output: &mut [u8]) {
141    use std::arch::x86_64::*;
142
143    if !get_simd_support().sse2 {
144        return encode_alaw_scalar(samples, output);
145    }
146
147    let mut chunks = samples.chunks_exact(8);
148    let mut out_idx = 0;
149
150    unsafe {
151        for chunk in chunks.by_ref() {
152            // Load 8 samples at once
153            let samples_vec = _mm_loadu_si128(chunk.as_ptr().cast::<__m128i>());
154
155            // Process each sample - need to unroll or use different approach
156            // _mm_extract_epi16 requires compile-time constant, so we unroll
157            output[out_idx] =
158                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 0)));
159            output[out_idx + 1] =
160                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 1)));
161            output[out_idx + 2] =
162                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 2)));
163            output[out_idx + 3] =
164                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 3)));
165            output[out_idx + 4] =
166                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 4)));
167            output[out_idx + 5] =
168                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 5)));
169            output[out_idx + 6] =
170                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 6)));
171            output[out_idx + 7] =
172                linear_to_alaw_scalar(extracted_i16(_mm_extract_epi16(samples_vec, 7)));
173            out_idx += 8;
174        }
175    }
176
177    // Handle remainder
178    for &sample in chunks.remainder() {
179        output[out_idx] = linear_to_alaw_scalar(sample);
180        out_idx += 1;
181    }
182}
183
184/// SIMD-optimized A-law encoding (`AArch64` NEON)
185#[cfg(target_arch = "aarch64")]
186pub fn encode_alaw_simd_neon(samples: &[i16], output: &mut [u8]) {
187    if !get_simd_support().neon {
188        return encode_alaw_scalar(samples, output);
189    }
190
191    // For now, fall back to scalar implementation for simplicity
192    encode_alaw_scalar(samples, output);
193}
194
195/// Scalar A-law encoding fallback
196pub fn encode_alaw_scalar(samples: &[i16], output: &mut [u8]) {
197    for (i, &sample) in samples.iter().enumerate() {
198        output[i] = linear_to_alaw_scalar(sample);
199    }
200}
201
202/// Cross-platform μ-law encoding dispatcher
203pub fn encode_mulaw_optimized(samples: &[i16], output: &mut [u8]) {
204    #[cfg(target_arch = "x86_64")]
205    {
206        if get_simd_support().sse2 {
207            return encode_mulaw_simd_sse2(samples, output);
208        }
209    }
210
211    #[cfg(target_arch = "aarch64")]
212    {
213        if get_simd_support().neon {
214            return encode_mulaw_simd_neon(samples, output);
215        }
216    }
217
218    encode_mulaw_scalar(samples, output);
219}
220
221/// Cross-platform A-law encoding dispatcher
222pub fn encode_alaw_optimized(samples: &[i16], output: &mut [u8]) {
223    #[cfg(target_arch = "x86_64")]
224    {
225        if get_simd_support().sse2 {
226            return encode_alaw_simd_sse2(samples, output);
227        }
228    }
229
230    #[cfg(target_arch = "aarch64")]
231    {
232        if get_simd_support().neon {
233            return encode_alaw_simd_neon(samples, output);
234        }
235    }
236
237    encode_alaw_scalar(samples, output);
238}
239
240/// Scalar μ-law conversion (ITU-T G.711)
241#[must_use]
242pub const fn linear_to_mulaw_scalar(sample: i16) -> u8 {
243    const CLIP: i16 = 32635;
244    const BIAS: i16 = 0x84;
245    const MULAW_MAX: u8 = 0x7F;
246
247    let mut sample = sample;
248    let sign = if sample < 0 {
249        // Handle i16::MIN case to avoid overflow
250        sample = if sample == i16::MIN {
251            i16::MAX
252        } else {
253            -sample
254        };
255        0x80
256    } else {
257        0x00
258    };
259
260    if sample > CLIP {
261        sample = CLIP;
262    }
263
264    sample += BIAS;
265
266    let exponent = if sample <= 0x1F {
267        0
268    } else if sample <= 0x3F {
269        1
270    } else if sample <= 0x7F {
271        2
272    } else if sample <= 0xFF {
273        3
274    } else if sample <= 0x1FF {
275        4
276    } else if sample <= 0x3FF {
277        5
278    } else if sample <= 0x7FF {
279        6
280    } else {
281        7
282    };
283
284    let mantissa = (sample >> (exponent + 3)) & 0x0F;
285    let mulaw = ((exponent << 4) | mantissa).to_le_bytes()[0];
286
287    (mulaw ^ MULAW_MAX) | sign
288}
289
290/// Scalar A-law conversion (ITU-T G.711)
291#[must_use]
292pub const fn linear_to_alaw_scalar(sample: i16) -> u8 {
293    const CLIP: i16 = 32635;
294    const ALAW_MAX: u8 = 0x7F;
295
296    let mut sample = sample;
297    let sign = if sample < 0 {
298        // Handle i16::MIN case to avoid overflow
299        sample = if sample == i16::MIN {
300            i16::MAX
301        } else {
302            -sample
303        };
304        0x80
305    } else {
306        0x00
307    };
308
309    if sample > CLIP {
310        sample = CLIP;
311    }
312
313    let alaw = if sample < 256 {
314        sample >> 4
315    } else {
316        let exponent = if sample < 512 {
317            1
318        } else if sample < 1024 {
319            2
320        } else if sample < 2048 {
321            3
322        } else if sample < 4096 {
323            4
324        } else if sample < 8192 {
325            5
326        } else if sample < 16384 {
327            6
328        } else {
329            7
330        };
331
332        let mantissa = (sample >> (exponent + 3)) & 0x0F;
333        ((exponent << 4) | mantissa) + 16
334    };
335
336    (alaw.to_le_bytes()[0] ^ ALAW_MAX) | sign
337}
338
339/// Scalar μ-law to linear conversion
340#[must_use]
341#[allow(clippy::cast_lossless)]
342pub const fn mulaw_to_linear_scalar(mulaw: u8) -> i16 {
343    const BIAS: i16 = 0x84;
344    const MULAW_MAX: u8 = 0x7F;
345
346    let mulaw = mulaw ^ MULAW_MAX;
347    let sign = mulaw & 0x80;
348    let exponent = (mulaw >> 4) & 0x07;
349    let mantissa = mulaw & 0x0F;
350
351    let mut sample = ((mantissa as i16) << (exponent + 3)) + BIAS;
352
353    if exponent > 0 {
354        sample += 1i16 << (exponent + 2);
355    }
356
357    if sign != 0 {
358        -sample
359    } else {
360        sample
361    }
362}
363
364/// Scalar A-law to linear conversion
365#[must_use]
366pub fn alaw_to_linear_scalar(alaw: u8) -> i16 {
367    const ALAW_MAX: u8 = 0x7F;
368
369    let alaw = alaw ^ ALAW_MAX;
370    let sign = alaw & 0x80;
371    let magnitude = alaw & 0x7F;
372
373    let sample = if magnitude < 16 {
374        u16::from(magnitude) << 4
375    } else {
376        let exponent = (magnitude >> 4) & 0x07;
377        let mantissa = magnitude & 0x0F;
378
379        // Prevent overflow by clamping shift amounts and using wider types
380        let exp_shift = u32::from(exponent + 3).min(15);
381        let gain_shift = u32::from(exponent + 2).min(15);
382
383        (u16::from(mantissa) << exp_shift) + (1_u16 << gain_shift)
384    } + 8;
385
386    if sign != 0 {
387        -sample.cast_signed()
388    } else {
389        sample.cast_signed()
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_simd_support_detection() {
399        init_simd_support();
400        let support = get_simd_support();
401
402        // At least one of the fields should be accessible
403        #[cfg(target_arch = "x86_64")]
404        {
405            // SSE2 is widely supported on x86_64
406            println!("SSE2 support: {}", support.sse2);
407        }
408
409        #[cfg(target_arch = "aarch64")]
410        {
411            // NEON is standard on AArch64
412            println!("NEON support: {}", support.neon);
413        }
414    }
415
416    #[test]
417    fn test_mulaw_roundtrip() {
418        let original = 12345i16;
419        let encoded = linear_to_mulaw_scalar(original);
420        let decoded = mulaw_to_linear_scalar(encoded);
421
422        // G.711 is lossy, so we expect some difference
423        let error = (original - decoded).abs();
424        assert!(error < 1000, "Error too large: {error}");
425    }
426
427    #[test]
428    fn test_alaw_roundtrip() {
429        let original = 12345i16;
430        let encoded = linear_to_alaw_scalar(original);
431        let decoded = alaw_to_linear_scalar(encoded);
432
433        // G.711 A-law is lossy, so we expect some difference
434        // A-law has different quantization than μ-law, so use more lenient threshold
435        // A-law can have significant quantization errors for certain values
436        let error = (original - decoded).abs();
437        assert!(
438            error < 5000,
439            "Error too large: {error} (original: {original}, decoded: {decoded})"
440        );
441    }
442
443    #[test]
444    fn test_simd_vs_scalar() {
445        let samples = vec![0, 1000, -1000, 16000, -16000, 32000, -32000, 12345];
446        let mut simd_output = vec![0u8; samples.len()];
447        let mut scalar_output = vec![0u8; samples.len()];
448
449        encode_mulaw_optimized(&samples, &mut simd_output);
450        encode_mulaw_scalar(&samples, &mut scalar_output);
451
452        // Results should be identical
453        assert_eq!(simd_output, scalar_output);
454    }
455
456    #[test]
457    fn test_empty_input() {
458        let samples: Vec<i16> = vec![];
459        let mut output: Vec<u8> = vec![];
460
461        encode_mulaw_optimized(&samples, &mut output);
462        assert_eq!(output.len(), 0);
463    }
464
465    #[test]
466    fn test_edge_cases() {
467        let samples = vec![i16::MAX, i16::MIN, 0];
468        let mut output = vec![0u8; samples.len()];
469
470        encode_mulaw_optimized(&samples, &mut output);
471
472        // Should not panic and produce valid output
473        assert_eq!(output.len(), samples.len());
474    }
475}