Skip to main content

voirs_conversion/
optimizations.rs

1//! Performance and Memory Optimizations for VoiRS Conversion
2//!
3//! This module provides optimizations for memory usage and performance,
4//! particularly for handling small audio samples and reducing memory overhead.
5
6use crate::prelude::*;
7use std::alloc::{GlobalAlloc, Layout, System};
8use std::collections::{BTreeMap, HashMap, VecDeque};
9use std::sync::{
10    atomic::{AtomicUsize, Ordering},
11    Arc, Mutex, RwLock,
12};
13use std::thread;
14use std::time::{Duration, Instant};
15
16/// Memory-efficient audio buffer pool to reduce allocations
17#[derive(Debug)]
18pub struct AudioBufferPool {
19    small_buffers: Arc<Mutex<Vec<Vec<f32>>>>, // For audio < 1 second
20    medium_buffers: Arc<Mutex<Vec<Vec<f32>>>>, // For audio 1-5 seconds
21    large_buffers: Arc<Mutex<Vec<Vec<f32>>>>, // For audio > 5 seconds
22    stats: Arc<Mutex<PoolStats>>,
23}
24
25/// Buffer pool statistics tracking hits and misses for performance analysis
26#[derive(Debug, Default, Clone)]
27pub struct PoolStats {
28    small_hits: usize,
29    small_misses: usize,
30    medium_hits: usize,
31    medium_misses: usize,
32    large_hits: usize,
33    large_misses: usize,
34}
35
36impl Default for AudioBufferPool {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl AudioBufferPool {
43    /// Create a new audio buffer pool with pre-allocated capacity for different buffer sizes
44    pub fn new() -> Self {
45        Self {
46            small_buffers: Arc::new(Mutex::new(Vec::with_capacity(10))),
47            medium_buffers: Arc::new(Mutex::new(Vec::with_capacity(5))),
48            large_buffers: Arc::new(Mutex::new(Vec::with_capacity(2))),
49            stats: Arc::new(Mutex::new(PoolStats::default())),
50        }
51    }
52
53    /// Get a buffer from the pool, creating one if necessary
54    pub fn get_buffer(&self, min_size: usize) -> Vec<f32> {
55        let category = self.categorize_size(min_size);
56
57        match category {
58            BufferCategory::Small => {
59                let mut buffers = self
60                    .small_buffers
61                    .lock()
62                    .expect("Small buffers lock poisoned");
63                if let Some(mut buffer) = buffers.pop() {
64                    if buffer.capacity() >= min_size {
65                        buffer.clear();
66                        buffer.resize(min_size, 0.0);
67                        self.stats.lock().expect("Stats lock poisoned").small_hits += 1;
68                        return buffer;
69                    } else {
70                        // Buffer too small, return it and create new one
71                        buffers.push(buffer);
72                    }
73                }
74                self.stats.lock().expect("Stats lock poisoned").small_misses += 1;
75                let mut buffer = Vec::with_capacity(std::cmp::max(min_size, 22050)); // 1 second at 22kHz
76                buffer.resize(min_size, 0.0);
77                buffer
78            }
79            BufferCategory::Medium => {
80                let mut buffers = self
81                    .medium_buffers
82                    .lock()
83                    .expect("Medium buffers lock poisoned");
84                if let Some(mut buffer) = buffers.pop() {
85                    if buffer.capacity() >= min_size {
86                        buffer.clear();
87                        buffer.resize(min_size, 0.0);
88                        self.stats.lock().expect("Stats lock poisoned").medium_hits += 1;
89                        return buffer;
90                    } else {
91                        buffers.push(buffer);
92                    }
93                }
94                self.stats
95                    .lock()
96                    .expect("Stats lock poisoned")
97                    .medium_misses += 1;
98                Vec::with_capacity(std::cmp::max(min_size, 110250)) // 5 seconds at 22kHz
99            }
100            BufferCategory::Large => {
101                let mut buffers = self
102                    .large_buffers
103                    .lock()
104                    .expect("Large buffers lock poisoned");
105                if let Some(mut buffer) = buffers.pop() {
106                    if buffer.capacity() >= min_size {
107                        buffer.clear();
108                        buffer.resize(min_size, 0.0);
109                        self.stats.lock().expect("Stats lock poisoned").large_hits += 1;
110                        return buffer;
111                    } else {
112                        buffers.push(buffer);
113                    }
114                }
115                self.stats.lock().expect("Stats lock poisoned").large_misses += 1;
116                Vec::with_capacity(min_size)
117            }
118        }
119    }
120
121    /// Return a buffer to the pool for reuse
122    pub fn return_buffer(&self, buffer: Vec<f32>) {
123        let category = self.categorize_size(buffer.capacity());
124
125        match category {
126            BufferCategory::Small => {
127                let mut buffers = self
128                    .small_buffers
129                    .lock()
130                    .expect("Small buffers lock poisoned");
131                if buffers.len() < 10 {
132                    // Limit pool size
133                    buffers.push(buffer);
134                }
135            }
136            BufferCategory::Medium => {
137                let mut buffers = self
138                    .medium_buffers
139                    .lock()
140                    .expect("Medium buffers lock poisoned");
141                if buffers.len() < 5 {
142                    buffers.push(buffer);
143                }
144            }
145            BufferCategory::Large => {
146                let mut buffers = self
147                    .large_buffers
148                    .lock()
149                    .expect("Large buffers lock poisoned");
150                if buffers.len() < 2 {
151                    buffers.push(buffer);
152                }
153            }
154        }
155    }
156
157    fn categorize_size(&self, size: usize) -> BufferCategory {
158        if size <= 22050 {
159            // <= 1 second at 22kHz
160            BufferCategory::Small
161        } else if size <= 110250 {
162            // <= 5 seconds at 22kHz
163            BufferCategory::Medium
164        } else {
165            BufferCategory::Large
166        }
167    }
168
169    /// Get buffer pool statistics including hit rates and cache performance
170    pub fn get_stats(&self) -> PoolStats {
171        self.stats.lock().expect("Stats lock poisoned").clone()
172    }
173}
174
175impl PoolStats {
176    fn hit_rate(&self) -> f64 {
177        let total_hits = self.small_hits + self.medium_hits + self.large_hits;
178        let total_requests =
179            total_hits + self.small_misses + self.medium_misses + self.large_misses;
180
181        if total_requests == 0 {
182            0.0
183        } else {
184            total_hits as f64 / total_requests as f64
185        }
186    }
187}
188
189/// Buffer size categories for memory pool management
190#[derive(Debug)]
191enum BufferCategory {
192    /// Small buffers for audio under 1 second
193    Small,
194    /// Medium buffers for audio 1-5 seconds
195    Medium,
196    /// Large buffers for audio over 5 seconds
197    Large,
198}
199
200/// Fast-path optimization for small audio samples to reduce memory overhead
201#[derive(Debug)]
202pub struct SmallAudioOptimizer {
203    buffer_pool: AudioBufferPool,
204    small_sample_cache: Arc<Mutex<HashMap<String, CachedResult>>>,
205}
206
207/// Cached conversion result with metadata
208#[derive(Debug, Clone)]
209struct CachedResult {
210    result: Vec<f32>,
211    timestamp: Instant,
212    access_count: usize,
213}
214
215impl Default for SmallAudioOptimizer {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl SmallAudioOptimizer {
222    /// Create a new small audio optimizer with buffer pooling and caching enabled
223    pub fn new() -> Self {
224        Self {
225            buffer_pool: AudioBufferPool::new(),
226            small_sample_cache: Arc::new(Mutex::new(HashMap::new())),
227        }
228    }
229
230    /// Optimize conversion for very small audio samples (< 0.5 seconds)
231    pub fn optimize_small_conversion(
232        &self,
233        audio: &[f32],
234        conversion_type: &ConversionType,
235        target: &ConversionTarget,
236    ) -> Option<Vec<f32>> {
237        // Only optimize for very small samples to reduce memory overhead
238        if audio.len() > 11025 {
239            // 0.5 seconds at 22kHz
240            return None;
241        }
242
243        // Create cache key
244        let cache_key = self.create_cache_key(audio, conversion_type, target);
245
246        // Check cache first
247        if let Some(cached) = self.get_cached_result(&cache_key) {
248            return Some(cached);
249        }
250
251        // For very small audio, use simplified processing
252        let optimized_result = match conversion_type {
253            ConversionType::PitchShift => self.fast_pitch_shift(audio, target),
254            ConversionType::SpeedTransformation => self.fast_speed_transform(audio, target),
255            ConversionType::PassThrough => {
256                // Ultra-fast passthrough - just copy
257                Some(audio.to_vec())
258            }
259            _ => None, // Use full processing for complex conversions
260        };
261
262        // Cache result if successful
263        if let Some(ref result) = optimized_result {
264            self.cache_result(cache_key, result.clone());
265        }
266
267        optimized_result
268    }
269
270    fn create_cache_key(
271        &self,
272        audio: &[f32],
273        conversion_type: &ConversionType,
274        target: &ConversionTarget,
275    ) -> String {
276        use std::collections::hash_map::DefaultHasher;
277        use std::hash::{Hash, Hasher};
278
279        let mut hasher = DefaultHasher::new();
280
281        // Hash audio characteristics instead of full audio to reduce memory
282        let audio_hash = if audio.len() <= 100 {
283            // For very small audio, hash the whole thing
284            audio
285                .iter()
286                .fold(0u64, |acc, &x| acc.wrapping_add((x * 1000.0) as u64))
287        } else {
288            // For larger small audio, hash samples at regular intervals
289            audio
290                .iter()
291                .step_by(audio.len() / 20)
292                .fold(0u64, |acc, &x| acc.wrapping_add((x * 1000.0) as u64))
293        };
294
295        audio_hash.hash(&mut hasher);
296        conversion_type.hash(&mut hasher);
297
298        // Hash key target characteristics
299        match conversion_type {
300            ConversionType::PitchShift => {
301                (target.characteristics.pitch.mean_f0 as u32).hash(&mut hasher);
302            }
303            ConversionType::SpeedTransformation => {
304                ((target.characteristics.timing.speaking_rate * 1000.0) as u32).hash(&mut hasher);
305            }
306            _ => {}
307        }
308
309        format!("small_audio_{}", hasher.finish())
310    }
311
312    fn get_cached_result(&self, key: &str) -> Option<Vec<f32>> {
313        let mut cache = self
314            .small_sample_cache
315            .lock()
316            .expect("Sample cache lock poisoned");
317
318        if let Some(cached) = cache.get_mut(key) {
319            // Check if cache entry is still valid (5 minutes)
320            if cached.timestamp.elapsed() < Duration::from_secs(300) {
321                cached.access_count += 1;
322                return Some(cached.result.clone());
323            } else {
324                // Remove expired entry
325                cache.remove(key);
326            }
327        }
328        None
329    }
330
331    fn cache_result(&self, key: String, result: Vec<f32>) {
332        let mut cache = self
333            .small_sample_cache
334            .lock()
335            .expect("Sample cache lock poisoned");
336
337        // Limit cache size to prevent memory bloat
338        if cache.len() >= 100 {
339            // Remove least recently used entries
340            let mut entries: Vec<_> = cache
341                .iter()
342                .map(|(k, v)| (k.clone(), v.timestamp))
343                .collect();
344            entries.sort_by_key(|(_, timestamp)| *timestamp);
345
346            // Remove oldest 10 entries
347            for (k, _) in entries.iter().take(10) {
348                cache.remove(k);
349            }
350        }
351
352        cache.insert(
353            key,
354            CachedResult {
355                result,
356                timestamp: Instant::now(),
357                access_count: 1,
358            },
359        );
360    }
361
362    fn fast_pitch_shift(&self, audio: &[f32], target: &ConversionTarget) -> Option<Vec<f32>> {
363        let target_f0 = target.characteristics.pitch.mean_f0;
364        let base_f0 = 220.0; // Assume base frequency
365        let ratio = target_f0 / base_f0;
366
367        // Simple pitch shifting for small samples - linear interpolation
368        let output_len = audio.len();
369        let mut result = self.buffer_pool.get_buffer(output_len);
370
371        for (i, result_item) in result.iter_mut().enumerate().take(output_len) {
372            let src_index = (i as f32 * ratio) as usize;
373            if src_index < audio.len() {
374                *result_item = audio[src_index];
375            } else {
376                *result_item = 0.0;
377            }
378        }
379
380        Some(result)
381    }
382
383    fn fast_speed_transform(&self, audio: &[f32], target: &ConversionTarget) -> Option<Vec<f32>> {
384        let speed_ratio = target.characteristics.timing.speaking_rate;
385        let output_len = (audio.len() as f32 / speed_ratio) as usize;
386
387        let mut result = self.buffer_pool.get_buffer(output_len);
388
389        for (i, result_item) in result.iter_mut().enumerate().take(output_len) {
390            let src_index = (i as f32 * speed_ratio) as usize;
391            if src_index < audio.len() {
392                *result_item = audio[src_index];
393            } else {
394                *result_item = 0.0;
395            }
396        }
397
398        Some(result)
399    }
400
401    /// Get buffer pool statistics
402    pub fn get_pool_stats(&self) -> PoolStats {
403        self.buffer_pool.get_stats()
404    }
405
406    /// Clear cache to free memory
407    pub fn clear_cache(&self) {
408        self.small_sample_cache
409            .lock()
410            .expect("Sample cache lock poisoned")
411            .clear();
412    }
413}
414
415/// Performance monitor for conversion operations tracking timing and memory
416#[derive(Debug)]
417pub struct ConversionPerformanceMonitor {
418    conversion_times: Arc<Mutex<Vec<Duration>>>,
419    memory_usage: Arc<Mutex<Vec<usize>>>,
420    start_time: Option<Instant>,
421}
422
423impl Default for ConversionPerformanceMonitor {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429impl ConversionPerformanceMonitor {
430    /// Create a new performance monitor for tracking conversion timing and memory usage
431    pub fn new() -> Self {
432        Self {
433            conversion_times: Arc::new(Mutex::new(Vec::new())),
434            memory_usage: Arc::new(Mutex::new(Vec::new())),
435            start_time: None,
436        }
437    }
438
439    /// Start timing a conversion operation
440    pub fn start_timing(&mut self) {
441        self.start_time = Some(Instant::now());
442    }
443
444    /// End timing and record the elapsed duration for the current conversion operation
445    pub fn end_timing(&mut self) {
446        if let Some(start) = self.start_time.take() {
447            let duration = start.elapsed();
448            self.conversion_times
449                .lock()
450                .expect("Conversion times lock poisoned")
451                .push(duration);
452        }
453    }
454
455    /// Record memory usage in bytes for performance analysis
456    pub fn record_memory_usage(&self, bytes: usize) {
457        self.memory_usage
458            .lock()
459            .expect("Memory usage lock poisoned")
460            .push(bytes);
461    }
462
463    /// Get average conversion time across all recorded operations
464    pub fn get_average_conversion_time(&self) -> Duration {
465        let times = self
466            .conversion_times
467            .lock()
468            .expect("Conversion times lock poisoned");
469        if times.is_empty() {
470            Duration::from_millis(0)
471        } else {
472            let total_nanos: u64 = times.iter().map(|d| d.as_nanos() as u64).sum();
473            Duration::from_nanos(total_nanos / times.len() as u64)
474        }
475    }
476
477    /// Get memory statistics including minimum, maximum, and average usage in bytes
478    pub fn get_memory_stats(&self) -> (usize, usize, f64) {
479        let usage = self
480            .memory_usage
481            .lock()
482            .expect("Memory usage lock poisoned");
483        if usage.is_empty() {
484            (0, 0, 0.0)
485        } else {
486            let min = *usage.iter().min().expect("Memory usage is not empty");
487            let max = *usage.iter().max().expect("Memory usage is not empty");
488            let avg = usage.iter().sum::<usize>() as f64 / usage.len() as f64;
489            (min, max, avg)
490        }
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_audio_buffer_pool() {
500        let pool = AudioBufferPool::new();
501
502        // Test small buffer
503        let buffer1 = pool.get_buffer(1000);
504        assert!(buffer1.capacity() >= 1000);
505
506        pool.return_buffer(buffer1);
507
508        // Should reuse the buffer
509        let buffer2 = pool.get_buffer(500);
510        assert!(buffer2.capacity() >= 500);
511
512        let stats = pool.get_stats();
513        assert!(stats.hit_rate() > 0.0);
514    }
515
516    #[test]
517    fn test_small_audio_optimizer() {
518        let optimizer = SmallAudioOptimizer::new();
519
520        // Create small test audio
521        let audio: Vec<f32> = (0..1000)
522            .map(|i| (i as f32 * 440.0 * 2.0 * std::f32::consts::PI / 22050.0).sin() * 0.1)
523            .collect();
524
525        let mut target_chars = VoiceCharacteristics::default();
526        target_chars.pitch.mean_f0 = 880.0;
527        let target = ConversionTarget::new(target_chars);
528
529        // Test optimization
530        let result =
531            optimizer.optimize_small_conversion(&audio, &ConversionType::PitchShift, &target);
532        assert!(result.is_some());
533
534        // Test caching - second call should be faster
535        let result2 =
536            optimizer.optimize_small_conversion(&audio, &ConversionType::PitchShift, &target);
537        assert!(result2.is_some());
538    }
539
540    #[test]
541    fn test_performance_monitor() {
542        let mut monitor = ConversionPerformanceMonitor::new();
543
544        monitor.start_timing();
545        std::thread::sleep(Duration::from_millis(10));
546        monitor.end_timing();
547
548        monitor.record_memory_usage(1024);
549        monitor.record_memory_usage(2048);
550
551        let avg_time = monitor.get_average_conversion_time();
552        assert!(avg_time.as_millis() >= 10);
553
554        let (min_mem, max_mem, avg_mem) = monitor.get_memory_stats();
555        assert_eq!(min_mem, 1024);
556        assert_eq!(max_mem, 2048);
557        assert_eq!(avg_mem, 1536.0);
558    }
559}