Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active August 13, 2024 11:27
Show Gist options
  • Save endolith/3066664 to your computer and use it in GitHub Desktop.
Save endolith/3066664 to your computer and use it in GitHub Desktop.
Sethares dissmeasure function in Python

Adaptation of Sethares' dissonance measurement function to Python

Example is meant to match the curve in Figure 3:

Figure 3

Original model used products of the two amplitudes a1⋅a2, but this was changed to minimum of the two amplitudes min(a1, a2), as explained in G: Analysis of the Time Domain Model appendix of Tuning, Timbre, Spectrum, Scale.

This weighting is incorporated into the dissonance model (E.2) by assuming that the roughness is proportional to the loudness of the beating. ... Thus, the amplitude of the beating is given by the minimum of the two amplitudes.

With the first 6 harmonics at amplitudes 1/n starting at 261.63 Hz, using the product model, it also perfectly matches Figure 4 of Davide Verotta - Dissonance & Composition, so it should be trustworthy.

"""
Python translation of http://sethares.engr.wisc.edu/comprog.html
"""
import numpy as np
def dissmeasure(fvec, amp, model='min'):
"""
Given a list of partials in fvec, with amplitudes in amp, this routine
calculates the dissonance by summing the roughness of every sine pair
based on a model of Plomp-Levelt's roughness curve.
The older model (model='product') was based on the product of the two
amplitudes, but the newer model (model='min') is based on the minimum
of the two amplitudes, since this matches the beat frequency amplitude.
"""
# Sort by frequency
sort_idx = np.argsort(fvec)
am_sorted = np.asarray(amp)[sort_idx]
fr_sorted = np.asarray(fvec)[sort_idx]
# Used to stretch dissonance curve for different freqs:
Dstar = 0.24 # Point of maximum dissonance
S1 = 0.0207
S2 = 18.96
C1 = 5
C2 = -5
# Plomp-Levelt roughness curve:
A1 = -3.51
A2 = -5.75
# Generate all combinations of frequency components
idx = np.transpose(np.triu_indices(len(fr_sorted), 1))
fr_pairs = fr_sorted[idx]
am_pairs = am_sorted[idx]
Fmin = fr_pairs[:, 0]
S = Dstar / (S1 * Fmin + S2)
Fdif = fr_pairs[:, 1] - fr_pairs[:, 0]
if model == 'min':
a = np.amin(am_pairs, axis=1)
elif model == 'product':
a = np.prod(am_pairs, axis=1) # Older model
else:
raise ValueError('model should be "min" or "product"')
SFdif = S * Fdif
D = np.sum(a * (C1 * np.exp(A1 * SFdif) + C2 * np.exp(A2 * SFdif)))
return D
if __name__ == '__main__':
from numpy import array, linspace, empty, concatenate
import matplotlib.pyplot as plt
"""
Reproduce Sethares Figure 3
http://sethares.engr.wisc.edu/consemi.html#anchor15619672
"""
freq = 500 * array([1, 2, 3, 4, 5, 6])
amp = 0.88**array([0, 1, 2, 3, 4, 5])
r_low = 1
alpharange = 2.3
method = 'product'
# # Davide Verotta Figure 4 example
# freq = 261.63 * array([1, 2, 3, 4, 5, 6])
# amp = 1 / array([1, 2, 3, 4, 5, 6])
# r_low = 1
# alpharange = 2.0
# method = 'product'
n = 3000
diss = empty(n)
a = concatenate((amp, amp))
for i, alpha in enumerate(linspace(r_low, alpharange, n)):
f = concatenate((freq, alpha*freq))
d = dissmeasure(f, a, method)
diss[i] = d
plt.figure(figsize=(7, 3))
plt.plot(linspace(r_low, alpharange, len(diss)), diss)
plt.xscale('log')
plt.xlim(r_low, alpharange)
plt.xlabel('frequency ratio')
plt.ylabel('sensory dissonance')
intervals = [(1, 1), (6, 5), (5, 4), (4, 3), (3, 2), (5, 3), (2, 1)]
for n, d in intervals:
plt.axvline(n/d, color='silver')
plt.yticks([])
plt.minorticks_off()
plt.xticks([n/d for n, d in intervals],
['{}/{}'.format(n, d) for n, d in intervals])
plt.tight_layout()
plt.show()
@jpivarski
Copy link

Just modified the code to replace the vibrating string (energy levels are integer multiples of the base) with a hydrogen atom (energy levels scale as $1/n^2$).

The two curves are for a thermal distribution of energy ($\exp(-E/T)$) and a flat one, to see that it doesn't move the consonant intervals. They're all at small integer squared frequency ratios. The most consonant intervals are things like 9/4, 16/9, 25/9, 25/16, 36/25, ... The next thing to do would be to generate the tones and see if those bizarre intervals actually do sound better with that tibre.

https://gist.github.com/jpivarski/5e4908b09f4f9a244496d4d214e5c769

@endolith
Copy link
Author

endolith commented Aug 9, 2021

YES I WANT TO HEAR HYDROGEN MUSIC

@jpivarski
Copy link

jpivarski commented Aug 9, 2021

Well, here is a scale consisting of intervals 1, 36/25, 25/16, 16/9, 9/4, 25/9 (the most significant intervals picked out by the dissonance curve, above, in order). It's a bit wrong to call these all "hydrogen," since we'd have to vary the mass or coupling strength to get anything but the lowest tone, and that one has been shifted many octaves into human hearing. It's like we had a musical instrument consisting of many onia, such as muonium and positronium, each with a mass scaled to be in harmony with the others, where harmony is defined by the squares of small integer ratios.

https://gist.github.com/jpivarski/5e4908b09f4f9a244496d4d214e5c769/raw/c43bfaec1927400f0ec39acf0f592ca82d2927ff/hydrogen-scale.wav

Since the amplitudes matter a lot in determining the sound, I found a realistic table of transition probabilities (see my script): what we're hearing is the sound of photons released from atoms that are completely saturated with energy when "plucked." (I think a guitar string would do the same.) Each tone is rough just by itself because this spectrum includes many nearly equal frequencies ("fine splittings"). I didn't take that into consideration when making the dissonance curve. It wouldn't change the locations of the consonant dips, but it would make all of them a lot less consonant.

Maybe I should do one with a less realistic spectrum, so that we can try to hear how the tones of this scale are actually more consonant with each other than randomly chosen ones? (This was already a rabbithole!)

@jpivarski
Copy link

jpivarski commented Aug 10, 2021

Hmmm.

https://gist.github.com/jpivarski/5e4908b09f4f9a244496d4d214e5c769/raw/c43bfaec1927400f0ec39acf0f592ca82d2927ff/hydrogen-scale-2.wav

That's all from me. (Making the tone less realistic by focusing on only one kind of transition didn't make it any less warbly. There are doublets/nearly equal frequencies in every transition; it has to do with having so many energy levels that are nearly equal to each other.)

@jpivarski
Copy link

jpivarski commented Aug 10, 2021

Okay, okay, one last attempt:

https://gist.github.com/jpivarski/5e4908b09f4f9a244496d4d214e5c769/raw/c43bfaec1927400f0ec39acf0f592ca82d2927ff/hydrogen-scale-3.wav

This one cleans up the tone of a single "onium" by only allowing E1 transitions to the ground state—excluding all the doublets and fine splitting. Now they sound more like strange bells, a pure sound, but not like any bell I've ever heard. Maybe the intervals of this musical scale make sense. Certainly the octave-squared does.

@endolith
Copy link
Author

@jpivarski That actually sounds more musical than I expected it to. Cool!

Are you familiar with the "tritave"?

To avoid clipping, normalize the array before writing to the wav file:

scipy.io.wavfile.write("hydrogen-scale.wav", 44100, scale/abs(scale).max())

@BradKML
Copy link

BradKML commented Aug 11, 2021

@lynzrand what is the best measure of harmony between two tones (cus I want to do a music harmony index)

@jpivarski
Copy link

jpivarski commented Aug 11, 2021

@endolith I didn't know that scipy.io.wavfile.write wouldn't automatically scale it so that it doesn't clip. I re-synthesized the scales, replacing the links in my comments above. They sound more like IPython.display.Audio, which presumably does scale to avoid clipping. Thanks!

I haven't heard of tritave (until now). Whereas that only admits odd-numbered small integer ratios of frequencies, this "onium scale" only admits perfect-square small integer ratios of frequencies. Whereas the tritone is consonant with overtones of equally spaced, odd-numbered frequency ratios, the onium scale is consonant with overtones of $1/n$-spaced frequencies. Equally spaced frequencies/energies of waves are common in physics, from plucked strings and air vibrating in clarinets to excitations of the fields that make particles themselves—we call particles "things" because fundamental fields have an energy spectrum of equally spaced levels: "one particle," "two particles," and "three particles" are excitations of a field into the 1×mass, 2×mass, and 3×mass energy levels. Equal spacing is so ubiquitous because it's the wave solution to a parabolic potential energy curve, and any smooth minimum can be approximated by a parabola if you zoom in enough. (For example, the Higgs field is not purely parabolic, but it's approximately parabolic around its minimum energy, so Higgses can be created as particles.)

The $1/n$-spaced frequency/energy spectrum is a solution of $-1/r$ potentials, which are also pretty common, but not common macroscopically. An orbiting planet has the same $-1/r$ potential as a Hydrogen atom, but a planet is in too high of an energy state to be wave-like, the way that vibrations on a string are, pressure waves in air are, and the probability distribution of an electron's position is. Other particles have been electomagnetically bound to protons, such as muons in muonium, and other particles have been bound to each other, such as electrons and positrons in positronium. Quarkonia almost count as another example, except that the potential between quarks is only $-1/r$ in the small-$r$ limit.

But generalizing from the tritave's picking out small odd-integer ratios and the onium scale's picking out small squared-integer ratios, why not some other restriction on the integers, such as cubes or only admitting prime numbers? One could work backward to find the timbre that makes prime-numbered ratios consonant. A quantum particle in a box (physically realized in a quantum well laser, for instance) has an $n^2$ energy spectrum, which would pick out a different set of consonant intervals.

I found my way here from listening to Gamelan music while washing the dishes, to looking up some of the theory behind it, to this video:

https://youtu.be/ksX-saQVL40

which applied Plomp and Levelt's dissonance curve to the instruments' timbre and derived something close to the Slendro and Pelog scales. I was dumbfounded: this was the answer to a long-standing question of mine—why do we pick the musical scales that we do? Apart from Gamelan, most cultures pick subsets of the same set of small-integer ratios, which seemed like more agreement than I would expect. The words and even phonemes of languages around the world are a lot more diverse than the choice of pitches in music. This explanation—that it's a relatively simple, universal dissonance curve (perhaps hard-wired in our biology), integrated over the overtones of the instruments that we use—not only explains the common scales, but outliers like Gamelan. It boils down to biology (maybe the frisson of dissonance is the "uncanny valley" between putting two pitches in the same category and putting them in different ones? brains like categorizing continuous quantities...) and what the common instruments are, and the choice of instruments accounts for cultural variance. Most instruments have equally spaced harmonics, because of common physics, so their scales are similar, but the keys in Gamelan instruments are suspended, so they have a different timbre and therefore a different consonant scale.

The video also pointed to Sethares's book, which is how I ended up here. I'm still reading the book, but I had to try the dissonance summation procedure on different timbres. (Thanks for porting it to Python! That made it easier to just pick it up and try things out!) I also read Plomp and Levelt's paper. They didn't explain the procedure in much detail (and how you weight the sum by loudness has been debated since), and they didn't mention Gamelan. It's a much stronger theory when it's shown to work for an outlier as well as the common scales. Sethares's book includes Gamelan instruments and scales in many sections—is he the one who first applied Plomp-Levelt dissonance to Gamelan? Because that's an important step that clinches this very powerful explanation of where scales come from.

@BrandonKMLee, chapter 5 of Sethares's book (Tuning, Timbre, Spectrum, Scale by W. Sethares) has a survey of different dissonance measures, some very different from the Plomp-Levelt measure (Tonal Consonance and Critical Bandwidth R.Plomp & W.J.M.Levelt), including one that uses entropy. (I haven't read that one yet, but I'm getting there.) All of them are about the harmony of notes and intervals when choosing frequencies to include in a scale, not the notes in a piece of music, drawn from a common scale. That is, if you're trying to make an index over musical pieces that are all in the same 12-tone equal-tempered scale (i.e. nearly all Western music), I don't think that index could distinguish them. I guess some intervals in the 12-tone scale are more consonant than others, particularly where the equal-temperament makes them miss the minima of the curves above, and an index could rate songs differently if they use more of these "bad notes." Or even if the scale is just-tempered, usage of smaller integer ratios (fourth and fifth) could be scored differently from usage of the others.

@endolith
Copy link
Author

endolith commented Aug 11, 2021

@jpivarski

Gamelan instruments and scales in many sections—is he the one who first applied Plomp-Levelt dissonance to Gamelan?

I think so. Marc Leman talks about it, but cites Sethares 1997 http://www.musicandmeaning.net/issues/showArticle.php?artID=1.3#illu_3

All of my knowledge on this topic is summarized here: https://music.stackexchange.com/a/6556/2489

@BradKML
Copy link

BradKML commented Aug 12, 2021

@jpivarski

I guess some intervals in the 12-tone scale are more consonant than others, particularly where the equal-temperament makes them miss the minima of the curves above, and an index could rate songs differently if they use more of these "bad notes."

Somewhat this. Imagine a musical hash and visualization based on interval changes

@mauro-belgiovine
Copy link

mauro-belgiovine commented Sep 25, 2023

Just modified the code to calculate up to 19th harmonic and it looks great for microtonal usages. Thanks!

dissonance_map

Code: https://replit.com/@01010101lzy/Dissonance

@lynzrand Sorry to resurrect this thread, but this is not available anymore. I'd be curious to check your implementation, thanks!

@endolith Thanks for this translated code.

@lynzrand
Copy link

lynzrand commented Sep 26, 2023

@lynzrand Sorry to resurrect this thread, but this is not available anymore. I'd be curious to check your implementation, thanks!

Hi! The old code repository was accidentally deleted at some time by me, probably because I was cleaning up my replit account. The implementation is just a simple edit from the code in the original code.

Anyway, the code should be available again in the same repository (https://replit.com/@01010101lzy/Dissonance). I also created a gist for it: https://gist.github.com/lynzrand/e65c777d501289ae3876b59b27bd0f62


Update: I have updated my code to also contain (microtonal) scales as a reference, and also different frequency functions!

out

@mauro-belgiovine
Copy link

@lynzrand Sorry to resurrect this thread, but this is not available anymore. I'd be curious to check your implementation, thanks!

Hi! The old code repository was accidentally deleted at some time by me, probably because I was cleaning up my replit account. The implementation is just a simple edit from the code in the original code.

Anyway, the code should be available again in the same repository (https://replit.com/@01010101lzy/Dissonance). I also created a gist for it: https://gist.github.com/lynzrand/e65c777d501289ae3876b59b27bd0f62

Update: I have updated my code to also contain (microtonal) scales as a reference, and also different frequency functions!

out

@lynzrand thank you so much for this!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment