Repository: librosa/librosa

Issue:
librosa.display.waveshow memory leak?
**Describe the bug**
When I try to run `librosa.display.waveshow` on a long wav file (hour long), 44.1kHz, and plot the waveform, it works in Colab. However, try running the same cell a few times and the memory just keeps growing until the session crashes. This is problematic because I'd like to be able to run this waveform generating function in a loop over many hour-long files. 


**To Reproduce**

Example:
Here's a colab link (assuming you have access to 1 hour-long wav files: https://colab.research.google.com/drive/1HIXqFM4NIw6qflcS4Ss5yAMvJD_XNmta )
```
import librosa.display
import librosa
import matplotlib.pyplot as plt

file_path = '/content/drive/MyDrive/wav_files/denoised_hour_long_wav_file.wav'
y, sr = librosa.load(file_path, mono=False, sr=None)

orig_path ='/content/drive/MyDrive/wav_files/hour_long_wav_file.wav'
y_2, sr_2 = librosa.load(orig_path, mono=False, sr=None)


fig = plt.figure(figsize=(20,4), dpi=100)
for i in range(10):
  #Running this in a loop to show how it will crash after roughly 3-5 times of doing this.
  librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4)
  librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5)
fig.tight_layout()
fig.show()
```
Try running this code in a cell multiple times. If you isolate just these 2 lines:

```
librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4)
librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5)
```

And run those in their own cell multiple times (or in a while loop, or for loop if you have an hour long file), you'll notice the memory growing until it crashes and restarts the session.



**Expected behavior**
I generate waveforms without the memory filling up and crashing Colab.

**Software versions***

Please run the following Python code snippet and paste the output below.
```
import platform; print(platform.platform())
import sys; print("Python", sys.version)
import numpy; print("NumPy", numpy.__version__)
import scipy; print("SciPy", scipy.__version__)
import librosa; print("librosa", librosa.__version__)

librosa.show_versions()

Linux-5.4.188+-x86_64-with-Ubuntu-18.04-bionic
Python 3.7.13 (default, Apr 24 2022, 01:04:09) 
[GCC 7.5.0]
NumPy 1.21.6
SciPy 1.7.3
librosa 0.9.2
INSTALLED VERSIONS
------------------
python: 3.7.13 (default, Apr 24 2022, 01:04:09) 
[GCC 7.5.0]

librosa: 0.9.2

audioread: 2.1.9
numpy: 1.21.6
scipy: 1.7.3
sklearn: 1.0.2
joblib: 1.1.0
decorator: 4.4.2
soundfile: 0.10.3
resampy: 0.3.1
numba: 0.51.2
pooch: v1.6.0
packaging: 21.3

numpydoc: None
sphinx: 1.8.6
sphinx_rtd_theme: None
sphinx_multiversion: None
sphinx_gallery: None
mir_eval: None
ipython: None
sphinxcontrib-svg2pdfconverter: None
pytest: 3.6.4
pytest-mpl: None
pytest-cov: None
matplotlib: 3.2.2
samplerate: None
soxr: None
contextlib2: installed, no version number available
presets: None

```

librosa.display.waveshow memory leak?
**Describe the bug**
When I try to run `librosa.display.waveshow` on a long wav file (hour long), 44.1kHz, and plot the waveform, it works in Colab. However, try running the same cell a few times and the memory just keeps growing until the session crashes. This is problematic because I'd like to be able to run this waveform generating function in a loop over many hour-long files. 


**To Reproduce**

Example:
Here's a colab link (assuming you have access to 1 hour-long wav files: https://colab.research.google.com/drive/1HIXqFM4NIw6qflcS4Ss5yAMvJD_XNmta )
```
import librosa.display
import librosa
import matplotlib.pyplot as plt

file_path = '/content/drive/MyDrive/wav_files/denoised_hour_long_wav_file.wav'
y, sr = librosa.load(file_path, mono=False, sr=None)

orig_path ='/content/drive/MyDrive/wav_files/hour_long_wav_file.wav'
y_2, sr_2 = librosa.load(orig_path, mono=False, sr=None)


fig = plt.figure(figsize=(20,4), dpi=100)
for i in range(10):
  #Running this in a loop to show how it will crash after roughly 3-5 times of doing this.
  librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4)
  librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5)
fig.tight_layout()
fig.show()
```
Try running this code in a cell multiple times. If you isolate just these 2 lines:

```
librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4)
librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5)
```

And run those in their own cell multiple times (or in a while loop, or for loop if you have an hour long file), you'll notice the memory growing until it crashes and restarts the session.



**Expected behavior**
I generate waveforms without the memory filling up and crashing Colab.

**Software versions***

Please run the following Python code snippet and paste the output below.
```
import platform; print(platform.platform())
import sys; print("Python", sys.version)
import numpy; print("NumPy", numpy.__version__)
import scipy; print("SciPy", scipy.__version__)
import librosa; print("librosa", librosa.__version__)

librosa.show_versions()

Linux-5.4.188+-x86_64-with-Ubuntu-18.04-bionic
Python 3.7.13 (default, Apr 24 2022, 01:04:09) 
[GCC 7.5.0]
NumPy 1.21.6
SciPy 1.7.3
librosa 0.9.2
INSTALLED VERSIONS
------------------
python: 3.7.13 (default, Apr 24 2022, 01:04:09) 
[GCC 7.5.0]

librosa: 0.9.2

audioread: 2.1.9
numpy: 1.21.6
scipy: 1.7.3
sklearn: 1.0.2
joblib: 1.1.0
decorator: 4.4.2
soundfile: 0.10.3
resampy: 0.3.1
numba: 0.51.2
pooch: v1.6.0
packaging: 21.3

numpydoc: None
sphinx: 1.8.6
sphinx_rtd_theme: None
sphinx_multiversion: None
sphinx_gallery: None
mir_eval: None
ipython: None
sphinxcontrib-svg2pdfconverter: None
pytest: 3.6.4
pytest-mpl: None
pytest-cov: None
matplotlib: 3.2.2
samplerate: None
soxr: None
contextlib2: installed, no version number available
presets: None

```

librosa.display.waveshow memory leak?
**Describe the bug**
When I try to run `librosa.display.waveshow` on a long wav file (hour long), 44.1kHz, and plot the waveform, it works in Colab. However, try running the same cell a few times and the memory just keeps growing until the session crashes. This is problematic because I'd like to be able to run this waveform generating function in a loop over many hour-long files. 


**To Reproduce**

Example:
Here's a colab link (assuming you have access to 1 hour-long wav files: https://colab.research.google.com/drive/1HIXqFM4NIw6qflcS4Ss5yAMvJD_XNmta )
```
import librosa.display
import librosa
import matplotlib.pyplot as plt

file_path = '/content/drive/MyDrive/wav_files/denoised_hour_long_wav_file.wav'
y, sr = librosa.load(file_path, mono=False, sr=None)

orig_path ='/content/drive/MyDrive/wav_files/hour_long_wav_file.wav'
y_2, sr_2 = librosa.load(orig_path, mono=False, sr=None)


fig = plt.figure(figsize=(20,4), dpi=100)
for i in range(10):
  #Running this in a loop to show how it will crash after roughly 3-5 times of doing this.
  librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4)
  librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5)
fig.tight_layout()
fig.show()
```
Try running this code in a cell multiple times. If you isolate just these 2 lines:

```
librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4)
librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5)
```

And run those in their own cell multiple times (or in a while loop, or for loop if you have an hour long file), you'll notice the memory growing until it crashes and restarts the session.



**Expected behavior**
I generate waveforms without the memory filling up and crashing Colab.

**Software versions***

Please run the following Python code snippet and paste the output below.
```
import platform; print(platform.platform())
import sys; print("Python", sys.version)
import numpy; print("NumPy", numpy.__version__)
import scipy; print("SciPy", scipy.__version__)
import librosa; print("librosa", librosa.__version__)

librosa.show_versions()

Linux-5.4.188+-x86_64-with-Ubuntu-18.04-bionic
Python 3.7.13 (default, Apr 24 2022, 01:04:09) 
[GCC 7.5.0]
NumPy 1.21.6
SciPy 1.7.3
librosa 0.9.2
INSTALLED VERSIONS
------------------
python: 3.7.13 (default, Apr 24 2022, 01:04:09) 
[GCC 7.5.0]

librosa: 0.9.2

audioread: 2.1.9
numpy: 1.21.6
scipy: 1.7.3
sklearn: 1.0.2
joblib: 1.1.0
decorator: 4.4.2
soundfile: 0.10.3
resampy: 0.3.1
numba: 0.51.2
pooch: v1.6.0
packaging: 21.3

numpydoc: None
sphinx: 1.8.6
sphinx_rtd_theme: None
sphinx_multiversion: None
sphinx_gallery: None
mir_eval: None
ipython: None
sphinxcontrib-svg2pdfconverter: None
pytest: 3.6.4
pytest-mpl: None
pytest-cov: None
matplotlib: 3.2.2
samplerate: None
soxr: None
contextlib2: installed, no version number available
presets: None

```



Discussion:
Thanks for the detailed report!

I think this will take a bit of investigation to determine whether this is something on the librosa side or the matplotlib side.  (Possibly it's a bit of each.)

For context, it might help to outline what exactly waveshow is doing here.  When you call waveshow, it constructs an `AdaptiveWaveplot` object.  This object contains two matplotlib artists: one is a step plot that is limited to at most `max_points` observations (default is 1/2 second worth) and the other is a fill-between artist that operates on a decimated amplitude envelope of the signal.

To facilitate interaction, the `AdaptiveWaveplot` object needs to retain handles to both the raw signal (which should invoke no copies) and the envelope (which is a new array).  I suspect the latter is what is causing you trouble here, but I don't think it's a "bug" if so - it's intended functionality.

~~One way you could test this is to try ramping up the `max_points` threshold.  Higher values of this quantity will result in a more aggressively decimated envelope, which will reduce memory consumption.  The default here was set with shorter signals in mind, so it might just be a matter of tuning the parameters to better suit your use case.~~

~~I expect that tweaking max_points will probably delay the memory blow-up for you, but not fix it entirely.  (But I'd love to be wrong!)  If that's the case,~~ (*EDIT*: after some offline discussion and a few cups of coffee, I don't think maxpoints is the culprit here.)

I'd like to hear more about your specific needs here.  Is dynamic zoom a necessity for these plots?  If it's not, and you just need a long time-scale visualization, it ought to be possible to hack out the dynamic parts of the display and eliminate unnecessary objects after the plot is constructed.  (And if that works, we can then go back and figure out if this can be codified to simplify things in general.)

If the above is not the problem, it might be something deeper in the stack (eg in matplotlib).  I know that there are some unresolved issues in MPL relating to connecting multiple callback functions on the same signal, and this does actually make trouble for us when overlaying multiple waveshows in the same figure.  But let's hold off on digging into that until we've ruled out the envelope calculation as a source of problems.
Thank you so much for responding! So helpful!

For the context of my needs, I don't think dynamic zoom will be necessary because I want to export images (although high quality images with preferably as much detail as possible) I have a large set of audio files (approx. 2K). Each one is about 1-2 hours. I'm denoising each automatically, but before denoising in order to avoid running out of memory during the denoising process, I'm splitting the audio files, then stitching them back together again after denoising is complete. 

For my own sanity check, I'd like to export corresponding plots where I see the waveform of both overlayed on top of one another to check if: 

1. most of the information content / speech content is still there or if the denoising process did not capture it and 
2. that the splitting + stitching algorithm I wrote did not have some weird bug and stitch the segments out of order or something else unusual. 

 I don't have time to check each one fully, so I need the fastest most accurate sanity check, and waveform seems to best I could think of. 
 
 So I don't think I'm going to need dynamic zoom, just essentially a screenshot of the waveform (at a high dpi, maybe 100 or 200), just enough so that I can discriminate between the 2 waveforms and see if there are significant differences.

I'll try playing with max_points just as a check either way and report back!
Yeah you were right, I just tested increasing max_points to 2000, 20,000 and 200,000, and also dropping to 2. Same result everytime, it crashes after 2 iterations:
```
for i in range(10):
  #Running this in a loop to show how it will crash after roughly 3-5 times of doing this.
  librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4, max_points=2000)
  librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5, max_points=2000)
  print(i)
```
Thanks, that's a good data point.  We're actually doing a sprint right now at scipy and trying to work out if this is a librosa, matplotlib, or jupyter issue exactly.
I did some digging and it looks like every call to `librosa.display.waveshow` makes a copy of data _somehow_ and it stick around even though it's not in use. Given that each wav files is 600+ MB, memory usage grows really quickly. The Colab notebook probably crashes because you hit the RAM limit. I can see the memory grow and grow and grow on my laptop, but at least it doesn't crash.

There isn't an easy fix I could find right now, but here are some suggestions to work around this issue in the short term:

1. Downsample your signal before plotting so each plot call takes less memory.
3. If what you're after is the screenshot, consider something like this:


```
for i in range(10):
  a1 = librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4, max_points=2000)
  a2 = librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5, max_points=2000)

  # save the image here

  # Then delete the AdaptiveWaveplot objects to free the memory.
  del a1, a2
```
Confirming @achabotl 's solution here - deleting the handle to the adaptor object does seem to release memory in the way that we want it to.  But I think downsampling your data in advance is also a good idea. There's very little value for an amplitude envelope plot to have high sampling rates.  Downsampling to something like 1-5 Hz first would probably still yield a reasonable plot with significant reduction to ram usage overall.
sounds like this deletion is something we could have automatically by means of a custom `with` statement? or is that a bee's nest?

another (more reasonable) option is to raise an OOM error inside librosa with some helpful advice, such as downsampling the input or deleting `AdaptiveWaveplot` objects. It would still error, but at least it wouldn't crash the notebook.
@lostanlen both of those seem pretty dicey from the perspective of the `waveshow` function.  It would be very difficult to know the general context.

I think it might be more helpful to provide an example in the gallery that demonstrates some best practices for very large arrays.

Alternatively, we could also detect large inputs and throw a warning, but "large" might be a difficult thing to pin down here.

It might also be worth considering adding a self cleanup method to the adaptor object.  The `del adaptor` idea works, but it makes me nervous because it leaves a dangling reference from the matplotlib signal callback registry.  Really what should happen is that the signal connections are handled from an adaptor method (right now it's done by waveshow) so that we can retain the callback id.  Then we can provide a cleanup function that first disconnects and then deletes `self`.
Thanks for the detailed report!

I think this will take a bit of investigation to determine whether this is something on the librosa side or the matplotlib side.  (Possibly it's a bit of each.)

For context, it might help to outline what exactly waveshow is doing here.  When you call waveshow, it constructs an `AdaptiveWaveplot` object.  This object contains two matplotlib artists: one is a step plot that is limited to at most `max_points` observations (default is 1/2 second worth) and the other is a fill-between artist that operates on a decimated amplitude envelope of the signal.

To facilitate interaction, the `AdaptiveWaveplot` object needs to retain handles to both the raw signal (which should invoke no copies) and the envelope (which is a new array).  I suspect the latter is what is causing you trouble here, but I don't think it's a "bug" if so - it's intended functionality.

~~One way you could test this is to try ramping up the `max_points` threshold.  Higher values of this quantity will result in a more aggressively decimated envelope, which will reduce memory consumption.  The default here was set with shorter signals in mind, so it might just be a matter of tuning the parameters to better suit your use case.~~

~~I expect that tweaking max_points will probably delay the memory blow-up for you, but not fix it entirely.  (But I'd love to be wrong!)  If that's the case,~~ (*EDIT*: after some offline discussion and a few cups of coffee, I don't think maxpoints is the culprit here.)

I'd like to hear more about your specific needs here.  Is dynamic zoom a necessity for these plots?  If it's not, and you just need a long time-scale visualization, it ought to be possible to hack out the dynamic parts of the display and eliminate unnecessary objects after the plot is constructed.  (And if that works, we can then go back and figure out if this can be codified to simplify things in general.)

If the above is not the problem, it might be something deeper in the stack (eg in matplotlib).  I know that there are some unresolved issues in MPL relating to connecting multiple callback functions on the same signal, and this does actually make trouble for us when overlaying multiple waveshows in the same figure.  But let's hold off on digging into that until we've ruled out the envelope calculation as a source of problems.
Thank you so much for responding! So helpful!

For the context of my needs, I don't think dynamic zoom will be necessary because I want to export images (although high quality images with preferably as much detail as possible) I have a large set of audio files (approx. 2K). Each one is about 1-2 hours. I'm denoising each automatically, but before denoising in order to avoid running out of memory during the denoising process, I'm splitting the audio files, then stitching them back together again after denoising is complete. 

For my own sanity check, I'd like to export corresponding plots where I see the waveform of both overlayed on top of one another to check if: 

1. most of the information content / speech content is still there or if the denoising process did not capture it and 
2. that the splitting + stitching algorithm I wrote did not have some weird bug and stitch the segments out of order or something else unusual. 

 I don't have time to check each one fully, so I need the fastest most accurate sanity check, and waveform seems to best I could think of. 
 
 So I don't think I'm going to need dynamic zoom, just essentially a screenshot of the waveform (at a high dpi, maybe 100 or 200), just enough so that I can discriminate between the 2 waveforms and see if there are significant differences.

I'll try playing with max_points just as a check either way and report back!
Yeah you were right, I just tested increasing max_points to 2000, 20,000 and 200,000, and also dropping to 2. Same result everytime, it crashes after 2 iterations:
```
for i in range(10):
  #Running this in a loop to show how it will crash after roughly 3-5 times of doing this.
  librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4, max_points=2000)
  librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5, max_points=2000)
  print(i)
```
Thanks, that's a good data point.  We're actually doing a sprint right now at scipy and trying to work out if this is a librosa, matplotlib, or jupyter issue exactly.
I did some digging and it looks like every call to `librosa.display.waveshow` makes a copy of data _somehow_ and it stick around even though it's not in use. Given that each wav files is 600+ MB, memory usage grows really quickly. The Colab notebook probably crashes because you hit the RAM limit. I can see the memory grow and grow and grow on my laptop, but at least it doesn't crash.

There isn't an easy fix I could find right now, but here are some suggestions to work around this issue in the short term:

1. Downsample your signal before plotting so each plot call takes less memory.
3. If what you're after is the screenshot, consider something like this:


```
for i in range(10):
  a1 = librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4, max_points=2000)
  a2 = librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5, max_points=2000)

  # save the image here

  # Then delete the AdaptiveWaveplot objects to free the memory.
  del a1, a2
```
Confirming @achabotl 's solution here - deleting the handle to the adaptor object does seem to release memory in the way that we want it to.  But I think downsampling your data in advance is also a good idea. There's very little value for an amplitude envelope plot to have high sampling rates.  Downsampling to something like 1-5 Hz first would probably still yield a reasonable plot with significant reduction to ram usage overall.
sounds like this deletion is something we could have automatically by means of a custom `with` statement? or is that a bee's nest?

another (more reasonable) option is to raise an OOM error inside librosa with some helpful advice, such as downsampling the input or deleting `AdaptiveWaveplot` objects. It would still error, but at least it wouldn't crash the notebook.
@lostanlen both of those seem pretty dicey from the perspective of the `waveshow` function.  It would be very difficult to know the general context.

I think it might be more helpful to provide an example in the gallery that demonstrates some best practices for very large arrays.

Alternatively, we could also detect large inputs and throw a warning, but "large" might be a difficult thing to pin down here.

It might also be worth considering adding a self cleanup method to the adaptor object.  The `del adaptor` idea works, but it makes me nervous because it leaves a dangling reference from the matplotlib signal callback registry.  Really what should happen is that the signal connections are handled from an adaptor method (right now it's done by waveshow) so that we can retain the callback id.  Then we can provide a cleanup function that first disconnects and then deletes `self`.
Thanks for the detailed report!

I think this will take a bit of investigation to determine whether this is something on the librosa side or the matplotlib side.  (Possibly it's a bit of each.)

For context, it might help to outline what exactly waveshow is doing here.  When you call waveshow, it constructs an `AdaptiveWaveplot` object.  This object contains two matplotlib artists: one is a step plot that is limited to at most `max_points` observations (default is 1/2 second worth) and the other is a fill-between artist that operates on a decimated amplitude envelope of the signal.

To facilitate interaction, the `AdaptiveWaveplot` object needs to retain handles to both the raw signal (which should invoke no copies) and the envelope (which is a new array).  I suspect the latter is what is causing you trouble here, but I don't think it's a "bug" if so - it's intended functionality.

~~One way you could test this is to try ramping up the `max_points` threshold.  Higher values of this quantity will result in a more aggressively decimated envelope, which will reduce memory consumption.  The default here was set with shorter signals in mind, so it might just be a matter of tuning the parameters to better suit your use case.~~

~~I expect that tweaking max_points will probably delay the memory blow-up for you, but not fix it entirely.  (But I'd love to be wrong!)  If that's the case,~~ (*EDIT*: after some offline discussion and a few cups of coffee, I don't think maxpoints is the culprit here.)

I'd like to hear more about your specific needs here.  Is dynamic zoom a necessity for these plots?  If it's not, and you just need a long time-scale visualization, it ought to be possible to hack out the dynamic parts of the display and eliminate unnecessary objects after the plot is constructed.  (And if that works, we can then go back and figure out if this can be codified to simplify things in general.)

If the above is not the problem, it might be something deeper in the stack (eg in matplotlib).  I know that there are some unresolved issues in MPL relating to connecting multiple callback functions on the same signal, and this does actually make trouble for us when overlaying multiple waveshows in the same figure.  But let's hold off on digging into that until we've ruled out the envelope calculation as a source of problems.
Thank you so much for responding! So helpful!

For the context of my needs, I don't think dynamic zoom will be necessary because I want to export images (although high quality images with preferably as much detail as possible) I have a large set of audio files (approx. 2K). Each one is about 1-2 hours. I'm denoising each automatically, but before denoising in order to avoid running out of memory during the denoising process, I'm splitting the audio files, then stitching them back together again after denoising is complete. 

For my own sanity check, I'd like to export corresponding plots where I see the waveform of both overlayed on top of one another to check if: 

1. most of the information content / speech content is still there or if the denoising process did not capture it and 
2. that the splitting + stitching algorithm I wrote did not have some weird bug and stitch the segments out of order or something else unusual. 

 I don't have time to check each one fully, so I need the fastest most accurate sanity check, and waveform seems to best I could think of. 
 
 So I don't think I'm going to need dynamic zoom, just essentially a screenshot of the waveform (at a high dpi, maybe 100 or 200), just enough so that I can discriminate between the 2 waveforms and see if there are significant differences.

I'll try playing with max_points just as a check either way and report back!
Yeah you were right, I just tested increasing max_points to 2000, 20,000 and 200,000, and also dropping to 2. Same result everytime, it crashes after 2 iterations:
```
for i in range(10):
  #Running this in a loop to show how it will crash after roughly 3-5 times of doing this.
  librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4, max_points=2000)
  librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5, max_points=2000)
  print(i)
```
Thanks, that's a good data point.  We're actually doing a sprint right now at scipy and trying to work out if this is a librosa, matplotlib, or jupyter issue exactly.
I did some digging and it looks like every call to `librosa.display.waveshow` makes a copy of data _somehow_ and it stick around even though it's not in use. Given that each wav files is 600+ MB, memory usage grows really quickly. The Colab notebook probably crashes because you hit the RAM limit. I can see the memory grow and grow and grow on my laptop, but at least it doesn't crash.

There isn't an easy fix I could find right now, but here are some suggestions to work around this issue in the short term:

1. Downsample your signal before plotting so each plot call takes less memory.
3. If what you're after is the screenshot, consider something like this:


```
for i in range(10):
  a1 = librosa.display.waveshow(y_2, sr=sr_2, alpha=0.4, max_points=2000)
  a2 = librosa.display.waveshow(y, sr=sr, color='r', alpha=0.5, max_points=2000)

  # save the image here

  # Then delete the AdaptiveWaveplot objects to free the memory.
  del a1, a2
```
Confirming @achabotl 's solution here - deleting the handle to the adaptor object does seem to release memory in the way that we want it to.  But I think downsampling your data in advance is also a good idea. There's very little value for an amplitude envelope plot to have high sampling rates.  Downsampling to something like 1-5 Hz first would probably still yield a reasonable plot with significant reduction to ram usage overall.
sounds like this deletion is something we could have automatically by means of a custom `with` statement? or is that a bee's nest?

another (more reasonable) option is to raise an OOM error inside librosa with some helpful advice, such as downsampling the input or deleting `AdaptiveWaveplot` objects. It would still error, but at least it wouldn't crash the notebook.
@lostanlen both of those seem pretty dicey from the perspective of the `waveshow` function.  It would be very difficult to know the general context.

I think it might be more helpful to provide an example in the gallery that demonstrates some best practices for very large arrays.

Alternatively, we could also detect large inputs and throw a warning, but "large" might be a difficult thing to pin down here.

It might also be worth considering adding a self cleanup method to the adaptor object.  The `del adaptor` idea works, but it makes me nervous because it leaves a dangling reference from the matplotlib signal callback registry.  Really what should happen is that the signal connections are handled from an adaptor method (right now it's done by waveshow) so that we can retain the callback id.  Then we can provide a cleanup function that first disconnects and then deletes `self`.

Resolving patch:
diff --git a/librosa/display.py b/librosa/display.py
--- a/librosa/display.py
+++ b/librosa/display.py
@@ -162,11 +162,11 @@ def __call__(self, x, pos=None):
             s = "{:d}:{:02d}".format(int(value / 60.0), int(np.mod(value, 60)))
         elif self.unit == "s":
             s = "{:.3g}".format(value)
-        elif self.unit==None and (vmax - vmin >= 1):
+        elif self.unit == None and (vmax - vmin >= 1):
             s = "{:.2g}".format(value)
         elif self.unit == "ms":
             s = "{:.3g}".format(value * 1000)
-        elif self.unit==None and (vmax - vmin < 1):
+        elif self.unit == None and (vmax - vmin < 1):
             s = "{:.3f}".format(value)
 
         return "{:s}{:s}".format(sign, s)
@@ -509,6 +509,57 @@ def __init__(self, times, y, steps, envelope, sr=22050, max_samples=11025):
         self.envelope = envelope
         self.sr = sr
         self.max_samples = max_samples
+        self.cid = None
+        self.ax = None
+
+    def __del__(self):
+        self.disconnect(strict=True)
+
+    def connect(self, ax, *, signal="xlim_changed"):
+        """Connect the adaptor to a signal on an axes object.
+
+        Note that if the adaptor has already been connected to an axes object,
+        that connect is first broken and then replaced by a new callback.
+
+        Parameters
+        ----------
+        ax : matplotlib.axes.Axes
+            The axes to connect with this adaptor's `update`
+        signal : string, {"xlim_changed", "ylim_changed"}
+            The signal to connect
+
+        See Also
+        --------
+        disconnect
+        """
+        # Disconnect any existing callback first
+        self.disconnect()
+
+        # Attach to axes and store the connection id
+        self.ax = ax
+        self.cid = ax.callbacks.connect(signal, self.update)
+
+    def disconnect(self, *, strict=False):
+        """Disconnect the adaptor's update callback.
+
+        Parameters
+        ----------
+        strict : bool
+            If `True`, remove references to the connected axes.
+            If `False` (default), only disconnect the callback.
+
+            This functionality is intended primarily for internal use,
+            and should have no observable effects for users.
+
+        See Also
+        --------
+        connect
+        """
+        if self.ax:
+            self.ax.callbacks.disconnect(self.cid)
+            self.cid = None
+            if strict:
+                self.ax = None
 
     def update(self, ax):
         """Update the matplotlib display according to the current viewport limits.
@@ -517,7 +568,7 @@ def update(self, ax):
 
         Parameters
         ----------
-        ax : matplotlib axes object
+        ax : matplotlib.axes.Axes
             The axes object to update
         """
         lims = ax.viewLim
@@ -614,20 +665,53 @@ def __envelope(x, hop):
     return x_frame.max(axis=1)
 
 
-_chroma_ax_types = ('chroma', 'chroma_h', 'chroma_c',)
-_cqt_ax_types = ('cqt_hz', 'cqt_note', 'cqt_svara',)
-_freq_ax_types = ('linear', 'fft', 'hz', 'fft_note', 'fft_svara',)
-_time_ax_types = ('time', 'h', 'm', 's', 'ms',)
-_lag_ax_types = ('lag', 'lag_h', 'lag_m', 'lag_s', 'lag_ms',)
-_misc_ax_types = ('tempo', 'fourier_tempo', 'mel', 'log', 'tonnetz', 'frames',)
+_chroma_ax_types = (
+    "chroma",
+    "chroma_h",
+    "chroma_c",
+)
+_cqt_ax_types = (
+    "cqt_hz",
+    "cqt_note",
+    "cqt_svara",
+)
+_freq_ax_types = (
+    "linear",
+    "fft",
+    "hz",
+    "fft_note",
+    "fft_svara",
+)
+_time_ax_types = (
+    "time",
+    "h",
+    "m",
+    "s",
+    "ms",
+)
+_lag_ax_types = (
+    "lag",
+    "lag_h",
+    "lag_m",
+    "lag_s",
+    "lag_ms",
+)
+_misc_ax_types = (
+    "tempo",
+    "fourier_tempo",
+    "mel",
+    "log",
+    "tonnetz",
+    "frames",
+)
 
 _AXIS_COMPAT = set(
-    [(t, t) for t in _misc_ax_types] +
-    [t for t in product(_chroma_ax_types, _chroma_ax_types)] +
-    [t for t in product(_cqt_ax_types, _cqt_ax_types)] +
-    [t for t in product(_freq_ax_types, _freq_ax_types)] +
-    [t for t in product(_time_ax_types, _time_ax_types)] +
-    [t for t in product(_lag_ax_types, _lag_ax_types)]
+    [(t, t) for t in _misc_ax_types]
+    + [t for t in product(_chroma_ax_types, _chroma_ax_types)]
+    + [t for t in product(_cqt_ax_types, _cqt_ax_types)]
+    + [t for t in product(_freq_ax_types, _freq_ax_types)]
+    + [t for t in product(_time_ax_types, _time_ax_types)]
+    + [t for t in product(_lag_ax_types, _lag_ax_types)]
 )
 
 
@@ -1050,8 +1134,7 @@ def __decorate_axis(
     axis, ax_type, key="C:maj", Sa=None, mela=None, thaat=None, unicode=True
 ):
     """Configure axis tickers, locators, and labels"""
-    time_units = {
-        "h": "hours", "m": "minutes", "s": "seconds", "ms": "milliseconds"}
+    time_units = {"h": "hours", "m": "minutes", "s": "seconds", "ms": "milliseconds"}
 
     if ax_type == "tonnetz":
         axis.set_major_formatter(TonnetzFormatter())
@@ -1502,7 +1585,7 @@ def waveshow(
         times, y[0], steps, envelope, sr=sr, max_samples=max_points
     )
 
-    axes.callbacks.connect("xlim_changed", adaptor.update)
+    adaptor.connect(axes, signal="xlim_changed")
 
     # Force an initial update to ensure the state is consistent
     adaptor.update(axes)


Test patch:
diff --git a/tests/test_display.py b/tests/test_display.py
--- a/tests/test_display.py
+++ b/tests/test_display.py
@@ -950,3 +950,58 @@ def test_specshow_unicode_false(C, sr):
         axi.label_outer()
 
     return fig
+
+
+def test_waveshow_disconnect(y, sr):
+    fig, ax = plt.subplots()
+    ad = librosa.display.waveshow(y=y, sr=sr, ax=ax)
+
+    # By default, envelope should be visible and steps should not
+    assert ad.envelope.get_visible() and not ad.steps.get_visible()
+
+    # Zoom in to a 0.25 second range
+    ax.set(xlim=[0, 0.25])
+
+    # Steps should be visible but not envelope
+    assert ad.steps.get_visible() and not ad.envelope.get_visible()
+
+    # Zoom back out
+    ax.set(xlim=[0, 4])
+    assert ad.envelope.get_visible() and not ad.steps.get_visible()
+
+    # Disconnect
+    ad.disconnect()
+
+    # Zoom back in to a 0.25 second range
+    ax.set(xlim=[0, 0.25])
+
+    # Envelope should now still be visible
+    assert ad.envelope.get_visible() and not ad.steps.get_visible()
+
+
+def test_waveshow_deladaptor(y, sr):
+    fig, ax = plt.subplots()
+    ad = librosa.display.waveshow(y=y, sr=sr, ax=ax)
+
+    envelope, steps = ad.envelope, ad.steps
+    # By default, envelope should be visible and steps should not
+    assert envelope.get_visible() and not steps.get_visible()
+
+    # Zoom in to a 0.25 second range
+    ax.set(xlim=[0, 0.25])
+
+    # Steps should be visible but not envelope
+    assert steps.get_visible() and not envelope.get_visible()
+
+    # Zoom back out
+    ax.set(xlim=[0, 4])
+    assert envelope.get_visible() and not steps.get_visible()
+
+    # Disconnect
+    del ad
+
+    # Zoom back in to a 0.25 second range
+    ax.set(xlim=[0, 0.25])
+
+    # Envelope should now still be visible
+    assert envelope.get_visible() and not steps.get_visible()