Skip to main content

opus_head_sys/
lib.rs

1//! # opus-head-sys
2//!
3//! Low-level FFI bindings to the Opus audio codec.
4//!
5//! ## Safety
6//!
7//! This crate provides raw FFI bindings. All functions that call into the C
8//! library are unsafe. Users should consider using a safe wrapper crate.
9//!
10//! ## License
11//!
12//! The Opus codec is licensed under the BSD 3-Clause License.
13//! See the NOTICE file for full copyright information.
14
15#![allow(non_upper_case_globals)]
16#![allow(non_camel_case_types)]
17#![allow(non_snake_case)]
18#![allow(dead_code)]
19#![allow(clippy::all)]
20
21mod bindings;
22pub use bindings::*;
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27
28    #[test]
29    fn test_opus_version() {
30        unsafe {
31            let version = opus_get_version_string();
32            assert!(!version.is_null());
33            let version_str = std::ffi::CStr::from_ptr(version);
34            let version_string = version_str.to_string_lossy();
35            println!("Opus version: {}", version_string);
36            assert!(version_string.contains("opus") || version_string.contains("libopus"));
37        }
38    }
39
40    #[test]
41    fn test_encoder_size() {
42        unsafe {
43            let size_mono = opus_encoder_get_size(1);
44            let size_stereo = opus_encoder_get_size(2);
45            assert!(size_mono > 0);
46            assert!(size_stereo > 0);
47            assert!(size_stereo >= size_mono);
48        }
49    }
50
51    #[test]
52    fn test_decoder_size() {
53        unsafe {
54            let size_mono = opus_decoder_get_size(1);
55            let size_stereo = opus_decoder_get_size(2);
56            assert!(size_mono > 0);
57            assert!(size_stereo > 0);
58        }
59    }
60
61    #[test]
62    fn test_error_string() {
63        unsafe {
64            let ok_str = opus_strerror(OPUS_OK as i32);
65            assert!(!ok_str.is_null());
66
67            let bad_arg_str = opus_strerror(OPUS_BAD_ARG);
68            assert!(!bad_arg_str.is_null());
69        }
70    }
71
72    #[test]
73    fn test_multistream_encoder_size() {
74        unsafe {
75            // 2 channels, 1 stream, 1 coupled stream (stereo)
76            let size = opus_multistream_encoder_get_size(1, 1);
77            assert!(size > 0);
78
79            // 6 channels (5.1 surround), 4 streams, 2 coupled
80            let size_surround = opus_multistream_surround_encoder_get_size(6, 1);
81            assert!(size_surround > 0);
82        }
83    }
84
85    #[test]
86    fn test_multistream_decoder_size() {
87        unsafe {
88            // 2 channels, 1 stream, 1 coupled stream (stereo)
89            let size = opus_multistream_decoder_get_size(1, 1);
90            assert!(size > 0);
91        }
92    }
93
94    #[test]
95    fn test_projection_encoder_size() {
96        unsafe {
97            // Ambisonics: 4 channels (first-order ambisonics), mapping_family=3
98            let size = opus_projection_ambisonics_encoder_get_size(4, 3);
99            assert!(size > 0, "Expected positive size, got {}", size);
100        }
101    }
102
103    #[test]
104    fn test_projection_decoder_size() {
105        unsafe {
106            // 4 output channels, 2 streams, 1 coupled
107            let size = opus_projection_decoder_get_size(4, 2, 1);
108            assert!(size > 0);
109        }
110    }
111
112    /// Test loading DNN weights from target/model and setting them via OPUS_SET_DNN_BLOB.
113    ///
114    /// This test requires the weights to be generated first:
115    /// ```bash
116    /// python generate_weights.py
117    /// ```
118    ///
119    /// Note: As of now, the actual weight loading may crash on some configurations.
120    /// This test verifies that the weight file can be read and has the correct format.
121    #[test]
122    #[cfg(feature = "dnn")]
123    fn test_dnn_blob_loading() {
124        use std::path::PathBuf;
125
126        // Find the weights file in target/model
127        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
128        let model_dir = manifest_dir.join("target").join("model");
129
130        println!("Looking for weights in: {:?}", model_dir);
131
132        // Look for any opus_data-*.bin file
133        let weights_path = match std::fs::read_dir(&model_dir) {
134            Ok(dir) => {
135                match dir.filter_map(|e| e.ok()).map(|e| e.path()).find(|p| {
136                    p.file_name()
137                        .and_then(|n| n.to_str())
138                        .map(|n| n.starts_with("opus_data-") && n.ends_with(".bin"))
139                        .unwrap_or(false)
140                }) {
141                    Some(path) => path,
142                    None => {
143                        panic!(
144                            "No opus_data-*.bin found in {:?}.\n\
145                            Run 'python generate_weights.py' to generate weights.",
146                            model_dir
147                        );
148                    }
149                }
150            }
151            Err(e) => {
152                panic!(
153                    "target/model directory not found ({:?}): {}\n\
154                    Run 'python generate_weights.py' to generate weights.",
155                    model_dir, e
156                );
157            }
158        };
159
160        println!("Loading weights from: {:?}", weights_path);
161
162        // Read the weights file
163        let weights_data = std::fs::read(&weights_path).expect("Failed to read weights file");
164
165        println!("Loaded {} bytes of DNN weights", weights_data.len());
166
167        // Verify the file has the correct magic header "DNNw"
168        assert!(weights_data.len() > 4, "Weights file too small");
169        assert_eq!(
170            &weights_data[0..4],
171            b"DNNw",
172            "Invalid weights file magic header"
173        );
174
175        // Verify reasonable size (should be ~14MB for full weights)
176        assert!(
177            weights_data.len() > 1_000_000,
178            "Weights file seems too small"
179        );
180        assert!(
181            weights_data.len() < 100_000_000,
182            "Weights file seems too large"
183        );
184
185        println!("Weight file format validation passed");
186        println!("  Magic: DNNw");
187        println!(
188            "  Size: {} bytes ({:.2} MB)",
189            weights_data.len(),
190            weights_data.len() as f64 / 1_000_000.0
191        );
192
193        // Test encoder with DNN blob loading
194        unsafe {
195            let mut error: i32 = 0;
196
197            // Create encoder
198            println!("Creating encoder...");
199            let encoder = opus_encoder_create(48000, 1, OPUS_APPLICATION_VOIP as i32, &mut error);
200            println!(
201                "opus_encoder_create returned error={}, encoder={:?}",
202                error, encoder
203            );
204            if error != OPUS_OK as i32 || encoder.is_null() {
205                println!("Failed to create encoder");
206                return;
207            }
208
209            // Load DNN weights into encoder
210            println!("Calling opus_encoder_ctl(OPUS_SET_DNN_BLOB)...");
211            let ret = opus_encoder_ctl(
212                encoder,
213                OPUS_SET_DNN_BLOB_REQUEST as i32,
214                weights_data.as_ptr() as *const std::ffi::c_void,
215                weights_data.len() as i32,
216            );
217            println!("opus_encoder_ctl(OPUS_SET_DNN_BLOB) returned: {}", ret);
218
219            println!("Destroying encoder...");
220            opus_encoder_destroy(encoder);
221            println!("Encoder destroyed");
222
223            if ret != OPUS_OK as i32 {
224                println!("DNN blob loading failed on encoder with code {}", ret);
225                println!("This may indicate the weights file format is incompatible.");
226                return;
227            }
228
229            println!("Encoder DNN blob loading: OK");
230
231            // Create decoder
232            println!("Creating decoder...");
233            let decoder = opus_decoder_create(48000, 1, &mut error);
234            println!(
235                "opus_decoder_create returned error={}, decoder={:?}",
236                error, decoder
237            );
238            if error != OPUS_OK as i32 || decoder.is_null() {
239                println!("Failed to create decoder");
240                return;
241            }
242
243            // Load DNN weights into decoder
244            println!("Calling opus_decoder_ctl(OPUS_SET_DNN_BLOB)...");
245            let ret = opus_decoder_ctl(
246                decoder,
247                OPUS_SET_DNN_BLOB_REQUEST as i32,
248                weights_data.as_ptr() as *const std::ffi::c_void,
249                weights_data.len() as i32,
250            );
251            println!("opus_decoder_ctl(OPUS_SET_DNN_BLOB) returned: {}", ret);
252
253            println!("Destroying decoder...");
254            opus_decoder_destroy(decoder);
255            println!("Decoder destroyed");
256
257            if ret != OPUS_OK as i32 {
258                println!("DNN blob loading failed on decoder with code {}", ret);
259                return;
260            }
261
262            println!("Decoder DNN blob loading: OK");
263        }
264
265        println!("DNN blob loading test passed!");
266    }
267
268    /// Helper function to create deterministic random audio data (noise)
269    /// Uses a seed for reproducibility across tests
270    fn generate_noise_with_seed(samples: usize, seed: u64) -> Vec<i16> {
271        use std::collections::hash_map::DefaultHasher;
272        use std::hash::{Hash, Hasher};
273
274        let mut data = Vec::with_capacity(samples);
275        let mut hasher = DefaultHasher::new();
276        seed.hash(&mut hasher);
277
278        for i in 0..samples {
279            i.hash(&mut hasher);
280            let hash = hasher.finish();
281            // Scale to i16 range with reduced amplitude to avoid clipping
282            let sample = ((hash as i32 % 32768) - 16384) as i16;
283            data.push(sample);
284        }
285        data
286    }
287
288    /// Helper function to create random audio data (noise) - default seed
289    fn generate_noise(samples: usize) -> Vec<i16> {
290        generate_noise_with_seed(samples, 12345)
291    }
292
293    /// Helper function to load DNN weights if available
294    fn load_dnn_weights() -> Option<Vec<u8>> {
295        use std::path::PathBuf;
296
297        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
298        let model_dir = manifest_dir.join("target").join("model");
299
300        let weights_path = std::fs::read_dir(&model_dir)
301            .ok()?
302            .filter_map(|e| e.ok())
303            .map(|e| e.path())
304            .find(|p| {
305                p.file_name()
306                    .and_then(|n| n.to_str())
307                    .map(|n| n.starts_with("opus_data-") && n.ends_with(".bin"))
308                    .unwrap_or(false)
309            })?;
310
311        std::fs::read(&weights_path).ok()
312    }
313
314    /// Test basic encode/decode roundtrip WITHOUT DNN model
315    /// Also tries to enable DRED (should fail without DNN weights loaded)
316    #[test]
317    fn test_encode_decode_without_dnn() {
318        const SAMPLE_RATE: i32 = 48000;
319        const CHANNELS: i32 = 1; // Mono
320        const FRAME_SIZE: usize = 960; // 20ms at 48kHz
321        const BITRATE: i32 = 32000; // 32kbit/s
322
323        unsafe {
324            let mut error: i32 = 0;
325
326            // Create encoder
327            let encoder = opus_encoder_create(
328                SAMPLE_RATE,
329                CHANNELS,
330                OPUS_APPLICATION_VOIP as i32,
331                &mut error,
332            );
333            assert_eq!(error, OPUS_OK as i32, "Failed to create encoder");
334            assert!(!encoder.is_null());
335
336            // Set bitrate
337            let ret = opus_encoder_ctl(encoder, OPUS_SET_BITRATE_REQUEST as i32, BITRATE);
338            assert_eq!(ret, OPUS_OK as i32, "Failed to set bitrate");
339
340            // Set packet loss (needed for DRED to add redundancy)
341            let packet_loss_perc: i32 = 25;
342            let ret = opus_encoder_ctl(
343                encoder,
344                OPUS_SET_PACKET_LOSS_PERC_REQUEST as i32,
345                packet_loss_perc,
346            );
347            assert_eq!(ret, OPUS_OK as i32, "Failed to set packet loss");
348
349            // Try to enable DRED without DNN weights (should fail or be ignored)
350            let dred_duration_ms: i32 = 100; // 100ms of DRED redundancy
351            let ret = opus_encoder_ctl(
352                encoder,
353                OPUS_SET_DRED_DURATION_REQUEST as i32,
354                dred_duration_ms,
355            );
356            println!("OPUS_SET_DRED_DURATION (without DNN) returned: {}", ret);
357            // Note: This may return OPUS_OK but DRED won't actually work without weights
358
359            // Create decoder
360            let decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &mut error);
361            assert_eq!(error, OPUS_OK as i32, "Failed to create decoder");
362            assert!(!decoder.is_null());
363
364            // Generate noise input
365            let input = generate_noise(FRAME_SIZE);
366            let mut encoded = vec![0u8; 4000]; // Max packet size
367            let mut decoded = vec![0i16; FRAME_SIZE];
368
369            // Encode
370            let encoded_len = opus_encode(
371                encoder,
372                input.as_ptr(),
373                FRAME_SIZE as i32,
374                encoded.as_mut_ptr(),
375                encoded.len() as i32,
376            );
377            assert!(
378                encoded_len > 0,
379                "Encoding failed with error {}",
380                encoded_len
381            );
382            println!(
383                "Encoded {} samples to {} bytes (WITHOUT DNN, DRED attempted)",
384                FRAME_SIZE, encoded_len
385            );
386
387            // Decode
388            let decoded_len = opus_decode(
389                decoder,
390                encoded.as_ptr(),
391                encoded_len,
392                decoded.as_mut_ptr(),
393                FRAME_SIZE as i32,
394                0,
395            );
396            assert_eq!(
397                decoded_len, FRAME_SIZE as i32,
398                "Decoding failed or wrong frame size"
399            );
400            println!(
401                "Decoded {} bytes to {} samples (WITHOUT DNN)",
402                encoded_len, decoded_len
403            );
404
405            // Cleanup
406            opus_encoder_destroy(encoder);
407            opus_decoder_destroy(decoder);
408        }
409
410        println!("Encode/decode test WITHOUT DNN passed!");
411    }
412
413    /// Test basic encode/decode roundtrip WITH DNN model loaded and DRED enabled
414    #[test]
415    #[cfg(feature = "dnn")]
416    fn test_encode_decode_with_dnn() {
417        const SAMPLE_RATE: i32 = 48000;
418        const CHANNELS: i32 = 1;
419        const FRAME_SIZE: usize = 960; // 20ms at 48kHz
420        const BITRATE: i32 = 64000;
421
422        // Load DNN weights
423        let weights_data = match load_dnn_weights() {
424            Some(data) => data,
425            None => {
426                panic!(
427                    "DNN weights not found. Run 'python generate_weights.py' to generate weights."
428                );
429            }
430        };
431
432        println!("Loaded {} bytes of DNN weights", weights_data.len());
433
434        unsafe {
435            let mut error: i32 = 0;
436
437            // Create encoder
438            let encoder = opus_encoder_create(
439                SAMPLE_RATE,
440                CHANNELS,
441                OPUS_APPLICATION_VOIP as i32,
442                &mut error,
443            );
444            assert_eq!(error, OPUS_OK as i32, "Failed to create encoder");
445            assert!(!encoder.is_null());
446
447            // Set bitrate
448            let ret = opus_encoder_ctl(encoder, OPUS_SET_BITRATE_REQUEST as i32, BITRATE);
449            assert_eq!(ret, OPUS_OK as i32, "Failed to set bitrate");
450
451            // Set expected packet loss (needed for DRED to add redundancy)
452            let packet_loss_perc: i32 = 25; // 25% expected packet loss
453            let ret = opus_encoder_ctl(
454                encoder,
455                OPUS_SET_PACKET_LOSS_PERC_REQUEST as i32,
456                packet_loss_perc,
457            );
458            assert_eq!(ret, OPUS_OK as i32, "Failed to set packet loss");
459            println!("Packet loss set to {}%", packet_loss_perc);
460
461            // Load DNN weights into encoder
462            let ret = opus_encoder_ctl(
463                encoder,
464                OPUS_SET_DNN_BLOB_REQUEST as i32,
465                weights_data.as_ptr() as *const std::ffi::c_void,
466                weights_data.len() as i32,
467            );
468            assert_eq!(
469                ret, OPUS_OK as i32,
470                "Failed to load DNN weights into encoder"
471            );
472            println!("DNN weights loaded into encoder");
473
474            // Enable DRED with 100ms of redundancy
475            let dred_duration_ms: i32 = 100;
476            let ret = opus_encoder_ctl(
477                encoder,
478                OPUS_SET_DRED_DURATION_REQUEST as i32,
479                dred_duration_ms,
480            );
481            println!("OPUS_SET_DRED_DURATION (with DNN) returned: {}", ret);
482            assert_eq!(ret, OPUS_OK as i32, "Failed to enable DRED");
483            println!("DRED enabled with {}ms redundancy", dred_duration_ms);
484
485            // Create decoder
486            let decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &mut error);
487            assert_eq!(error, OPUS_OK as i32, "Failed to create decoder");
488            assert!(!decoder.is_null());
489
490            // Load DNN weights into decoder (may not be supported for all decoder types)
491            let ret = opus_decoder_ctl(
492                decoder,
493                OPUS_SET_DNN_BLOB_REQUEST as i32,
494                weights_data.as_ptr() as *const std::ffi::c_void,
495                weights_data.len() as i32,
496            );
497            if ret == OPUS_OK as i32 {
498                println!("DNN weights loaded into decoder");
499            } else {
500                println!(
501                    "Note: Decoder DNN blob returned {} (OSCE may use different API)",
502                    ret
503                );
504            }
505
506            // Generate noise input
507            let input = generate_noise(FRAME_SIZE);
508            let mut encoded = vec![0u8; 4000]; // Max packet size
509            let mut decoded = vec![0i16; FRAME_SIZE];
510
511            // Encode (with DRED enabled, packet may be larger)
512            let encoded_len = opus_encode(
513                encoder,
514                input.as_ptr(),
515                FRAME_SIZE as i32,
516                encoded.as_mut_ptr(),
517                encoded.len() as i32,
518            );
519            assert!(
520                encoded_len > 0,
521                "Encoding failed with error {}",
522                encoded_len
523            );
524            println!(
525                "Encoded {} samples to {} bytes (with DNN + DRED)",
526                FRAME_SIZE, encoded_len
527            );
528
529            // Decode
530            let decoded_len = opus_decode(
531                decoder,
532                encoded.as_ptr(),
533                encoded_len,
534                decoded.as_mut_ptr(),
535                FRAME_SIZE as i32,
536                0,
537            );
538            assert_eq!(
539                decoded_len, FRAME_SIZE as i32,
540                "Decoding failed or wrong frame size"
541            );
542            println!(
543                "Decoded {} bytes to {} samples (with DNN + DRED)",
544                encoded_len, decoded_len
545            );
546
547            // Cleanup
548            opus_encoder_destroy(encoder);
549            opus_decoder_destroy(decoder);
550        }
551
552        println!("Encode/decode test WITH DNN + DRED passed!");
553    }
554
555    /// Test multiple frames encode/decode WITHOUT DNN
556    #[test]
557    fn test_multi_frame_encode_decode_without_dnn() {
558        const SAMPLE_RATE: i32 = 48000;
559        const CHANNELS: i32 = 1; // Mono
560        const FRAME_SIZE: usize = 960; // 20ms at 48kHz
561        const NUM_FRAMES: usize = 10;
562        const BITRATE: i32 = 32000; // 32kbit/s
563
564        unsafe {
565            let mut error: i32 = 0;
566
567            // Create encoder (VOIP for voice)
568            let encoder = opus_encoder_create(
569                SAMPLE_RATE,
570                CHANNELS,
571                OPUS_APPLICATION_VOIP as i32,
572                &mut error,
573            );
574            assert_eq!(error, OPUS_OK as i32, "Failed to create encoder");
575
576            // Set bitrate
577            opus_encoder_ctl(encoder, OPUS_SET_BITRATE_REQUEST as i32, BITRATE);
578
579            // Set packet loss (needed for DRED to add redundancy)
580            let packet_loss_perc: i32 = 25;
581            let ret = opus_encoder_ctl(
582                encoder,
583                OPUS_SET_PACKET_LOSS_PERC_REQUEST as i32,
584                packet_loss_perc,
585            );
586            assert_eq!(ret, OPUS_OK as i32, "Failed to set packet loss");
587
588            // Try to enable DRED (should fail or be ignored without DNN)
589            let dred_duration_ms: i32 = 100;
590            let ret = opus_encoder_ctl(
591                encoder,
592                OPUS_SET_DRED_DURATION_REQUEST as i32,
593                dred_duration_ms,
594            );
595            println!("OPUS_SET_DRED_DURATION (WITHOUT DNN) returned: {}", ret);
596
597            // Create decoder
598            let decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &mut error);
599            assert_eq!(error, OPUS_OK as i32, "Failed to create decoder");
600
601            let mut total_encoded_bytes = 0;
602            let mut frame_sizes = Vec::new();
603
604            for frame_num in 0..NUM_FRAMES {
605                // Generate mono noise input
606                let input = generate_noise(FRAME_SIZE);
607                let mut encoded = vec![0u8; 4000];
608                let mut decoded = vec![0i16; FRAME_SIZE];
609
610                // Encode
611                let encoded_len = opus_encode(
612                    encoder,
613                    input.as_ptr(),
614                    FRAME_SIZE as i32,
615                    encoded.as_mut_ptr(),
616                    encoded.len() as i32,
617                );
618                assert!(encoded_len > 0, "Frame {} encoding failed", frame_num);
619                total_encoded_bytes += encoded_len;
620                frame_sizes.push(encoded_len);
621
622                // Decode
623                let decoded_len = opus_decode(
624                    decoder,
625                    encoded.as_ptr(),
626                    encoded_len,
627                    decoded.as_mut_ptr(),
628                    FRAME_SIZE as i32,
629                    0,
630                );
631                assert_eq!(
632                    decoded_len, FRAME_SIZE as i32,
633                    "Frame {} decoding failed",
634                    frame_num
635                );
636            }
637
638            let duration_ms = NUM_FRAMES * 20;
639            let bitrate_actual = (total_encoded_bytes * 8 * 1000) / duration_ms as i32;
640            println!("\n=== WITHOUT DNN MODEL ===");
641            println!("Frame sizes: {:?}", frame_sizes);
642            println!(
643                "Encoded {} frames ({} ms) to {} bytes, actual bitrate: {} bps",
644                NUM_FRAMES, duration_ms, total_encoded_bytes, bitrate_actual
645            );
646
647            opus_encoder_destroy(encoder);
648            opus_decoder_destroy(decoder);
649        }
650
651        println!("Multi-frame encode/decode test WITHOUT DNN passed!");
652    }
653
654    /// Test multiple frames encode/decode WITH DNN
655    #[test]
656    #[cfg(feature = "dnn")]
657    fn test_multi_frame_encode_decode_with_dnn() {
658        const SAMPLE_RATE: i32 = 48000;
659        const CHANNELS: i32 = 1; // Mono - DRED works better with mono voice
660        const FRAME_SIZE: usize = 960; // 20ms at 48kHz
661        const NUM_FRAMES: usize = 10;
662        const BITRATE: i32 = 32000; // 32kbit/s - typical for voice, forces SILK mode
663
664        // Load DNN weights
665        let weights_data = match load_dnn_weights() {
666            Some(data) => data,
667            None => {
668                panic!(
669                    "DNN weights not found. Run 'python generate_weights.py' to generate weights."
670                );
671            }
672        };
673
674        unsafe {
675            let mut error: i32 = 0;
676
677            // Create encoder (VOIP mode for voice)
678            let encoder = opus_encoder_create(
679                SAMPLE_RATE,
680                CHANNELS,
681                OPUS_APPLICATION_VOIP as i32,
682                &mut error,
683            );
684            assert_eq!(error, OPUS_OK as i32, "Failed to create encoder");
685
686            // Set bitrate
687            opus_encoder_ctl(encoder, OPUS_SET_BITRATE_REQUEST as i32, BITRATE);
688
689            // Set expected packet loss (needed for DRED to add redundancy)
690            let packet_loss_perc: i32 = 25; // 25% expected packet loss
691            let ret = opus_encoder_ctl(
692                encoder,
693                OPUS_SET_PACKET_LOSS_PERC_REQUEST as i32,
694                packet_loss_perc,
695            );
696            assert_eq!(ret, OPUS_OK as i32, "Failed to set packet loss");
697            println!("Packet loss set to {}%", packet_loss_perc);
698
699            // Load DNN weights into encoder
700            let ret = opus_encoder_ctl(
701                encoder,
702                OPUS_SET_DNN_BLOB_REQUEST as i32,
703                weights_data.as_ptr() as *const std::ffi::c_void,
704                weights_data.len() as i32,
705            );
706            assert_eq!(
707                ret, OPUS_OK as i32,
708                "Failed to load DNN weights into encoder"
709            );
710
711            // Enable DRED with 100ms of redundancy
712            let dred_duration_ms: i32 = 100;
713            let ret = opus_encoder_ctl(
714                encoder,
715                OPUS_SET_DRED_DURATION_REQUEST as i32,
716                dred_duration_ms,
717            );
718            assert_eq!(ret, OPUS_OK as i32, "Failed to enable DRED");
719            println!("DRED enabled with {}ms redundancy", dred_duration_ms);
720
721            // Create decoder
722            let decoder = opus_decoder_create(SAMPLE_RATE, CHANNELS, &mut error);
723            assert_eq!(error, OPUS_OK as i32, "Failed to create decoder");
724
725            // Load DNN weights into decoder (may not be supported for all decoder types)
726            let ret = opus_decoder_ctl(
727                decoder,
728                OPUS_SET_DNN_BLOB_REQUEST as i32,
729                weights_data.as_ptr() as *const std::ffi::c_void,
730                weights_data.len() as i32,
731            );
732            if ret == OPUS_OK as i32 {
733                println!("DNN weights loaded into decoder");
734            } else {
735                println!(
736                    "Note: Decoder DNN blob returned {} (OSCE may use different API)",
737                    ret
738                );
739            }
740
741            let mut total_encoded_bytes = 0;
742            let mut frame_sizes = Vec::new();
743
744            for frame_num in 0..NUM_FRAMES {
745                // Generate mono noise input with frame-specific seed for reproducibility
746                let input = generate_noise_with_seed(FRAME_SIZE, frame_num as u64);
747                let mut encoded = vec![0u8; 4000];
748                let mut decoded = vec![0i16; FRAME_SIZE];
749
750                // Encode
751                let encoded_len = opus_encode(
752                    encoder,
753                    input.as_ptr(),
754                    FRAME_SIZE as i32,
755                    encoded.as_mut_ptr(),
756                    encoded.len() as i32,
757                );
758                assert!(encoded_len > 0, "Frame {} encoding failed", frame_num);
759                total_encoded_bytes += encoded_len;
760                frame_sizes.push(encoded_len);
761
762                // Decode
763                let decoded_len = opus_decode(
764                    decoder,
765                    encoded.as_ptr(),
766                    encoded_len,
767                    decoded.as_mut_ptr(),
768                    FRAME_SIZE as i32,
769                    0,
770                );
771                assert_eq!(
772                    decoded_len, FRAME_SIZE as i32,
773                    "Frame {} decoding failed",
774                    frame_num
775                );
776            }
777
778            let duration_ms = NUM_FRAMES * 20;
779            let bitrate_actual = (total_encoded_bytes * 8 * 1000) / duration_ms as i32;
780            println!("\n=== WITH DNN MODEL + DRED ===");
781            println!("Frame sizes: {:?}", frame_sizes);
782            println!(
783                "Encoded {} frames ({} ms) to {} bytes, actual bitrate: {} bps",
784                NUM_FRAMES, duration_ms, total_encoded_bytes, bitrate_actual
785            );
786
787            opus_encoder_destroy(encoder);
788            opus_decoder_destroy(decoder);
789        }
790
791        println!("Multi-frame encode/decode test WITH DNN + DRED passed!");
792    }
793}