// Copyright (C) 2020 Natanael Mojica // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Library General Public // License as published by the Free Software Foundation; either // version 2 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Library General Public License for more details. // // You should have received a copy of the GNU Library General Public // License along with this library; if not, write to the // Free Software Foundation, Inc., 51 Franklin Street, Suite 500, // Boston, MA 02110-1335, USA. use gstcsound; use glib; use gst; use gst_check; use glib::prelude::*; use gst::prelude::*; use byte_slice_cast::*; // This macro allows us to create a kind of dynamic CSD file, // we need to pass in the ksmps, channels and input/output // operations that are going to be done by Csound over input and output // audio samples macro_rules! CSD { ($ksmps:expr, $ichannels:expr, $ochannels:expr, $ins:expr, $out:expr) => { format!( " sr = 44100 ; default sample rate ksmps = {} nchnls_i = {} nchnls = {} 0dbfs = 1 instr 1 {} ;input {} ; csound output endin i 1 0 2 e ", $ksmps, $ichannels, $ochannels, $ins, $out ); }; } fn init() { use std::sync::Once; static INIT: Once = Once::new(); INIT.call_once(|| { gst::init().unwrap(); gstcsound::plugin_register_static().expect("Failed to register csound plugin"); }); } fn build_harness(src_caps: gst::Caps, sink_caps: gst::Caps, csd: &str) -> gst_check::Harness { let filter = gst::ElementFactory::make("csoundfilter", None).unwrap(); filter.set_property("csd-text", &csd).unwrap(); let mut h = gst_check::Harness::new_with_element(&filter, Some("sink"), Some("src")); h.set_caps(src_caps, sink_caps); h } fn duration_from_samples(num_samples: u64, rate: u64) -> gst::ClockTime { gst::ClockTime(num_samples.mul_div_round(gst::SECOND_VAL, rate)) } // This test verifies the well functioning of the EOS logic, // we generate EOS_NUM_BUFFERS=10 buffers with EOS_NUM_SAMPLES=62 samples each one, // for a total of 10 * 62 = 620 samples, but 620%32(ksmps)= 12 will be leftover and should be processed when // the eos event is received, which generates another buffer, so that, the total amount of buffers that // the harness would have at its sinkpad should be EOS_NUM_BUFFERS + 1, being the total amount of processed samples // equals to EOS_NUM_BUFFERS * EOS_NUM_SAMPLES = 620 samples.It is important to mention that the created buffers have silenced samples(being 0), // but csoundfilter would add 1.0 to each incoming sample. // at the end, all of the output samples should have a value of 1.0. const EOS_NUM_BUFFERS: usize = 10; const EOS_NUM_SAMPLES: usize = 62; #[test] fn csound_filter_eos() { init(); // Sets the ksmps to 32, // input = output channels = 1 let ksmps: usize = 32; let num_channels = 1; let sr: i32 = 44_100; let caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &sr), ("channels", &num_channels), ("layout", &"interleaved"), ], ); let mut h = build_harness( caps.clone(), caps, // this score instructs Csound to add 1.0 to each input sample &CSD!(ksmps, num_channels, num_channels, "ain in", "out ain + 1.0"), ); h.play(); // The input buffer pts and duration let mut in_pts = gst::ClockTime(Some(0)); let in_duration = duration_from_samples(EOS_NUM_SAMPLES as _, sr as _); // The number of samples that were leftover during the previous iteration let mut samples_offset = 0; // Output samples and buffers counters let mut num_samples: usize = 0; let mut num_buffers = 0; // The expected pts of output buffers let mut expected_pts = gst::ClockTime(Some(0)); for _ in 0..EOS_NUM_BUFFERS { let mut buffer = gst::Buffer::with_size(EOS_NUM_SAMPLES * std::mem::size_of::()).unwrap(); buffer.make_mut().set_pts(in_pts); buffer.make_mut().set_duration(in_duration); let in_samples = samples_offset + EOS_NUM_SAMPLES as u64; // Gets amount of samples that are going to be processed, // the output buffer must be in_process_samples length let in_process_samples = in_samples - (in_samples % ksmps as u64); // Push an input buffer and pull the result of processing it let buffer = h.push_and_pull(buffer); assert!(buffer.is_ok()); let buffer = buffer.unwrap(); // Checks output buffer timestamp and duration assert_eq!( buffer.as_ref().get_duration(), duration_from_samples(in_process_samples, sr as _) ); assert_eq!(buffer.as_ref().get_pts(), expected_pts); // Get the number of samples that were not processed samples_offset = in_samples % ksmps as u64; // Calculates the next output buffer timestamp expected_pts = in_pts + duration_from_samples(EOS_NUM_SAMPLES as u64 - samples_offset, sr as _); // Calculates the next input buffer timestamp in_pts += in_duration; let map = buffer.into_mapped_buffer_readable().unwrap(); let output = map.as_slice().as_slice_of::().unwrap(); // all samples in the output buffers must value 1 assert_eq!(output.iter().any(|sample| *sample as u16 != 1u16), false); num_samples += output.len(); num_buffers += 1; } h.push_event(gst::Event::new_eos().build()); // pull the buffer produced after the EOS event let buffer = h.pull().unwrap(); let samples_at_eos = (EOS_NUM_BUFFERS * EOS_NUM_SAMPLES) % ksmps; assert_eq!( buffer.as_ref().get_pts(), in_pts - duration_from_samples(samples_at_eos as _, sr as _) ); let map = buffer.into_mapped_buffer_readable().unwrap(); let output = map.as_slice().as_slice_of::().unwrap(); num_samples += output.len(); num_buffers += 1; assert_eq!(output.len(), samples_at_eos); assert_eq!(output.iter().any(|sample| *sample as u16 != 1u16), false); // All the generated samples should have been processed at this point assert_eq!(num_samples, EOS_NUM_SAMPLES * EOS_NUM_BUFFERS); assert_eq!(num_buffers, EOS_NUM_BUFFERS + 1); } // In this test, we generate UNDERFLOW_NUM_BUFFERS buffers with UNDERFLOW_NUM_SAMPLES samples each one, however, // Csound is waiting for UNDERFLOW_NUM_SAMPLES * 2 samples per buffer at its input, so that, // internally, the output will be only generated when enough data is available. // It happens, after every 2 * UNDERFLOW_NUM_BUFFERS input buffers, after processing, we should have UNDERFLOW_NUM_BUFFERS/2 // output buffers containing UNDERFLOW_NUM_SAMPLES * 2 samples. const UNDERFLOW_NUM_BUFFERS: usize = 200; const UNDERFLOW_NUM_SAMPLES: usize = 2; #[test] fn csound_filter_underflow() { init(); let ksmps: usize = UNDERFLOW_NUM_SAMPLES * 2; let num_channels = 1; let sr: i32 = 44_100; let caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &sr), ("channels", &num_channels), ("layout", &"interleaved"), ], ); let mut h = build_harness( caps.clone(), caps, &CSD!(ksmps, num_channels, num_channels, "ain in", "out ain"), ); h.play(); // Input buffers timestamp let mut in_pts = gst::ClockTime(Some(0)); let in_samples_duration = duration_from_samples(UNDERFLOW_NUM_SAMPLES as _, sr as _); for _ in 0..UNDERFLOW_NUM_BUFFERS { let mut buffer = gst::Buffer::with_size(UNDERFLOW_NUM_SAMPLES * std::mem::size_of::()).unwrap(); buffer.make_mut().set_pts(in_pts); buffer.make_mut().set_duration(in_samples_duration); in_pts += in_samples_duration; assert!(h.push(buffer).is_ok()); } h.push_event(gst::Event::new_eos().build()); // From here we check our output data let mut num_buffers = 0; let mut num_samples = 0; let expected_duration = duration_from_samples(UNDERFLOW_NUM_SAMPLES as u64 * 2, sr as _); let expected_buffers = UNDERFLOW_NUM_BUFFERS / 2; let mut expected_pts = gst::ClockTime(Some(0)); for _ in 0..expected_buffers { let buffer = h.pull().unwrap(); let samples = buffer.get_size() / std::mem::size_of::(); assert_eq!(buffer.as_ref().get_pts(), expected_pts); assert_eq!(buffer.as_ref().get_duration(), expected_duration); assert_eq!(samples, UNDERFLOW_NUM_SAMPLES * 2); // Output data is produced after 2 input buffers // so that, the next output buffer's PTS should be // equal to the last PTS plus the duration of 2 input buffers expected_pts += in_samples_duration * 2; num_buffers += 1; num_samples += samples; } assert_eq!(num_buffers, UNDERFLOW_NUM_BUFFERS / 2); assert_eq!( num_samples as usize, UNDERFLOW_NUM_SAMPLES * UNDERFLOW_NUM_BUFFERS ); } // Verifies that the caps negotiation is properly done, by pushing buffers whose caps // are the same as the one configured in csound, into the harness sink pad. Csoundfilter is expecting 2 channels audio // at a sample rate of 44100. // the output caps configured in the harness are not fixated but when the caps negotiation ends, // those caps must be fixated according to the csound output format which is defined once the csd file is compiled #[test] fn csound_filter_caps_negotiation() { init(); let ksmps = 4; let ichannels = 2; let ochannels = 1; let sr: i32 = 44_100; let src_caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &sr), ("channels", &ichannels), ("layout", &"interleaved"), ], ); // Define the output caps which would be fixated // at the end of the caps negotiation let sink_caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &gst::IntRange::::new(1, 48000)), ("channels", &gst::IntRange::::new(1, 2)), ("layout", &"interleaved"), ], ); // build the harness setting its src and sink caps, // also passing the csd score to the filter element let mut h = build_harness( src_caps, sink_caps.clone(), // creates a csd score that defines the input and output formats on the csound side // the output fomart would be 1 channel audio samples at 44100 &CSD!(ksmps, ichannels, ochannels, "ain, ain2 ins", "out ain"), ); h.play(); assert!(h.push(gst::Buffer::with_size(2048).unwrap()).is_ok()); h.push_event(gst::Event::new_eos().build()); let buffer = h.pull().unwrap(); // Pushing a buffer without a timestamp should produce a no timestamp output assert!(buffer.as_ref().get_pts().is_none()); // But It should have a duration assert_eq!( buffer.as_ref().get_duration(), duration_from_samples(1024 / std::mem::size_of::() as u64, sr as u64) ); // get the negotiated harness sink caps let harness_sink_caps = h .get_sinkpad() .expect("harness has no sinkpad") .get_current_caps() .expect("pad has no caps"); // our expected caps at the harness sinkpad let expected_caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &44_100i32), ("channels", &ochannels), ("layout", &"interleaved"), ], ); assert_eq!(harness_sink_caps, expected_caps); } // Similar to caps negotiation, but in this case, we configure a fixated caps in the harness sinkpad, // such caps are incompatible with the csoundfilter and it leads to an error during the caps negotiation, // because there is not a common intersection between both caps. #[test] fn csound_filter_caps_negotiation_fail() { init(); let ksmps = 4; let ichannels = 2; let ochannels = 1; let src_caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &44_100i32), ("channels", &ichannels), ("layout", &"interleaved"), ], ); // instead of having a range for channels/rate fields // we fixate them to 2 and 48_000 respectively, which would cause the negotiation error let sink_caps = gst::Caps::new_simple( "audio/x-raw", &[ ("format", &gst_audio::AUDIO_FORMAT_F64.to_str()), ("rate", &48_000i32), ("channels", &ichannels), ("layout", &"interleaved"), ], ); let mut h = build_harness( src_caps, sink_caps, // creates a csd score that defines the input and output formats on the csound side // the output fomart would be 1 channel audio samples at 44100 &CSD!(ksmps, ichannels, ochannels, "ain, ain2 ins", "out ain"), ); h.play(); let buffer = gst::Buffer::with_size(2048).unwrap(); assert!(h.push(buffer).is_err()); h.push_event(gst::Event::new_eos().build()); // The harness sinkpad end up not having defined caps // so, the get_current_caps should be None let current_caps = h .get_sinkpad() .expect("harness has no sinkpad") .get_current_caps(); assert!(current_caps.is_none()); }