use super::{Backend, BackendOptions, ChannelMode};
use crate::audio::sanitize_sample;
use crate::denoiser::DenoiserConfig;
pub struct BackendSession {
backend: Backend,
options: BackendOptions,
prepared: PreparedBackend,
}
impl std::fmt::Debug for BackendSession {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("BackendSession")
.field("backend", &self.backend)
.field("options", &self.options)
.finish_non_exhaustive()
}
}
enum PreparedBackend {
Classical,
#[cfg(feature = "rnnoise")]
Rnnoise,
#[cfg(feature = "deepfilter")]
DeepFilter(super::deepfilter::DeepFilterModel),
#[cfg(feature = "onnx")]
Onnx(super::onnx::OnnxWaveformModel),
#[cfg(feature = "mpsenet")]
MpSenet(super::mpsenet::MpSenetModel),
#[cfg(feature = "bsrnn")]
Bsrnn(super::bsrnn::BsrnnModel),
#[cfg(feature = "mossformer2")]
Mossformer2(super::mossformer2::Mossformer2Model),
#[cfg(feature = "sgmse")]
Sgmse(super::sgmse::SgmseModel),
#[cfg(feature = "gtcrn")]
Gtcrn(super::gtcrn::GtcrnModel),
}
impl BackendSession {
pub fn prepare(backend: Backend, options: BackendOptions) -> Result<Self, String> {
options.validate_resolved_resources(backend)?;
let prepared = match backend {
Backend::Classical => PreparedBackend::Classical,
#[cfg(feature = "rnnoise")]
Backend::Rnnoise => PreparedBackend::Rnnoise,
#[cfg(feature = "deepfilter")]
Backend::DeepFilter => {
PreparedBackend::DeepFilter(super::deepfilter::DeepFilterModel::load()?)
}
#[cfg(feature = "onnx")]
Backend::Onnx => PreparedBackend::Onnx(super::onnx::OnnxWaveformModel::load(
required_model(&options, "ONNX")?.clone(),
)?),
#[cfg(feature = "mpsenet")]
Backend::MpSenet => PreparedBackend::MpSenet(super::mpsenet::MpSenetModel::load(
required_model(&options, "MP-SENet")?,
)?),
#[cfg(feature = "bsrnn")]
Backend::Bsrnn => PreparedBackend::Bsrnn(super::bsrnn::BsrnnModel::load(
required_model(&options, "BSRNN")?,
)?),
#[cfg(feature = "mossformer2")]
Backend::Mossformer2 => {
PreparedBackend::Mossformer2(super::mossformer2::Mossformer2Model::load(
required_model(&options, "MossFormer2")?,
)?)
}
#[cfg(feature = "sgmse")]
Backend::Sgmse => PreparedBackend::Sgmse(super::sgmse::SgmseModel::load(
required_model(&options, "SGMSE+")?,
)?),
#[cfg(feature = "gtcrn")]
Backend::Gtcrn => PreparedBackend::Gtcrn(super::gtcrn::GtcrnModel::load(
required_model(&options, "GTCRN")?,
)?),
};
Ok(Self {
backend,
options,
prepared,
})
}
#[must_use]
pub const fn backend(&self) -> Backend {
self.backend
}
#[must_use]
pub fn options(&self) -> &BackendOptions {
&self.options
}
pub fn process(
&self,
channels: &[Vec<f64>],
sample_rate: u32,
classical_config: &DenoiserConfig,
) -> Result<Vec<Vec<f64>>, String> {
let mut effective_config = classical_config.clone();
effective_config.sample_rate = sample_rate;
effective_config
.validate_config()
.map_err(|error| error.to_string())?;
let needs_sanitization = channels
.iter()
.flatten()
.any(|sample| !sample.is_finite() || *sample < -1.0 || *sample > 1.0);
let sanitized;
let channels = if needs_sanitization {
sanitized = channels
.iter()
.map(|channel| channel.iter().copied().map(sanitize_sample).collect())
.collect::<Vec<Vec<f64>>>();
&sanitized
} else {
channels
};
let result = if channels.len() == 2 && self.options.channel_mode != ChannelMode::Independent
{
self.process_stereo(channels, sample_rate, &effective_config)
} else {
self.process_independent(channels, sample_rate, &effective_config)
}?;
Ok(result
.into_iter()
.map(|channel| channel.into_iter().map(sanitize_sample).collect())
.collect())
}
fn process_stereo(
&self,
channels: &[Vec<f64>],
sample_rate: u32,
classical_config: &DenoiserConfig,
) -> Result<Vec<Vec<f64>>, String> {
if channels[0].len() != channels[1].len() {
return Err("stereo channels must contain the same number of frames".into());
}
let mid: Vec<f64> = channels[0]
.iter()
.zip(&channels[1])
.map(|(left, right)| (left + right) * 0.5)
.collect();
match self.options.channel_mode {
ChannelMode::StereoLinked => {
let enhanced = self
.process_independent(std::slice::from_ref(&mid), sample_rate, classical_config)?
.pop()
.unwrap_or_default();
if enhanced.len() != mid.len() {
return Err("linked backend changed the input duration".into());
}
let mut result = channels.to_vec();
let (left_channels, right_channels) = result.split_at_mut(1);
for ((left, right), (original, clean)) in left_channels[0]
.iter_mut()
.zip(&mut right_channels[0])
.zip(mid.iter().zip(enhanced.iter()))
{
let correction = clean - original;
*left += correction;
*right += correction;
}
Ok(result)
}
ChannelMode::MidSide => {
let (mid, side) = super::encode_mid_side(&channels[0], &channels[1])?;
let processed =
self.process_independent(&[mid, side], sample_rate, classical_config)?;
if processed.len() != 2 {
return Err("mid-side backend must return exactly two channels".into());
}
let (left, right) = super::decode_mid_side(&processed[0], &processed[1])?;
Ok(vec![left, right])
}
ChannelMode::Independent => unreachable!(),
}
}
fn process_independent(
&self,
channels: &[Vec<f64>],
sample_rate: u32,
classical_config: &DenoiserConfig,
) -> Result<Vec<Vec<f64>>, String> {
let _ = sample_rate;
match &self.prepared {
PreparedBackend::Classical => Ok(super::process_classical(channels, classical_config)),
#[cfg(feature = "rnnoise")]
PreparedBackend::Rnnoise => super::rnnoise::process(channels, sample_rate),
#[cfg(feature = "deepfilter")]
PreparedBackend::DeepFilter(model) => model.process(channels, sample_rate),
#[cfg(feature = "onnx")]
PreparedBackend::Onnx(model) => {
model.process(channels, sample_rate, self.options.deterministic)
}
#[cfg(feature = "mpsenet")]
PreparedBackend::MpSenet(model) => model.process(channels, sample_rate),
#[cfg(feature = "bsrnn")]
PreparedBackend::Bsrnn(model) => model.process(channels, sample_rate),
#[cfg(feature = "mossformer2")]
PreparedBackend::Mossformer2(model) => model.process(channels, sample_rate),
#[cfg(feature = "sgmse")]
PreparedBackend::Sgmse(model) => model.process(
channels,
sample_rate,
self.options.sgmse_profile,
self.options.seed,
),
#[cfg(feature = "gtcrn")]
PreparedBackend::Gtcrn(model) => model.process(channels, sample_rate),
}
}
}
#[cfg(any(
feature = "onnx",
feature = "mpsenet",
feature = "bsrnn",
feature = "mossformer2",
feature = "sgmse",
feature = "gtcrn"
))]
fn required_model<'a>(
options: &'a BackendOptions,
backend: &str,
) -> Result<&'a super::OnnxModelConfig, String> {
options
.onnx
.as_ref()
.ok_or_else(|| format!("{backend} backend requires a resolved model"))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "onnx")]
use prost::Message;
#[cfg(feature = "onnx")]
use tract_onnx::pb::{
tensor_proto, tensor_shape_proto, type_proto, GraphProto, ModelProto, NodeProto,
OperatorSetIdProto, TensorShapeProto, TypeProto, ValueInfoProto,
};
#[test]
fn classical_session_preserves_geometry_and_is_shareable() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BackendSession>();
let session =
BackendSession::prepare(Backend::Classical, BackendOptions::default()).unwrap();
let input = vec![vec![0.1; 1024], vec![-0.1; 1024]];
let output = session
.process(&input, 48_000, &DenoiserConfig::default(48_000))
.unwrap();
assert_eq!(output.len(), input.len());
assert!(output.iter().all(|channel| channel.len() == 1024));
}
#[cfg(feature = "onnx")]
#[test]
fn prepared_waveform_session_survives_replacement_and_multiple_lengths() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("identity.onnx");
let mut bytes = Vec::new();
waveform_identity_model().encode(&mut bytes).unwrap();
std::fs::write(&path, bytes).unwrap();
let session = BackendSession::prepare(
Backend::Onnx,
BackendOptions {
onnx: Some(super::super::OnnxModelConfig {
path: path.clone(),
sample_rate: 16_000,
}),
deterministic: true,
..Default::default()
},
)
.unwrap();
std::fs::remove_file(&path).unwrap();
std::fs::write(&path, b"replaced after preparation").unwrap();
for frames in [17, 31, 17] {
let input = vec![vec![0.25; frames]];
let output = session
.process(&input, 16_000, &DenoiserConfig::default(16_000))
.unwrap();
assert_eq!(output, input);
}
}
#[cfg(feature = "onnx")]
fn waveform_identity_model() -> ModelProto {
let value_info = |name: &str| ValueInfoProto {
name: name.into(),
r#type: Some(TypeProto {
denotation: String::new(),
value: Some(type_proto::Value::TensorType(type_proto::Tensor {
elem_type: tensor_proto::DataType::Float as i32,
shape: Some(TensorShapeProto {
dim: vec![dimension_value(1), dimension_parameter("samples")],
}),
})),
}),
doc_string: String::new(),
};
ModelProto {
ir_version: 8,
opset_import: vec![OperatorSetIdProto {
domain: String::new(),
version: 13,
}],
graph: Some(GraphProto {
name: "session-identity".into(),
node: vec![NodeProto {
input: vec!["input".into()],
output: vec!["output".into()],
name: "identity".into(),
op_type: "Identity".into(),
..Default::default()
}],
input: vec![value_info("input")],
output: vec![value_info("output")],
..Default::default()
}),
..Default::default()
}
}
#[cfg(feature = "onnx")]
fn dimension_value(value: i64) -> tensor_shape_proto::Dimension {
tensor_shape_proto::Dimension {
value: Some(tensor_shape_proto::dimension::Value::DimValue(value)),
denotation: String::new(),
}
}
#[cfg(feature = "onnx")]
fn dimension_parameter(name: &str) -> tensor_shape_proto::Dimension {
tensor_shape_proto::Dimension {
value: Some(tensor_shape_proto::dimension::Value::DimParam(name.into())),
denotation: String::new(),
}
}
}