pythonmatplotlibspectrogram

Matplotlib spectrogram animation without calling pyplot.specgram directly every cycle


I have made an animated spectrogram this way:

import matplotlib.pyplot as plt
from matplotlib import animation
import librosa

WINDOW = 100_000
JUMP = 1000
INTERVAL = 1
FILENAME = 'sound.wav'

sound, rate = librosa.load(FILENAME, sr=None)
fig = plt.figure()


def animate(i):
    chunk = sound[i * JUMP: i * JUMP + WINDOW]
    _, _, _, im = plt.specgram(chunk, Fs=rate)
    return im,


ani = animation.FuncAnimation(fig, animate, interval=INTERVAL, blit=True)

plt.ion()
plt.show()

It works, but in examples of using FuncAnimation I've seen, people don't call the whole plotting function for every animation frame but update the data directly instead and it feels as if there are probably reasons (performance?) to do this. The examples gave some idea of how to do this for other images (ones where you were basically doing your own math to fill the array that is the image) but with something a little more blackbox-ish like pyplot.specgram my (maybe) hack was to just call the plotting function over and over. My question is can this be done in a way more like the example in the link above on the matplotlib site and, if yes, how?


Solution

  • The reason you don't want to repeatedly call the plt.specgram function inside the animation is indeed performance. After N cycles you have N images in your figure, which makes drawing more and more expensive.

    Of course a possible solution is to remove the previous image in each iteration, e.g. by having a handle to it, (im.remove()) or via the list of images (ax.images[0].remove()).

    However, you are right that the more desireable solution is to not recreate any image at all, but instead only change the image data.

    In that case you will want to call matplotlib.mlab.specgram to obtain the spectrum as numpy array and use the set_array() method of the image to update the image in the animation.

    Note however, that this might require you to update the color limits of the image as well, if different spectra have different minimum or maximum amplitudes.


    Because the image shown by plt.specgram is not directly the spectrogram returned by mlab.specgram, you may then need to set some parameters manually. Especially, by default, the image is shown on a dB scale.

    I think the equivalent to

    Fs = rate
    NFFT = 256
    noverlap= 128
    spec, freqs, t, im = plt.specgram(sound, Fs=Fs, NFFT=NFFT, noverlap=noverlap)
    

    would be

    spec, freqs, t = plt.mlab.specgram(sound, Fs=rate, NFFT=NFFT, noverlap=noverlap)
    pad_xextent = (NFFT-noverlap) / Fs / 2
    xmin, xmax = np.min(t) - pad_xextent, np.max(t) + pad_xextent
    extent = xmin, xmax, freqs[0], freqs[-1]
    im = plt.imshow(np.flipud(10. * np.log10(spec)), extent=extent, aspect="auto")