14. Statistical Models

import matplotlib
if not hasattr(matplotlib.RcParams, "_get"):
    matplotlib.RcParams._get = dict.get

14. Statistical Models#

In this chapter, we will examine statistical models in a computer music context: models that describe “data”, such as musical scores or recordings, in terms of probabilities and other statistics.

Statistical models are among the most rudimentary algorithms for machine learning, which builds models from examples rather than from rules written by hand. Accordingly, some of the principles in this chapter underlie modern AI systems that learn from large datasets, not only for music but also for language. We will study those systems in Chapter 18. Here, we focus on two simpler use cases for statistical models: algorithmic composition and voice modeling.

Algorithmic composition. A primary use case for statistical models is algorithmic composition, the idea of generating musical scores with an algorithmic process. For example, the code below implements a simplified version of tintinnabuli, a compositional technique developed by Arvo Pärt. A melody moves step by step through a scale. A second voice accompanies each note of the melody with the nearest note above it from a chord of three notes, A, C, and E (in any octave). The result is a slowly unfolding series of harmonies, rendered here with a simple Karplus Strong instrument (pluck).

# hide
import numpy as np
import pyquist as pq
from scipy.signal import lfilter

Hide setup

# collapse
def pluck(pitch: float, duration: float, f_s: int = 44100, **kwargs) -> pq.Audio:
    """A Karplus-Strong plucked string (Chapter 12) that decays by 60 dB in `duration`
    seconds. An all-pass filter (Chapter 13) in the loop tunes it precisely."""
    P = f_s / pq.helper.pitch_to_frequency(pitch)   # the period, in (fractional) samples
    M = int(P - 0.6)                                 # the delay line (the low-pass adds 1/2 sample)
    D = P - 0.5 - M                                  # the all-pass adds the remaining D samples
    c = (1 - D) / (1 + D)                            # the all-pass gain for a delay of D samples
    g = 10 ** (-3 * P / (f_s * duration))            # decay by 60 dB over the duration
    x = np.zeros(int(duration * f_s))
    x[:M] = np.random.uniform(-1.0, 1.0, M)          # excitation: a burst of white noise
    # The whole loop is one recursive filter (Chapter 9), which SciPy runs quickly:
    # y[n] = x[n] + c x[n-1] - c y[n-1] + (g/2) (c y[n-M] + (1 + c) y[n-M-1] + y[n-M-2])
    a = np.zeros(M + 3)
    a[:2] = [1.0, c]
    a[M:] -= 0.5 * g * np.array([c, 1 + c, 1.0])
    return pq.Audio(lfilter([1.0, c], a, x), f_s)
def tintinnabuli(melody: list[int], chord: list[int], ioi: float = 0.5) -> pq.Score:
    """Accompany each note of the melody with the nearest note of the chord above it."""
    events = []
    for i, pitch in enumerate(melody):
        above = pitch + 1
        while above % 12 not in chord:          # step up until we reach a note of the chord
            above += 1
        events.append((i * ioi, {"pitch": pitch, "duration": 3 * ioi}))
        events.append((i * ioi, {"pitch": above, "duration": 3 * ioi}))
    return pq.Score(events)


a_minor = [9, 0, 4]                             # the pitch classes of A, C, and E
melody = [69, 71, 72, 74, 72, 71, 69, 67, 65, 67, 69, 71]
score = tintinnabuli(melody * 2, a_minor)
pq.play(score.render(pluck), normalize=True)

This algorithm is deterministic—no matter how many times we run this code, it composes the same score. In this chapter, we will instead study stochastic techniques, which center around probability, and whose outputs may change every time we run them. Specifically, we will study Markov chains and \(n\)-gram models, whose probabilities are configured based on statistics extracted from musical examples.

Voice modeling. Similar statistical methods apply to audio, not just scores. At the end of this chapter, we will see how a technique called linear predictive coding extracts statistics from a recording of the human voice, and uses them to design a filter that imitates it:

A sung vowel

The same vowel, synthesized by linear predictive coding

A voice singing the vowel “A” at \(110\) Hz, and the same vowel synthesized from its statistics. Recording from Freesound: 368728 by antoinevg, License: CC0.