Week 10 · Key and Chord Estimation

Author

John Ashley Burgoyne

Published

March 4, 2026

You can download the raw source code for these lecture notes here.

Course Meeting Plan

Wednesday · 4 March · Lecture

  • Demo: Chordify (10 min)
  • Lecture: Chordograms (30 min)
  • Breakout: Grease and the sitar 1 (10 min)
  • Discussion: Breakout results (10 min)
  • Breakout: Grease and the sitar 2 (10 min)
  • Discussion: Breakout results (10 min)
  • Wrap-up (10 min)

Wednesday · 6 March · Lab

  • Demo: Chordograms with chroma features (15 min)
  • Breakout: Chordograms (20 min)
  • Discussion: Breakout results (10 min)
  • Demo: Quarto dashboards (15 min)
  • Breakout: Quarto dashboards (25 min)
  • Wrap-up (5 min)

Set-up

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.0     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.2     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(compmus)

Breakout 1: Grease and the sitar

Part 1

The following figure is a chordogram for ‘Those Magic Changes’ from the 1978 musical film Grease. The film is set in the 1950s and the soundtrack is a pastiche of musical tropes from the popular music of that era. When performed on stage, the back-up singers often sing out the ‘changes’ for this number. A chordogram of ‘Those Magic Changes’ from the 1994 Broadway revival appears below; the back-up singers start reciting the harmonies around 1:30.

Listen to the track and discuss the following questions.

  • How well does the chordogram seem to capture the harmonies? Where are there ambiguities?
  • What happens at about 3:00? Why does the pattern in the chordogram change?
  • What are the yellow bars?

You can download the chroma features for this track here.

Rows: 6363 Columns: 13
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (13): TIME, A, Bb, B, C, C#, D, Eb, E, F, F#, G, Ab

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Warning: Using `by = character()` to perform a cross join was deprecated in dplyr 1.1.0.
ℹ Please use `cross_join()` instead.
ℹ The deprecated feature was likely used in the compmus package.
  Please report the issue to the authors.

Part 2

Spotify’s pitch features are designed for Western tonal music, but it computes them for every track in its catalogue. What happens if you try to use them for other musics?

The chordogram below uses the same algorithm as the chordogram for ‘Those Magic Changes’, but is for ‘Dhun’ by Ravi Shankar, a famous performer of Indian classical music.

Listen to a little bit of the track and discuss the following questions.

  • What does the chordogram seem to say? Is Ravi Shankar using particular harmonies?
  • How could we improve MIR features to be more appropriate for this style of music?

You can download the chroma features for this track here.

Rows: 8379 Columns: 13
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (13): TIME, A, Bb, B, C, C#, D, Eb, E, F, F#, G, Ab

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Breakout 2: Chordograms

The focus of the readings this week were chord and key estimation. One set of standard templates is below: 1–0 coding for the chord templates and the Krumhansl–Kessler key profiles.

circshift <- function(v, n) {
  if (n == 0) v else c(tail(v, n), head(v, -n))
}

#      C     C#    D     Eb    E     F     F#    G     Ab    A     Bb    B
major_chord <-
  c(   1,    0,    0,    0,    1,    0,    0,    1,    0,    0,    0,    0)
minor_chord <-
  c(   1,    0,    0,    1,    0,    0,    0,    1,    0,    0,    0,    0)
seventh_chord <-
  c(   1,    0,    0,    0,    1,    0,    0,    1,    0,    0,    1,    0)

major_key <-
  c(6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88)
minor_key <-
  c(6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17)

chord_templates <-
  tribble(
    ~name, ~template,
    "Gb:7", circshift(seventh_chord, 6),
    "Gb:maj", circshift(major_chord, 6),
    "Bb:min", circshift(minor_chord, 10),
    "Db:maj", circshift(major_chord, 1),
    "F:min", circshift(minor_chord, 5),
    "Ab:7", circshift(seventh_chord, 8),
    "Ab:maj", circshift(major_chord, 8),
    "C:min", circshift(minor_chord, 0),
    "Eb:7", circshift(seventh_chord, 3),
    "Eb:maj", circshift(major_chord, 3),
    "G:min", circshift(minor_chord, 7),
    "Bb:7", circshift(seventh_chord, 10),
    "Bb:maj", circshift(major_chord, 10),
    "D:min", circshift(minor_chord, 2),
    "F:7", circshift(seventh_chord, 5),
    "F:maj", circshift(major_chord, 5),
    "A:min", circshift(minor_chord, 9),
    "C:7", circshift(seventh_chord, 0),
    "C:maj", circshift(major_chord, 0),
    "E:min", circshift(minor_chord, 4),
    "G:7", circshift(seventh_chord, 7),
    "G:maj", circshift(major_chord, 7),
    "B:min", circshift(minor_chord, 11),
    "D:7", circshift(seventh_chord, 2),
    "D:maj", circshift(major_chord, 2),
    "F#:min", circshift(minor_chord, 6),
    "A:7", circshift(seventh_chord, 9),
    "A:maj", circshift(major_chord, 9),
    "C#:min", circshift(minor_chord, 1),
    "E:7", circshift(seventh_chord, 4),
    "E:maj", circshift(major_chord, 4),
    "G#:min", circshift(minor_chord, 8),
    "B:7", circshift(seventh_chord, 11),
    "B:maj", circshift(major_chord, 11),
    "D#:min", circshift(minor_chord, 3)
  )

key_templates <-
  tribble(
    ~name, ~template,
    "Gb:maj", circshift(major_key, 6),
    "Bb:min", circshift(minor_key, 10),
    "Db:maj", circshift(major_key, 1),
    "F:min", circshift(minor_key, 5),
    "Ab:maj", circshift(major_key, 8),
    "C:min", circshift(minor_key, 0),
    "Eb:maj", circshift(major_key, 3),
    "G:min", circshift(minor_key, 7),
    "Bb:maj", circshift(major_key, 10),
    "D:min", circshift(minor_key, 2),
    "F:maj", circshift(major_key, 5),
    "A:min", circshift(minor_key, 9),
    "C:maj", circshift(major_key, 0),
    "E:min", circshift(minor_key, 4),
    "G:maj", circshift(major_key, 7),
    "B:min", circshift(minor_key, 11),
    "D:maj", circshift(major_key, 2),
    "F#:min", circshift(minor_key, 6),
    "A:maj", circshift(major_key, 9),
    "C#:min", circshift(minor_key, 1),
    "E:maj", circshift(major_key, 4),
    "G#:min", circshift(minor_key, 8),
    "B:maj", circshift(major_key, 11),
    "D#:min", circshift(minor_key, 3)
  )

Armed with these templates, we can make chordograms and keygrams for individual pieces. Similar to previous weeks, we start by choosing a level of hierarchy and then summarise the chroma features a that level. Higher levels like section are more appropriate for key profiles; lower levels like beat are more appropriate for chord profiles.

The following code fetches the analysis for Zager and Evans’s ‘In the Year 2525’ (1969). You can download the chroma features for this track here.

twenty_five <- read_csv("../dat/year-2025-pitches.csv")
Rows: 4304 Columns: 13
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (13): TIME, A, Bb, B, C, C#, D, Eb, E, F, F#, G, Ab

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

The new helper function compmus_match_pitch_template compares the averaged chroma vectors against templates to yield a chordo- or keygram. The two truck-driver modulations from G-sharp minor through A minor to B-flat minor are clear.

twenty_five |> 
  compmus_wrangle_chroma() |> 
  filter(row_number() %% 50L == 0L) |> 
  compmus_match_pitch_template(
    key_templates,         # Change to chord_templates if desired
    method = "euclidean",  # Try different distance metrics
    norm = "manhattan"     # Try different norms
  ) |>
  ggplot(
    aes(x = start + duration / 2, width = 50 * duration, y = name, fill = d)
  ) +
  geom_tile() +
  scale_fill_viridis_c(guide = "none") +
  theme_minimal() +
  labs(x = "Time (s)", y = "")

Instructions

Once you have the code running, try the following adaptations.

  1. Try making a chordogram instead of the keygram above.
  2. Replace the key profiles above with Temperley’s proposed improvements from the reading this week. (Don’t forget to re-run the chunk after you are finished.) Do the revised profiles work any better? Can you think of a way to improve the chord profiles, too?

Common Norm and Distance Combinations

Domain Normalisation Distance
Non-negative (e.g., chroma) Manhattan Manhattan
Aitchison
Euclidean cosine
angular
Chebyshev [none]
Full-range (e.g., timbre) [none] Euclidean
Euclidean cosine
angular