-
Notifications
You must be signed in to change notification settings - Fork 689
Backend switch #355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backend switch #355
Conversation
torchaudio/__init__.py
Outdated
encodinginfo, | ||
filetype) | ||
if get_audio_backend() == "sox": | ||
waveform, sample_rate = sox_backend.load( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way to branch would be by doing a conditional import but with the same name
if get_audio_backend() == "sox":
from sox_backend import load
elif get_audio_backend() == "soundfile":
from _soundfile_backend import load
else:
raise NotImplementedError
waveform, sample_rate = load(
filepath,
out=out,
normalization=normalization,
channels_first=channels_first,
num_frames=num_frames,
offset=offset,
filetype=filetype,
)
Thoughts?
torchvision uses the backend switch like so but most of the time simply uses PIL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be able to save on these if statements if you overwrite the load functions etc. when switching backends, but i'd almost chalk that up under a performance optimization, so it's not necessary yet
@cpuhrsch -- For this PR, I'd focus on the interface for the user, and leave a new backend for later. This still addresses the main issue of completely blocking the import of torchaudio when there is an issue with sox. Thoughts? |
torchaudio/__init__.py
Outdated
""" | ||
Specifies the package used to load. | ||
Args: | ||
backend (string): Name of the backend. one of {'sox'}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: add soundfile
Having references to the sources of those backends in the doctstring could be useful too
torchaudio/__init__.py
Outdated
encodinginfo, | ||
filetype) | ||
if get_audio_backend() == "sox": | ||
waveform, sample_rate = sox_backend.load( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could still assign to a local load function and then move these branches higher (which will save on indentations) and make the code more readable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point
Looks good so far! From what I gather from the PR description there are no BC-breaking changes introduced here? |
test/test.py
Outdated
x_sine_part, _ = torchaudio.load( | ||
input_sine_path, num_frames=num_frames, offset=offset | ||
) | ||
l1_error = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want to be "that guy", but these code format changes make this harder to review. You could make them the last commit and continue to maintain that, or you could send a separate PR later on. They also introduce a lot of meaningless git blame changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, (1) this was definitely not meant to be in, and (2) this was not quite ready for review :)
torchaudio/__init__.py
Outdated
filetype=None): | ||
r"""Saves a tensor of an audio signal to disk as a standard format like mp3, wav, etc. | ||
if get_audio_backend() == "sox": | ||
from torchaudio import sox_backend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd carefully make sure that these repeated imports aren't expensive due to some kind of initialization code in sox.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or soundfile for that matter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One way of avoiding repeated import is to import at the beginning and catch import errors, as done in vision. However, local test on my mac don't seem to see a cost to repeated import and seem to properly fetch the cached version:
In [1]: %timeit import torchaudio
The slowest run took 17.73 times longer than the fastest. This could mean that an intermediate result is being cached.
637 ns ± 1.07 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [2]: %timeit import _torch_sox
76.3 ns ± 1.84 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
torchaudio/__init__.py
Outdated
src = src.contiguous() | ||
_torch_sox.write_audio_file(filepath, src, signalinfo, encodinginfo, filetype) | ||
|
||
def save_encinfo(*args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current docs for encinfo don't even reference the function in the example. This is a strange one.
torchaudio/__init__.py
Outdated
|
||
def sox_encodinginfo_t(*args, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
encoding information, signal information etc. are very useful functions in general. we could also think about adding some backend independent interfaces, but maybe after the release.
num_frames=0, | ||
offset=0, | ||
filetype=None, | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I notice you're repeating the docstring here. But you call into the load backends separately. I don't think the user will be able to see this unless she looks at this function which is part of a private backend. Here assigning a backend function to the module unction could help use actually choose the correct doc string at runtime.
However, the static documentation won't be able to do that.
So instead I'd say it makes sense to have a single docstring for our load function and then reference it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you say "reference", I assume you mean
"""See torchaudio.save"""
Unless you meant copying docstring from another with something like functools.wraps()
or @functools.docs-decorator
as you mentioned in this comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's correct.
6763600
to
505618c
Compare
I can't reproduce locally the librosa error.
|
test/test.py
Outdated
@@ -171,5 +300,21 @@ def test_5_get_info(self): | |||
self.assertEqual(si.rate, rate) | |||
self.assertEqual(ei.bits_per_sample, precision) | |||
|
|||
torchaudio.set_audio_backend(self.default_audio_backend) | |||
|
|||
def _test_5_get_info_soundfile(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's repetition again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There isn't because info returns a different struct depending on the backend.
test/test.py
Outdated
self.assertEqual(si.channels, channels) | ||
self.assertEqual(si.frames, samples) | ||
self.assertEqual(si.samplerate, rate) | ||
si_precision = _extract_digits(si.subtype) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is necessary we should at least write out as a todo to make this consistent. That can be done via a wrapper class that uses getattribute etc to align these attributes consistently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should make sense to standardize on sox since otherwise we'll introduce BC-breaking changes to support this new backend.
torchaudio/__init__.py
Outdated
@@ -242,6 +280,11 @@ def sox_signalinfo_t(): | |||
>>> si.precision = 16 | |||
>>> si.length = 0 | |||
""" | |||
|
|||
if get_audio_backend() != "sox": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could create a generic decorator to write this less often
def _backend_guard(backends):
def decorator(fn):
@functools.docs-decorator(fn) # not sure about the name
def _fn(*args, **kwargs):
if get_audio_backend() not in backends:
raise Runtime("fn {} requires backend to be one of".format(fn.__name__, backends)
fn(*args, **kwargs)
return _fn
@backend_support(['sox'])
def sox_signalinfo_t():
....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, but let's make that a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, added to standardize import error. :)
torchaudio/__init__.py
Outdated
return save_encinfo(filepath, src, channels_first, si) | ||
|
||
if get_audio_backend() == "sox": | ||
func = _sox_backend.save |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could do
getattr(get_audio_backend_module() , 'save')
Do we want get_audio_backend() to return a module or a string that is the name?
A module can yield the name as well via introspection
test/test.py
Outdated
with AudioBackendScope(backend2): | ||
tensor2, sample_rate2 = torchaudio.load(output_path) | ||
|
||
# tensor1 = tensor1.type(torch.FloatTensor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe you wanted to remove these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for catching this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Added a small nit
Is there any reason why
was commented out? |
Introduce a backend switch in a similar way to torchvision from pytorch/vision#153.
__init__
totorchaudio.sox_backend
. (torchaudio.sox_effects
does not change.)torchaudio.initialize_sox
.Fix librosa test appearing here if not flaky? deactivate failing test #372For later:
Add to release notes:
SoxEffectsChain.EFFECTS_AVAILABLE
replaced bySoxEffectsChain().EFFECTS_AVAILABLE
Comparison of backends
Internal doc
Fixes #329 by offering other backend as options. As such, this also is a first step in addressing #357.