Polar tonnetz display
I've never been happy with our tonnetz displays (via specshow). At the same time, the tonnetz visualization in Harte et al. 2006 was never very intuitive to me either, since it doesn't present an obvious way to represent time.
Riffing on the time=radius idea above for chroma, I took a whack at tonnetz displays under the same principle. Here I'm retaining the angles from each of the three subspaces (P5, m3, M3), mapping time to radius, and magnitude at each time step in each subspace is encoded by alpha channel. Subspace angle is redundantly coded as color, just to put some more visual interest in the display. (Monochrome would be fine, but I think it would make no sense to have multiple tonnetz plots overlaid this way, so i opted for a colormap.)
Here's a rough cut (EDIT lightly revised to standardize color scales):
fig, ax = plt.subplots(nrows=1, ncols=3, subplot_kw=dict(projection='polar'), sharey=True, figsize=(9, 3), constrained_layout=True) times = librosa.times_like(T) th1 = np.arctan2(T[0], T[1]) mag1 = np.sqrt(T[1]**2 + T[0]**2) th2 = np.arctan2(T[2], T[3]) mag2 = np.sqrt(T[3]**2 + T[2]**2) th3 = np.arctan2(T[4], T[5]) mag3 = np.sqrt(T[5]**2 + T[4]**2) ax[0].scatter(th1, times, alpha=mag1/mag1.max(), c=th1, cmap='twilight', marker='.', s=5, vmin=-np.pi, vmax=np.pi) ax[1].scatter(th2, times, alpha=mag2/mag2.max(), c=th2, cmap='twilight', marker='.', s=5, vmin=-np.pi, vmax=np.pi) ax[2].scatter(th3, times, alpha=mag3/mag3.max(), c=th3, cmap='twilight', marker='.', s=5, vmin=-np.pi, vmax=np.pi) ax[0].set(title='P5') perm = np.mod(7 * np.arange(12), 12) ax[0].set_thetagrids(360 * np.arange(12) / 12, labels=librosa.midi_to_note(perm, octave=False, key='Eb:maj')) ax[1].set(title='m3') ax[1].set_thetagrids(360 * np.arange(4) / 4, labels=['C·E·A♭', 'E♭·G·B', 'D·G♭·B♭', 'D♭·F·A']) ax[2].set(title='M3') ax[2].set_thetagrids(360 * np.arange(3) / 3, labels=['C·Eb·G♭·A', 'D♭·E·G·B♭', 'D·F·A♭·B']) for axi in ax.flat: axi.set_yscale('function', functions=(np.sqrt, lambda r: r**2)) axi.set_rticks([]) axi.set_theta_direction(-1) axi.set_theta_offset(np.pi/2)Using the Hungarian Dance ♯5 example results in the following:
Here's sweetwaltz:
and the first 90 seconds of Giant steps, just for fun:
What can we read from these? I'm not really sure. I'm not convinced the tonnetz features powered by our simple cqt-based chroma are all that clean. I don't think they're less informative than the specshow-based display (except where it comes to temporal discontinuities, which I guess were kinda the point of these features in the first place...).
EDIT: I revised the plots so that radius scales time by a square-root law. This makes the early part of the song a bit easier to see. Zooming into the first 15 seconds of Giant Steps again:
And one more example, Vibe Ace, looks extremely salient:
EDIT: Fixed the x/y transposition identified in the comment below.
Originally posted by @bmcfee in #1975
Some issues to resolve before going ahead with this:
- API for subplot construction?
- Conventions for ticking radial time axes
- How to label the subplot ticks in a key-correct way. This is easy enough for the P5 dimensions, but the min3 and maj3 dimensions get a little weird. On the one hand, you could label each tick as an augmented triad (min3 plot) or a dim7 tetrad (maj3 plot) spelled under the given key. However, one could also reasonably label all the pitch classes according to the given key first and then join them according to their equivalence class; it's not obvious to me that this will produce the same results.
Source: librosa/librosa