Skip to content
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

Use Transformer in-memory with stdin/stdout #102

Merged
merged 12 commits into from
May 21, 2020
Merged

Conversation

pseeth
Copy link
Contributor

@pseeth pseeth commented May 16, 2020

This PR tries to address #6. I basically followed the code by @carlthome in pysndfx here: https://github.com/carlthome/python-audio-effects/blob/master/pysndfx/dsp.py#L472. I followed the discussion in the issue and implemented a new function that belongs to Transformer called build_array, which takes in a numpy array. The main issue is keeping track of all of the information that would normally be in the header of the audio file. Instead, these have to be passed as arguments to build_array, both for the input and output.

I modified all of the tests. Each piece of the Transformer is tested by taking an input file and passing it through sox to write to an output file. So in the tests, what I do is load both the input file and the output file as numpy arrays, pass the input array into tfm.build_array and collect the output array (which is the second argument in the tuple, matching the API of tfm.build). Then, I do an np.allclose between the loaded output array from the output file and the output array. Every test was modified in this way, except for one, which I can't get working in the time I tried to put this together today. That test is test_bitdepth_valid, here: https://github.com/pseeth/pysox/blob/master/tests/test_transform.py#L1347.

This change to the tests is encapsulated in a single function tfm_assert_array_to_file_output which you'll see calls to sprinkled throughout the test.

To do all this, I modified the sox function in a backwards compatible way. I probably need to up coverage a bit still but this is hopefully okay to PR now for feedback.

Let me know if this is a good start and how to get this merged, if possible! It would go a long way to making pysox super efficient for Scaper, thus why I'm here. :)

Thanks!

@coveralls
Copy link

coveralls commented May 16, 2020

Coverage Status

Coverage decreased (-0.08%) to 98.678% when pulling de89d97 on pseeth:master into 99de814 on rabitt:master.

@hadware
Copy link
Contributor

hadware commented May 16, 2020

The import of numpy could probably be made optional (if it's deemed to be too "heavy" of a dependency for this package).

Great work though!

@pseeth
Copy link
Contributor Author

pseeth commented May 18, 2020

I got this up to 100% coverage in transforms.py but I'm getting coverage has dropped for some reason still. As far as I can tell, all the lines I wrote are covered but there are some missing lines in core, as there was before. Let me know if I missed something...

I also added docstrings for the new function.

I can easily make numpy optional by checking for it or importing it dynamically but I'm guessing everyone using pysox for something might already have numpy installed, anyway?

@rabitt
Copy link
Collaborator

rabitt commented May 19, 2020

@pseeth this is great, thank you! 🚀 I'm reviewing now.

Copy link
Collaborator

@rabitt rabitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the work on this @pseeth ! Take a look at my comments and let me know what you think. Seeing the PR I'm reconsidering if it's better as part of build directly or as a separate function. Something else to consider - does it make sense to support the same functionality now in the Combiner, and in the preview functions?

If you need/want, I'm happy to help with any of this, let me know if you want me to and I'll add some commits on this PR.

setup.py Outdated
@@ -20,12 +20,14 @@
keywords='audio effects SoX',
license='BSD-3-Clause',
install_requires=[
'numpy',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the minimum version we can get away with here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's 1.9.0. According to this: https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.tobytes.html, it's new in 1.9.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

setup.py Outdated
],
extras_require={
'tests': [
'pytest',
'pytest-cov',
'pytest-pep8',
'pysoundfile',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for tests, but I think we need at least 0.9.0 to get the dtype functionality:

https://pysoundfile.readthedocs.io/en/latest/#breaking-changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

sox/core.py Outdated
process_handle = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if src_array is not None and isinstance(src_array, np.ndarray):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raise a TypeError if not isinstance(src_array, np.ndarray) - otherwise the behavior will be confusing if a user passes e.g. a list and it gets ignored.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - swap the order of this if/else to start with the "standard" case - if src_array is None: ..., elif isinstance(src_array, np.ndarray): ... else ... raise TypeError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah makes sense! I updated this.

sox/core.py Show resolved Hide resolved
sox/transform.py Outdated
extra_args : list or None, default=None
If a list is given, these additional arguments are passed to SoX
at the end of the list of effects.
Don't use this argument unless you know exactly what you're doing!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document the returns

sox/transform.py Outdated
if encoding_out is None:
encoding_out = encoding

self.set_input_format(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want to call these functions inside build_array - they change the internal state of the Transformer. The way it's written now, calling build_array will silently change the behavior of build.

Brainstorming how to get around this, we could:

  1. write a function that builds the args list for input/output formats, which gets called by set_input_format/set_output_format and by this function.
  2. reduce the number of inputs to this function. as far as I can tell, the only needed input format argument is sample_rate_in. The rest can (should!?) be inferred from the input array. The output format support is already built into sox.build in several ways, either by calling set_output_format or by transformer commands like rate and channels. The only one I'd leave optionally specified here is encoding_out.

so, in short this function would become

build_array(self, input_array, sample_rate_in, encoding_out=None, 
            extra_args=None,  return_output=True)

Thoughts/objections?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the argument list for build_array was initially what you said, but had to be expanded quite a bit to get through all of the test cases. The ones I can get rid of are channels_in and bits_in as they can be inferred from the numpy array. But the output types can't be inferred.

What if we had set_output_format and set_input_format return (optionally) the arg list rather than set it as a variable in the object? Then we could extend it dynamically in build_array instead without side effects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified set_[input, output]_format with a flag return_only where the arg list will be built without saving it to self. I had to keep some of the arguments to build_array, but as you'll see further down, build_array is now merged with build.

sox/transform.py Outdated
at the end of the list of effects.
Don't use this argument unless you know exactly what you're doing!
'''
output_filepath = '-'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud... does it also make sense to support array in --> file out, and file in --> array out? My thinking is yes, in which case maybe all or this function should be part of build? What do you think @pseeth ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally possible. Do you have a suggested function signature for build? It seems like a lot of the arguments will become optional, or do type-checking on input/output (in which case the arguments will be renamed). Then we'll have some sort of merge between the build_array and build functions, which I think would look reasonable. Let me know your thoughts!

Copy link
Contributor Author

@pseeth pseeth May 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is supported now! I merged build and build_array to be able to support this. There's some finicky typechecking that needed to happen, and a flag that tells the core.sox function whether to decode out with utf-8 but it now works! I expanded the test cases to test file -> array, array -> array, and array -> file. All of these are checked against the original file -> file.

@rabitt
Copy link
Collaborator

rabitt commented May 19, 2020

by the way, I'm totally OK with adding numpy as a dependency.

@pseeth
Copy link
Contributor Author

pseeth commented May 19, 2020

I'm not sure if I can get this to work for the Combiner as there are multiple input arrays and I'm not sure how that works with stdin. Seems like there might be some black magic...but if a user is using numpy arrays after the transformer, they really should just use np.stack or sum to implement functionality like the combiner as it's much faster than doing an exec call to sox.

Thanks for the review, @rabitt! Quick question - do you know why the coverage test is failing?

@pseeth
Copy link
Contributor Author

pseeth commented May 19, 2020

Updated the PR in response to comments! And it seems coverage checks are now passing. :)

@pseeth
Copy link
Contributor Author

pseeth commented May 20, 2020

I think this will require a version bump and associated text in the changelog. Let me know what to do there, or if you'd like, you can add it yourself once this PR is ready to be merged!

@rabitt
Copy link
Collaborator

rabitt commented May 20, 2020

I was reviewing and had some comments requiring nontrivial changes so I'm going to push to this branch -

  • create separate functions _input_format_args(...) and _output_format_args(...) called by set_input/output_format and by build.
  • rework build to have input_filepath and input_array as separate inputs
  • fix some problems all of this causes with a few special Transformer functions like .stat

working on it now!

@rabitt
Copy link
Collaborator

rabitt commented May 20, 2020

@pseeth just opened a PR on your fork with those changes.

@pseeth
Copy link
Contributor Author

pseeth commented May 20, 2020

Looks good to me! Thanks! I merged it into my PR, so now we wait on tests...

@rabitt
Copy link
Collaborator

rabitt commented May 20, 2020

It looks like the docs build is failing because we added numpy, and there are a few lines I added that aren't tested. Going to make one more PR to your PR to fix!

Could you add the changelog notes and maybe add an example to the docs for how to use the new functionality?

@pseeth
Copy link
Contributor Author

pseeth commented May 20, 2020

Sounds good! I'll update the changelog.

@pseeth
Copy link
Contributor Author

pseeth commented May 21, 2020

Actually, some guidance on this would be helpful. What should the version bump to?

@rabitt
Copy link
Collaborator

rabitt commented May 21, 2020

I found some issues with the docs build (the returns aren't properly documented so they're not showing up in the docs) - I'm going ahead and merging this without bumping the version.

I'll fix the docs and do the version bump in a separate PR.

Thanks!

@rabitt rabitt merged commit bdb1058 into marl:master May 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants