import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
matplotlib.RcParams._get = dict.get
14.0 Probability in music#
The algorithmic composition techniques in this chapter require an initial leap of faith: that composing music can be viewed as a probabilistic process. Before we take this leap, we need a few concepts from probability.
At the core of probability is the random variable, a variable whose value is the outcome of a random process. In this chapter, we will primarily study random variables that have a finite set of possible outcomes, such as the six faces of a die, or a set of musical pitches. A categorical distribution describes the probability of each outcome:
Definition 42 (Categorical distribution)
Let \(X\) be a random variable whose possible outcomes form a finite set \(\mathcal{X}\), called its alphabet. A categorical distribution assigns a probability \(P(X = x) \in [0, 1]\) to each outcome \(x \in \mathcal{X}\), such that the probabilities sum to one:
When it is clear which random variable we are referring to, we will often abbreviate \(P(X = x)\) as \(P(x)\).
Example 2 (A fair die)
The roll of a fair, six-sided die is a random variable with the alphabet \(\mathcal{X} = \{1, 2, 3, 4, 5, 6\}\). Every outcome is equally likely, so \(P(x) = \frac{1}{|\mathcal{X}|} = \frac{1}{6}\) for every \(x \in \mathcal{X}\), where \(|\mathcal{X}|\) is the size of the alphabet (the number of outcomes).
The code below simulates rolls of this die with NumPy’s np.random.choice, which takes an alphabet and its probabilities. Ten rolls look fairly arbitrary, but over six thousand rolls, each face comes up about one thousand times:
alphabet = [1, 2, 3, 4, 5, 6]
weights = np.ones(6)
weights /= weights.sum() # a fair die
print(np.random.choice(alphabet, size=10, p=weights))
rolls = np.random.choice(alphabet, size=6000, p=weights)
for face in alphabet:
print(f"{face}: {np.sum(rolls == face)} rolls")
[2 4 4 5 2 2 6 3 3 6]
1: 1057 rolls
2: 981 rolls
3: 940 rolls
4: 1050 rolls
5: 995 rolls
6: 977 rolls
Try loading the die, by setting weights[-1] = 5.0 before the division. How often does a \(6\) come up now?
Stochastic processes#
A categorical distribution models a single outcome. But a musical score contains a sequence of notes, requiring a more sophisticated abstraction:
Definition 43 (Stochastic process)
A stochastic process is a sequence of random variables, \(X_1, X_2, \ldots, X_N\). Each random variable \(X_i\) has its own alphabet \(\mathcal{X}_i\) and distribution, which may depend on its position \(i\) in the sequence.
For a stochastic process, we abbreviate \(P(X_i = x_i)\) as \(P(x_i)\).
Example 3 (A pentatonic melody)
Here is a basic stochastic process that composes a melody of \(N\) notes. Every position uses the same alphabet of five pitches, \(\mathcal{X}_i = \{60, 62, 64, 67, 69\}\) (the notes C, D, E, G, and A, which form a pentatonic scale), and every pitch is equally likely:
A distribution like this one, in which every outcome is equally likely, is called uniform.
How do we generate a melody from this process? We draw a sample of each random variable according to its distribution:
where \(x_i \sim P(X_i)\) means that the outcome \(x_i\) is drawn at random with the probabilities of \(X_i\). In this simple process, each \(X_i\) is drawn independently, without regard to the outcomes of the others, so we could just as well draw them in any order, or all at once. Later in this chapter, we will study processes in which each outcome depends on the outcomes before it.
These \(N\) pitches do not yet make a score, though. We also need to decide when to play each note, and for how long. The inter-onset interval (IOI) is the amount of time between the onsets of consecutive notes, and the duration is the amount of time that each note lasts. When the duration exceeds the IOI, consecutive notes overlap.
The code below implements this example process, with an IOI of a quarter of a beat at \(120\) BPM, and prints the first four events of the score that it generates:
def uniform(N: int, pitches: list[int] = [60, 62, 64, 67, 69], ioi: float = 0.25,
duration: float = 0.5) -> pq.Score:
"""A melody of N notes, whose pitches are drawn independently and uniformly at random.
Onsets are in beats, and durations (decay times) are in seconds."""
choices = np.random.choice(pitches, size=N)
return pq.Score([(i * ioi, {"pitch": int(pitch), "duration": duration})
for i, pitch in enumerate(choices)])
score = uniform(64)
for event in score[:4]:
print(event)
pq.play(score.render(pluck, metronome=pq.BasicMetronome(bpm=120)), normalize=True)
Event(time=0.0, kwargs={'pitch': 69, 'duration': 0.5})
Event(time=0.25, kwargs={'pitch': 60, 'duration': 0.5})
Event(time=0.5, kwargs={'pitch': 60, 'duration': 0.5})
Event(time=0.75, kwargs={'pitch': 64, 'duration': 0.5})
Musical dice games#
One of the earliest examples of algorithmic composition is the musical dice game (in German, Musikalisches Würfelspiel), which was popular in 18th-century Europe. A composer would write many interchangeable snippets of a score, usually one measure (a short span with a fixed number of beats) each. Players would then roll dice to choose which snippet to play at each position in a piece.
The most famous of these games (K. 516f) is often attributed to Mozart, though the attribution is unconfirmed. Published in the 1790s, it contains \(176\) one-measure snippets for a short waltz in \(16\) measures, along with a table that assigns \(11\) of the snippets to each of the \(16\) measures. For each measure, the player rolls two dice, and looks up the snippet for their sum, from \(2\) to \(12\) (\(11\) possible outcomes):
Fig. 111 The first three measures of the dice game attributed to Mozart, for the dice sums \(S = 2\), \(3\), and \(4\) (out of \(2\) through \(12\)). Each roll of the dice picks one snippet for its measure. Here, the rolls are \(4\), then \(2\), then \(3\).#
Example 4 (The sum of two dice)
The sum of two dice, \(S\), is itself a categorical random variable, but its distribution is not uniform. Each die has \(6\) equally likely faces, so a pair of dice has \(6 \cdot 6 = 36\) equally likely outcomes. Of these, only one, \((1, 1)\), sums to \(2\), while six sum to \(7\). So \(P(S = 2) = \frac{1}{36} \approx 3\%\), but \(P(S = 7) = \frac{6}{36} \approx 17\%\).
Fig. 112 The distribution of the sum \(S\) of two fair dice.#
In stochastic processes, some outcomes can be much more likely than others. Because each roll of the dice has no effect on the others, the probability of a particular sequence of rolls is the product of the probabilities of its \(16\) rolls. For example, rolling a \(7\) for every measure has probability \(\left(\frac{1}{6}\right)^{16} \approx 3.5 \times 10^{-13}\), while rolling a \(2\) for every measure has probability \(\left(\frac{1}{36}\right)^{16} \approx 1.3 \times 10^{-25}\). With \(11^{16} \approx 4.6 \times 10^{16}\) possible sequences of rolls, both are astronomically unlikely. But rolling a \(2\) for every measure is about three trillion (\(6^{16} \approx 2.8 \times 10^{12}\)) times less likely than rolling a \(7\) for every measure.
The widget below, inspired by an online version of the game, lets you play it. Each column is one of the \(16\) measures (M1 through M16), and each row is one possible sum of the dice. Click a snippet to choose it, or roll the dice for every measure at once, and then listen to your waltz:
Score data from the 1793 edition (public domain), based on the Humdrum encoding by Craig Sapp, Daniel Shanahan, and Mauricio Rodriguez.