diff --git a/.gitignore b/.gitignore index cb7fb14..21c6f09 100644 --- a/.gitignore +++ b/.gitignore @@ -199,8 +199,9 @@ pip-selfcheck.json # git rm -r .ipynb_checkpoints/ # User added -.idea -exports +.idea/ +exports/ +export_model/ +models/ +out/ scratch.py -export_model -models diff --git a/examples/example_delayed_passthrough.py b/examples/example_delayed_passthrough.py new file mode 100644 index 0000000..b4e8bee --- /dev/null +++ b/examples/example_delayed_passthrough.py @@ -0,0 +1,109 @@ +import logging +import os +import pathlib +from argparse import ArgumentParser +from typing import Dict, List + +import torch as tr +import torch.nn as nn +from torch import Tensor + +from neutone_sdk import WaveformToWaveformBase, NeutoneParameter +from neutone_sdk.utils import save_neutone_model + +logging.basicConfig() +log = logging.getLogger(__name__) +log.setLevel(level=os.environ.get("LOGLEVEL", "INFO")) + + +class DelayedPassthroughModel(nn.Module): + def __init__(self, delay_n_samples: int, in_ch: int = 2) -> None: + super().__init__() + self.delay_n_samples = delay_n_samples + self.delay_buf = tr.zeros((in_ch, delay_n_samples)) + + def forward(self, x: Tensor) -> Tensor: + x = tr.cat([self.delay_buf, x], dim=-1) + self.delay_buf[:, :] = x[:, -self.delay_n_samples:] + x = x[:, :-self.delay_n_samples] + return x + + +class DelayedPassthroughModelWrapper(WaveformToWaveformBase): + def get_model_name(self) -> str: + return "delayed.passthrough" + + def get_model_authors(self) -> List[str]: + return ["Christopher Mitcheltree"] + + def get_model_short_description(self) -> str: + return "Delayed passthrough model." + + def get_model_long_description(self) -> str: + return "Delays the input audio by some number of samples. Should be tested with 50/50 dry/wet." + + def get_technical_description(self) -> str: + return "Delays the input audio by some number of samples. Should be tested with 50/50 dry/wet." + + def get_technical_links(self) -> Dict[str, str]: + return {} + + def get_tags(self) -> List[str]: + return [] + + def get_model_version(self) -> str: + return "1.0.0" + + def is_experimental(self) -> bool: + return True + + def get_neutone_parameters(self) -> List[NeutoneParameter]: + return [] + + @tr.jit.export + def is_input_mono(self) -> bool: + return False + + @tr.jit.export + def is_output_mono(self) -> bool: + return False + + @tr.jit.export + def get_native_sample_rates(self) -> List[int]: + return [44100] # Change this to test different scenarios + + @tr.jit.export + def get_native_buffer_sizes(self) -> List[int]: + return [2048] # Change this to test different scenarios + + @tr.jit.export + def reset_model(self) -> bool: + self.model.delay_buf.fill_(0) + return True + + @tr.jit.export + def calc_model_delay_samples(self) -> int: + return self.model.delay_n_samples + + @tr.jit.export + def get_wet_default_value(self) -> float: + return 0.5 + + @tr.jit.export + def get_dry_default_value(self) -> float: + return 0.5 + + def do_forward_pass(self, x: Tensor, params: Dict[str, Tensor]) -> Tensor: + x = self.model.forward(x) + return x + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("-o", "--output", default="export_model") + args = parser.parse_args() + root_dir = pathlib.Path(args.output) + + model = DelayedPassthroughModel(delay_n_samples=500) # Change delay_n_samples to test different scenarios + wrapper = DelayedPassthroughModelWrapper(model) + save_neutone_model(wrapper, root_dir, dump_samples=True, submission=True) diff --git a/examples/example_rave_prefilter.py b/examples/example_rave_prefilter.py index 08dbfd5..f875546 100644 --- a/examples/example_rave_prefilter.py +++ b/examples/example_rave_prefilter.py @@ -1,4 +1,3 @@ -import json import logging import os from argparse import ArgumentParser @@ -6,18 +5,17 @@ from typing import Dict, List import torch +import torchaudio from torch import Tensor, nn -import torchaudio +from neutone_sdk import WaveformToWaveformBase, NeutoneParameter from neutone_sdk.audio import ( AudioSample, AudioSamplePair, render_audio_sample, ) - -from neutone_sdk import WaveformToWaveformBase, NeutoneParameter +from neutone_sdk.filters import FIRFilter, FilterType from neutone_sdk.utils import save_neutone_model -from neutone_sdk.filters import FIRFilter, IIRFilter, FilterType logging.basicConfig() log = logging.getLogger(__name__) @@ -103,7 +101,7 @@ def get_native_sample_rates(self) -> List[int]: def get_native_buffer_sizes(self) -> List[int]: return [2048] - def calc_min_delay_samples(self) -> int: + def calc_model_delay_samples(self) -> int: # model latency should also be added if non-causal return self.pre_filter.delay diff --git a/examples/example_rave_v1_prefilter.py b/examples/example_rave_v1_prefilter.py index 5359071..f68c7f4 100644 --- a/examples/example_rave_v1_prefilter.py +++ b/examples/example_rave_v1_prefilter.py @@ -1,4 +1,3 @@ -import json import logging import os from argparse import ArgumentParser @@ -6,18 +5,17 @@ from typing import Dict, List import torch +import torchaudio from torch import Tensor, nn -import torchaudio +from neutone_sdk import WaveformToWaveformBase, NeutoneParameter from neutone_sdk.audio import ( AudioSample, AudioSamplePair, render_audio_sample, ) - -from neutone_sdk import WaveformToWaveformBase, NeutoneParameter +from neutone_sdk.filters import FIRFilter, FilterType from neutone_sdk.utils import save_neutone_model -from neutone_sdk.filters import FIRFilter, IIRFilter, FilterType logging.basicConfig() log = logging.getLogger(__name__) @@ -103,7 +101,7 @@ def get_native_sample_rates(self) -> List[int]: def get_native_buffer_sizes(self) -> List[int]: return [2048] - def calc_min_delay_samples(self) -> int: + def calc_model_delay_samples(self) -> int: # model latency should also be added if non-causal return self.pre_filter.delay diff --git a/examples/example_spectral_filter.py b/examples/example_spectral_filter.py index 91b94b1..daa9570 100644 --- a/examples/example_spectral_filter.py +++ b/examples/example_spectral_filter.py @@ -134,7 +134,7 @@ def __init__( if use_debug_mode: log.info(f"Supported buffer sizes = {self.get_native_buffer_sizes()}") log.info(f"Supported sample rate = {self.get_native_sample_rates()}") - log.info(f"STFT delay = {self.calc_min_delay_samples()}") + log.info(f"STFT delay = {self.calc_model_delay_samples()}") def get_model_name(self) -> str: return "spectral.filter" @@ -201,9 +201,9 @@ def get_native_buffer_sizes(self) -> List[int]: ) # Possible buffer sizes are determined by the STFT parameters @tr.jit.export - def calc_min_delay_samples(self) -> int: + def calc_model_delay_samples(self) -> int: # TODO(cm): make a model specific version of this method? - return self.stft.calc_min_delay_samples() # This is equal to `fade_n_samples` + return self.stft.calc_model_delay_samples() # This is equal to `fade_n_samples` def set_model_buffer_size(self, n_samples: int) -> bool: self.stft.set_buffer_size(n_samples) diff --git a/neutone_sdk/constants.py b/neutone_sdk/constants.py index 9e111b4..0723c2e 100644 --- a/neutone_sdk/constants.py +++ b/neutone_sdk/constants.py @@ -1,6 +1,6 @@ from pathlib import Path -SDK_VERSION = "1.2.1" +SDK_VERSION = "1.3.0" MAX_N_PARAMS = 4 MAX_N_AUDIO_SAMPLES = 3 diff --git a/neutone_sdk/realtime_stft.py b/neutone_sdk/realtime_stft.py index fbc374d..49ec782 100644 --- a/neutone_sdk/realtime_stft.py +++ b/neutone_sdk/realtime_stft.py @@ -213,7 +213,7 @@ def set_buffer_size(self, io_n_samples: int) -> None: self.reset() @tr.jit.export - def calc_min_delay_samples(self) -> int: + def calc_model_delay_samples(self) -> int: return self.fade_n_samples @tr.jit.export diff --git a/neutone_sdk/sqw.py b/neutone_sdk/sqw.py index 39bd10c..bbc7404 100644 --- a/neutone_sdk/sqw.py +++ b/neutone_sdk/sqw.py @@ -321,14 +321,18 @@ def is_resampling(self) -> bool: return self.resample_sandwich.is_resampling() @tr.jit.export - def calc_min_delay_samples(self) -> int: - model_min_delay = self.w2w_base.calc_min_delay_samples() - wrapper_min_delay = self.calc_delay_samples(self.io_bs, self.model_bs) - min_delay = model_min_delay + wrapper_min_delay + def calc_buffering_delay_samples(self) -> int: + delay_samples = self.calc_delay_samples(self.io_bs, self.model_bs) if self.is_resampling(): - min_delay = int(min_delay * self.daw_bs / self.io_bs) + delay_samples = int(delay_samples * self.daw_bs / self.io_bs) + return delay_samples - return min_delay + @tr.jit.export + def calc_model_delay_samples(self) -> int: + delay_samples = self.w2w_base.calc_model_delay_samples() + if self.is_resampling(): + delay_samples = int(delay_samples * self.daw_bs / self.io_bs) + return delay_samples @tr.jit.export def set_daw_sample_rate_and_buffer_size( @@ -433,7 +437,8 @@ def get_preserved_attributes(self) -> List[str]: "get_input_gain_default_value", "get_output_gain_default_value", "is_resampling", - "calc_min_delay_samples", + "calc_buffering_delay_samples", + "calc_model_delay_samples", "set_daw_sample_rate_and_buffer_size", "reset", "get_preserved_attributes", diff --git a/neutone_sdk/utils.py b/neutone_sdk/utils.py index c6685ca..5454338 100644 --- a/neutone_sdk/utils.py +++ b/neutone_sdk/utils.py @@ -1,4 +1,3 @@ -import copy import io import json import logging @@ -6,6 +5,11 @@ import random from pathlib import Path from typing import Tuple, Dict, List + +import torch as tr +from torch import Tensor +from torch.jit import ScriptModule + from neutone_sdk.audio import ( AudioSample, AudioSamplePair, @@ -16,10 +20,6 @@ from neutone_sdk.core import NeutoneModel from neutone_sdk.metadata import validate_metadata -import torch as tr -from torch import Tensor -from torch.jit import ScriptModule - logging.basicConfig() log = logging.getLogger(__name__) log.setLevel(level=os.environ.get("LOGLEVEL", "INFO")) @@ -157,13 +157,16 @@ def save_neutone_model( loaded_model, loaded_model.get_preserved_attributes() ) log.info("Testing methods used by the VST...") - loaded_model.calc_min_delay_samples() loaded_model.set_daw_sample_rate_and_buffer_size(48000, 512) loaded_model.reset() loaded_model.is_resampling() log.info( - f"Delay reported to the DAW for 48000 Hz sampling rate and 512 buffer size: " - f"{loaded_model.calc_min_delay_samples()}" + f"Buffering delay reported to the DAW for 48000 Hz sampling rate and 512 buffer size: " + f"{loaded_model.calc_buffering_delay_samples()}" + ) + log.info( + f"Model delay reported to the DAW for 48000 Hz sampling rate and 512 buffer size: " + f"{loaded_model.calc_model_delay_samples()}" ) if submission: # Do extra checks diff --git a/neutone_sdk/wavform_to_wavform.py b/neutone_sdk/wavform_to_wavform.py index b429861..e1326cb 100644 --- a/neutone_sdk/wavform_to_wavform.py +++ b/neutone_sdk/wavform_to_wavform.py @@ -256,9 +256,9 @@ def is_resampling(self) -> bool: return False @tr.jit.export - def calc_min_delay_samples(self) -> int: + def calc_model_delay_samples(self) -> int: """ - If the model introduces a minimum amount of delay to the output audio, + If the model introduces an amount of delay to the output audio, for example due to a lookahead buffer or cross-fading, return it here so that it can be forwarded to the DAW to compensate. Defaults to 0. @@ -344,7 +344,7 @@ def get_preserved_attributes(self) -> List[str]: "get_native_sample_rates", "get_native_buffer_sizes", "is_resampling", - "calc_min_delay_samples", + "calc_model_delay_samples", "set_sample_rate_and_buffer_size", "set_daw_sample_rate_and_buffer_size", "reset", diff --git a/pyproject.toml b/pyproject.toml index d889f84..f0ee87e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "neutone_sdk" -version = "1.2.1" +version = "1.3.0" description = "SDK for wrapping deep learning models for usage in the Neutone plugin" readme = "README.md" authors = ["Qosmo "] diff --git a/setup.py b/setup.py index 58d3e4f..791d963 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup_kwargs = { "name": "neutone-sdk", - "version": "1.2.1", + "version": "1.3.0", "description": "SDK for wrapping deep learning models for usage in the Neutone plugin", "long_description": '# Neutone SDK\n\nWe open source this SDK so researchers can wrap their own audio models and run them in a DAW using our [Neutone Plugin](https://neutone.space/). We offer both functionality for loading the models locally in the plugin as well as contributing them to the default list of models that is available to anyone running the plugin. We hope this will both give an opportunity for researchers to easily try their models in a DAW, but also provide creators with a collection of interesting models.\n\n\n\n## Examples and Notebooks\n\n- Full clipper distortion model example can be found [here](examples/example_clipper.py).\n- Example of a random overdrive model based on [micro-tcn](https://github.com/csteinmetz1/micro-tcn) can be found [here](examples/example_overdrive-random.py)\n- Notebooks for different models showing the entire workflow from training to exporting it using Neutone\n - [DDSP Timbre Transfer](https://colab.research.google.com/drive/1yPHU6PRWw1lRWZLUxXimIa6chFQ2JdRW?usp=sharing)\n - [RAVE Timbre Transfer](https://colab.research.google.com/drive/1qlN6xLvDYrLcAwS8yh2ecmNG_bEKlVI9?usp=sharing)\n - [TCN FX Emulation](https://colab.research.google.com/drive/1gHZ-AEoYmfmWrjlKpKkK_SW1xzfxD24-?usp=sharing)\n\n## v1 Release\n\nThe Neutone SDK is currently on version 1.0.0. Models exported with this version of the SDK will be incompatible with beta versions of the plugin to please make sure you are using the right version. \n\n\nThe restriction for a sampling rate of 48kHz and a buffer size of 2048 is now gone and the SDK contains a wrapper that supports on the fly resampling and queueing to accomodate the requirements of both the models and the DAW thanks to great work by [@christhetree](https://github.com/christhetree).\n\n\nThe following are known shortcomings:\n- Freezing models on save can cause instabilities, we recommend trying to save models both with and without freeze.\n- Displaying metadata information does not currently work with local model loading in the plugin.\n- Lookahead and on the fly STFT transforms will be implemented at the SDK level in the near future but is currently possible with additional code.\n- Windows and M1 acceleration are currently not supported.\n\nLogs are currently dumped to `/Users//Library/Application Support/Qosmo/Neutone/neutone.log`\n\n## Table of Contents\n- [Downloading the Neutone Plugin](#download)\n- [Installing the SDK](#install)\n- [SDK Description](#description)\n- [SDK Usage](#usage)\n- [Examples](#examples)\n- [Contributing to the SDK](#contributing)\n- [Credits](#credits)\n\n--- \n\n\n\n\n## Downloading the Plugin\n\nThe Neutone Plugin is available at [https://neutone.space](https://neutone.space). We currently offer VST3 and AU plugins that can be used to load the models created with this SDK. Please visit the website for more information.\n\n\n## Installing the SDK\n\n\n\nYou can install `neutone_sdk` using pip: \n\n```\npip install neutone_sdk\n```\n\n\n\n## SDK Description\n\nThe SDK provides functionality for wrapping existing PyTorch models in a way that can make them executable within the VST plugin. At its core the plugin is sending chunks of audio samples at a certain sample rate as an input and expects the same amount of samples at the output. Thus the simplest models also follow this input-output format and an example can be seen in [example_clipper.py](https://github.com/QosmoInc/neutone_sdk/blob/main/examples/example_clipper.py).\n\n\n\n## SDK Usage\n\n### General Usage\n\nWe provide several models in the [examples](https://github.com/QosmoInc/neutone-sdk/blob/main/examples) directory. We will go through one of the simplest models, a distortion model, to illustrate.\n\nAssume we have the following PyTorch model. Parameters will be covered later on, we will focus on the inputs and outputs for now. Assume this model receives a Tensor of shape `(2, buffer_size)` as an input where `buffer_size` is a parameter that can be specified.\n\n```python\nclass ClipperModel(nn.Module):\n def forward(self, x: Tensor, min_val: float, max_val: float, gain: float) -> Tensor:\n return torch.clip(x, min=min_val * gain, max=max_val * gain)\n```\n\nTo run this inside the VST the simplest wrapper we can write is by subclassing the WaveformToWaveformBase baseclass.\n```python\nclass ClipperModelWrapper(WaveformToWaveformBase):\n @torch.jit.export \n def is_input_mono(self) -> bool:\n return False\n \n @torch.jit.export\n def is_output_mono(self) -> bool:\n return False\n \n @torch.jit.export\n def get_native_sample_rates(self) -> List[int]:\n return [] # Supports all sample rates\n \n @torch.jit.export\n def get_native_buffer_sizes(self) -> List[int]:\n return [] # Supports all buffer sizes\n\n def do_forward_pass(self, x: Tensor, params: Dict[str, Tensor]) -> Tensor:\n # ... Parameter unwrap logic\n x = self.model.forward(x, min_val, max_val, gain)\n return x\n ```\n\nThe method that does most of the work is `do_forward_pass`. In this case it is just a simple passthrough, but we will use it to handle parameters later on.\n\nBy default the VST runs as `stereo-stereo` but when mono is desired for the model we can use the `is_input_mono` and `is_output_mono` to inform the SDK and have the inputs and outputs converted automatically. If `is_input_mono` is toggled an averaged `(1, buffer_size)` shaped Tensor will be passed as an input instead of `(2, buffer_size)`. If `is_output_mono` is toggled, `do_forward_pass` is expected to return a mono Tensor (shape `(1, buffer_size)`) that will then be duplicated across both channels at the output of the VST. This is done within the SDK to avoid unnecessary memory allocations on each pass.\n\n`get_native_sample_rates` and `get_native_buffer_sizes` can be used to specify any preferred sample rates or buffer sizes. In most cases these are expected to only have one element but extra flexibility is provided for more complex models. In case multiple options are provided the SDK will try to find the best one for the current setting of the DAW. Whenever the sample rate or buffer size is different from the one of the DAW a wrapper is automatically triggered that converts to the correct sampling rate or implements a FIFO queue for the requested buffer size or both. This will incur a small performance penalty and add some amount of delay. In case a model is compatible with any sample rate and/or buffer_size these lists can be left empty.\n\nThis means that the tensor `x` in the `do_forward_pass` method is guaranteed to be of shape `(1 if is_input_mono else 2, buffer_size)` where `buffer_size` will be chosen at runtime from the list provided in the `get_native_buffer_sizes` method.\n\n### Exporting models and loading in the plugin\n\nWe provide a `save_neutone_model` helper function that saves models to disk. By default this will convert models to TorchScript and run them through a series of checks to ensure they can be loaded by the plugin. The resulting `model.nm` file can be loaded within the plugin using the `load your own` button. Read below for how to submit models to the default collection.\n\n### Parameters\n\nFor models that can use conditioning signals we currently provide four configurable knob parameters. Within the `ClipperModelWrapper` defined above we can include the following:\n```python\nclass ClipperModelWrapper(WaveformToWaveformBase):\n ...\n \n def get_neutone_parameters(self) -> List[NeutoneParameter]:\n return [NeutoneParameter(name="min", description="min clip threshold", default_value=0.5),\n NeutoneParameter(name="max", description="max clip threshold", default_value=1.0),\n NeutoneParameter(name="gain", description="scale clip threshold", default_value=1.0)]\n \n def do_forward_pass(self, x: Tensor, params: Dict[str, Tensor]) -> Tensor:\n min_val, max_val, gain = params["min"], params["max"], params["gain"]\n x = self.model.forward(x, min_val, max_val, gain)\n return x\n```\n\nDuring the forward pass the `params` variable will be a dictionary like the following:\n```python\n{\n "min": torch.Tensor([0.5] * buffer_size),\n "max": torch.Tensor([1.0] * buffer_size),\n "gain": torch.Tensor([1.0] * buffer_size)\n}\n```\nThe keys of the dictionary are specified in the `get_parameters` function.\n\nThe parameters will always take values between 0 and 1 and the `do_forward_pass` function can be used to do any necessary rescaling before running the internal forward method of the model.\n\nMoreover, the parameters sent by the plugin come in at a sample level granularity. By default, we take the mean of each buffer and return a single float (as a Tensor), but the `aggregate_param` method can be used to override the aggregation method. See the full clipper export file for an example of preserving this granularity.\n\n\n### Submitting models\n\nThe plugin contains a default list of models aimed at creators that want to make use of them during their creative process. We encourage users to submit their models once they are happy with the results they get so they can be used by the community at large. For submission we require some additional metadata that will be used to display information about the model aimed at both creators and other researchers. This will be displayed on both the [Neutone website](https://neutone.space) and inside the plugin.\n\nSkipping the previous clipper model, here is a more realistic example based on a random TCN overdrive model inspired by [micro-tcn](https://github.com/csteinmetz1/micro-tcn).\n\n```python\nclass OverdriveModelWrapper(WaveformToWaveformBase):\n def get_model_name(self) -> str:\n return "conv1d-overdrive.random"\n\n def get_model_authors(self) -> List[str]:\n return ["Nao Tokui"]\n\n def get_model_short_description(self) -> str:\n return "Neural distortion/overdrive effect"\n\n def get_model_long_description(self) -> str:\n return "Neural distortion/overdrive effect through randomly initialized Convolutional Neural Network"\n\n def get_technical_description(self) -> str:\n return "Random distortion/overdrive effect through randomly initialized Temporal-1D-convolution layers. You\'ll get different types of distortion by re-initializing the weight or changing the activation function. Based on the idea proposed by Steinmetz et al."\n\n def get_tags(self) -> List[str]:\n return ["distortion", "overdrive"]\n\n def get_model_version(self) -> str:\n return "1.0.0"\n\n def is_experimental(self) -> bool:\n return False\n\n def get_technical_links(self) -> Dict[str, str]:\n return {\n "Paper": "https://arxiv.org/abs/2010.04237",\n "Code": "https://github.com/csteinmetz1/ronn"\n }\n\n def get_citation(self) -> str:\n return "Steinmetz, C. J., & Reiss, J. D. (2020). Randomized overdrive neural networks. arXiv preprint arXiv:2010.04237."\n```\n\nCheck out the documentation of the methods inside [core.py](neutone_sdk/core.py), as well as the random overdrive model on the [website](https://neutone.space/models/) and in the plugin to understand where each field will be displayed.\n\nTo submit a model, please [open an issue on the GitHub repository](https://github.com/QosmoInc/neutone_sdk/issues/new?assignees=bogdanteleaga%2C+christhetree&labels=enhancement&template=request-add-model.md&title=%5BMODEL%5D+%3CNAME%3E). We currently need the following:\n- A short description of what the model does and how it can contribute to the community\n- A link to the `model.nm` file outputted by the `save_neutone_model` helper function\n\n\n\n## Contributing to the SDK\n\nWe welcome any contributions to the SDK. Please add types wherever possible and use the `black` formatter for readability.\n\nThe current roadmap is:\n- Additional testing and benchmarking of models during or after exporting\n- Implement lookahead and on the fly STFT transforms\n\n\n\n## Credits\n\nThe audacitorch project was a major inspiration for the development of the SDK. [Check it out here](\nhttps://github.com/hugofloresgarcia/audacitorch)\n\n', "author": "Qosmo", diff --git a/testing/test_sqw.py b/testing/test_sqw.py index 68438d4..2d118dd 100644 --- a/testing/test_sqw.py +++ b/testing/test_sqw.py @@ -154,7 +154,7 @@ def delay_test( wrapper.model_sr = model_sr wrapper.model_bs = model_bs sqw.set_daw_sample_rate_and_buffer_size(daw_sr, daw_bs) - expected_delay = sqw.calc_min_delay_samples() + expected_delay = sqw.calc_buffering_delay_samples() assert expected_delay >= 0 n_samples = expected_delay + (2 * max(daw_bs, model_bs)) @@ -208,7 +208,7 @@ def test_calc_saturation_n() -> None: log.info("No saturation inconsistencies found") -def test_calc_min_delay_samples() -> None: +def test_calc_buffering_delay_samples() -> None: wrapper = TestModelWrapper() sqw = SampleQueueWrapper(wrapper)