1

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?

2 Answers 2

1

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")
Sign up to request clarification or add additional context in comments.

6 Comments

To clarify: I should calculate the specgram for the whole sound file into a numpy array beforehand and then hand a sliding slice of it to im.set_array() at each animation cycle?
That's possible. You could also calculate all the sprecgrams individually, either in a loop before the animation or within the animation.
I have just made a go of doing what you suggested. I have been struggling with a remaining issue that I have tried to document in my edit to my question.
Now edited my edit as I figured out what at least most of what the remaining problem was (some new questions had cropped up for me but they aren't totally germane to my original question)
I guess it would make more sense to provide your edit as an answer; answering the question within the question seems a bit strange. The differences you see compared to the previous case are probably due to the dB scale in use? I updated the answer with what I think is the propper way to display the spectrum.
|
1

Per the answer I accepted, I now have this:

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(figsize=(10, 5))

arr = plt.mlab.specgram(sound[:WINDOW], Fs=rate)[0]
im = plt.imshow(arr, animated=True)

def animate(i):
    arr = plt.mlab.specgram(sound[i * JUMP:i * JUMP + WINDOW], Fs=rate)[0]
    im.set_array(arr)
    return im,

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

However, in that code, plt.mlab.specgram yields a slightly different picture than plt.specgram because of different default settings. That is not totally key to the thrust of my original question but in the below code the adjustments that were suggested in the other answer to resolve this disparity are integrated so that one does end up with the same scaling etc.

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

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

NFFT = 256
NOVERLAP = 128

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

pad_xextent = (NFFT - NOVERLAP) / rate / 2


def create_specgram(sound_bit, rate):
    spec, freqs, t = plt.mlab.specgram(sound_bit, Fs=rate)
    xmin = np.min(t) - pad_xextent
    xmax = np.max(t) + pad_xextent
    extent = xmin, xmax, freqs[0], freqs[-1]
    arr = np.flipud(10. * np.log10(spec))
    return arr, extent


def animate(i):
    chunk = sound[i * JUMP: i * JUMP + WINDOW]
    arr, _ = create_specgram(chunk, rate)
    image.set_array(arr)
    return image,


fig = plt.figure(figsize=(10, 5))

arr, extent = create_specgram(sound[:WINDOW], rate)
image = plt.imshow(arr,
                   animated=True,
                   extent=extent,
                   aspect='auto')

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

plt.ion()
plt.show()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.