2020-01-10 01:52:31 +00:00
|
|
|
// Copyright (C) 2020 Natanael Mojica <neithanmo@gmail.com>
|
|
|
|
//
|
|
|
|
// 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 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!(
|
|
|
|
"
|
|
|
|
<CsoundSynthesizer>
|
|
|
|
<CsOptions>
|
|
|
|
</CsOptions>
|
|
|
|
<CsInstruments>
|
|
|
|
sr = 44100 ; default sample rate
|
|
|
|
ksmps = {}
|
2021-06-03 18:20:54 +00:00
|
|
|
nchnls_i = {}
|
2020-01-10 01:52:31 +00:00
|
|
|
nchnls = {}
|
|
|
|
0dbfs = 1
|
|
|
|
|
2021-06-03 18:20:54 +00:00
|
|
|
instr 1
|
2020-01-10 01:52:31 +00:00
|
|
|
|
2021-06-03 18:20:54 +00:00
|
|
|
{} ;input
|
2020-01-10 01:52:31 +00:00
|
|
|
{} ; csound output
|
|
|
|
|
|
|
|
endin
|
|
|
|
</CsInstruments>
|
|
|
|
<CsScore>
|
|
|
|
i 1 0 2
|
|
|
|
e
|
|
|
|
</CsScore>
|
|
|
|
</CsoundSynthesizer>",
|
|
|
|
$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();
|
|
|
|
|
2020-06-11 10:45:15 +00:00
|
|
|
let mut h = gst_check::Harness::with_element(&filter, Some("sink"), Some("src"));
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
h.set_caps(src_caps, sink_caps);
|
|
|
|
h
|
|
|
|
}
|
|
|
|
|
2021-05-25 14:37:48 +00:00
|
|
|
fn duration_from_samples(num_samples: u64, rate: u64) -> Option<gst::ClockTime> {
|
|
|
|
num_samples
|
|
|
|
.mul_div_round(*gst::ClockTime::SECOND, rate)
|
|
|
|
.map(gst::ClockTime::from_nseconds)
|
2020-01-10 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2021-05-25 14:37:48 +00:00
|
|
|
let mut in_pts = gst::ClockTime::ZERO;
|
|
|
|
let in_duration = duration_from_samples(EOS_NUM_SAMPLES as _, sr as _)
|
|
|
|
.expect("duration defined because sr is > 0");
|
2020-01-10 01:52:31 +00:00
|
|
|
// 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
|
2021-05-25 14:37:48 +00:00
|
|
|
let mut expected_pts = gst::ClockTime::ZERO;
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
for _ in 0..EOS_NUM_BUFFERS {
|
|
|
|
let mut buffer =
|
|
|
|
gst::Buffer::with_size(EOS_NUM_SAMPLES * std::mem::size_of::<f64>()).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!(
|
2021-04-12 12:49:54 +00:00
|
|
|
buffer.as_ref().duration(),
|
2020-01-10 01:52:31 +00:00
|
|
|
duration_from_samples(in_process_samples, sr as _)
|
|
|
|
);
|
2021-05-25 14:37:48 +00:00
|
|
|
assert_eq!(buffer.as_ref().pts(), Some(expected_pts));
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
// Get the number of samples that were not processed
|
|
|
|
samples_offset = in_samples % ksmps as u64;
|
|
|
|
// Calculates the next output buffer timestamp
|
2021-05-25 14:37:48 +00:00
|
|
|
expected_pts = in_pts
|
|
|
|
+ duration_from_samples(EOS_NUM_SAMPLES as u64 - samples_offset, sr as _)
|
|
|
|
.expect("duration defined because sr is > 0");
|
2020-01-10 01:52:31 +00:00
|
|
|
// 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::<f64>().unwrap();
|
|
|
|
|
|
|
|
// all samples in the output buffers must value 1
|
2021-06-04 22:16:05 +00:00
|
|
|
assert!(output.iter().all(|sample| *sample as u16 == 1u16));
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
num_samples += output.len();
|
|
|
|
num_buffers += 1;
|
|
|
|
}
|
|
|
|
|
2020-06-24 22:33:57 +00:00
|
|
|
h.push_event(gst::event::Eos::new());
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
// 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!(
|
2021-04-12 12:49:54 +00:00
|
|
|
buffer.as_ref().pts(),
|
2021-05-25 14:37:48 +00:00
|
|
|
duration_from_samples(samples_at_eos as _, sr as _).map(|duration| in_pts - duration)
|
2020-01-10 01:52:31 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
let map = buffer.into_mapped_buffer_readable().unwrap();
|
|
|
|
let output = map.as_slice().as_slice_of::<f64>().unwrap();
|
|
|
|
num_samples += output.len();
|
|
|
|
num_buffers += 1;
|
|
|
|
|
|
|
|
assert_eq!(output.len(), samples_at_eos);
|
2021-06-04 22:16:05 +00:00
|
|
|
assert!(output.iter().all(|sample| *sample as u16 == 1u16));
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
// 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
|
2021-05-25 14:37:48 +00:00
|
|
|
let mut in_pts = gst::ClockTime::ZERO;
|
|
|
|
let in_samples_duration = duration_from_samples(UNDERFLOW_NUM_SAMPLES as _, sr as _)
|
|
|
|
.expect("duration defined because sr is > 0");
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
for _ in 0..UNDERFLOW_NUM_BUFFERS {
|
|
|
|
let mut buffer =
|
|
|
|
gst::Buffer::with_size(UNDERFLOW_NUM_SAMPLES * std::mem::size_of::<f64>()).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());
|
|
|
|
}
|
|
|
|
|
2020-06-24 22:33:57 +00:00
|
|
|
h.push_event(gst::event::Eos::new());
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
// From here we check our output data
|
|
|
|
let mut num_buffers = 0;
|
|
|
|
let mut num_samples = 0;
|
|
|
|
|
2021-05-25 14:37:48 +00:00
|
|
|
let expected_duration = duration_from_samples(UNDERFLOW_NUM_SAMPLES as u64 * 2, sr as _)
|
|
|
|
.expect("duration defined because sr is > 0");
|
2020-01-10 01:52:31 +00:00
|
|
|
let expected_buffers = UNDERFLOW_NUM_BUFFERS / 2;
|
2021-05-25 14:37:48 +00:00
|
|
|
let mut expected_pts = gst::ClockTime::ZERO;
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
for _ in 0..expected_buffers {
|
|
|
|
let buffer = h.pull().unwrap();
|
2021-04-12 12:49:54 +00:00
|
|
|
let samples = buffer.size() / std::mem::size_of::<f64>();
|
2020-01-10 01:52:31 +00:00
|
|
|
|
2021-05-25 14:37:48 +00:00
|
|
|
assert_eq!(buffer.as_ref().pts(), Some(expected_pts));
|
|
|
|
assert_eq!(buffer.as_ref().duration(), Some(expected_duration));
|
2020-01-10 01:52:31 +00:00
|
|
|
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
|
2021-05-25 14:37:48 +00:00
|
|
|
expected_pts += 2 * in_samples_duration;
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
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::<i32>::new(1, 48000)),
|
|
|
|
("channels", &gst::IntRange::<i32>::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,
|
2020-03-26 08:59:43 +00:00
|
|
|
sink_caps,
|
2020-01-10 01:52:31 +00:00
|
|
|
// 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());
|
|
|
|
|
2020-06-24 22:33:57 +00:00
|
|
|
h.push_event(gst::event::Eos::new());
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
let buffer = h.pull().unwrap();
|
|
|
|
|
|
|
|
// Pushing a buffer without a timestamp should produce a no timestamp output
|
2021-04-12 12:49:54 +00:00
|
|
|
assert!(buffer.as_ref().pts().is_none());
|
2020-01-10 01:52:31 +00:00
|
|
|
// But It should have a duration
|
|
|
|
assert_eq!(
|
2021-04-12 12:49:54 +00:00
|
|
|
buffer.as_ref().duration(),
|
2020-01-10 01:52:31 +00:00
|
|
|
duration_from_samples(1024 / std::mem::size_of::<f64>() as u64, sr as u64)
|
|
|
|
);
|
|
|
|
|
|
|
|
// get the negotiated harness sink caps
|
|
|
|
let harness_sink_caps = h
|
2021-04-12 12:49:54 +00:00
|
|
|
.sinkpad()
|
2020-01-10 01:52:31 +00:00
|
|
|
.expect("harness has no sinkpad")
|
2021-04-12 12:49:54 +00:00
|
|
|
.current_caps()
|
2020-01-10 01:52:31 +00:00
|
|
|
.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());
|
|
|
|
|
2020-06-24 22:33:57 +00:00
|
|
|
h.push_event(gst::event::Eos::new());
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
// The harness sinkpad end up not having defined caps
|
|
|
|
// so, the get_current_caps should be None
|
2021-04-12 12:49:54 +00:00
|
|
|
let current_caps = h.sinkpad().expect("harness has no sinkpad").current_caps();
|
2020-01-10 01:52:31 +00:00
|
|
|
|
|
|
|
assert!(current_caps.is_none());
|
|
|
|
}
|