Repository: librosa/librosa

Issue:
Improve autocorrelation implementation
**Is your feature request related to a problem? Please describe.**

Our autocorrelation implementation could be a bit more efficient.  For reference, here's how the code currently looks:

https://github.com/librosa/librosa/blob/a3095313a2ba9e37e24f6180c4decc77cd6a2b0d/librosa/core/audio.py#L846-L868

**Describe the solution you'd like**

If `y` is real-valued (as it usually is), we can optimize this a bit by using `rfft` instead of `fft`.  The only catch here is that we need to be careful with maintaining shapes, but that shouldn't be a big deal.

I propose that we rearrange this a little so that the complex check happens first (not last), and just having two branches for real or complex input handling.

Additionally, once #745 is merged, we can use `abs2` to optimize the power spectrum calculation.


**Describe alternatives you've considered**
This isn't a high-priority item, so we could leave it as is.  But it would be a nice and easy optimization to have, and autocorrelation does pop up elsewhere in the package (eg for rhythm analysis).
Improve autocorrelation implementation
**Is your feature request related to a problem? Please describe.**

Our autocorrelation implementation could be a bit more efficient.  For reference, here's how the code currently looks:

https://github.com/librosa/librosa/blob/a3095313a2ba9e37e24f6180c4decc77cd6a2b0d/librosa/core/audio.py#L846-L868

**Describe the solution you'd like**

If `y` is real-valued (as it usually is), we can optimize this a bit by using `rfft` instead of `fft`.  The only catch here is that we need to be careful with maintaining shapes, but that shouldn't be a big deal.

I propose that we rearrange this a little so that the complex check happens first (not last), and just having two branches for real or complex input handling.

Additionally, once #745 is merged, we can use `abs2` to optimize the power spectrum calculation.


**Describe alternatives you've considered**
This isn't a high-priority item, so we could leave it as is.  But it would be a nice and easy optimization to have, and autocorrelation does pop up elsewhere in the package (eg for rhythm analysis).


Discussion:
This issue should be blocked until #745 is resolved.
We had a couple of scipy sprinters looking into this one, but they didn't comment here and I don't have their contact information.  (If this is you, please chime in!)

If there's no action on this issue in the next couple of weeks, we can reassign it to someone else.
Started digging into this and it's a bit subtler than I anticipated.  I'm pretty sure we can bring the memory footprint down here with a combination of clever fft usage and some shifting / phase hacking.  Will report back when I have something working.
Quick followup: it turns out that we can't rely on `ifft(..., n=max_size)` to handle the array allocation for us.  The phrasing of the documentation on this point is a little ambiguous, and probably I should have known this already, but the `n=` parameter truncates the input here instead of the output.  So I think we're stuck with the over-allocation.

That said, we can still use rfft/irfft to cut this down a little.
This issue should be blocked until #745 is resolved.
We had a couple of scipy sprinters looking into this one, but they didn't comment here and I don't have their contact information.  (If this is you, please chime in!)

If there's no action on this issue in the next couple of weeks, we can reassign it to someone else.
Started digging into this and it's a bit subtler than I anticipated.  I'm pretty sure we can bring the memory footprint down here with a combination of clever fft usage and some shifting / phase hacking.  Will report back when I have something working.
Quick followup: it turns out that we can't rely on `ifft(..., n=max_size)` to handle the array allocation for us.  The phrasing of the documentation on this point is a little ambiguous, and probably I should have known this already, but the `n=` parameter truncates the input here instead of the output.  So I think we're stuck with the over-allocation.

That said, we can still use rfft/irfft to cut this down a little.

Resolving patch:
diff --git a/librosa/core/audio.py b/librosa/core/audio.py
--- a/librosa/core/audio.py
+++ b/librosa/core/audio.py
@@ -847,24 +847,30 @@ def autocorrelate(y, *, max_size=None, axis=-1):
 
     max_size = int(min(max_size, y.shape[axis]))
 
-    # Compute the power spectrum along the chosen axis
-    # Pad out the signal to support full-length auto-correlation.
     fft = get_fftlib()
-    powspec = util.abs2(fft.fft(y, n=2 * y.shape[axis] + 1, axis=axis))
 
-    # Convert back to time domain
-    autocorr = fft.ifft(powspec, axis=axis)
+    # Pad out the signal to support full-length auto-correlation.
+    n_pad = 2 * y.shape[axis] - 1
+
+    if np.iscomplexobj(y):
+        # Compute the power spectrum along the chosen axis
+        powspec = util.abs2(fft.fft(y, n=n_pad, axis=axis))
+
+        # Convert back to time domain
+        autocorr = fft.ifft(powspec, n=n_pad, axis=axis)
+    else:
+        # Compute the power spectrum along the chosen axis
+        # Pad out the signal to support full-length auto-correlation.
+        powspec = util.abs2(fft.rfft(y, n=n_pad, axis=axis))
+
+        # Convert back to time domain
+        autocorr = fft.irfft(powspec, n=n_pad, axis=axis)
 
     # Slice down to max_size
     subslice = [slice(None)] * autocorr.ndim
     subslice[axis] = slice(max_size)
 
-    autocorr = autocorr[tuple(subslice)]
-
-    if not np.iscomplexobj(y):
-        autocorr = autocorr.real
-
-    return autocorr
+    return autocorr[tuple(subslice)]
 
 
 def lpc(y, *, order, axis=-1):


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -966,7 +966,7 @@ def test_get_duration_fail():
 
 @pytest.mark.parametrize(
     "y",
-    [np.random.randn(256, 256), np.exp(1.0j * np.random.randn(256, 256))],
+    [np.random.randn(256, 384), np.exp(1.0j * np.random.randn(256, 384))],
     ids=["real", "complex"],
 )
 @pytest.mark.parametrize("axis", [0, 1, -1])