Repository: librosa/librosa

Issue:
Improve filter visualization for semitone_filterbank docstring
The visualization of IIR filters that we use here is cluttered and difficult to parse.  

![Semitone filterbank example](https://librosa.org/doc/latest/_images/librosa-filters-semitone_filterbank-1.png)

I think we could do much better with something more like a spectrogram display, or at least reducing the number of filters and plotting on separate axes.

https://librosa.org/doc/latest/generated/librosa.filters.semitone_filterbank.html


Discussion:
Hi,

I have played around a little and tried to create a more appealing display.
 
In a first approach I tried to create a Spectrogram like representation:
```
frequencies = np.arange(0, 22000,10)
magnitudes = []
fig = plt.figure(figsize=(10,3))
ax = fig.add_subplot()
for cur_sr, cur_filter in zip(sample_rates[::3], semitone_filterbank[::3]):
    w, h = scipy.signal.freqz(cur_filter[0], cur_filter[1], worN=2000)
    magnitudes.append([interp1d((cur_sr / (2 * np.pi)) * w, 20 * np.log10(abs(h)), bounds_error=False, fill_value=-50)(frequencies)])
magnitudes = np.row_stack(magnitudes)

img = specshow(magnitudes, x_axis="log", vmin=-60, vmax=3)
plt.gca().set_xlabel('Log-Frequency (Hz)')
plt.ylabel("Semitone filter")
plt.xlim(20, 10e3)
fig.colorbar(img, ax=plt.gca(), format="%+2.f dB")
plt.show()
```

![image](https://user-images.githubusercontent.com/107742782/190229877-20a2ab6d-d514-4f01-b04b-30bbae233d0a.png)
 
For this I found no other way than to interpolate the individual filter responses, which is of course a bit awkward.
After that I tried to implement the second suggestion and reduced the number of filters to every 3rd and changed the format:

```
fig = plt.figure(figsize=(10,3))
ax = fig.add_subplot()
for cur_sr, cur_filter in zip(sample_rates[::3], semitone_filterbank[::3]):
    w, h = scipy.signal.freqz(cur_filter[0], cur_filter[1], worN=2000)
    ax.semilogx((cur_sr / (2 * np.pi)) * w, 20 * np.log10(abs(h)))
ax.set(xlim=[20, 10e3], ylim=[-60, 3], title='Magnitude Responses of the Pitch Filterbank', xlabel='Log-Frequency (Hz)', ylabel='Magnitude (dB)')
plt.show()
```

![image](https://user-images.githubusercontent.com/107742782/190230024-d83e089e-9593-43bc-9239-dd600409cce4.png)

I personally find this representation a bit more helpful. But also a combination of the two would perhaps not be bad?
Thanks for taking a crack at this!

A couple of thoughts:

- I think the spectrogram-like display makes the most sense here.  You can bypass the interpolation step by passing the sampling rate directly to `freqz` and specifying `worN=` however many frequencies you want (1025 would align nicely with our default STFT settings).
- For display purposes, it might look better (be more numerically stable) to use `flayout='sos'` and replace `freqz` by `sosfreqz`.  That should knock out some of the spurious ripple in the pass-bands of the second plot you have.
- I think keeping the second plot could be nice, with a couple of caveats: 1) i'm not too keen on subsampling the frequencies like this, because it gives a false impression of sparsity.  Better to reduce to a single octave to illustrate the idea.  2) it we use a sequential colormap, it will be easier to link up to a legend and make the plot easier to read.

I'm totally open to iterating on this btw, just throwing out some ideas to streamline it!
After your feedback I tried again to create a better display. 
I have now selected only the C4 octave and switched to second order filter.
The result is much better I think.
```
semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank(
    center_freqs=librosa.midi_to_hz(np.arange(60, 72)),
    sample_rates=np.repeat(4410.0, 12),
    flayout='sos'
)

magnitudes = []
for cur_sr, cur_filter in zip(sample_rates, semitone_filterbank):
    w, h = scipy.signal.sosfreqz(cur_filter,fs=cur_sr, worN=1025)
    magnitudes.append(20 * np.log10(np.abs(h)))


fig, ax = plt.subplots(figsize=(10,4))
img = librosa.display.specshow(
    np.array(magnitudes), 
    x_axis="hz", 
    sr=4410, 
    y_coords=librosa.midi_to_hz(np.arange(60, 72)), 
    vmin=-60, 
    vmax=3, 
    ax=ax
)
fig.colorbar(img, ax=ax, format="%+2.f dB", label="Magnitude (dB)")
ax.set(
    xlim=[200, 600], 
    yticks=librosa.midi_to_hz(np.arange(60, 72)),
    title='Magnitude Responses of the Pitch Filterbank', 
    xlabel='Frequency (Hz)', 
    ylabel='Semitone filter center frequency (Hz)'
)
```
![image](https://user-images.githubusercontent.com/107742782/190385482-37e4a278-5d2c-4ff5-8594-dfa87e3fbb0c.png)
That looks great, thanks!
Thanks for the quick feedback. I will now create an corresponding PR.

Resolving patch:
diff --git a/librosa/filters.py b/librosa/filters.py
--- a/librosa/filters.py
+++ b/librosa/filters.py
@@ -1432,13 +1432,33 @@ def semitone_filterbank(
     >>> import matplotlib.pyplot as plt
     >>> import numpy as np
     >>> import scipy.signal
-    >>> semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank()
-    >>> fig, ax = plt.subplots()
+    >>> semitone_filterbank, sample_rates = librosa.filters.semitone_filterbank(
+    ...     center_freqs=librosa.midi_to_hz(np.arange(60, 72)),
+    ...     sample_rates=np.repeat(4410.0, 12),
+    ...     flayout='sos'
+    ...     )
+    >>> magnitudes = []
     >>> for cur_sr, cur_filter in zip(sample_rates, semitone_filterbank):
-    ...    w, h = scipy.signal.freqz(cur_filter[0], cur_filter[1], worN=2000)
-    ...    ax.semilogx((cur_sr / (2 * np.pi)) * w, 20 * np.log10(abs(h)))
-    >>> ax.set(xlim=[20, 10e3], ylim=[-60, 3], title='Magnitude Responses of the Pitch Filterbank',
-    ...        xlabel='Log-Frequency (Hz)', ylabel='Magnitude (dB)')
+    ...     w, h = scipy.signal.sosfreqz(cur_filter,fs=cur_sr, worN=1025)
+    ...     magnitudes.append(20 * np.log10(np.abs(h)))
+    >>> fig, ax = plt.subplots(figsize=(12,6))
+    >>> img = librosa.display.specshow(
+    ...     np.array(magnitudes), 
+    ...     x_axis="hz", 
+    ...     sr=4410, 
+    ...     y_coords=librosa.midi_to_hz(np.arange(60, 72)), 
+    ...     vmin=-60, 
+    ...     vmax=3, 
+    ...     ax=ax
+    ...     )
+    >>> fig.colorbar(img, ax=ax, format="%+2.f dB", label="Magnitude (dB)")
+    >>> ax.set(
+    ...     xlim=[200, 600], 
+    ...     yticks=librosa.midi_to_hz(np.arange(60, 72)),
+    ...     title='Magnitude Responses of the Pitch Filterbank', 
+    ...     xlabel='Frequency (Hz)', 
+    ...     ylabel='Semitone filter center frequency (Hz)'
+    ... )
     """
 
     if (center_freqs is None) and (sample_rates is None):


Test patch: