1use crate::types::Position3D;
9use crate::{Error, Result};
10use scirs2_core::ndarray::{Array1, Array2, Array3, Axis};
11use scirs2_core::Complex32;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::f32::consts::PI;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum CompressionCodec {
19 PerceptualSpatial,
21 AmbisonicsOptimized,
23 PositionalCompression,
25 Hybrid,
27 Lossless,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum CompressionQuality {
34 Low,
36 Medium,
38 High,
40 VeryHigh,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SpatialCompressionConfig {
47 pub codec: CompressionCodec,
49 pub quality: CompressionQuality,
51 pub target_bitrate: u32,
53 pub sample_rate: f32,
55 pub channel_count: usize,
57 pub perceptual_params: PerceptualParams,
59 pub spatial_params: SpatialParams,
61 pub adaptive_params: AdaptiveParams,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PerceptualParams {
68 pub masking_enabled: bool,
70 pub frequency_bands: usize,
72 pub spatial_masking_threshold: f32,
74 pub temporal_masking: TemporalMasking,
76 pub loudness_compensation: bool,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct TemporalMasking {
83 pub enabled: bool,
85 pub pre_masking_ms: f32,
87 pub post_masking_ms: f32,
89 pub threshold_db: f32,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct SpatialParams {
96 pub spatial_resolution: f32,
98 pub distance_quantization: usize,
100 pub ambisonics_order: usize,
102 pub source_clustering: SourceClustering,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct SourceClustering {
109 pub enabled: bool,
111 pub max_cluster_distance: f32,
113 pub max_sources_per_cluster: usize,
115 pub update_interval_ms: f32,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct AdaptiveParams {
122 pub adaptive_bitrate: bool,
124 pub min_bitrate: u32,
126 pub max_bitrate: u32,
128 pub adaptation_window: f32,
130 pub quality_threshold: f32,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct CompressedFrame {
137 pub audio_data: Vec<u8>,
139 pub spatial_metadata: SpatialMetadata,
141 pub compression_stats: CompressionStats,
143 pub timestamp_ms: f64,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct SpatialMetadata {
150 pub source_positions: Vec<Position3D>,
152 pub ambisonics_coefficients: Vec<f32>,
154 pub spatial_covariance: Vec<f32>,
156 pub distance_factors: Vec<f32>,
158 pub listener_orientation: (f32, f32, f32), }
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct CompressionStats {
165 pub original_size: usize,
167 pub compressed_size: usize,
169 pub compression_ratio: f32,
171 pub achieved_bitrate: f32,
173 pub quality_loss: f32,
175 pub processing_time_ms: f32,
177}
178
179pub struct SpatialCompressor {
181 config: SpatialCompressionConfig,
183 perceptual_model: PerceptualModel,
185 spatial_encoder: SpatialEncoder,
187 adaptive_controller: AdaptiveController,
189 input_buffer: Array2<f32>,
191 output_buffer: Vec<u8>,
192 frame_count: u64,
194}
195
196#[derive(Debug)]
198struct PerceptualModel {
199 frequency_bands: Array1<f32>,
201 masking_thresholds: Array1<f32>,
203 bark_scale: Array1<f32>,
205 temporal_state: TemporalMaskingState,
207}
208
209#[derive(Debug)]
211struct TemporalMaskingState {
212 prev_energy: Array1<f32>,
214 pre_masking_buffer: Array2<f32>,
216 post_masking_buffer: Array2<f32>,
218}
219
220#[derive(Debug)]
222struct SpatialEncoder {
223 method: CompressionCodec,
225 quantization_tables: HashMap<String, Array1<f32>>,
227 huffman_tables: HashMap<String, Vec<(u8, Vec<bool>)>>,
229 source_clusters: Vec<SourceCluster>,
231}
232
233#[derive(Debug, Clone)]
235struct SourceCluster {
236 center: Position3D,
238 source_indices: Vec<usize>,
240 representative_signal: Array1<f32>,
242 mixing_weights: Array1<f32>,
244}
245
246#[derive(Debug)]
248struct AdaptiveController {
249 current_bitrate: u32,
251 quality_history: Vec<f32>,
253 bitrate_history: Vec<u32>,
255 window_samples: usize,
257}
258
259impl Default for SpatialCompressionConfig {
260 fn default() -> Self {
261 Self {
262 codec: CompressionCodec::PerceptualSpatial,
263 quality: CompressionQuality::Medium,
264 target_bitrate: 128000, sample_rate: 48000.0,
266 channel_count: 8,
267 perceptual_params: PerceptualParams {
268 masking_enabled: true,
269 frequency_bands: 32,
270 spatial_masking_threshold: -40.0,
271 temporal_masking: TemporalMasking {
272 enabled: true,
273 pre_masking_ms: 2.0,
274 post_masking_ms: 100.0,
275 threshold_db: -20.0,
276 },
277 loudness_compensation: true,
278 },
279 spatial_params: SpatialParams {
280 spatial_resolution: 5.0, distance_quantization: 32,
282 ambisonics_order: 3,
283 source_clustering: SourceClustering {
284 enabled: true,
285 max_cluster_distance: 1.0,
286 max_sources_per_cluster: 4,
287 update_interval_ms: 100.0,
288 },
289 },
290 adaptive_params: AdaptiveParams {
291 adaptive_bitrate: true,
292 min_bitrate: 64000,
293 max_bitrate: 320000,
294 adaptation_window: 5.0,
295 quality_threshold: 0.85,
296 },
297 }
298 }
299}
300
301impl SpatialCompressor {
302 pub fn new(config: SpatialCompressionConfig) -> Result<Self> {
304 let perceptual_model = PerceptualModel::new(&config.perceptual_params, config.sample_rate)?;
305 let spatial_encoder = SpatialEncoder::new(&config)?;
306 let adaptive_controller = AdaptiveController::new(&config.adaptive_params)?;
307
308 let buffer_size = 1024; let input_buffer = Array2::zeros((config.channel_count, buffer_size));
310 let output_buffer = Vec::with_capacity(buffer_size * config.channel_count);
311
312 Ok(Self {
313 config,
314 perceptual_model,
315 spatial_encoder,
316 adaptive_controller,
317 input_buffer,
318 output_buffer,
319 frame_count: 0,
320 })
321 }
322
323 pub fn compress_frame(
325 &mut self,
326 audio_data: &Array2<f32>,
327 spatial_metadata: &SpatialMetadata,
328 ) -> Result<CompressedFrame> {
329 let start_time = std::time::Instant::now();
330
331 if audio_data.nrows() != self.config.channel_count {
332 return Err(Error::LegacyProcessing(format!(
333 "Expected {} channels, got {}",
334 self.config.channel_count,
335 audio_data.nrows()
336 )));
337 }
338
339 if self.config.adaptive_params.adaptive_bitrate {
341 self.adaptive_controller.update(&self.config)?;
342 }
343
344 let masked_audio = self.apply_perceptual_masking(audio_data)?;
346
347 let compressed_audio = match self.config.codec {
349 CompressionCodec::PerceptualSpatial => {
350 self.compress_perceptual_spatial(&masked_audio, spatial_metadata)?
351 }
352 CompressionCodec::AmbisonicsOptimized => {
353 self.compress_ambisonics_optimized(&masked_audio, spatial_metadata)?
354 }
355 CompressionCodec::PositionalCompression => {
356 self.compress_positional(&masked_audio, spatial_metadata)?
357 }
358 CompressionCodec::Hybrid => self.compress_hybrid(&masked_audio, spatial_metadata)?,
359 CompressionCodec::Lossless => {
360 self.compress_lossless(&masked_audio, spatial_metadata)?
361 }
362 };
363
364 let processing_time = start_time.elapsed().as_secs_f32() * 1000.0;
365
366 let original_size = audio_data.len() * std::mem::size_of::<f32>();
368 let compressed_size = compressed_audio.len();
369 let compression_ratio = original_size as f32 / compressed_size as f32;
370 let achieved_bitrate =
371 (compressed_size as f32 * 8.0 * self.config.sample_rate) / audio_data.ncols() as f32;
372
373 let compression_stats = CompressionStats {
374 original_size,
375 compressed_size,
376 compression_ratio,
377 achieved_bitrate,
378 quality_loss: self.estimate_quality_loss(&masked_audio, &compressed_audio)?,
379 processing_time_ms: processing_time,
380 };
381
382 self.frame_count += 1;
383
384 Ok(CompressedFrame {
385 audio_data: compressed_audio,
386 spatial_metadata: spatial_metadata.clone(),
387 compression_stats,
388 timestamp_ms: self.frame_count as f64 * 1000.0 * audio_data.ncols() as f64
389 / self.config.sample_rate as f64,
390 })
391 }
392
393 fn apply_perceptual_masking(&mut self, audio_data: &Array2<f32>) -> Result<Array2<f32>> {
395 if !self.config.perceptual_params.masking_enabled {
396 return Ok(audio_data.clone());
397 }
398
399 let mut masked_audio = audio_data.clone();
400
401 for channel_idx in 0..audio_data.nrows() {
403 let channel_data = audio_data.row(channel_idx).to_owned();
404 let masked_channel = self.perceptual_model.apply_masking(&channel_data)?;
405 masked_audio.row_mut(channel_idx).assign(&masked_channel);
406 }
407
408 if self.config.perceptual_params.temporal_masking.enabled {
410 self.perceptual_model
411 .apply_temporal_masking(&mut masked_audio)?;
412 }
413
414 Ok(masked_audio)
415 }
416
417 fn compress_perceptual_spatial(
419 &mut self,
420 audio_data: &Array2<f32>,
421 _spatial_metadata: &SpatialMetadata,
422 ) -> Result<Vec<u8>> {
423 let mut compressed = Vec::new();
425
426 for channel in audio_data.rows() {
428 let channel_owned = channel.to_owned();
429 let quantized = self.quantize_channel(&channel_owned, self.config.quality)?;
430 compressed.extend_from_slice(&quantized);
431 }
432
433 self.apply_entropy_coding(&compressed)
435 }
436
437 fn compress_ambisonics_optimized(
439 &mut self,
440 audio_data: &Array2<f32>,
441 spatial_metadata: &SpatialMetadata,
442 ) -> Result<Vec<u8>> {
443 let ambisonics_data = self.convert_to_ambisonics(audio_data, spatial_metadata)?;
445
446 let mut compressed = Vec::new();
448 let order = self.config.spatial_params.ambisonics_order;
449
450 for (idx, channel) in ambisonics_data.rows().into_iter().enumerate() {
451 let channel_order = self.get_ambisonics_channel_order(idx);
452 let quality_factor = if channel_order == 0 {
453 1.0
454 } else {
455 0.7 / channel_order as f32
456 };
457
458 let adjusted_quality = match self.config.quality {
459 CompressionQuality::Low => CompressionQuality::Low,
460 CompressionQuality::Medium => {
461 if quality_factor > 0.5 {
462 CompressionQuality::Medium
463 } else {
464 CompressionQuality::Low
465 }
466 }
467 CompressionQuality::High => {
468 if quality_factor > 0.7 {
469 CompressionQuality::High
470 } else {
471 CompressionQuality::Medium
472 }
473 }
474 CompressionQuality::VeryHigh => CompressionQuality::High,
475 };
476
477 let channel_owned = channel.to_owned();
478 let quantized = self.quantize_channel(&channel_owned, adjusted_quality)?;
479 compressed.extend_from_slice(&quantized);
480 }
481
482 self.apply_entropy_coding(&compressed)
483 }
484
485 fn compress_positional(
487 &mut self,
488 audio_data: &Array2<f32>,
489 spatial_metadata: &SpatialMetadata,
490 ) -> Result<Vec<u8>> {
491 self.spatial_encoder
493 .update_clusters(&spatial_metadata.source_positions)?;
494
495 let mut compressed = Vec::new();
496
497 for cluster in &self.spatial_encoder.source_clusters {
499 let quantized =
501 self.quantize_channel(&cluster.representative_signal, self.config.quality)?;
502 compressed.extend_from_slice(&quantized);
503
504 let weight_bytes = self.quantize_weights(&cluster.mixing_weights)?;
506 compressed.extend_from_slice(&weight_bytes);
507 }
508
509 let cluster_metadata = self.compress_cluster_metadata()?;
511 compressed.extend_from_slice(&cluster_metadata);
512
513 self.apply_entropy_coding(&compressed)
514 }
515
516 fn compress_hybrid(
518 &mut self,
519 audio_data: &Array2<f32>,
520 spatial_metadata: &SpatialMetadata,
521 ) -> Result<Vec<u8>> {
522 let mut compressed = Vec::new();
524
525 let low_freq_data = self.filter_frequency_range(audio_data, 0.0, 1000.0)?;
527 let low_compressed = self.compress_perceptual_spatial(&low_freq_data, spatial_metadata)?;
528 compressed.extend_from_slice(&low_compressed);
529
530 let mid_freq_data = self.filter_frequency_range(audio_data, 1000.0, 8000.0)?;
532 let mid_compressed =
533 self.compress_ambisonics_optimized(&mid_freq_data, spatial_metadata)?;
534 compressed.extend_from_slice(&mid_compressed);
535
536 let high_freq_data = self.filter_frequency_range(audio_data, 8000.0, 20000.0)?;
538 let high_compressed = self.compress_positional(&high_freq_data, spatial_metadata)?;
539 compressed.extend_from_slice(&high_compressed);
540
541 Ok(compressed)
542 }
543
544 fn compress_lossless(
546 &mut self,
547 audio_data: &Array2<f32>,
548 _spatial_metadata: &SpatialMetadata,
549 ) -> Result<Vec<u8>> {
550 let mut data_bytes = Vec::new();
552 for &sample in audio_data.iter() {
553 data_bytes.extend_from_slice(&sample.to_le_bytes());
554 }
555
556 self.apply_lossless_compression(&data_bytes)
558 }
559
560 fn quantize_channel(
562 &self,
563 channel_data: &Array1<f32>,
564 quality: CompressionQuality,
565 ) -> Result<Vec<u8>> {
566 let bit_depth = match quality {
567 CompressionQuality::Low => 8,
568 CompressionQuality::Medium => 12,
569 CompressionQuality::High => 16,
570 CompressionQuality::VeryHigh => 20,
571 };
572
573 let max_value = (1 << (bit_depth - 1)) - 1;
574 let mut quantized = Vec::new();
575
576 for &sample in channel_data.iter() {
577 let quantized_sample = (sample * max_value as f32) as i32;
578 let clamped_sample = quantized_sample.clamp(-max_value, max_value);
579
580 match bit_depth {
582 8 => quantized.push(clamped_sample as u8),
583 12 => {
584 quantized.push((clamped_sample & 0xFF) as u8);
585 quantized.push(((clamped_sample >> 8) & 0x0F) as u8);
586 }
587 16 => quantized.extend_from_slice(&(clamped_sample as i16).to_le_bytes()),
588 20 => {
589 quantized.extend_from_slice(&(clamped_sample & 0xFFFFFF).to_le_bytes()[..3]);
590 }
591 _ => return Err(Error::LegacyProcessing("Unsupported bit depth".to_string())),
592 }
593 }
594
595 Ok(quantized)
596 }
597
598 fn apply_entropy_coding(&self, data: &[u8]) -> Result<Vec<u8>> {
600 let mut compressed = Vec::new();
602 let mut i = 0;
603
604 while i < data.len() {
605 let current_byte = data[i];
606 let mut run_length = 1;
607
608 while i + run_length < data.len()
610 && data[i + run_length] == current_byte
611 && run_length < 255
612 {
613 run_length += 1;
614 }
615
616 if run_length > 3 {
617 compressed.push(0xFF); compressed.push(current_byte);
619 compressed.push(run_length as u8);
620 } else {
621 for _ in 0..run_length {
622 compressed.push(current_byte);
623 }
624 }
625
626 i += run_length;
627 }
628
629 Ok(compressed)
630 }
631
632 fn convert_to_ambisonics(
634 &self,
635 audio_data: &Array2<f32>,
636 spatial_metadata: &SpatialMetadata,
637 ) -> Result<Array2<f32>> {
638 let order = self.config.spatial_params.ambisonics_order;
639 let ambisonics_channels = (order + 1) * (order + 1);
640 let mut ambisonics_data = Array2::zeros((ambisonics_channels, audio_data.ncols()));
641
642 for (source_idx, &position) in spatial_metadata.source_positions.iter().enumerate() {
644 if source_idx >= audio_data.nrows() {
645 break;
646 }
647
648 let azimuth = position.y.atan2(position.x);
649 let elevation = position
650 .z
651 .atan2((position.x * position.x + position.y * position.y).sqrt());
652
653 ambisonics_data
655 .row_mut(0)
656 .scaled_add(1.0, &audio_data.row(source_idx));
657
658 if ambisonics_channels > 1 {
660 ambisonics_data
661 .row_mut(1)
662 .scaled_add(azimuth.cos() * elevation.cos(), &audio_data.row(source_idx));
663 }
664 if ambisonics_channels > 2 {
665 ambisonics_data
666 .row_mut(2)
667 .scaled_add(azimuth.sin() * elevation.cos(), &audio_data.row(source_idx));
668 }
669 if ambisonics_channels > 3 {
670 ambisonics_data
671 .row_mut(3)
672 .scaled_add(elevation.sin(), &audio_data.row(source_idx));
673 }
674 }
675
676 Ok(ambisonics_data)
677 }
678
679 fn get_ambisonics_channel_order(&self, channel_idx: usize) -> usize {
681 if channel_idx == 0 {
683 0
684 } else if channel_idx <= 3 {
685 1
686 } else if channel_idx <= 8 {
687 2
688 } else {
689 3
690 }
691 }
692
693 fn filter_frequency_range(
695 &self,
696 audio_data: &Array2<f32>,
697 _low_freq: f32,
698 _high_freq: f32,
699 ) -> Result<Array2<f32>> {
700 Ok(audio_data.clone())
702 }
703
704 fn quantize_weights(&self, weights: &Array1<f32>) -> Result<Vec<u8>> {
706 let mut quantized = Vec::new();
707 for &weight in weights.iter() {
708 let quantized_weight = (weight * 255.0) as u8;
709 quantized.push(quantized_weight);
710 }
711 Ok(quantized)
712 }
713
714 fn compress_cluster_metadata(&self) -> Result<Vec<u8>> {
716 let mut metadata = Vec::new();
717
718 metadata.push(self.spatial_encoder.source_clusters.len() as u8);
720
721 for cluster in &self.spatial_encoder.source_clusters {
723 let x_quantized = ((cluster.center.x + 10.0) * 25.5) as u8; let y_quantized = ((cluster.center.y + 10.0) * 25.5) as u8;
725 let z_quantized = ((cluster.center.z + 10.0) * 25.5) as u8;
726
727 metadata.extend_from_slice(&[x_quantized, y_quantized, z_quantized]);
728 metadata.push(cluster.source_indices.len() as u8);
729 }
730
731 Ok(metadata)
732 }
733
734 fn apply_lossless_compression(&self, data: &[u8]) -> Result<Vec<u8>> {
736 let mut compressed = Vec::new();
738 let mut i = 0;
739
740 while i < data.len() {
741 let mut best_length = 0;
742 let mut best_distance = 0;
743
744 let search_start = i.saturating_sub(4096);
746 for j in search_start..i {
747 let mut length = 0;
748 while i + length < data.len()
749 && j + length < i
750 && data[i + length] == data[j + length]
751 && length < 255
752 {
753 length += 1;
754 }
755
756 if length > best_length && length >= 3 {
757 best_length = length;
758 best_distance = i - j;
759 }
760 }
761
762 if best_length > 0 {
763 compressed.push(0xFF); compressed.push(0xFE); compressed.extend_from_slice(&(best_distance as u16).to_le_bytes());
767 compressed.push(best_length as u8);
768 i += best_length;
769 } else {
770 compressed.push(data[i]);
772 i += 1;
773 }
774 }
775
776 Ok(compressed)
777 }
778
779 fn estimate_quality_loss(&self, _original: &Array2<f32>, _compressed: &[u8]) -> Result<f32> {
781 let compression_ratio = _original.len() as f32 * 4.0 / _compressed.len() as f32;
783 let quality_loss = (compression_ratio - 1.0) / 10.0;
784 Ok(quality_loss.clamp(0.0, 1.0))
785 }
786
787 pub fn config(&self) -> &SpatialCompressionConfig {
789 &self.config
790 }
791
792 pub fn get_stats(&self) -> Option<CompressionStats> {
794 None
796 }
797}
798
799impl PerceptualModel {
801 fn new(params: &PerceptualParams, sample_rate: f32) -> Result<Self> {
802 let frequency_bands = Array1::linspace(0.0, sample_rate / 2.0, params.frequency_bands);
803 let masking_thresholds = Array1::zeros(params.frequency_bands);
804 let bark_scale = Self::compute_bark_scale(&frequency_bands);
805
806 let temporal_state = TemporalMaskingState {
807 prev_energy: Array1::zeros(params.frequency_bands),
808 pre_masking_buffer: Array2::zeros((params.frequency_bands, 10)),
809 post_masking_buffer: Array2::zeros((params.frequency_bands, 100)),
810 };
811
812 Ok(Self {
813 frequency_bands,
814 masking_thresholds,
815 bark_scale,
816 temporal_state,
817 })
818 }
819
820 fn compute_bark_scale(frequencies: &Array1<f32>) -> Array1<f32> {
821 frequencies.mapv(|f| 13.0 * (0.00076 * f).atan() + 3.5 * ((f / 7500.0).powi(2)).atan())
822 }
823
824 fn apply_masking(&mut self, channel_data: &Array1<f32>) -> Result<Array1<f32>> {
825 Ok(channel_data.mapv(|x| x * 0.9))
827 }
828
829 fn apply_temporal_masking(&mut self, _audio_data: &mut Array2<f32>) -> Result<()> {
830 Ok(())
832 }
833}
834
835impl SpatialEncoder {
836 fn new(config: &SpatialCompressionConfig) -> Result<Self> {
837 let quantization_tables = HashMap::new();
838 let huffman_tables = HashMap::new();
839 let source_clusters = Vec::new();
840
841 Ok(Self {
842 method: config.codec,
843 quantization_tables,
844 huffman_tables,
845 source_clusters,
846 })
847 }
848
849 fn update_clusters(&mut self, _positions: &[Position3D]) -> Result<()> {
850 Ok(())
852 }
853}
854
855impl AdaptiveController {
856 fn new(params: &AdaptiveParams) -> Result<Self> {
857 Ok(Self {
858 current_bitrate: params.min_bitrate,
859 quality_history: Vec::new(),
860 bitrate_history: Vec::new(),
861 window_samples: (params.adaptation_window * 48000.0) as usize, })
863 }
864
865 fn update(&mut self, _config: &SpatialCompressionConfig) -> Result<()> {
866 Ok(())
868 }
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 #[test]
876 fn test_compression_config_default() {
877 let config = SpatialCompressionConfig::default();
878 assert_eq!(config.codec, CompressionCodec::PerceptualSpatial);
879 assert_eq!(config.quality, CompressionQuality::Medium);
880 assert_eq!(config.target_bitrate, 128000);
881 }
882
883 #[test]
884 fn test_compressor_creation() {
885 let config = SpatialCompressionConfig::default();
886 let compressor = SpatialCompressor::new(config);
887 assert!(compressor.is_ok());
888 }
889
890 #[test]
891 fn test_frame_compression() {
892 let config = SpatialCompressionConfig::default();
893 let mut compressor = SpatialCompressor::new(config).unwrap();
894
895 let audio_data = Array2::ones((8, 1024));
896 let spatial_metadata = SpatialMetadata {
897 source_positions: vec![Position3D {
898 x: 1.0,
899 y: 0.0,
900 z: 0.0,
901 }],
902 ambisonics_coefficients: vec![],
903 spatial_covariance: vec![],
904 distance_factors: vec![1.0],
905 listener_orientation: (0.0, 0.0, 0.0),
906 };
907
908 let result = compressor.compress_frame(&audio_data, &spatial_metadata);
909 assert!(result.is_ok());
910
911 let compressed_frame = result.unwrap();
912 assert!(!compressed_frame.audio_data.is_empty());
913 assert!(compressed_frame.compression_stats.compression_ratio > 1.0);
914 }
915
916 #[test]
917 fn test_quality_levels() {
918 let qualities = [
919 CompressionQuality::Low,
920 CompressionQuality::Medium,
921 CompressionQuality::High,
922 CompressionQuality::VeryHigh,
923 ];
924
925 for quality in &qualities {
926 let mut config = SpatialCompressionConfig::default();
927 config.quality = *quality;
928 let compressor = SpatialCompressor::new(config);
929 assert!(compressor.is_ok());
930 }
931 }
932
933 #[test]
934 fn test_compression_codecs() {
935 let codecs = [
936 CompressionCodec::PerceptualSpatial,
937 CompressionCodec::AmbisonicsOptimized,
938 CompressionCodec::PositionalCompression,
939 CompressionCodec::Hybrid,
940 CompressionCodec::Lossless,
941 ];
942
943 for codec in &codecs {
944 let mut config = SpatialCompressionConfig::default();
945 config.codec = *codec;
946 let compressor = SpatialCompressor::new(config);
947 assert!(compressor.is_ok());
948 }
949 }
950
951 #[test]
952 fn test_perceptual_model() {
953 let params = PerceptualParams {
954 masking_enabled: true,
955 frequency_bands: 32,
956 spatial_masking_threshold: -40.0,
957 temporal_masking: TemporalMasking {
958 enabled: true,
959 pre_masking_ms: 2.0,
960 post_masking_ms: 100.0,
961 threshold_db: -20.0,
962 },
963 loudness_compensation: true,
964 };
965
966 let model = PerceptualModel::new(¶ms, 48000.0);
967 assert!(model.is_ok());
968 }
969}